diff --git a/docs/plan.md b/docs/plan.md new file mode 100644 index 0000000..8e20f7e --- /dev/null +++ b/docs/plan.md @@ -0,0 +1,42 @@ +- perform_phase1_message.md + - ここから status Rule を排除する(phase3に書けばいい) +- perform_phase2_message.md + - 「上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。」は上記ってのがいらないのではないか + - 「**このフェーズではツールは使えません。レポート内容をテキストとして直接回答してください。**」が重複することがあるので削除せよ。 + - JSON形式について触れる必要はない。 +- perform_phase3_message.md + - status Rule を追加する聞く +- perform_agent_system_prompt.md + - これ、エージェントのデータを挿入してないの……? +- 全体的に + - 音楽にひもづける + - つまり、workflowsをやめて pieces にする + - 現workflowファイルにあるstepsもmovementsにする(全ファイルの修正) + - stepという言葉はmovementになる。phaseもmovementが適しているだろう(これは interactive における phase のことをいっていない) + - _language パラメータは消せ + - ワークフローを指定すると実際に送られるプロンプトを組み立てて表示する機能かツールを作れるか + - メタ領域を用意して説明、どこで利用されるかの説明、使えるテンプレートとその説明をかいて、その他必要な情報あれば入れて。 + - 英語と日本語が共通でもかならずファイルはわけて同じ文章を書いておく + - 無駄な空行とか消してほしい + ``` + {{#if hasPreviousResponse}} + + ## Previous Response + {{previousResponse}} + {{/if}} + {{#if hasUserInputs}} + + ## Additional User Inputs + {{userInputs}} + ``` + これは↓のがいいんじゃない? + ``` + {{#if hasPreviousResponse}} + ## Previous Response + {{previousResponse}} + {{/if}} + + {{#if hasUserInputs}} + ## Additional User Inputs + {{userInputs}} + ``` \ No newline at end of file diff --git a/package.json b/package.json index 7176db2..b14b56f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "takt-cli": "./dist/app/cli/index.js" }, "scripts": { - "build": "tsc && mkdir -p dist/shared/prompts dist/shared/i18n && cp src/shared/prompts/prompts_en.yaml src/shared/prompts/prompts_ja.yaml dist/shared/prompts/ && cp src/shared/i18n/labels_en.yaml src/shared/i18n/labels_ja.yaml dist/shared/i18n/", + "build": "tsc && mkdir -p dist/shared/prompts/en dist/shared/prompts/ja dist/shared/i18n && cp src/shared/prompts/en/*.md dist/shared/prompts/en/ && cp src/shared/prompts/ja/*.md dist/shared/prompts/ja/ && cp src/shared/i18n/labels_en.yaml src/shared/i18n/labels_ja.yaml dist/shared/i18n/", "watch": "tsc --watch", "test": "vitest run", "test:watch": "vitest", diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/pieces/default.yaml similarity index 94% rename from resources/global/en/workflows/default.yaml rename to resources/global/en/pieces/default.yaml index f215c82..d938555 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/pieces/default.yaml @@ -3,17 +3,17 @@ # # Boilerplate sections (Workflow Context, User Request, Previous Response, # Additional User Inputs, Instructions heading) are auto-injected by buildInstruction(). -# Only step-specific content belongs in instruction_template. +# Only movement-specific content belongs in instruction_template. # # Template Variables (available in instruction_template): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) -# {previous_response} - Output from the previous step (only when pass_previous_response: true) +# {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") # -# Step-level Fields: -# report: - Report file(s) for the step (auto-injected as Report File/Files in Workflow Context) +# Movement-level Fields: +# report: - Report file(s) for the movement (auto-injected as Report File/Files in Workflow Context) # Single: report: 00-plan.md # Multiple: report: # - Scope: 01-coder-scope.md @@ -24,9 +24,9 @@ description: Standard development workflow with planning and specialized reviews max_iterations: 30 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan edit: false agent: ../agents/default/planner.md @@ -75,7 +75,7 @@ steps: instruction_template: | Analyze the task and create an implementation plan. - **Note:** If returned from implement step (Previous Response exists), + **Note:** If returned from implement movement (Previous Response exists), review and revise the plan based on that feedback (replan). **Tasks (for implementation tasks):** @@ -177,7 +177,7 @@ steps: requires_user_input: true interactive_only: true instruction_template: | - Follow the plan from the plan step and the design from the architect step. + Follow the plan from the plan movement and the design from the architect movement. **Reports to reference:** - Plan: {report:00-plan.md} @@ -185,7 +185,7 @@ steps: Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. - **Important:** Do not make design decisions; follow the design determined in the architect step. + **Important:** Do not make design decisions; follow the design determined in the architect movement. Report if you encounter unclear points or need design changes. **Scope report format (create at implementation start):** @@ -300,7 +300,7 @@ steps: - condition: Cannot proceed, insufficient info next: plan instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** @@ -387,8 +387,8 @@ steps: - condition: approved - condition: needs_fix instruction_template: | - **Verify that the implementation follows the design from the architect step.** - Do NOT review AI-specific issues (that's the ai_review step). + **Verify that the implementation follows the design from the architect movement.** + Do NOT review AI-specific issues (that's the ai_review movement). **Reports to reference:** - Design: {report:01-architecture.md} (if exists) @@ -402,7 +402,7 @@ steps: - Dead code - Call chain verification - **Note:** For small tasks that skipped the architect step, review design validity as usual. + **Note:** For small tasks that skipped the architect movement, review design validity as usual. - name: security-review edit: false @@ -517,7 +517,7 @@ steps: **Workflow 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 step issues addressed? + 2. Were all review movement issues addressed? 3. Was the original task objective achieved? **Review Reports:** Read all reports in Report Directory and diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/pieces/expert-cqrs.yaml similarity index 95% rename from resources/global/en/workflows/expert-cqrs.yaml rename to resources/global/en/pieces/expert-cqrs.yaml index 270b3df..15e561f 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/pieces/expert-cqrs.yaml @@ -10,11 +10,11 @@ # any("needs_fix") → fix → reviewers # # Template Variables: -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed) # {task} - Original user request -# {previous_response} - Output from the previous step +# {previous_response} - Output from the previous movement # {user_inputs} - Accumulated user inputs during workflow # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -23,11 +23,11 @@ description: CQRS+ES, Frontend, Security, QA Expert Review max_iterations: 30 -initial_step: plan +initial_movement: plan -steps: +movements: # =========================================== - # Phase 0: Planning + # Movement 0: Planning # =========================================== - name: plan edit: false @@ -65,7 +65,7 @@ steps: instruction_template: | Analyze the task and create an implementation plan. - **Note:** If returned from implement step (Previous Response exists), + **Note:** If returned from implement movement (Previous Response exists), review and revise the plan based on that feedback (replan). **Tasks:** @@ -80,7 +80,7 @@ steps: next: ABORT # =========================================== - # Phase 1: Implementation + # Movement 1: Implementation # =========================================== - name: implement edit: true @@ -99,7 +99,7 @@ steps: - WebSearch - WebFetch instruction_template: | - Follow the plan from the plan step and implement. + 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. @@ -153,7 +153,7 @@ steps: interactive_only: true # =========================================== - # Phase 2: AI Review + # Movement 2: AI Review # =========================================== - name: ai_review edit: false @@ -219,7 +219,7 @@ steps: - WebSearch - WebFetch instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** @@ -271,7 +271,7 @@ steps: next: plan # =========================================== - # Phase 3: Expert Reviews (Parallel) + # Movement 3: Expert Reviews (Parallel) # =========================================== - name: reviewers parallel: @@ -320,7 +320,7 @@ steps: - condition: needs_fix instruction_template: | Review the changes from the CQRS (Command Query Responsibility Segregation) - and Event Sourcing perspective. Do NOT review AI-specific issues (that's the ai_review step). + and Event Sourcing perspective. Do NOT review AI-specific issues (that's the ai_review movement). **Review Criteria:** - Aggregate design validity @@ -382,7 +382,7 @@ steps: - TypeScript type safety **Note**: If this project does not include frontend code, - approve and proceed to the next step. + approve and proceed to the next movement. - name: security-review edit: false @@ -525,7 +525,7 @@ steps: pass_previous_response: true # =========================================== - # Phase 4: Supervision + # Movement 4: Supervision # =========================================== - name: supervise edit: false @@ -541,7 +541,7 @@ steps: - WebFetch instruction_template: | ## Previous Reviews Summary - Reaching this step means all the following reviews have been APPROVED: + Reaching this movement means all the following reviews have been APPROVED: - AI Review: APPROVED - CQRS+ES Review: APPROVED - Frontend Review: APPROVED @@ -552,7 +552,7 @@ steps: **Workflow Overall Review:** 1. Does the implementation match the plan ({report:00-plan.md})? - 2. Were all review step issues addressed? + 2. Were all review movement issues addressed? 3. Was the original task objective achieved? **Review Reports:** Read all reports in Report Directory and diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/pieces/expert.yaml similarity index 94% rename from resources/global/en/workflows/expert.yaml rename to resources/global/en/pieces/expert.yaml index 5798464..0fea35d 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/pieces/expert.yaml @@ -14,17 +14,17 @@ # # Boilerplate sections (Workflow Context, User Request, Previous Response, # Additional User Inputs, Instructions heading) are auto-injected by buildInstruction(). -# Only step-specific content belongs in instruction_template. +# Only movement-specific content belongs in instruction_template. # # Template Variables (available in instruction_template): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) -# {previous_response} - Output from the previous step (only when pass_previous_response: true) +# {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") # -# Step-level Fields: -# report: - Report file(s) for the step (auto-injected as Report File/Files in Workflow Context) +# Movement-level Fields: +# report: - Report file(s) for the movement (auto-injected as Report File/Files in Workflow Context) # Single: report: 00-plan.md # Multiple: report: # - Scope: 01-coder-scope.md @@ -35,11 +35,11 @@ description: Architecture, Frontend, Security, QA Expert Review max_iterations: 30 -initial_step: plan +initial_movement: plan -steps: +movements: # =========================================== - # Phase 0: Planning + # Movement 0: Planning # =========================================== - name: plan edit: false @@ -77,7 +77,7 @@ steps: instruction_template: | Analyze the task and create an implementation plan. - **Note:** If returned from implement step (Previous Response exists), + **Note:** If returned from implement movement (Previous Response exists), review and revise the plan based on that feedback (replan). **Tasks:** @@ -92,7 +92,7 @@ steps: next: ABORT # =========================================== - # Phase 1: Implementation + # Movement 1: Implementation # =========================================== - name: implement edit: true @@ -111,7 +111,7 @@ steps: - WebSearch - WebFetch instruction_template: | - Follow the plan from the plan step and implement. + 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. @@ -165,7 +165,7 @@ steps: interactive_only: true # =========================================== - # Phase 2: AI Review + # Movement 2: AI Review # =========================================== - name: ai_review edit: false @@ -231,7 +231,7 @@ steps: - WebSearch - WebFetch instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** @@ -284,7 +284,7 @@ steps: next: plan # =========================================== - # Phase 3: Expert Reviews (Parallel) + # Movement 3: Expert Reviews (Parallel) # =========================================== - name: reviewers parallel: @@ -335,7 +335,7 @@ steps: - condition: approved - condition: needs_fix instruction_template: | - Focus on **architecture and design** review. Do NOT review AI-specific issues (that's the ai_review step). + Focus on **architecture and design** review. Do NOT review AI-specific issues (that's the ai_review movement). **Review Criteria:** - Structure/design validity @@ -395,7 +395,7 @@ steps: - TypeScript type safety **Note**: If this project does not include frontend code, - approve and proceed to the next step. + approve and proceed to the next movement. - name: security-review edit: false @@ -538,7 +538,7 @@ steps: pass_previous_response: true # =========================================== - # Phase 4: Supervision + # Movement 4: Supervision # =========================================== - name: supervise edit: false @@ -554,7 +554,7 @@ steps: - WebFetch instruction_template: | ## Previous Reviews Summary - Reaching this step means all the following reviews have been APPROVED: + Reaching this movement means all the following reviews have been APPROVED: - Architecture Review: APPROVED - Frontend Review: APPROVED - AI Review: APPROVED @@ -565,7 +565,7 @@ steps: **Workflow Overall Review:** 1. Does the implementation match the plan ({report:00-plan.md})? - 2. Were all review step issues addressed? + 2. Were all review movement issues addressed? 3. Was the original task objective achieved? **Review Reports:** Read all reports in Report Directory and diff --git a/resources/global/en/workflows/magi.yaml b/resources/global/en/pieces/magi.yaml similarity index 93% rename from resources/global/en/workflows/magi.yaml rename to resources/global/en/pieces/magi.yaml index 38f1b01..4133ab8 100644 --- a/resources/global/en/workflows/magi.yaml +++ b/resources/global/en/pieces/magi.yaml @@ -3,11 +3,11 @@ # Three personas (scientist, nurturer, pragmatist) analyze from different perspectives and vote # # Template Variables: -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed) # {task} - Original user request -# {previous_response} - Output from the previous step +# {previous_response} - Output from the previous movement # {user_inputs} - Accumulated user inputs during workflow # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -16,7 +16,7 @@ description: MAGI Deliberation System - Analyze from 3 perspectives and decide b max_iterations: 5 -steps: +movements: - name: melchior agent: ../agents/magi/melchior.md allowed_tools: diff --git a/resources/global/en/workflows/minimal.yaml b/resources/global/en/pieces/minimal.yaml similarity index 96% rename from resources/global/en/workflows/minimal.yaml rename to resources/global/en/pieces/minimal.yaml index 8361a34..be41bf7 100644 --- a/resources/global/en/workflows/minimal.yaml +++ b/resources/global/en/pieces/minimal.yaml @@ -3,11 +3,11 @@ # (Simplest configuration - no plan, no architect review) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {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 step (auto-injected) +# {previous_response} - Output from the previous movement (auto-injected) # {user_inputs} - Accumulated user inputs during workflow (auto-injected) # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -16,9 +16,9 @@ description: Minimal development workflow (implement -> parallel review -> fix i max_iterations: 20 -initial_step: implement +initial_movement: implement -steps: +movements: - name: implement edit: true agent: ../agents/default/coder.md @@ -248,7 +248,7 @@ steps: - condition: No fix needed (verified target files/spec) - condition: Cannot proceed, insufficient info instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** @@ -350,7 +350,7 @@ steps: - condition: Cannot proceed, insufficient info next: implement instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** diff --git a/resources/global/en/workflows/research.yaml b/resources/global/en/pieces/research.yaml similarity index 84% rename from resources/global/en/workflows/research.yaml rename to resources/global/en/pieces/research.yaml index 4f3aa1e..299d93e 100644 --- a/resources/global/en/workflows/research.yaml +++ b/resources/global/en/pieces/research.yaml @@ -7,11 +7,11 @@ # -> plan (rejected: restart from planning) # # Template Variables: -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed) # {task} - Original user request -# {previous_response} - Output from the previous step +# {previous_response} - Output from the previous movement # {user_inputs} - Accumulated user inputs during workflow # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -20,7 +20,7 @@ description: Research workflow - autonomously executes research without asking q max_iterations: 10 -steps: +movements: - name: plan agent: ../agents/research/planner.md allowed_tools: @@ -32,8 +32,8 @@ steps: instruction_template: | ## Workflow Status - Iteration: {iteration}/{max_iterations} (workflow-wide) - - Step Iteration: {step_iteration} (times this step has run) - - Step: plan + - Movement Iteration: {movement_iteration} (times this movement has run) + - Movement: plan ## Research Request {task} @@ -69,8 +69,8 @@ steps: instruction_template: | ## Workflow Status - Iteration: {iteration}/{max_iterations} (workflow-wide) - - Step Iteration: {step_iteration} (times this step has run) - - Step: dig + - Movement Iteration: {movement_iteration} (times this movement has run) + - Movement: dig ## Original Research Request {task} @@ -111,8 +111,8 @@ steps: instruction_template: | ## Workflow Status - Iteration: {iteration}/{max_iterations} (workflow-wide) - - Step Iteration: {step_iteration} (times this step has run) - - Step: supervise (research quality evaluation) + - Movement Iteration: {movement_iteration} (times this movement has run) + - Movement: supervise (research quality evaluation) ## Original Research Request {task} @@ -131,4 +131,4 @@ steps: - condition: Research results are insufficient and replanning is needed next: plan -initial_step: plan +initial_movement: plan diff --git a/resources/global/en/workflows/review-fix-minimal.yaml b/resources/global/en/pieces/review-fix-minimal.yaml similarity index 96% rename from resources/global/en/workflows/review-fix-minimal.yaml rename to resources/global/en/pieces/review-fix-minimal.yaml index 8796b21..0a8beff 100644 --- a/resources/global/en/workflows/review-fix-minimal.yaml +++ b/resources/global/en/pieces/review-fix-minimal.yaml @@ -1,13 +1,13 @@ # Review-Fix Minimal TAKT Workflow # Review -> Fix (if needed) -> Re-review -> Complete -# (Starts with review, no implementation step) +# (Starts with review, no implementation movement) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {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 step (auto-injected) +# {previous_response} - Output from the previous movement (auto-injected) # {user_inputs} - Accumulated user inputs during workflow (auto-injected) # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -16,9 +16,9 @@ description: Review and fix workflow for existing code (starts with review, no i max_iterations: 20 -initial_step: reviewers +initial_movement: reviewers -steps: +movements: - name: implement edit: true agent: ../agents/default/coder.md @@ -248,7 +248,7 @@ steps: - condition: No fix needed (verified target files/spec) - condition: Cannot proceed, insufficient info instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** @@ -350,7 +350,7 @@ steps: - condition: Cannot proceed, insufficient info next: implement instruction_template: | - **This is AI Review iteration {step_iteration}.** + **This is AI Review iteration {movement_iteration}.** If this is iteration 2 or later, it means your previous fixes were not actually applied. **Your belief that you "already fixed it" is wrong.** diff --git a/resources/global/en/workflows/review-only.yaml b/resources/global/en/pieces/review-only.yaml similarity index 96% rename from resources/global/en/workflows/review-only.yaml rename to resources/global/en/pieces/review-only.yaml index e95c68e..73fe0da 100644 --- a/resources/global/en/workflows/review-only.yaml +++ b/resources/global/en/pieces/review-only.yaml @@ -8,14 +8,14 @@ # -> COMPLETE (local: console output only) # -> ABORT (rejected) # -# All steps have edit: false (no file modifications) +# All movements have edit: false (no file modifications) # # Template Variables: # {iteration} - Workflow-wide turn count # {max_iterations} - Maximum iterations allowed -# {step_iteration} - Per-step iteration count +# {movement_iteration} - Per-movement iteration count # {task} - Original user request -# {previous_response} - Output from the previous step +# {previous_response} - Output from the previous movement # {user_inputs} - Accumulated user inputs # {report_dir} - Report directory name @@ -24,9 +24,9 @@ description: Review-only workflow - reviews code without making edits max_iterations: 10 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan edit: false agent: ../agents/default/planner.md @@ -108,7 +108,7 @@ steps: - condition: approved - condition: needs_fix instruction_template: | - Focus on **architecture and design** review. Do NOT review AI-specific issues (that's the ai_review step). + Focus on **architecture and design** review. Do NOT review AI-specific issues (that's the ai_review movement). Review the code and provide feedback. @@ -248,7 +248,7 @@ steps: 3. Produce a consolidated review summary with overall verdict 4. Determine routing: - If the task mentions posting to a PR (e.g., "post comments to PR", "comment on PR"), - route to `pr-comment` step (condition: "approved, PR comment requested") + route to `pr-comment` movement (condition: "approved, PR comment requested") - If local review only, route to COMPLETE (condition: "approved") - If critical issues found, route to ABORT (condition: "rejected") diff --git a/resources/global/ja/agents/default/architecture-reviewer.md b/resources/global/ja/agents/default/architecture-reviewer.md index 874edb9..d6f3230 100644 --- a/resources/global/ja/agents/default/architecture-reviewer.md +++ b/resources/global/ja/agents/default/architecture-reviewer.md @@ -482,8 +482,8 @@ function createOrder(data: OrderData) { - 既存の設定ファイルが新しいスキーマと整合するか 3. ワークフロー定義を変更した場合: - - ステップ種別(通常/parallel)に応じた正しいフィールドが使われているか - - 不要なフィールド(parallelサブステップのnext等)が残っていないか + - ムーブメント種別(通常/parallel)に応じた正しいフィールドが使われているか + - 不要なフィールド(parallelサブムーブメントのnext等)が残っていないか **このパターンを見つけたら REJECT:** @@ -492,7 +492,7 @@ function createOrder(data: OrderData) { | 仕様に存在しないフィールドの使用 | 無視されるか予期しない動作 | | 仕様上無効な値の設定 | 実行時エラーまたは無視される | | 文書化された制約への違反 | 設計意図に反する | -| ステップ種別とフィールドの不整合 | コピペミスの兆候 | +| ムーブメント種別とフィールドの不整合 | コピペミスの兆候 | ### 9. 呼び出しチェーン検証 diff --git a/resources/global/ja/agents/default/supervisor.md b/resources/global/ja/agents/default/supervisor.md index 9b311ed..f4ee2b8 100644 --- a/resources/global/ja/agents/default/supervisor.md +++ b/resources/global/ja/agents/default/supervisor.md @@ -98,7 +98,7 @@ Architectが「正しく作られているか(Verification)」を確認す 確認すること: - 計画(00-plan.md)と実装結果が一致しているか -- 各レビューステップの指摘が適切に対応されているか +- 各レビュームーブメントの指摘が適切に対応されているか - タスクの本来の目的が達成されているか **ワークフロー全体の問題:** diff --git a/resources/global/ja/agents/templates/planner.md b/resources/global/ja/agents/templates/planner.md index 6b6530e..c84466a 100644 --- a/resources/global/ja/agents/templates/planner.md +++ b/resources/global/ja/agents/templates/planner.md @@ -34,7 +34,7 @@ ### 4. 実装アプローチ - 段階的な実装手順を設計する -- 各ステップの成果物を明示する +- 各ムーブメントの成果物を明示する - リスクと代替案を記載する ## 重要 diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/pieces/default.yaml similarity index 94% rename from resources/global/ja/workflows/default.yaml rename to resources/global/ja/pieces/default.yaml index b6ed7b1..b3c7a37 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/pieces/default.yaml @@ -2,11 +2,11 @@ # Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + Security) -> Supervisor Approval # # Template Variables (auto-injected by buildInstruction): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed) # {task} - Original user request -# {previous_response} - Output from the previous step +# {previous_response} - Output from the previous movement # {user_inputs} - Accumulated user inputs during workflow # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -15,9 +15,9 @@ description: Standard development workflow with planning and specialized reviews max_iterations: 30 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan edit: false agent: ../agents/default/planner.md @@ -168,7 +168,7 @@ steps: requires_user_input: true interactive_only: true instruction_template: | - planステップで立てた計画と、architectステップで決定した設計に従って実装してください。 + planムーブメントで立てた計画と、architectムーブメントで決定した設計に従って実装してください。 **参照するレポート:** - 計画: {report:00-plan.md} @@ -176,7 +176,7 @@ steps: Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 - **重要:** 設計判断はせず、architectステップで決定された設計に従ってください。 + **重要:** 設計判断はせず、architectムーブメントで決定された設計に従ってください。 不明点や設計の変更が必要な場合は報告してください。 **重要**: 実装と同時に単体テストを追加してください。 @@ -296,7 +296,7 @@ steps: - condition: 判断できない、情報不足 next: plan instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** @@ -385,8 +385,8 @@ steps: - condition: approved - condition: needs_fix instruction_template: | - **実装がarchitectステップの設計に従っているか**を確認してください。 - AI特有の問題はレビューしないでください(ai_reviewステップで行います)。 + **実装がarchitectムーブメントの設計に従っているか**を確認してください。 + AI特有の問題はレビューしないでください(ai_reviewムーブメントで行います)。 **参照するレポート:** - 設計: {report:01-architecture.md}(存在する場合) @@ -400,7 +400,7 @@ steps: - デッドコード - 呼び出しチェーン検証 - **注意:** architectステップをスキップした小規模タスクの場合は、従来通り設計の妥当性も確認してください。 + **注意:** architectムーブメントをスキップした小規模タスクの場合は、従来通り設計の妥当性も確認してください。 - name: security-review edit: false @@ -514,7 +514,7 @@ steps: **ワークフロー全体の確認:** 1. 計画({report:00-plan.md})と設計({report:01-architecture.md}、存在する場合)に従った実装か - 2. 各レビューステップの指摘が対応されているか + 2. 各レビュームーブメントの指摘が対応されているか 3. 元のタスク目的が達成されているか **レポートの確認:** Report Directory内の全レポートを読み、 diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/pieces/expert-cqrs.yaml similarity index 94% rename from resources/global/ja/workflows/expert-cqrs.yaml rename to resources/global/ja/pieces/expert-cqrs.yaml index fe81ee1..87065e0 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/pieces/expert-cqrs.yaml @@ -11,17 +11,17 @@ # # ボイラープレートセクション(Workflow Context, User Request, Previous Response, # Additional User Inputs, Instructions heading)はbuildInstruction()が自動挿入。 -# instruction_templateにはステップ固有の内容のみ記述。 +# instruction_templateにはムーブメント固有の内容のみ記述。 # # テンプレート変数(instruction_template内で使用可能): -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) +# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) # {max_iterations} - ワークフローの最大イテレーション数 -# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) -# {previous_response} - 前のステップの出力(pass_previous_response: true の場合のみ) +# {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) +# {previous_response} - 前のムーブメントの出力(pass_previous_response: true の場合のみ) # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") # -# ステップレベルフィールド: -# report: - ステップのレポートファイル(Workflow ContextにReport File/Filesとして自動挿入) +# ムーブメントレベルフィールド: +# report: - ムーブメントのレポートファイル(Workflow ContextにReport File/Filesとして自動挿入) # 単一: report: 00-plan.md # 複数: report: # - Scope: 01-coder-scope.md @@ -32,11 +32,11 @@ description: CQRS+ES・フロントエンド・セキュリティ・QA専門家 max_iterations: 30 -initial_step: plan +initial_movement: plan -steps: +movements: # =========================================== - # Phase 0: Planning + # Movement 0: Planning # =========================================== - name: plan edit: false @@ -89,7 +89,7 @@ steps: next: ABORT # =========================================== - # Phase 1: Implementation + # Movement 1: Implementation # =========================================== - name: implement edit: true @@ -108,7 +108,7 @@ steps: - WebSearch - WebFetch instruction_template: | - planステップで立てた計画に従って実装してください。 + planムーブメントで立てた計画に従って実装してください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 @@ -162,7 +162,7 @@ steps: interactive_only: true # =========================================== - # Phase 2: AI Review + # Movement 2: AI Review # =========================================== - name: ai_review edit: false @@ -228,7 +228,7 @@ steps: - WebSearch - WebFetch instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** @@ -279,7 +279,7 @@ steps: next: plan # =========================================== - # Phase 3: Expert Reviews (Parallel) + # Movement 3: Expert Reviews (Parallel) # =========================================== - name: reviewers parallel: @@ -328,7 +328,7 @@ steps: - condition: needs_fix instruction_template: | CQRS(コマンドクエリ責務分離)とEvent Sourcing(イベントソーシング)の観点から - 変更をレビューしてください。AI特有の問題のレビューは不要です(ai_reviewステップで実施済み)。 + 変更をレビューしてください。AI特有の問題のレビューは不要です(ai_reviewムーブメントで実施済み)。 **レビュー観点:** - Aggregate設計の妥当性 @@ -533,7 +533,7 @@ steps: pass_previous_response: true # =========================================== - # Phase 4: Supervision + # Movement 4: Supervision # =========================================== - name: supervise edit: false @@ -549,7 +549,7 @@ steps: - WebFetch instruction_template: | ## Previous Reviews Summary - このステップに到達したということは、以下のレビューがすべてAPPROVEされています: + このムーブメントに到達したということは、以下のレビューがすべてAPPROVEされています: - AI Review: APPROVED - CQRS+ES Review: APPROVED - Frontend Review: APPROVED @@ -560,7 +560,7 @@ steps: **ワークフロー全体の確認:** 1. 計画({report:00-plan.md})と実装結果が一致しているか - 2. 各レビューステップの指摘が対応されているか + 2. 各レビュームーブメントの指摘が対応されているか 3. 元のタスク目的が達成されているか **レポートの確認:** Report Directory内の全レポートを読み、 diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/pieces/expert.yaml similarity index 95% rename from resources/global/ja/workflows/expert.yaml rename to resources/global/ja/pieces/expert.yaml index 7107080..7297706 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/pieces/expert.yaml @@ -10,11 +10,11 @@ # any("needs_fix") → fix → reviewers # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) +# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) # {max_iterations} - ワークフローの最大イテレーション数 -# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) +# {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {task} - 元のユーザー要求 -# {previous_response} - 前のステップの出力 +# {previous_response} - 前のムーブメントの出力 # {user_inputs} - ワークフロー中に蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") @@ -23,11 +23,11 @@ description: アーキテクチャ・フロントエンド・セキュリティ max_iterations: 30 -initial_step: plan +initial_movement: plan -steps: +movements: # =========================================== - # Phase 0: Planning + # Movement 0: Planning # =========================================== - name: plan edit: false @@ -80,7 +80,7 @@ steps: next: ABORT # =========================================== - # Phase 1: Implementation + # Movement 1: Implementation # =========================================== - name: implement edit: true @@ -99,7 +99,7 @@ steps: - WebSearch - WebFetch instruction_template: | - planステップで立てた計画に従って実装してください。 + planムーブメントで立てた計画に従って実装してください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 @@ -153,7 +153,7 @@ steps: interactive_only: true # =========================================== - # Phase 2: AI Review + # Movement 2: AI Review # =========================================== - name: ai_review edit: false @@ -219,7 +219,7 @@ steps: - WebSearch - WebFetch instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** @@ -270,7 +270,7 @@ steps: next: plan # =========================================== - # Phase 3: Expert Reviews (Parallel) + # Movement 3: Expert Reviews (Parallel) # =========================================== - name: reviewers parallel: @@ -321,7 +321,7 @@ steps: - condition: approved - condition: needs_fix instruction_template: | - **アーキテクチャと設計**のレビューに集中してください。AI特有の問題のレビューは不要です(ai_reviewステップで実施済み)。 + **アーキテクチャと設計**のレビューに集中してください。AI特有の問題のレビューは不要です(ai_reviewムーブメントで実施済み)。 **レビュー観点:** - 構造・設計の妥当性 @@ -524,7 +524,7 @@ steps: pass_previous_response: true # =========================================== - # Phase 4: Supervision + # Movement 4: Supervision # =========================================== - name: supervise edit: false @@ -540,7 +540,7 @@ steps: - WebFetch instruction_template: | ## Previous Reviews Summary - このステップに到達したということは、以下のレビューがすべてAPPROVEされています: + このムーブメントに到達したということは、以下のレビューがすべてAPPROVEされています: - AI Review: APPROVED - Architecture Review: APPROVED - Frontend Review: APPROVED @@ -551,7 +551,7 @@ steps: **ワークフロー全体の確認:** 1. 計画({report:00-plan.md})と実装結果が一致しているか - 2. 各レビューステップの指摘が対応されているか + 2. 各レビュームーブメントの指摘が対応されているか 3. 元のタスク目的が達成されているか **レポートの確認:** Report Directory内の全レポートを読み、 diff --git a/resources/global/ja/workflows/magi.yaml b/resources/global/ja/pieces/magi.yaml similarity index 93% rename from resources/global/ja/workflows/magi.yaml rename to resources/global/ja/pieces/magi.yaml index 8f76983..540eb7e 100644 --- a/resources/global/ja/workflows/magi.yaml +++ b/resources/global/ja/pieces/magi.yaml @@ -3,11 +3,11 @@ # 3つの人格(科学者・育成者・実務家)が異なる観点から分析・投票する # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) +# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) # {max_iterations} - ワークフローの最大イテレーション数 -# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) +# {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {task} - 元のユーザー要求 -# {previous_response} - 前のステップの出力 +# {previous_response} - 前のムーブメントの出力 # {user_inputs} - ワークフロー中に蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") @@ -16,7 +16,7 @@ description: MAGI合議システム - 3つの観点から分析し多数決で max_iterations: 5 -steps: +movements: - name: melchior agent: ../agents/magi/melchior.md allowed_tools: diff --git a/resources/global/ja/workflows/minimal.yaml b/resources/global/ja/pieces/minimal.yaml similarity index 96% rename from resources/global/ja/workflows/minimal.yaml rename to resources/global/ja/pieces/minimal.yaml index 4cd2923..302c727 100644 --- a/resources/global/ja/workflows/minimal.yaml +++ b/resources/global/ja/pieces/minimal.yaml @@ -1,13 +1,13 @@ # Simple TAKT Workflow # Implement -> AI Review -> Supervisor Approval -# (最もシンプルな構成 - plan, architect review, fix ステップなし) +# (最もシンプルな構成 - plan, architect review, fix ムーブメントなし) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {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 step (auto-injected) +# {previous_response} - Output from the previous movement (auto-injected) # {user_inputs} - Accumulated user inputs during workflow (auto-injected) # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -16,9 +16,9 @@ description: Minimal development workflow (implement -> parallel review -> fix i max_iterations: 20 -initial_step: implement +initial_movement: implement -steps: +movements: - name: implement edit: true agent: ../agents/default/coder.md @@ -248,7 +248,7 @@ steps: - condition: 修正不要(指摘対象ファイル/仕様の確認済み) - condition: 判断できない、情報不足 instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** @@ -350,7 +350,7 @@ steps: - condition: 判断できない、情報不足 next: implement instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** diff --git a/resources/global/ja/workflows/research.yaml b/resources/global/ja/pieces/research.yaml similarity index 83% rename from resources/global/ja/workflows/research.yaml rename to resources/global/ja/pieces/research.yaml index 81c711d..d5871c3 100644 --- a/resources/global/ja/workflows/research.yaml +++ b/resources/global/ja/pieces/research.yaml @@ -7,11 +7,11 @@ # -> plan (rejected: 計画からやり直し) # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) +# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) # {max_iterations} - ワークフローの最大イテレーション数 -# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) +# {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {task} - 元のユーザー要求 -# {previous_response} - 前のステップの出力 +# {previous_response} - 前のムーブメントの出力 # {user_inputs} - ワークフロー中に蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") @@ -20,7 +20,7 @@ description: 調査ワークフロー - 質問せずに自律的に調査を実 max_iterations: 10 -steps: +movements: - name: plan agent: ../agents/research/planner.md allowed_tools: @@ -32,8 +32,8 @@ steps: instruction_template: | ## ワークフロー状況 - イテレーション: {iteration}/{max_iterations}(ワークフロー全体) - - ステップ実行回数: {step_iteration}(このステップの実行回数) - - ステップ: plan + - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数) + - ムーブメント: plan ## 調査依頼 {task} @@ -69,8 +69,8 @@ steps: instruction_template: | ## ワークフロー状況 - イテレーション: {iteration}/{max_iterations}(ワークフロー全体) - - ステップ実行回数: {step_iteration}(このステップの実行回数) - - ステップ: dig + - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数) + - ムーブメント: dig ## 元の調査依頼 {task} @@ -111,8 +111,8 @@ steps: instruction_template: | ## ワークフロー状況 - イテレーション: {iteration}/{max_iterations}(ワークフロー全体) - - ステップ実行回数: {step_iteration}(このステップの実行回数) - - ステップ: supervise (調査品質評価) + - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数) + - ムーブメント: supervise (調査品質評価) ## 元の調査依頼 {task} @@ -131,4 +131,4 @@ steps: - condition: 調査結果が不十分であり、計画からやり直す必要がある next: plan -initial_step: plan +initial_movement: plan diff --git a/resources/global/ja/workflows/review-fix-minimal.yaml b/resources/global/ja/pieces/review-fix-minimal.yaml similarity index 96% rename from resources/global/ja/workflows/review-fix-minimal.yaml rename to resources/global/ja/pieces/review-fix-minimal.yaml index 13028d7..3e17487 100644 --- a/resources/global/ja/workflows/review-fix-minimal.yaml +++ b/resources/global/ja/pieces/review-fix-minimal.yaml @@ -1,13 +1,13 @@ # Review-Fix Minimal TAKT Workflow # Review -> Fix (if needed) -> Re-review -> Complete -# (レビューから開始、実装ステップなし) +# (レビューから開始、実装ムーブメントなし) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {iteration} - Workflow-wide turn count (total movements executed across all agents) # {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {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 step (auto-injected) +# {previous_response} - Output from the previous movement (auto-injected) # {user_inputs} - Accumulated user inputs during workflow (auto-injected) # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") @@ -16,9 +16,9 @@ description: 既存コードのレビューと修正ワークフロー(レビ max_iterations: 20 -initial_step: reviewers +initial_movement: reviewers -steps: +movements: - name: implement edit: true agent: ../agents/default/coder.md @@ -248,7 +248,7 @@ steps: - condition: 修正不要(指摘対象ファイル/仕様の確認済み) - condition: 判断できない、情報不足 instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** @@ -350,7 +350,7 @@ steps: - condition: 判断できない、情報不足 next: implement instruction_template: | - **これは {step_iteration} 回目の AI Review です。** + **これは {movement_iteration} 回目の AI Review です。** 2回目以降は、前回の修正が実際には行われていなかったということです。 **あなたの「修正済み」という認識が間違っています。** diff --git a/resources/global/ja/workflows/review-only.yaml b/resources/global/ja/pieces/review-only.yaml similarity index 96% rename from resources/global/ja/workflows/review-only.yaml rename to resources/global/ja/pieces/review-only.yaml index f95d106..07eb069 100644 --- a/resources/global/ja/workflows/review-only.yaml +++ b/resources/global/ja/pieces/review-only.yaml @@ -8,14 +8,14 @@ # -> COMPLETE (ローカル: コンソール出力のみ) # -> ABORT (rejected) # -# 全ステップ edit: false(ファイル変更なし) +# 全ムーブメント edit: false(ファイル変更なし) # # テンプレート変数: # {iteration} - ワークフロー全体のターン数 # {max_iterations} - 最大イテレーション数 -# {step_iteration} - ステップごとのイテレーション数 +# {movement_iteration} - ムーブメントごとのイテレーション数 # {task} - 元のユーザー要求 -# {previous_response} - 前のステップの出力 +# {previous_response} - 前のムーブメントの出力 # {user_inputs} - 蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名 @@ -24,9 +24,9 @@ description: レビュー専用ワークフロー - コードをレビューす max_iterations: 10 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan edit: false agent: ../agents/default/planner.md @@ -108,7 +108,7 @@ steps: - condition: approved - condition: needs_fix instruction_template: | - **アーキテクチャと設計**のレビューに集中してください。AI特有の問題はレビューしないでください(ai_reviewステップで行います)。 + **アーキテクチャと設計**のレビューに集中してください。AI特有の問題はレビューしないでください(ai_reviewムーブメントで行います)。 コードをレビューしてフィードバックを提供してください。 @@ -249,7 +249,7 @@ steps: 4. ルーティング判断: - タスクにPRへのコメント投稿が含まれている場合 (例: "PRにコメントして"、"PRにレビュー結果を投稿") - → `pr-comment` ステップへ(condition: "approved, PR comment requested") + → `pr-comment` ムーブメントへ(condition: "approved, PR comment requested") - ローカルレビューのみ → COMPLETE(condition: "approved") - 重大な問題が見つかった場合 → ABORT(condition: "rejected") diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 2844311..fdb0652 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -45,7 +45,7 @@ describe('detectRuleIndex', () => { expect(detectRuleIndex('[Plan:2]', 'plan')).toBe(1); }); - it('should match step name case-insensitively', () => { + it('should match movement name case-insensitively', () => { expect(detectRuleIndex('[IMPLEMENT:1]', 'implement')).toBe(0); expect(detectRuleIndex('[REVIEW:2]', 'review')).toBe(1); }); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 1c27b5f..a2bc958 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -51,109 +51,109 @@ describe('getBuiltinWorkflow', () => { }); }); -describe('default workflow parallel reviewers step', () => { - it('should have a reviewers step with parallel sub-steps', () => { +describe('default workflow parallel reviewers movement', () => { + it('should have a reviewers movement with parallel sub-movements', () => { const workflow = getBuiltinWorkflow('default'); expect(workflow).not.toBeNull(); - const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers'); - expect(reviewersStep).toBeDefined(); - expect(reviewersStep!.parallel).toBeDefined(); - expect(reviewersStep!.parallel).toHaveLength(2); + const reviewersMovement = workflow!.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-steps', () => { + it('should have arch-review and security-review as parallel sub-movements', () => { const workflow = getBuiltinWorkflow('default'); - const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers')!; - const subStepNames = reviewersStep.parallel!.map((s) => s.name); + const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; + const subMovementNames = reviewersMovement.parallel!.map((s) => s.name); - expect(subStepNames).toContain('arch-review'); - expect(subStepNames).toContain('security-review'); + expect(subMovementNames).toContain('arch-review'); + expect(subMovementNames).toContain('security-review'); }); - it('should have aggregate conditions on the reviewers parent step', () => { + it('should have aggregate conditions on the reviewers parent movement', () => { const workflow = getBuiltinWorkflow('default'); - const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers')!; + const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; - expect(reviewersStep.rules).toBeDefined(); - expect(reviewersStep.rules).toHaveLength(2); + expect(reviewersMovement.rules).toBeDefined(); + expect(reviewersMovement.rules).toHaveLength(2); - const allRule = reviewersStep.rules!.find((r) => r.isAggregateCondition && r.aggregateType === 'all'); + const allRule = reviewersMovement.rules!.find((r) => r.isAggregateCondition && r.aggregateType === 'all'); expect(allRule).toBeDefined(); expect(allRule!.aggregateConditionText).toBe('approved'); expect(allRule!.next).toBe('supervise'); - const anyRule = reviewersStep.rules!.find((r) => r.isAggregateCondition && r.aggregateType === 'any'); + const anyRule = reviewersMovement.rules!.find((r) => r.isAggregateCondition && r.aggregateType === 'any'); expect(anyRule).toBeDefined(); expect(anyRule!.aggregateConditionText).toBe('needs_fix'); expect(anyRule!.next).toBe('fix'); }); - it('should have matching conditions on sub-steps for aggregation', () => { + it('should have matching conditions on sub-movements for aggregation', () => { const workflow = getBuiltinWorkflow('default'); - const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers')!; + const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; - for (const subStep of reviewersStep.parallel!) { - expect(subStep.rules).toBeDefined(); - const conditions = subStep.rules!.map((r) => r.condition); + for (const subMovement of reviewersMovement.parallel!) { + expect(subMovement.rules).toBeDefined(); + const conditions = subMovement.rules!.map((r) => r.condition); expect(conditions).toContain('approved'); expect(conditions).toContain('needs_fix'); } }); - it('should have ai_review transitioning to reviewers step', () => { + it('should have ai_review transitioning to reviewers movement', () => { const workflow = getBuiltinWorkflow('default'); - const aiReviewStep = workflow!.steps.find((s) => s.name === 'ai_review')!; + const aiReviewMovement = workflow!.movements.find((s) => s.name === 'ai_review')!; - const approveRule = aiReviewStep.rules!.find((r) => r.next === 'reviewers'); + const approveRule = aiReviewMovement.rules!.find((r) => r.next === 'reviewers'); expect(approveRule).toBeDefined(); }); - it('should have ai_fix transitioning to ai_review step', () => { + it('should have ai_fix transitioning to ai_review movement', () => { const workflow = getBuiltinWorkflow('default'); - const aiFixStep = workflow!.steps.find((s) => s.name === 'ai_fix')!; + const aiFixMovement = workflow!.movements.find((s) => s.name === 'ai_fix')!; - const fixedRule = aiFixStep.rules!.find((r) => r.next === 'ai_review'); + const fixedRule = aiFixMovement.rules!.find((r) => r.next === 'ai_review'); expect(fixedRule).toBeDefined(); }); - it('should have fix step transitioning back to reviewers', () => { + it('should have fix movement transitioning back to reviewers', () => { const workflow = getBuiltinWorkflow('default'); - const fixStep = workflow!.steps.find((s) => s.name === 'fix')!; + const fixMovement = workflow!.movements.find((s) => s.name === 'fix')!; - const fixedRule = fixStep.rules!.find((r) => r.next === 'reviewers'); + const fixedRule = fixMovement.rules!.find((r) => r.next === 'reviewers'); expect(fixedRule).toBeDefined(); }); - it('should not have old separate review/security_review/improve steps', () => { + it('should not have old separate review/security_review/improve movements', () => { const workflow = getBuiltinWorkflow('default'); - const stepNames = workflow!.steps.map((s) => s.name); + const movementNames = workflow!.movements.map((s) => s.name); - expect(stepNames).not.toContain('review'); - expect(stepNames).not.toContain('security_review'); - expect(stepNames).not.toContain('improve'); - expect(stepNames).not.toContain('security_fix'); + expect(movementNames).not.toContain('review'); + expect(movementNames).not.toContain('security_review'); + expect(movementNames).not.toContain('improve'); + expect(movementNames).not.toContain('security_fix'); }); - it('should have sub-steps with correct agents', () => { + it('should have sub-movements with correct agents', () => { const workflow = getBuiltinWorkflow('default'); - const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers')!; + const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; - const archReview = reviewersStep.parallel!.find((s) => s.name === 'arch-review')!; + const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!; expect(archReview.agent).toContain('architecture-reviewer'); - const secReview = reviewersStep.parallel!.find((s) => s.name === 'security-review')!; + const secReview = reviewersMovement.parallel!.find((s) => s.name === 'security-review')!; expect(secReview.agent).toContain('security-reviewer'); }); - it('should have reports configured on sub-steps', () => { + it('should have reports configured on sub-movements', () => { const workflow = getBuiltinWorkflow('default'); - const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers')!; + const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; - const archReview = reviewersStep.parallel!.find((s) => s.name === 'arch-review')!; + const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!; expect(archReview.report).toBeDefined(); - const secReview = reviewersStep.parallel!.find((s) => s.name === 'security-review')!; + const secReview = reviewersMovement.parallel!.find((s) => s.name === 'security-review')!; expect(secReview.report).toBeDefined(); }); }); @@ -180,7 +180,7 @@ describe('loadAllWorkflows', () => { name: test-workflow description: Test workflow max_iterations: 10 -steps: +movements: - name: step1 agent: coder instruction: "{task}" diff --git a/src/__tests__/engine-abort.test.ts b/src/__tests__/engine-abort.test.ts index 9c1a3a8..edf2f99 100644 --- a/src/__tests__/engine-abort.test.ts +++ b/src/__tests__/engine-abort.test.ts @@ -3,7 +3,7 @@ * * Covers: * - abort() sets state to aborted and emits workflow:abort - * - abort() during step execution interrupts the step + * - abort() during movement execution interrupts the movement * - isAbortRequested() reflects abort state * - Double abort() is idempotent */ @@ -39,7 +39,7 @@ import { WorkflowEngine } from '../core/workflow/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, - makeStep, + makeMovement, makeRule, mockRunAgentSequence, mockDetectMatchedRuleSequence, @@ -66,15 +66,15 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { return { name: 'test', maxIterations: 10, - initialStep: 'step1', - steps: [ - makeStep('step1', { + initialMovement: 'step1', + movements: [ + makeMovement('step1', { rules: [ makeRule('done', 'step2'), makeRule('fail', 'ABORT'), ], }), - makeStep('step2', { + makeMovement('step2', { rules: [ makeRule('done', 'COMPLETE'), ], @@ -84,7 +84,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { } describe('abort() before run loop iteration', () => { - it('should abort immediately when abort() called before step execution', async () => { + it('should abort immediately when abort() called before movement execution', async () => { const config = makeSimpleConfig(); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); @@ -100,17 +100,17 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { expect(state.status).toBe('aborted'); expect(abortFn).toHaveBeenCalledOnce(); expect(abortFn.mock.calls[0][1]).toContain('SIGINT'); - // runAgent should never be called since abort was requested before first step + // runAgent should never be called since abort was requested before first movement expect(vi.mocked(runAgent)).not.toHaveBeenCalled(); }); }); - describe('abort() during step execution', () => { + 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 }); - // Simulate abort during step execution: runAgent rejects after abort() is called + // Simulate abort during movement execution: runAgent rejects after abort() is called vi.mocked(runAgent).mockImplementation(async () => { engine.abort(); throw new Error('Query interrupted'); @@ -158,14 +158,14 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { }); }); - describe('abort between steps', () => { - it('should stop after completing current step when abort() is called', async () => { + 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 }); - // First step completes normally, but abort is called during it + // First movement completes normally, but abort is called during it vi.mocked(runAgent).mockImplementation(async () => { - // Simulate abort during execution (but the step itself completes) + // Simulate abort during execution (but the movement itself completes) engine.abort(); return makeResponse({ agent: 'step1', content: 'Step 1 done' }); }); @@ -181,7 +181,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { expect(state.status).toBe('aborted'); expect(state.iteration).toBe(1); - // Only step1 runs; step2 should not start because abort is checked at loop top + // Only step1 runs; step2 should not start because abort is checked at loop top (movement names kept as step1/step2 for simplicity) expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(1); expect(abortFn).toHaveBeenCalledOnce(); }); diff --git a/src/__tests__/engine-agent-overrides.test.ts b/src/__tests__/engine-agent-overrides.test.ts index 9805540..2982d26 100644 --- a/src/__tests__/engine-agent-overrides.test.ts +++ b/src/__tests__/engine-agent-overrides.test.ts @@ -1,8 +1,8 @@ /** * Tests for WorkflowEngine provider/model overrides. * - * Verifies that CLI-specified overrides take precedence over workflow step defaults, - * and that step-specific values are used when no overrides are present. + * Verifies that CLI-specified overrides take precedence over workflow movement defaults, + * and that movement-specific values are used when no overrides are present. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; @@ -32,7 +32,7 @@ import type { WorkflowConfig } from '../core/models/index.js'; import { makeResponse, makeRule, - makeStep, + makeMovement, mockRunAgentSequence, mockDetectMatchedRuleSequence, applyDefaultMocks, @@ -44,21 +44,21 @@ describe('WorkflowEngine agent overrides', () => { applyDefaultMocks(); }); - it('respects workflow step provider/model even when CLI overrides are provided', async () => { - const step = makeStep('plan', { + it('respects workflow movement provider/model even when CLI overrides are provided', async () => { + const movement = makeMovement('plan', { provider: 'claude', - model: 'claude-step', + model: 'claude-movement', rules: [makeRule('done', 'COMPLETE')], }); const config: WorkflowConfig = { name: 'override-test', - steps: [step], - initialStep: 'plan', + movements: [movement], + initialMovement: 'plan', maxIterations: 1, }; mockRunAgentSequence([ - makeResponse({ agent: step.agent, content: 'done' }), + makeResponse({ agent: movement.agent, content: 'done' }), ]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); @@ -72,22 +72,22 @@ describe('WorkflowEngine agent overrides', () => { const options = vi.mocked(runAgent).mock.calls[0][2]; expect(options.provider).toBe('claude'); - expect(options.model).toBe('claude-step'); + expect(options.model).toBe('claude-movement'); }); - it('allows CLI overrides when workflow step leaves provider/model undefined', async () => { - const step = makeStep('plan', { + it('allows CLI overrides when workflow movement leaves provider/model undefined', async () => { + const movement = makeMovement('plan', { rules: [makeRule('done', 'COMPLETE')], }); const config: WorkflowConfig = { name: 'override-fallback', - steps: [step], - initialStep: 'plan', + movements: [movement], + initialMovement: 'plan', maxIterations: 1, }; mockRunAgentSequence([ - makeResponse({ agent: step.agent, content: 'done' }), + makeResponse({ agent: movement.agent, content: 'done' }), ]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); @@ -104,29 +104,29 @@ describe('WorkflowEngine agent overrides', () => { expect(options.model).toBe('cli-model'); }); - it('falls back to workflow step provider/model when no overrides supplied', async () => { - const step = makeStep('plan', { + it('falls back to workflow movement provider/model when no overrides supplied', async () => { + const movement = makeMovement('plan', { provider: 'claude', - model: 'step-model', + model: 'movement-model', rules: [makeRule('done', 'COMPLETE')], }); const config: WorkflowConfig = { - name: 'step-defaults', - steps: [step], - initialStep: 'plan', + name: 'movement-defaults', + movements: [movement], + initialMovement: 'plan', maxIterations: 1, }; mockRunAgentSequence([ - makeResponse({ agent: step.agent, content: 'done' }), + makeResponse({ agent: movement.agent, content: 'done' }), ]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); - const engine = new WorkflowEngine(config, '/tmp/project', 'step task', { projectCwd: '/tmp/project' }); + const engine = new WorkflowEngine(config, '/tmp/project', 'movement task', { projectCwd: '/tmp/project' }); await engine.run(); const options = vi.mocked(runAgent).mock.calls[0][2]; expect(options.provider).toBe('claude'); - expect(options.model).toBe('step-model'); + expect(options.model).toBe('movement-model'); }); }); diff --git a/src/__tests__/engine-blocked.test.ts b/src/__tests__/engine-blocked.test.ts index 2ce408c..fe3f082 100644 --- a/src/__tests__/engine-blocked.test.ts +++ b/src/__tests__/engine-blocked.test.ts @@ -72,7 +72,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => { const blockedFn = vi.fn(); const abortFn = vi.fn(); - engine.on('step:blocked', blockedFn); + engine.on('movement:blocked', blockedFn); engine.on('workflow:abort', abortFn); const state = await engine.run(); @@ -132,7 +132,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => { ]); const userInputFn = vi.fn(); - engine.on('step:user_input', userInputFn); + engine.on('movement:user_input', userInputFn); const state = await engine.run(); diff --git a/src/__tests__/engine-error.test.ts b/src/__tests__/engine-error.test.ts index a67618e..ed027bc 100644 --- a/src/__tests__/engine-error.test.ts +++ b/src/__tests__/engine-error.test.ts @@ -39,7 +39,7 @@ import { runAgent } from '../agents/runner.js'; import { detectMatchedRule } from '../core/workflow/index.js'; import { makeResponse, - makeStep, + makeMovement, makeRule, buildDefaultWorkflowConfig, mockRunAgentSequence, @@ -119,9 +119,9 @@ describe('WorkflowEngine Integration: Error Handling', () => { const config = buildDefaultWorkflowConfig({ maxIterations: 100, loopDetection: { maxConsecutiveSameStep: 3, action: 'abort' }, - initialStep: 'loop-step', - steps: [ - makeStep('loop-step', { + initialMovement: 'loop-step', + movements: [ + makeMovement('loop-step', { rules: [makeRule('continue', 'loop-step')], }), ], diff --git a/src/__tests__/engine-happy-path.test.ts b/src/__tests__/engine-happy-path.test.ts index 3519ccf..f6a2dc3 100644 --- a/src/__tests__/engine-happy-path.test.ts +++ b/src/__tests__/engine-happy-path.test.ts @@ -7,13 +7,13 @@ * - AI review reject and fix * - ABORT transition * - Event emissions - * - Step output tracking + * - Movement output tracking * - Config validation */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { existsSync, rmSync } from 'node:fs'; -import type { WorkflowConfig, WorkflowStep } from '../core/models/index.js'; +import type { WorkflowConfig, WorkflowMovement } from '../core/models/index.js'; // --- Mock setup (must be before imports that use these modules) --- @@ -42,7 +42,7 @@ import { WorkflowEngine } from '../core/workflow/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, - makeStep, + makeMovement, makeRule, buildDefaultWorkflowConfig, mockRunAgentSequence, @@ -101,7 +101,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { expect(state.status).toBe('completed'); expect(state.iteration).toBe(5); // plan, implement, ai_review, reviewers, supervise expect(completeFn).toHaveBeenCalledOnce(); - expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(6); // 4 normal + 2 parallel sub-steps + expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(6); // 4 normal + 2 parallel sub-movements }); }); @@ -192,7 +192,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { // 4. ABORT transition // ===================================================== describe('ABORT transition', () => { - it('should abort when step transitions to ABORT', async () => { + it('should abort when movement transitions to ABORT', async () => { const config = buildDefaultWorkflowConfig(); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); @@ -219,7 +219,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { // 5. Event emissions // ===================================================== describe('Event emissions', () => { - it('should emit step:start and step:complete for each step', async () => { + 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 }); @@ -244,26 +244,26 @@ describe('WorkflowEngine Integration: Happy Path', () => { const startFn = vi.fn(); const completeFn = vi.fn(); - engine.on('step:start', startFn); - engine.on('step:complete', completeFn); + engine.on('movement:start', startFn); + engine.on('movement:complete', completeFn); await engine.run(); - // 5 steps: plan, implement, ai_review, reviewers, supervise + // 5 movements: plan, implement, ai_review, reviewers, supervise expect(startFn).toHaveBeenCalledTimes(5); expect(completeFn).toHaveBeenCalledTimes(5); - const startedSteps = startFn.mock.calls.map(call => (call[0] as WorkflowStep).name); - expect(startedSteps).toEqual(['plan', 'implement', 'ai_review', 'reviewers', 'supervise']); + const startedMovements = startFn.mock.calls.map(call => (call[0] as WorkflowMovement).name); + expect(startedMovements).toEqual(['plan', 'implement', 'ai_review', 'reviewers', 'supervise']); }); - it('should pass instruction to step:start for normal steps', async () => { + it('should pass instruction to movement:start for normal movements', async () => { const simpleConfig: WorkflowConfig = { name: 'test', maxIterations: 10, - initialStep: 'plan', - steps: [ - makeStep('plan', { + initialMovement: 'plan', + movements: [ + makeMovement('plan', { rules: [makeRule('done', 'COMPLETE')], }), ], @@ -278,18 +278,18 @@ describe('WorkflowEngine Integration: Happy Path', () => { ]); const startFn = vi.fn(); - engine.on('step:start', startFn); + engine.on('movement:start', startFn); await engine.run(); expect(startFn).toHaveBeenCalledTimes(1); - // step:start should receive (step, iteration, instruction) - const [_step, _iteration, instruction] = startFn.mock.calls[0]; + // movement:start should receive (movement, iteration, instruction) + const [_movement, _iteration, instruction] = startFn.mock.calls[0]; expect(typeof instruction).toBe('string'); expect(instruction.length).toBeGreaterThan(0); }); - it('should pass empty instruction to step:start for parallel steps', async () => { + 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 }); @@ -313,16 +313,16 @@ describe('WorkflowEngine Integration: Happy Path', () => { ]); const startFn = vi.fn(); - engine.on('step:start', startFn); + engine.on('movement:start', startFn); await engine.run(); - // Find the "reviewers" step:start call (parallel step) + // Find the "reviewers" movement:start call (parallel movement) const reviewersCall = startFn.mock.calls.find( - (call) => (call[0] as WorkflowStep).name === 'reviewers' + (call) => (call[0] as WorkflowMovement).name === 'reviewers' ); expect(reviewersCall).toBeDefined(); - // Parallel steps emit empty string for instruction + // Parallel movements emit empty string for instruction const [, , instruction] = reviewersCall!; expect(instruction).toBe(''); }); @@ -348,10 +348,10 @@ describe('WorkflowEngine Integration: Happy Path', () => { }); // ===================================================== - // 6. Step output tracking + // 6. Movement output tracking // ===================================================== - describe('Step output tracking', () => { - it('should store outputs for all executed steps', async () => { + 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 }); @@ -376,10 +376,10 @@ describe('WorkflowEngine Integration: Happy Path', () => { const state = await engine.run(); - expect(state.stepOutputs.get('plan')!.content).toBe('Plan output'); - expect(state.stepOutputs.get('implement')!.content).toBe('Implement output'); - expect(state.stepOutputs.get('ai_review')!.content).toBe('AI review output'); - expect(state.stepOutputs.get('supervise')!.content).toBe('Supervise output'); + expect(state.movementOutputs.get('plan')!.content).toBe('Plan output'); + expect(state.movementOutputs.get('implement')!.content).toBe('Implement output'); + expect(state.movementOutputs.get('ai_review')!.content).toBe('AI review output'); + expect(state.movementOutputs.get('supervise')!.content).toBe('Supervise output'); }); }); @@ -391,9 +391,9 @@ describe('WorkflowEngine Integration: Happy Path', () => { const simpleConfig: WorkflowConfig = { name: 'test', maxIterations: 10, - initialStep: 'plan', - steps: [ - makeStep('plan', { + initialMovement: 'plan', + movements: [ + makeMovement('plan', { rules: [makeRule('done', 'COMPLETE')], }), ], @@ -424,7 +424,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { ); }); - it('should emit phase events for all steps in happy path', async () => { + 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 }); @@ -454,7 +454,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { await engine.run(); - // 4 normal steps + 2 parallel sub-steps = 6 Phase 1 invocations + // 4 normal movements + 2 parallel sub-movements = 6 Phase 1 invocations expect(phaseStartFn).toHaveBeenCalledTimes(6); expect(phaseCompleteFn).toHaveBeenCalledTimes(6); @@ -470,21 +470,21 @@ describe('WorkflowEngine Integration: Happy Path', () => { // 8. Config validation // ===================================================== describe('Config validation', () => { - it('should throw when initial step does not exist', () => { - const config = buildDefaultWorkflowConfig({ initialStep: 'nonexistent' }); + it('should throw when initial movement does not exist', () => { + const config = buildDefaultWorkflowConfig({ initialMovement: 'nonexistent' }); expect(() => { new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); - }).toThrow('Unknown step: nonexistent'); + }).toThrow('Unknown movement: nonexistent'); }); - it('should throw when rule references nonexistent step', () => { + it('should throw when rule references nonexistent movement', () => { const config: WorkflowConfig = { name: 'test', maxIterations: 10, - initialStep: 'step1', - steps: [ - makeStep('step1', { + initialMovement: 'step1', + movements: [ + makeMovement('step1', { rules: [makeRule('done', 'nonexistent_step')], }), ], diff --git a/src/__tests__/engine-parallel.test.ts b/src/__tests__/engine-parallel.test.ts index 6db2b12..151c403 100644 --- a/src/__tests__/engine-parallel.test.ts +++ b/src/__tests__/engine-parallel.test.ts @@ -1,10 +1,10 @@ /** - * WorkflowEngine integration tests: parallel step aggregation. + * WorkflowEngine integration tests: parallel movement aggregation. * * Covers: * - Aggregated output format (## headers and --- separators) - * - Individual sub-step output storage - * - Concurrent execution of sub-steps + * - Individual sub-movement output storage + * - Concurrent execution of sub-movements */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -44,7 +44,7 @@ import { applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { +describe('WorkflowEngine Integration: Parallel Movement Aggregation', () => { let tmpDir: string; beforeEach(() => { @@ -59,7 +59,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { } }); - it('should aggregate sub-step outputs with ## headers and --- separators', async () => { + it('should aggregate sub-movement outputs with ## headers and --- separators', async () => { const config = buildDefaultWorkflowConfig(); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); @@ -86,7 +86,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { expect(state.status).toBe('completed'); - const reviewersOutput = state.stepOutputs.get('reviewers'); + const reviewersOutput = state.movementOutputs.get('reviewers'); expect(reviewersOutput).toBeDefined(); expect(reviewersOutput!.content).toContain('## arch-review'); expect(reviewersOutput!.content).toContain('Architecture review content'); @@ -96,7 +96,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { expect(reviewersOutput!.matchedRuleMethod).toBe('aggregate'); }); - it('should store individual sub-step outputs in stepOutputs', async () => { + it('should store individual sub-movement outputs in movementOutputs', async () => { const config = buildDefaultWorkflowConfig(); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); @@ -121,14 +121,14 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { const state = await engine.run(); - expect(state.stepOutputs.has('arch-review')).toBe(true); - expect(state.stepOutputs.has('security-review')).toBe(true); - expect(state.stepOutputs.has('reviewers')).toBe(true); - expect(state.stepOutputs.get('arch-review')!.content).toBe('Arch content'); - expect(state.stepOutputs.get('security-review')!.content).toBe('Sec content'); + expect(state.movementOutputs.has('arch-review')).toBe(true); + expect(state.movementOutputs.has('security-review')).toBe(true); + expect(state.movementOutputs.has('reviewers')).toBe(true); + expect(state.movementOutputs.get('arch-review')!.content).toBe('Arch content'); + expect(state.movementOutputs.get('security-review')!.content).toBe('Sec content'); }); - it('should execute sub-steps concurrently (both runAgent calls happen)', async () => { + it('should execute sub-movements concurrently (both runAgent calls happen)', async () => { const config = buildDefaultWorkflowConfig(); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); @@ -153,7 +153,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { await engine.run(); - // 6 total: 4 normal + 2 parallel sub-steps + // 6 total: 4 normal + 2 parallel sub-movements expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(6); const calledAgents = vi.mocked(runAgent).mock.calls.map(call => call[0]); diff --git a/src/__tests__/engine-report.test.ts b/src/__tests__/engine-report.test.ts index ab73825..0d5722b 100644 --- a/src/__tests__/engine-report.test.ts +++ b/src/__tests__/engine-report.test.ts @@ -1,5 +1,5 @@ /** - * Tests for engine report event emission (step:report) + * Tests for engine report event emission (movement:report) */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -9,50 +9,50 @@ import { tmpdir } from 'node:os'; import { EventEmitter } from 'node:events'; import { existsSync } from 'node:fs'; import { isReportObjectConfig } from '../core/workflow/index.js'; -import type { WorkflowStep, ReportObjectConfig, ReportConfig } from '../core/models/index.js'; +import type { WorkflowMovement, ReportObjectConfig, ReportConfig } from '../core/models/index.js'; /** - * Extracted emitStepReports logic for unit testing. - * Mirrors engine.ts emitStepReports + emitIfReportExists. + * Extracted emitMovementReports logic for unit testing. + * Mirrors engine.ts emitMovementReports + emitIfReportExists. * * reportDir already includes the `.takt/reports/` prefix (set by engine constructor). */ -function emitStepReports( +function emitMovementReports( emitter: EventEmitter, - step: WorkflowStep, + movement: WorkflowMovement, reportDir: string, projectCwd: string, ): void { - if (!step.report || !reportDir) return; + if (!movement.report || !reportDir) return; const baseDir = join(projectCwd, reportDir); - if (typeof step.report === 'string') { - emitIfReportExists(emitter, step, baseDir, step.report); - } else if (isReportObjectConfig(step.report)) { - emitIfReportExists(emitter, step, baseDir, step.report.name); + if (typeof movement.report === 'string') { + emitIfReportExists(emitter, movement, baseDir, movement.report); + } else if (isReportObjectConfig(movement.report)) { + emitIfReportExists(emitter, movement, baseDir, movement.report.name); } else { - for (const rc of step.report) { - emitIfReportExists(emitter, step, baseDir, rc.path); + for (const rc of movement.report) { + emitIfReportExists(emitter, movement, baseDir, rc.path); } } } function emitIfReportExists( emitter: EventEmitter, - step: WorkflowStep, + movement: WorkflowMovement, baseDir: string, fileName: string, ): void { const filePath = join(baseDir, fileName); if (existsSync(filePath)) { - emitter.emit('step:report', step, filePath, fileName); + emitter.emit('movement:report', movement, filePath, fileName); } } -/** Create a minimal WorkflowStep for testing */ -function createStep(overrides: Partial = {}): WorkflowStep { +/** Create a minimal WorkflowMovement for testing */ +function createMovement(overrides: Partial = {}): WorkflowMovement { return { - name: 'test-step', + name: 'test-movement', agent: 'coder', agentDisplayName: 'Coder', instructionTemplate: '', @@ -61,7 +61,7 @@ function createStep(overrides: Partial = {}): WorkflowStep { }; } -describe('emitStepReports', () => { +describe('emitMovementReports', () => { let tmpDir: string; let reportBaseDir: string; // reportDir now includes .takt/reports/ prefix (matches engine constructor behavior) @@ -77,100 +77,100 @@ describe('emitStepReports', () => { rmSync(tmpDir, { recursive: true, force: true }); }); - it('should emit step:report when string report file exists', () => { - // Given: a step with string report and the file exists - const step = createStep({ report: 'plan.md' }); + it('should emit movement:report when string report file exists', () => { + // Given: a movement with string report and the file exists + const movement = createMovement({ report: 'plan.md' }); writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8'); const emitter = new EventEmitter(); const handler = vi.fn(); - emitter.on('step:report', handler); + emitter.on('movement:report', handler); // When - emitStepReports(emitter, step, reportDirName, tmpDir); + emitMovementReports(emitter, movement, reportDirName, tmpDir); // Then expect(handler).toHaveBeenCalledOnce(); - expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, 'plan.md'), 'plan.md'); + expect(handler).toHaveBeenCalledWith(movement, join(reportBaseDir, 'plan.md'), 'plan.md'); }); it('should not emit when string report file does not exist', () => { - // Given: a step with string report but file doesn't exist - const step = createStep({ report: 'missing.md' }); + // Given: a movement with string report but file doesn't exist + const movement = createMovement({ report: 'missing.md' }); const emitter = new EventEmitter(); const handler = vi.fn(); - emitter.on('step:report', handler); + emitter.on('movement:report', handler); // When - emitStepReports(emitter, step, reportDirName, tmpDir); + emitMovementReports(emitter, movement, reportDirName, tmpDir); // Then expect(handler).not.toHaveBeenCalled(); }); - it('should emit step:report when ReportObjectConfig report file exists', () => { - // Given: a step with ReportObjectConfig and the file exists + it('should emit movement:report when ReportObjectConfig report file exists', () => { + // Given: a movement with ReportObjectConfig and the file exists const report: ReportObjectConfig = { name: '03-review.md', format: '# Review' }; - const step = createStep({ report }); + const movement = createMovement({ report }); writeFileSync(join(reportBaseDir, '03-review.md'), '# Review\nOK', 'utf-8'); const emitter = new EventEmitter(); const handler = vi.fn(); - emitter.on('step:report', handler); + emitter.on('movement:report', handler); // When - emitStepReports(emitter, step, reportDirName, tmpDir); + emitMovementReports(emitter, movement, reportDirName, tmpDir); // Then expect(handler).toHaveBeenCalledOnce(); - expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '03-review.md'), '03-review.md'); + expect(handler).toHaveBeenCalledWith(movement, join(reportBaseDir, '03-review.md'), '03-review.md'); }); it('should emit for each existing file in ReportConfig[] array', () => { - // Given: a step with array report, two files exist, one missing + // Given: a movement with array report, two files exist, one missing const report: ReportConfig[] = [ { label: 'Scope', path: '01-scope.md' }, { label: 'Decisions', path: '02-decisions.md' }, { label: 'Missing', path: '03-missing.md' }, ]; - const step = createStep({ report }); + const movement = createMovement({ report }); writeFileSync(join(reportBaseDir, '01-scope.md'), '# Scope', 'utf-8'); writeFileSync(join(reportBaseDir, '02-decisions.md'), '# Decisions', 'utf-8'); const emitter = new EventEmitter(); const handler = vi.fn(); - emitter.on('step:report', handler); + emitter.on('movement:report', handler); // When - emitStepReports(emitter, step, reportDirName, tmpDir); + emitMovementReports(emitter, movement, reportDirName, tmpDir); // Then: emitted for scope and decisions, not for missing expect(handler).toHaveBeenCalledTimes(2); - expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '01-scope.md'), '01-scope.md'); - expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '02-decisions.md'), '02-decisions.md'); + expect(handler).toHaveBeenCalledWith(movement, join(reportBaseDir, '01-scope.md'), '01-scope.md'); + expect(handler).toHaveBeenCalledWith(movement, join(reportBaseDir, '02-decisions.md'), '02-decisions.md'); }); - it('should not emit when step has no report', () => { - // Given: a step without report - const step = createStep({ report: undefined }); + it('should not emit when movement has no report', () => { + // Given: a movement without report + const movement = createMovement({ report: undefined }); const emitter = new EventEmitter(); const handler = vi.fn(); - emitter.on('step:report', handler); + emitter.on('movement:report', handler); // When - emitStepReports(emitter, step, reportDirName, tmpDir); + emitMovementReports(emitter, movement, reportDirName, tmpDir); // Then expect(handler).not.toHaveBeenCalled(); }); it('should not emit when reportDir is empty', () => { - // Given: a step with report but empty reportDir - const step = createStep({ report: 'plan.md' }); + // Given: a movement with report but empty reportDir + const movement = createMovement({ report: 'plan.md' }); writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8'); const emitter = new EventEmitter(); const handler = vi.fn(); - emitter.on('step:report', handler); + emitter.on('movement:report', handler); // When: empty reportDir - emitStepReports(emitter, step, '', tmpDir); + emitMovementReports(emitter, movement, '', tmpDir); // Then expect(handler).not.toHaveBeenCalled(); diff --git a/src/__tests__/engine-test-helpers.ts b/src/__tests__/engine-test-helpers.ts index 99f0711..470523b 100644 --- a/src/__tests__/engine-test-helpers.ts +++ b/src/__tests__/engine-test-helpers.ts @@ -10,7 +10,7 @@ import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; -import type { WorkflowConfig, WorkflowStep, AgentResponse, WorkflowRule } from '../core/models/index.js'; +import type { WorkflowConfig, WorkflowMovement, AgentResponse, WorkflowRule } from '../core/models/index.js'; // --- Mock imports (consumers must call vi.mock before importing this) --- @@ -37,7 +37,7 @@ export function makeRule(condition: string, next: string, extra: Partial = {}): WorkflowStep { +export function makeMovement(name: string, overrides: Partial = {}): WorkflowMovement { return { name, agent: `../agents/${name}.md`, @@ -53,14 +53,14 @@ export function makeStep(name: string, overrides: Partial = {}): W * plan → implement → ai_review → (ai_fix↔) → reviewers(parallel) → (fix↔) → supervise */ export function buildDefaultWorkflowConfig(overrides: Partial = {}): WorkflowConfig { - const archReviewSubStep = makeStep('arch-review', { + const archReviewSubMovement = makeMovement('arch-review', { rules: [ makeRule('approved', 'COMPLETE'), makeRule('needs_fix', 'fix'), ], }); - const securityReviewSubStep = makeStep('security-review', { + const securityReviewSubMovement = makeMovement('security-review', { rules: [ makeRule('approved', 'COMPLETE'), makeRule('needs_fix', 'fix'), @@ -71,34 +71,34 @@ export function buildDefaultWorkflowConfig(overrides: Partial = name: 'test-default', description: 'Test workflow', maxIterations: 30, - initialStep: 'plan', - steps: [ - makeStep('plan', { + initialMovement: 'plan', + movements: [ + makeMovement('plan', { rules: [ makeRule('Requirements are clear', 'implement'), makeRule('Requirements unclear', 'ABORT'), ], }), - makeStep('implement', { + makeMovement('implement', { rules: [ makeRule('Implementation complete', 'ai_review'), makeRule('Cannot proceed', 'plan'), ], }), - makeStep('ai_review', { + makeMovement('ai_review', { rules: [ makeRule('No AI-specific issues', 'reviewers'), makeRule('AI-specific issues found', 'ai_fix'), ], }), - makeStep('ai_fix', { + makeMovement('ai_fix', { rules: [ makeRule('AI issues fixed', 'reviewers'), makeRule('Cannot proceed', 'plan'), ], }), - makeStep('reviewers', { - parallel: [archReviewSubStep, securityReviewSubStep], + makeMovement('reviewers', { + parallel: [archReviewSubMovement, securityReviewSubMovement], rules: [ makeRule('all("approved")', 'supervise', { isAggregateCondition: true, @@ -112,13 +112,13 @@ export function buildDefaultWorkflowConfig(overrides: Partial = }), ], }), - makeStep('fix', { + makeMovement('fix', { rules: [ makeRule('Fix complete', 'reviewers'), makeRule('Cannot proceed', 'plan'), ], }), - makeStep('supervise', { + makeMovement('supervise', { rules: [ makeRule('All checks passed', 'COMPLETE'), makeRule('Requirements unmet', 'plan'), diff --git a/src/__tests__/engine-worktree-report.test.ts b/src/__tests__/engine-worktree-report.test.ts index f83cba1..eba2ba2 100644 --- a/src/__tests__/engine-worktree-report.test.ts +++ b/src/__tests__/engine-worktree-report.test.ts @@ -38,7 +38,7 @@ import { WorkflowEngine } from '../core/workflow/index.js'; import { runReportPhase } from '../core/workflow/index.js'; import { makeResponse, - makeStep, + makeMovement, makeRule, mockRunAgentSequence, mockDetectMatchedRuleSequence, @@ -69,9 +69,9 @@ function buildSimpleConfig(): WorkflowConfig { name: 'worktree-test', description: 'Test workflow for worktree', maxIterations: 10, - initialStep: 'review', - steps: [ - makeStep('review', { + initialMovement: 'review', + movements: [ + makeMovement('review', { report: '00-review.md', rules: [ makeRule('approved', 'COMPLETE'), @@ -132,14 +132,14 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { }); it('should pass projectCwd-based reportDir to buildInstruction (used by {report_dir} placeholder)', async () => { - // Given: worktree environment with a step that uses {report_dir} in template + // Given: worktree environment with a movement that uses {report_dir} in template const config: WorkflowConfig = { name: 'worktree-test', description: 'Test', maxIterations: 10, - initialStep: 'review', - steps: [ - makeStep('review', { + initialMovement: 'review', + movements: [ + makeMovement('review', { instructionTemplate: 'Write report to {report_dir}', report: '00-review.md', rules: [ diff --git a/src/__tests__/i18n.test.ts b/src/__tests__/i18n.test.ts index f317efc..e668e4b 100644 --- a/src/__tests__/i18n.test.ts +++ b/src/__tests__/i18n.test.ts @@ -102,7 +102,7 @@ describe('label integrity', () => { it('contains all expected workflow keys in en', () => { expect(() => getLabel('workflow.iterationLimit.maxReached')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.currentStep')).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(); diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 8879c0e..a9758bf 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -8,9 +8,7 @@ import { isReportObjectConfig, ReportInstructionBuilder, StatusJudgmentBuilder, - buildExecutionMetadata, - renderExecutionMetadata, - generateStatusRulesFromRules, + generateStatusRulesComponents, type ReportInstructionContext, type StatusJudgmentContext, type InstructionContext, @@ -44,8 +42,9 @@ function createMinimalContext(overrides: Partial = {}): Inst task: 'Test task', iteration: 1, maxIterations: 10, - stepIteration: 1, + movementIteration: 1, cwd: '/project', + projectCwd: '/project', userInputs: [], ...overrides, }; @@ -186,127 +185,7 @@ describe('instruction-builder', () => { }); }); - describe('buildExecutionMetadata', () => { - it('should set workingDirectory', () => { - const context = createMinimalContext({ cwd: '/project' }); - const metadata = buildExecutionMetadata(context); - - expect(metadata.workingDirectory).toBe('/project'); - }); - - it('should use cwd as workingDirectory even in worktree mode', () => { - const context = createMinimalContext({ - cwd: '/worktree-path', - projectCwd: '/project-path', - }); - const metadata = buildExecutionMetadata(context); - - expect(metadata.workingDirectory).toBe('/worktree-path'); - }); - - it('should default language to en when not specified', () => { - const context = createMinimalContext({ cwd: '/project' }); - const metadata = buildExecutionMetadata(context); - - expect(metadata.language).toBe('en'); - }); - - it('should propagate language from context', () => { - const context = createMinimalContext({ cwd: '/project', language: 'ja' }); - const metadata = buildExecutionMetadata(context); - - expect(metadata.language).toBe('ja'); - }); - - it('should propagate edit field when provided', () => { - const context = createMinimalContext({ cwd: '/project' }); - - expect(buildExecutionMetadata(context, true).edit).toBe(true); - expect(buildExecutionMetadata(context, false).edit).toBe(false); - }); - - it('should leave edit undefined when not provided', () => { - const context = createMinimalContext({ cwd: '/project' }); - const metadata = buildExecutionMetadata(context); - - expect(metadata.edit).toBeUndefined(); - }); - }); - - describe('renderExecutionMetadata', () => { - it('should render Working Directory and Execution Rules', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en' }); - - expect(rendered).toContain('## Execution Context'); - expect(rendered).toContain('- Working Directory: /project'); - expect(rendered).toContain('## Execution Rules'); - expect(rendered).toContain('Do NOT run git commit'); - expect(rendered).toContain('Do NOT use `cd`'); - }); - - it('should end with a trailing empty line', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en' }); - - expect(rendered).toMatch(/\n$/); - }); - - it('should render in Japanese when language is ja', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'ja' }); - - expect(rendered).toContain('## 実行コンテキスト'); - expect(rendered).toContain('- 作業ディレクトリ: /project'); - expect(rendered).toContain('## 実行ルール'); - expect(rendered).toContain('git commit を実行しないでください'); - expect(rendered).toContain('cd` を使用しないでください'); - }); - - it('should include English note only for en, not for ja', () => { - const enRendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en' }); - const jaRendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'ja' }); - - expect(enRendered).toContain('Note:'); - expect(jaRendered).not.toContain('Note:'); - }); - - it('should include edit enabled prompt when edit is true (en)', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en', edit: true }); - - expect(rendered).toContain('Editing is ENABLED'); - expect(rendered).not.toContain('Editing is DISABLED'); - }); - - it('should include edit disabled prompt when edit is false (en)', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en', edit: false }); - - expect(rendered).toContain('Editing is DISABLED'); - expect(rendered).not.toContain('Editing is ENABLED'); - }); - - it('should not include edit prompt when edit is undefined', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en' }); - - expect(rendered).not.toContain('Editing is ENABLED'); - expect(rendered).not.toContain('Editing is DISABLED'); - expect(rendered).not.toContain('編集が許可'); - expect(rendered).not.toContain('編集が禁止'); - }); - - it('should render edit enabled prompt in Japanese when language is ja', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'ja', edit: true }); - - expect(rendered).toContain('編集が許可されています'); - expect(rendered).not.toContain('編集が禁止'); - }); - - it('should render edit disabled prompt in Japanese when language is ja', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'ja', edit: false }); - - expect(rendered).toContain('編集が禁止されています'); - expect(rendered).not.toContain('編集が許可'); - }); - }); - - describe('generateStatusRulesFromRules', () => { + describe('generateStatusRulesComponents', () => { const rules: WorkflowRule[] = [ { condition: '要件が明確で実装可能', next: 'implement' }, { condition: 'ユーザーが質問をしている', next: 'COMPLETE' }, @@ -314,12 +193,11 @@ describe('instruction-builder', () => { ]; it('should generate criteria table with numbered tags (ja)', () => { - const result = generateStatusRulesFromRules('plan', rules, 'ja'); + const result = generateStatusRulesComponents('plan', rules, 'ja'); - expect(result).toContain('## 判定基準'); - expect(result).toContain('| 1 | 要件が明確で実装可能 | `[PLAN:1]` |'); - expect(result).toContain('| 2 | ユーザーが質問をしている | `[PLAN:2]` |'); - expect(result).toContain('| 3 | 要件が不明確、情報不足 | `[PLAN:3]` |'); + expect(result.criteriaTable).toContain('| 1 | 要件が明確で実装可能 | `[PLAN:1]` |'); + expect(result.criteriaTable).toContain('| 2 | ユーザーが質問をしている | `[PLAN:2]` |'); + expect(result.criteriaTable).toContain('| 3 | 要件が不明確、情報不足 | `[PLAN:3]` |'); }); it('should generate criteria table with numbered tags (en)', () => { @@ -327,47 +205,46 @@ describe('instruction-builder', () => { { condition: 'Requirements are clear', next: 'implement' }, { condition: 'User is asking a question', next: 'COMPLETE' }, ]; - const result = generateStatusRulesFromRules('plan', enRules, 'en'); + const result = generateStatusRulesComponents('plan', enRules, 'en'); - expect(result).toContain('## Decision Criteria'); - expect(result).toContain('| 1 | Requirements are clear | `[PLAN:1]` |'); - expect(result).toContain('| 2 | User is asking a question | `[PLAN:2]` |'); + expect(result.criteriaTable).toContain('| 1 | Requirements are clear | `[PLAN:1]` |'); + expect(result.criteriaTable).toContain('| 2 | User is asking a question | `[PLAN:2]` |'); }); - it('should generate output format section with condition labels', () => { - const result = generateStatusRulesFromRules('plan', rules, 'ja'); + it('should generate output list with condition labels', () => { + const result = generateStatusRulesComponents('plan', rules, 'ja'); - expect(result).toContain('## 出力フォーマット'); - expect(result).toContain('`[PLAN:1]` — 要件が明確で実装可能'); - expect(result).toContain('`[PLAN:2]` — ユーザーが質問をしている'); - expect(result).toContain('`[PLAN:3]` — 要件が不明確、情報不足'); + expect(result.outputList).toContain('`[PLAN:1]` — 要件が明確で実装可能'); + expect(result.outputList).toContain('`[PLAN:2]` — ユーザーが質問をしている'); + expect(result.outputList).toContain('`[PLAN:3]` — 要件が不明確、情報不足'); }); - it('should generate appendix template section when rules have appendix', () => { - const result = generateStatusRulesFromRules('plan', rules, 'ja'); + it('should generate appendix content when rules have appendix', () => { + const result = generateStatusRulesComponents('plan', rules, 'ja'); - expect(result).toContain('### 追加出力テンプレート'); - expect(result).toContain('`[PLAN:3]`'); - expect(result).toContain('確認事項:'); - expect(result).toContain('- {質問1}'); + expect(result.hasAppendix).toBe(true); + expect(result.appendixContent).toContain('[[PLAN:3]]'); + expect(result.appendixContent).toContain('確認事項:'); + expect(result.appendixContent).toContain('- {質問1}'); }); - it('should not generate appendix section when no rules have appendix', () => { + it('should not generate appendix when no rules have appendix', () => { const noAppendixRules: WorkflowRule[] = [ { condition: 'Done', next: 'review' }, { condition: 'Blocked', next: 'plan' }, ]; - const result = generateStatusRulesFromRules('implement', noAppendixRules, 'en'); + const result = generateStatusRulesComponents('implement', noAppendixRules, 'en'); - expect(result).not.toContain('Appendix Template'); + expect(result.hasAppendix).toBe(false); + expect(result.appendixContent).toBe(''); }); it('should uppercase step name in tags', () => { - const result = generateStatusRulesFromRules('ai_review', [ + const result = generateStatusRulesComponents('ai_review', [ { condition: 'No issues', next: 'supervise' }, ], 'en'); - expect(result).toContain('`[AI_REVIEW:1]`'); + expect(result.criteriaTable).toContain('`[AI_REVIEW:1]`'); }); it('should omit interactive-only rules when interactive is false', () => { @@ -376,12 +253,12 @@ describe('instruction-builder', () => { { condition: 'User input required', next: 'implement', interactiveOnly: true }, { condition: 'Blocked', next: 'plan' }, ]; - const result = generateStatusRulesFromRules('implement', filteredRules, 'en', { interactive: false }); + const result = generateStatusRulesComponents('implement', filteredRules, 'en', { interactive: false }); - expect(result).toContain('`[IMPLEMENT:1]`'); - expect(result).toContain('`[IMPLEMENT:3]`'); - expect(result).not.toContain('User input required'); - expect(result).not.toContain('`[IMPLEMENT:2]`'); + expect(result.criteriaTable).toContain('`[IMPLEMENT:1]`'); + expect(result.criteriaTable).toContain('`[IMPLEMENT:3]`'); + expect(result.criteriaTable).not.toContain('User input required'); + expect(result.criteriaTable).not.toContain('`[IMPLEMENT:2]`'); }); }); @@ -397,9 +274,8 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('Decision Criteria'); - expect(result).toContain('[PLAN:1]'); - expect(result).toContain('[PLAN:2]'); + // Status rules are no longer injected in Phase 1 (only in Phase 3) + expect(result).not.toContain('Decision Criteria'); }); it('should not add status rules when rules do not exist', () => { @@ -429,7 +305,7 @@ describe('instruction-builder', () => { const context = createMinimalContext({ iteration: 3, maxIterations: 20, - stepIteration: 2, + movementIteration: 2, language: 'en', }); @@ -437,8 +313,8 @@ describe('instruction-builder', () => { expect(result).toContain('## Workflow Context'); expect(result).toContain('- Iteration: 3/20'); - expect(result).toContain('- Step Iteration: 2'); - expect(result).toContain('- Step: implement'); + expect(result).toContain('- Movement Iteration: 2'); + expect(result).toContain('- Movement: implement'); }); it('should include report info in Phase 1 when step has report', () => { @@ -495,13 +371,13 @@ describe('instruction-builder', () => { it('should render Japanese step iteration suffix', () => { const step = createMinimalStep('Do work'); const context = createMinimalContext({ - stepIteration: 3, + movementIteration: 3, language: 'ja', }); const result = buildInstruction(step, context); - expect(result).toContain('- Step Iteration: 3(このステップの実行回数)'); + expect(result).toContain('- Movement Iteration: 3(このムーブメントの実行回数)'); }); it('should include workflow structure when workflowSteps is provided', () => { @@ -509,21 +385,21 @@ describe('instruction-builder', () => { step.name = 'implement'; const context = createMinimalContext({ language: 'en', - workflowSteps: [ + workflowMovements: [ { name: 'plan' }, { name: 'implement' }, { name: 'review' }, ], - currentStepIndex: 1, + currentMovementIndex: 1, }); const result = buildInstruction(step, context); - expect(result).toContain('This workflow consists of 3 steps:'); - expect(result).toContain('- Step 1: plan'); - expect(result).toContain('- Step 2: implement'); + expect(result).toContain('This workflow consists of 3 movements:'); + expect(result).toContain('- Movement 1: plan'); + expect(result).toContain('- Movement 2: implement'); expect(result).toContain('← current'); - expect(result).toContain('- Step 3: review'); + expect(result).toContain('- Movement 3: review'); }); it('should mark current step with marker', () => { @@ -531,17 +407,17 @@ describe('instruction-builder', () => { step.name = 'plan'; const context = createMinimalContext({ language: 'en', - workflowSteps: [ + workflowMovements: [ { name: 'plan' }, { name: 'implement' }, ], - currentStepIndex: 0, + currentMovementIndex: 0, }); const result = buildInstruction(step, context); - expect(result).toContain('- Step 1: plan ← current'); - expect(result).not.toContain('- Step 2: implement ← current'); + expect(result).toContain('- Movement 1: plan ← current'); + expect(result).not.toContain('- Movement 2: implement ← current'); }); it('should include description in parentheses when provided', () => { @@ -549,16 +425,16 @@ describe('instruction-builder', () => { step.name = 'plan'; const context = createMinimalContext({ language: 'ja', - workflowSteps: [ + workflowMovements: [ { name: 'plan', description: 'タスクを分析し実装計画を作成する' }, { name: 'implement' }, ], - currentStepIndex: 0, + currentMovementIndex: 0, }); const result = buildInstruction(step, context); - expect(result).toContain('- Step 1: plan(タスクを分析し実装計画を作成する) ← 現在'); + expect(result).toContain('- Movement 1: plan(タスクを分析し実装計画を作成する) ← 現在'); }); it('should skip workflow structure when workflowSteps is not provided', () => { @@ -574,8 +450,8 @@ describe('instruction-builder', () => { const step = createMinimalStep('Do work'); const context = createMinimalContext({ language: 'en', - workflowSteps: [], - currentStepIndex: -1, + workflowMovements: [], + currentMovementIndex: -1, }); const result = buildInstruction(step, context); @@ -588,34 +464,34 @@ describe('instruction-builder', () => { step.name = 'plan'; const context = createMinimalContext({ language: 'ja', - workflowSteps: [ + workflowMovements: [ { name: 'plan' }, { name: 'implement' }, ], - currentStepIndex: 0, + currentMovementIndex: 0, }); const result = buildInstruction(step, context); - expect(result).toContain('このワークフローは2ステップで構成されています:'); + expect(result).toContain('このワークフローは2ムーブメントで構成されています:'); expect(result).toContain('← 現在'); }); - it('should not show current marker when currentStepIndex is -1', () => { + it('should not show current marker when currentMovementIndex is -1', () => { const step = createMinimalStep('Do work'); step.name = 'sub-step'; const context = createMinimalContext({ language: 'en', - workflowSteps: [ + workflowMovements: [ { name: 'plan' }, { name: 'implement' }, ], - currentStepIndex: -1, + currentMovementIndex: -1, }); const result = buildInstruction(step, context); - expect(result).toContain('This workflow consists of 2 steps:'); + expect(result).toContain('This workflow consists of 2 movements:'); expect(result).not.toContain('← current'); }); }); @@ -689,7 +565,7 @@ describe('instruction-builder', () => { return { cwd: '/project', reportDir: '/project/.takt/reports/20260129-test', - stepIteration: 1, + movementIteration: 1, language: 'en', ...overrides, }; @@ -803,10 +679,10 @@ describe('instruction-builder', () => { expect(result).toContain('# Plan'); }); - it('should replace {step_iteration} in report output instruction', () => { + it('should replace {movement_iteration} in report output instruction', () => { const step = createMinimalStep('Do work'); step.report = '00-plan.md'; - const ctx = createReportContext({ stepIteration: 5 }); + const ctx = createReportContext({ movementIteration: 5 }); const result = buildReportInstruction(step, ctx); @@ -993,9 +869,9 @@ describe('instruction-builder', () => { expect(result).toContain('Step 3/20'); }); - it('should replace {step_iteration}', () => { - const step = createMinimalStep('Run #{step_iteration}'); - const context = createMinimalContext({ stepIteration: 2 }); + it('should replace {movement_iteration}', () => { + const step = createMinimalStep('Run #{movement_iteration}'); + const context = createMinimalContext({ movementIteration: 2 }); const result = buildInstruction(step, context); @@ -1018,7 +894,7 @@ describe('instruction-builder', () => { expect(result).not.toContain('[TEST-STEP:'); }); - it('should include status rules with mixed regular and ai() conditions', () => { + it('should NOT include status rules with mixed regular and ai() conditions (Phase 1 no longer has status rules)', () => { const step = createMinimalStep('Do work'); step.name = 'review'; step.rules = [ @@ -1029,11 +905,11 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('Decision Criteria'); - expect(result).toContain('[REVIEW:1]'); + // Status rules are no longer injected in Phase 1 (only in Phase 3) + expect(result).not.toContain('Decision Criteria'); }); - it('should include status rules with regular conditions only', () => { + it('should NOT include status rules with regular conditions only (Phase 1 no longer has status rules)', () => { const step = createMinimalStep('Do work'); step.name = 'plan'; step.rules = [ @@ -1044,9 +920,8 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('Decision Criteria'); - expect(result).toContain('[PLAN:1]'); - expect(result).toContain('[PLAN:2]'); + // Status rules are no longer injected in Phase 1 (only in Phase 3) + expect(result).not.toContain('Decision Criteria'); }); it('should NOT include status rules when all rules are aggregate conditions', () => { @@ -1076,7 +951,7 @@ describe('instruction-builder', () => { expect(result).not.toContain('Decision Criteria'); }); - it('should include status rules with mixed aggregate and regular conditions', () => { + it('should NOT include status rules with mixed aggregate and regular conditions (Phase 1 no longer has status rules)', () => { const step = createMinimalStep('Do work'); step.name = 'supervise'; step.rules = [ @@ -1087,8 +962,8 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('Decision Criteria'); - expect(result).toContain('[SUPERVISE:1]'); + // Status rules are no longer injected in Phase 1 (only in Phase 3) + expect(result).not.toContain('Decision Criteria'); }); }); diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index 302f8e6..54eb410 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -2,7 +2,7 @@ * Error recovery integration tests. * * Tests agent error, blocked responses, max iteration limits, - * loop detection, scenario queue exhaustion, and step execution exceptions. + * loop detection, scenario queue exhaustion, and movement execution exceptions. * * Mocked: UI, session, phase-runner, notifications, config, callAiJudge * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator @@ -59,7 +59,7 @@ function makeRule(condition: string, next: string): WorkflowRule { return { condition, next }; } -function makeStep(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep { +function makeMovement(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep { return { name, agent: `./agents/${name}.md`, @@ -78,7 +78,7 @@ function createTestEnv(): { dir: string; agentPaths: Record } { const agentsDir = join(dir, 'agents'); mkdirSync(agentsDir, { recursive: true }); - // Agent file names match step names used in makeStep() + // Agent file names match movement names used in makeMovement() const agents = ['plan', 'implement', 'review', 'supervisor']; const agentPaths: Record = {}; for (const agent of agents) { @@ -103,17 +103,17 @@ function buildWorkflow(agentPaths: Record, maxIterations: number name: 'it-error', description: 'IT error recovery workflow', maxIterations, - initialStep: 'plan', - steps: [ - makeStep('plan', agentPaths.plan, [ + initialMovement: 'plan', + movements: [ + makeMovement('plan', agentPaths.plan, [ makeRule('Requirements are clear', 'implement'), makeRule('Requirements unclear', 'ABORT'), ]), - makeStep('implement', agentPaths.implement, [ + makeMovement('implement', agentPaths.implement, [ makeRule('Implementation complete', 'review'), makeRule('Cannot proceed', 'plan'), ]), - makeStep('review', agentPaths.review, [ + makeMovement('review', agentPaths.review, [ makeRule('All checks passed', 'COMPLETE'), makeRule('Issues found', 'implement'), ]), @@ -189,7 +189,7 @@ 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 steps + // Only 2 iterations allowed, but workflow needs 3 movements setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, { agent: 'implement', status: 'done', content: '[IMPLEMENT:1]\n\nDone.' }, @@ -246,7 +246,7 @@ describe('Error Recovery IT: scenario queue exhaustion', () => { }); it('should handle scenario queue exhaustion mid-workflow', async () => { - // Only 1 entry, but workflow needs 3 steps + // Only 1 entry, but workflow needs 3 movements setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, ]); @@ -265,7 +265,7 @@ describe('Error Recovery IT: scenario queue exhaustion', () => { }); }); -describe('Error Recovery IT: step events on error paths', () => { +describe('Error Recovery IT: movement events on error paths', () => { let testDir: string; let agentPaths: Record; @@ -304,7 +304,7 @@ describe('Error Recovery IT: step events on error paths', () => { expect(abortReason).toBeDefined(); }); - it('should emit step:start and step:complete for each executed step before abort', async () => { + it('should emit movement:start and movement:complete for each executed movement before abort', async () => { setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:2]\n\nRequirements unclear.' }, ]); @@ -318,10 +318,10 @@ describe('Error Recovery IT: step events on error paths', () => { const startedSteps: string[] = []; const completedSteps: string[] = []; - engine.on('step:start', (step) => { + engine.on('movement:start', (step) => { startedSteps.push(step.name); }); - engine.on('step:complete', (step) => { + engine.on('movement:complete', (step) => { completedSteps.push(step.name); }); @@ -362,15 +362,15 @@ describe('Error Recovery IT: programmatic abort', () => { provider: 'mock', }); - // Abort after the first step completes - engine.on('step:complete', () => { + // Abort after the first movement completes + engine.on('movement:complete', () => { engine.abort(); }); const state = await engine.run(); expect(state.status).toBe('aborted'); - // Should have aborted after 1 step + // Should have aborted after 1 movement expect(state.iteration).toBeLessThanOrEqual(2); }); }); diff --git a/src/__tests__/it-instruction-builder.test.ts b/src/__tests__/it-instruction-builder.test.ts index 5041cf1..1025af1 100644 --- a/src/__tests__/it-instruction-builder.test.ts +++ b/src/__tests__/it-instruction-builder.test.ts @@ -2,13 +2,13 @@ * Instruction builder integration tests. * * Tests template variable expansion and auto-injection in buildInstruction(). - * Uses real workflow step configs (not mocked) against the buildInstruction function. + * Uses real workflow movement configs (not mocked) against the buildInstruction function. * * Not mocked: buildInstruction, buildReportInstruction, buildStatusJudgmentInstruction */ import { describe, it, expect, vi } from 'vitest'; -import type { WorkflowStep, WorkflowRule, AgentResponse } from '../core/models/index.js'; +import type { WorkflowMovement, WorkflowRule, AgentResponse } from '../core/models/index.js'; vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), @@ -22,14 +22,14 @@ import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../core/workf import type { InstructionContext } from '../core/workflow/index.js'; // Function wrappers for test readability -function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string { - return new InstructionBuilder(step, ctx).build(); +function buildInstruction(movement: WorkflowMovement, ctx: InstructionContext): string { + return new InstructionBuilder(movement, ctx).build(); } -function buildReportInstruction(step: WorkflowStep, ctx: ReportInstructionContext): string { - return new ReportInstructionBuilder(step, ctx).build(); +function buildReportInstruction(movement: WorkflowMovement, ctx: ReportInstructionContext): string { + return new ReportInstructionBuilder(movement, ctx).build(); } -function buildStatusJudgmentInstruction(step: WorkflowStep, ctx: StatusJudgmentContext): string { - return new StatusJudgmentBuilder(step, ctx).build(); +function buildStatusJudgmentInstruction(movement: WorkflowMovement, ctx: StatusJudgmentContext): string { + return new StatusJudgmentBuilder(movement, ctx).build(); } // --- Test helpers --- @@ -38,7 +38,7 @@ function makeRule(condition: string, next: string, extra?: Partial return { condition, next, ...extra }; } -function makeStep(overrides: Partial = {}): WorkflowStep { +function makeMovement(overrides: Partial = {}): WorkflowMovement { return { name: 'test-step', agent: 'test-agent', @@ -58,7 +58,7 @@ function makeContext(overrides: Partial = {}): InstructionCo task: 'Test task description', iteration: 3, maxIterations: 30, - stepIteration: 2, + movementIteration: 2, cwd: '/tmp/test-project', projectCwd: '/tmp/test-project', userInputs: [], @@ -69,7 +69,7 @@ function makeContext(overrides: Partial = {}): InstructionCo describe('Instruction Builder IT: task auto-injection', () => { it('should auto-inject task as "User Request" section when template has no {task}', () => { - const step = makeStep({ instructionTemplate: 'Do the work.' }); + const step = makeMovement({ instructionTemplate: 'Do the work.' }); const ctx = makeContext({ task: 'Build the login page' }); const result = buildInstruction(step, ctx); @@ -79,7 +79,7 @@ describe('Instruction Builder IT: task auto-injection', () => { }); it('should NOT auto-inject task section when template contains {task}', () => { - const step = makeStep({ instructionTemplate: 'Here is the task: {task}' }); + const step = makeMovement({ instructionTemplate: 'Here is the task: {task}' }); const ctx = makeContext({ task: 'Build the login page' }); const result = buildInstruction(step, ctx); @@ -94,7 +94,7 @@ describe('Instruction Builder IT: task auto-injection', () => { describe('Instruction Builder IT: previous_response auto-injection', () => { it('should auto-inject previous response when passPreviousResponse is true', () => { - const step = makeStep({ + const step = makeMovement({ passPreviousResponse: true, instructionTemplate: 'Continue the work.', }); @@ -113,7 +113,7 @@ describe('Instruction Builder IT: previous_response auto-injection', () => { }); it('should NOT inject previous response when passPreviousResponse is false', () => { - const step = makeStep({ + const step = makeMovement({ passPreviousResponse: false, instructionTemplate: 'Do fresh work.', }); @@ -132,7 +132,7 @@ describe('Instruction Builder IT: previous_response auto-injection', () => { }); it('should NOT auto-inject when template contains {previous_response}', () => { - const step = makeStep({ + const step = makeMovement({ passPreviousResponse: true, instructionTemplate: '## Context\n{previous_response}\n\nDo work.', }); @@ -153,7 +153,7 @@ describe('Instruction Builder IT: previous_response auto-injection', () => { describe('Instruction Builder IT: user_inputs auto-injection', () => { it('should auto-inject user inputs section', () => { - const step = makeStep(); + const step = makeMovement(); const ctx = makeContext({ userInputs: ['Fix the typo', 'Use TypeScript'] }); const result = buildInstruction(step, ctx); @@ -164,7 +164,7 @@ describe('Instruction Builder IT: user_inputs auto-injection', () => { }); it('should NOT auto-inject when template contains {user_inputs}', () => { - const step = makeStep({ instructionTemplate: 'Inputs: {user_inputs}' }); + const step = makeMovement({ instructionTemplate: 'Inputs: {user_inputs}' }); const ctx = makeContext({ userInputs: ['Input A'] }); const result = buildInstruction(step, ctx); @@ -176,31 +176,31 @@ describe('Instruction Builder IT: user_inputs auto-injection', () => { }); describe('Instruction Builder IT: iteration variables', () => { - it('should replace {iteration}, {max_iterations}, {step_iteration} in template', () => { - const step = makeStep({ - instructionTemplate: 'Iter: {iteration}/{max_iterations}, step iter: {step_iteration}', + it('should replace {iteration}, {max_iterations}, {movement_iteration} in template', () => { + const step = makeMovement({ + instructionTemplate: 'Iter: {iteration}/{max_iterations}, movement iter: {movement_iteration}', }); - const ctx = makeContext({ iteration: 5, maxIterations: 30, stepIteration: 2 }); + const ctx = makeContext({ iteration: 5, maxIterations: 30, movementIteration: 2 }); const result = buildInstruction(step, ctx); - expect(result).toContain('Iter: 5/30, step iter: 2'); + expect(result).toContain('Iter: 5/30, movement iter: 2'); }); it('should include iteration in Workflow Context section', () => { - const step = makeStep(); - const ctx = makeContext({ iteration: 7, maxIterations: 20, stepIteration: 3 }); + const step = makeMovement(); + const ctx = makeContext({ iteration: 7, maxIterations: 20, movementIteration: 3 }); const result = buildInstruction(step, ctx); expect(result).toContain('Iteration: 7/20'); - expect(result).toContain('Step Iteration: 3'); + expect(result).toContain('Movement Iteration: 3'); }); }); describe('Instruction Builder IT: report_dir expansion', () => { it('should replace {report_dir} in template', () => { - const step = makeStep({ + const step = makeMovement({ instructionTemplate: 'Read the plan from {report_dir}/00-plan.md', }); const ctx = makeContext({ reportDir: '/tmp/test-project/.takt/reports/20250126-task' }); @@ -211,7 +211,7 @@ describe('Instruction Builder IT: report_dir expansion', () => { }); it('should replace {report:filename} with full path', () => { - const step = makeStep({ + const step = makeMovement({ instructionTemplate: 'Read {report:00-plan.md} for the plan.', }); const ctx = makeContext({ reportDir: '/tmp/reports' }); @@ -224,7 +224,7 @@ describe('Instruction Builder IT: report_dir expansion', () => { describe('Instruction Builder IT: status output rules injection', () => { it('should inject status rules for steps with tag-based rules', () => { - const step = makeStep({ + const step = makeMovement({ name: 'plan', rules: [ makeRule('Requirements clear', 'implement'), @@ -235,14 +235,13 @@ describe('Instruction Builder IT: status output rules injection', () => { const result = buildInstruction(step, ctx); - // Should contain status rules section with the tag format - expect(result).toContain('[PLAN:'); - expect(result).toContain('Requirements clear'); - expect(result).toContain('Requirements unclear'); + // Status rules are NO LONGER injected in Phase 1 (buildInstruction). + // They are only injected in Phase 3 (buildStatusJudgmentInstruction). + expect(result).not.toContain('[PLAN:'); }); it('should NOT inject status rules for steps with only ai() conditions', () => { - const step = makeStep({ + const step = makeMovement({ name: 'review', rules: [ makeRule('ai("approved")', 'COMPLETE', { isAiCondition: true, aiConditionText: 'approved' }), @@ -260,7 +259,7 @@ describe('Instruction Builder IT: status output rules injection', () => { describe('Instruction Builder IT: edit permission in execution context', () => { it('should include edit permission rules when edit is true', () => { - const step = makeStep({ edit: true }); + const step = makeMovement({ edit: true }); const ctx = makeContext(); const result = buildInstruction(step, ctx); @@ -270,7 +269,7 @@ describe('Instruction Builder IT: edit permission in execution context', () => { }); it('should indicate read-only when edit is false', () => { - const step = makeStep({ edit: false }); + const step = makeMovement({ edit: false }); const ctx = makeContext(); const result = buildInstruction(step, ctx); @@ -283,15 +282,15 @@ describe('Instruction Builder IT: edit permission in execution context', () => { describe('Instruction Builder IT: buildReportInstruction', () => { it('should build report instruction with report context', () => { - const step = makeStep({ + const step = makeMovement({ name: 'plan', - report: { name: '00-plan.md', format: '# Plan\n{step_iteration}' }, + report: { name: '00-plan.md', format: '# Plan\n{movement_iteration}' }, }); const result = buildReportInstruction(step, { cwd: '/tmp/test', reportDir: '/tmp/test/.takt/reports/test-dir', - stepIteration: 1, + movementIteration: 1, language: 'en', }); @@ -301,13 +300,13 @@ describe('Instruction Builder IT: buildReportInstruction', () => { }); it('should throw for step without report config', () => { - const step = makeStep({ report: undefined }); + const step = makeMovement({ report: undefined }); expect(() => buildReportInstruction(step, { cwd: '/tmp', reportDir: '/tmp/reports', - stepIteration: 1, + movementIteration: 1, }), ).toThrow(/no report config/); }); @@ -315,7 +314,7 @@ describe('Instruction Builder IT: buildReportInstruction', () => { describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => { it('should build Phase 3 instruction with status rules', () => { - const step = makeStep({ + const step = makeMovement({ name: 'plan', rules: [ makeRule('Clear', 'implement'), @@ -331,7 +330,7 @@ describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => { }); it('should throw for step without rules', () => { - const step = makeStep({ rules: undefined }); + const step = makeMovement({ rules: undefined }); expect(() => buildStatusJudgmentInstruction(step, { language: 'en' }), @@ -341,7 +340,7 @@ describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => { describe('Instruction Builder IT: template injection prevention', () => { it('should escape curly braces in task content', () => { - const step = makeStep(); + const step = makeMovement(); const ctx = makeContext({ task: 'Use {variable} in code' }); const result = buildInstruction(step, ctx); @@ -352,7 +351,7 @@ describe('Instruction Builder IT: template injection prevention', () => { }); it('should escape curly braces in previous response content', () => { - const step = makeStep({ + const step = makeMovement({ passPreviousResponse: true, instructionTemplate: 'Continue.', }); diff --git a/src/__tests__/it-pipeline-modes.test.ts b/src/__tests__/it-pipeline-modes.test.ts index 5246be5..2da43f8 100644 --- a/src/__tests__/it-pipeline-modes.test.ts +++ b/src/__tests__/it-pipeline-modes.test.ts @@ -172,9 +172,9 @@ function createTestWorkflowDir(): { dir: string; workflowPath: string } { name: it-pipeline description: Pipeline test workflow max_iterations: 10 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan agent: ./agents/planner.md rules: diff --git a/src/__tests__/it-pipeline.test.ts b/src/__tests__/it-pipeline.test.ts index 2c0d283..f67669a 100644 --- a/src/__tests__/it-pipeline.test.ts +++ b/src/__tests__/it-pipeline.test.ts @@ -154,9 +154,9 @@ function createTestWorkflowDir(): { dir: string; workflowPath: string } { name: it-simple description: Integration test workflow max_iterations: 10 -initial_step: plan +initial_movement: plan -steps: +movements: - name: plan agent: ./agents/planner.md rules: @@ -209,7 +209,7 @@ describe('Pipeline Integration Tests', () => { it('should complete pipeline with workflow path + skip-git + mock scenario', async () => { // Scenario: plan -> implement -> review -> COMPLETE - // agent field must match extractAgentName(step.agent), i.e., the .md filename without extension + // agent field must match extractAgentName(movement.agent), i.e., the .md filename without extension setMockScenario([ { agent: 'planner', status: 'done', content: '[PLAN:1]\n\nPlan completed. Requirements are clear.' }, { agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' }, @@ -231,7 +231,7 @@ describe('Pipeline Integration Tests', () => { it('should complete pipeline with workflow name + skip-git + mock scenario', async () => { // Use builtin 'minimal' workflow // agent field: extractAgentName result (from .md filename) - // tag in content: [STEP_NAME:N] where STEP_NAME is the step name uppercased + // tag in content: [MOVEMENT_NAME:N] where MOVEMENT_NAME is the movement name uppercased setMockScenario([ { agent: 'coder', status: 'done', content: 'Implementation complete' }, { agent: 'ai-antipattern-reviewer', status: 'done', content: 'No AI-specific issues' }, diff --git a/src/__tests__/it-rule-evaluation.test.ts b/src/__tests__/it-rule-evaluation.test.ts index d1a7963..c6a42ab 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 { WorkflowStep, WorkflowState, WorkflowRule, AgentResponse } from '../core/models/index.js'; +import type { WorkflowMovement, WorkflowState, WorkflowRule, AgentResponse } from '../core/models/index.js'; // --- Mocks --- @@ -43,11 +43,11 @@ function makeRule(condition: string, next: string, extra?: Partial return { condition, next, ...extra }; } -function makeStep( +function makeMovement( name: string, rules: WorkflowRule[], - parallel?: WorkflowStep[], -): WorkflowStep { + parallel?: WorkflowMovement[], +): WorkflowMovement { return { name, agent: 'test-agent', @@ -59,22 +59,22 @@ function makeStep( }; } -function makeState(stepOutputs?: Map): WorkflowState { +function makeState(movementOutputs?: Map): WorkflowState { return { workflowName: 'it-test', - currentStep: 'test', + currentMovement: 'test', iteration: 1, status: 'running', - stepOutputs: stepOutputs ?? new Map(), - stepIterations: new Map(), + movementOutputs: movementOutputs ?? new Map(), + movementIterations: new Map(), agentSessions: new Map(), userInputs: [], }; } -function makeCtx(stepOutputs?: Map): RuleEvaluatorContext { +function makeCtx(movementOutputs?: Map): RuleEvaluatorContext { return { - state: makeState(stepOutputs), + state: makeState(movementOutputs), cwd: '/tmp/test', detectRuleIndex, callAiJudge: mockCallAiJudge, @@ -88,7 +88,7 @@ describe('Rule Evaluation IT: Phase 3 tag detection', () => { }); it('should detect rule from Phase 3 tag content', async () => { - const step = makeStep('plan', [ + const step = makeMovement('plan', [ makeRule('Clear', 'implement'), makeRule('Unclear', 'ABORT'), ]); @@ -99,7 +99,7 @@ describe('Rule Evaluation IT: Phase 3 tag detection', () => { }); it('should prefer Phase 3 tag over Phase 1 tag', async () => { - const step = makeStep('plan', [ + const step = makeMovement('plan', [ makeRule('Clear', 'implement'), makeRule('Unclear', 'ABORT'), ]); @@ -118,7 +118,7 @@ describe('Rule Evaluation IT: Phase 1 tag fallback', () => { }); it('should fall back to Phase 1 tag when Phase 3 has no tag', async () => { - const step = makeStep('plan', [ + const step = makeMovement('plan', [ makeRule('Clear', 'implement'), makeRule('Unclear', 'ABORT'), ]); @@ -129,7 +129,7 @@ describe('Rule Evaluation IT: Phase 1 tag fallback', () => { }); it('should detect last tag when multiple tags in Phase 1', async () => { - const step = makeStep('plan', [ + const step = makeMovement('plan', [ makeRule('Clear', 'implement'), makeRule('Unclear', 'ABORT'), ]); @@ -147,17 +147,17 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => { mockCallAiJudge.mockResolvedValue(-1); }); - it('should match all("approved") when all sub-steps have "approved"', () => { - const subStep1 = makeStep('arch-review', [ + it('should match all("approved") when all sub-movements have "approved"', () => { + const subStep1 = makeMovement('arch-review', [ makeRule('approved', ''), makeRule('needs_fix', ''), ]); - const subStep2 = makeStep('security-review', [ + const subStep2 = makeMovement('security-review', [ makeRule('approved', ''), makeRule('needs_fix', ''), ]); - const parentStep = makeStep('reviewers', [ + const parentStep = makeMovement('reviewers', [ makeRule('all("approved")', 'supervise', { isAggregateCondition: true, aggregateType: 'all', @@ -185,17 +185,17 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => { expect(result).toBe(0); // all("approved") is rule index 0 }); - it('should match any("needs_fix") when one sub-step has "needs_fix"', () => { - const subStep1 = makeStep('arch-review', [ + it('should match any("needs_fix") when one sub-movement has "needs_fix"', () => { + const subStep1 = makeMovement('arch-review', [ makeRule('approved', ''), makeRule('needs_fix', ''), ]); - const subStep2 = makeStep('security-review', [ + const subStep2 = makeMovement('security-review', [ makeRule('approved', ''), makeRule('needs_fix', ''), ]); - const parentStep = makeStep('reviewers', [ + const parentStep = makeMovement('reviewers', [ makeRule('all("approved")', 'supervise', { isAggregateCondition: true, aggregateType: 'all', @@ -224,16 +224,16 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => { }); it('should return -1 when no aggregate condition matches', () => { - const subStep1 = makeStep('review-a', [ + const subStep1 = makeMovement('review-a', [ makeRule('approved', ''), makeRule('needs_fix', ''), ]); - const subStep2 = makeStep('review-b', [ + const subStep2 = makeMovement('review-b', [ makeRule('approved', ''), makeRule('needs_fix', ''), ]); - const parentStep = makeStep('reviews', [ + const parentStep = makeMovement('reviews', [ makeRule('all("approved")', 'done', { isAggregateCondition: true, aggregateType: 'all', @@ -256,8 +256,8 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => { expect(result).toBe(-1); }); - it('should return -1 for non-parallel step', () => { - const step = makeStep('step', [ + it('should return -1 for non-parallel movement', () => { + const step = makeMovement('step', [ makeRule('all("done")', 'COMPLETE', { isAggregateCondition: true, aggregateType: 'all', @@ -279,7 +279,7 @@ describe('Rule Evaluation IT: ai() judge condition', () => { it('should call AI judge for ai() conditions when no tag match', async () => { mockCallAiJudge.mockResolvedValue(0); // Judge says first ai() condition matches - const step = makeStep('step', [ + const step = makeMovement('step', [ makeRule('ai("The code is approved")', 'COMPLETE', { isAiCondition: true, aiConditionText: 'The code is approved', @@ -297,7 +297,7 @@ describe('Rule Evaluation IT: ai() judge condition', () => { }); it('should skip AI judge if tag already matched', async () => { - const step = makeStep('plan', [ + const step = makeMovement('plan', [ makeRule('ai("Clear")', 'implement', { isAiCondition: true, aiConditionText: 'Clear', @@ -321,7 +321,7 @@ describe('Rule Evaluation IT: AI judge fallback', () => { // Second call (all conditions fallback): returns 0 mockCallAiJudge.mockResolvedValue(0); - const step = makeStep('review', [ + const step = makeMovement('review', [ makeRule('Approved', 'COMPLETE'), makeRule('Rejected', 'fix'), ]); @@ -335,7 +335,7 @@ describe('Rule Evaluation IT: AI judge fallback', () => { it('should throw when no rule matches (AI judge returns -1 for all phases)', async () => { mockCallAiJudge.mockResolvedValue(-1); - const step = makeStep('review', [ + const step = makeMovement('review', [ makeRule('Approved', 'COMPLETE'), makeRule('Rejected', 'fix'), ]); @@ -353,8 +353,8 @@ describe('Rule Evaluation IT: RuleMatchMethod tracking', () => { }); it('should record method as "aggregate" for aggregate matches', () => { - const subStep = makeStep('sub', [makeRule('ok', '')]); - const parentStep = makeStep('parent', [ + const subStep = makeMovement('sub', [makeRule('ok', '')]); + const parentStep = makeMovement('parent', [ makeRule('all("ok")', 'COMPLETE', { isAggregateCondition: true, aggregateType: 'all', @@ -374,7 +374,7 @@ describe('Rule Evaluation IT: RuleMatchMethod tracking', () => { }); it('should record method as "phase3_tag" for Phase 3 matches', async () => { - const step = makeStep('step', [ + const step = makeMovement('step', [ makeRule('Done', 'COMPLETE'), ]); @@ -383,7 +383,7 @@ describe('Rule Evaluation IT: RuleMatchMethod tracking', () => { }); it('should record method as "phase1_tag" for Phase 1 fallback matches', async () => { - const step = makeStep('step', [ + const step = makeMovement('step', [ makeRule('Done', 'COMPLETE'), ]); @@ -392,13 +392,13 @@ describe('Rule Evaluation IT: RuleMatchMethod tracking', () => { }); }); -describe('Rule Evaluation IT: steps without rules', () => { +describe('Rule Evaluation IT: movements without rules', () => { beforeEach(() => { vi.clearAllMocks(); }); - it('should return undefined for step with no rules', async () => { - const step: WorkflowStep = { + it('should return undefined for movement with no rules', async () => { + const step: WorkflowMovement = { name: 'step', agent: 'agent', agentDisplayName: 'step', @@ -410,8 +410,8 @@ describe('Rule Evaluation IT: steps without rules', () => { expect(result).toBeUndefined(); }); - it('should return undefined for step with empty rules array', async () => { - const step = makeStep('step', []); + it('should return undefined for movement with empty rules array', async () => { + const step = makeMovement('step', []); const result = await detectMatchedRule(step, 'content', '', makeCtx()); expect(result).toBeUndefined(); diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index ece1f53..7564366 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -2,7 +2,7 @@ * Three-phase execution integration tests. * * Tests Phase 1 (main) → Phase 2 (report) → Phase 3 (status judgment) lifecycle. - * Verifies that the correct combination of phases fires based on step config. + * Verifies that the correct combination of phases fires based on movement config. * * Mocked: UI, session, config, callAiJudge * Selectively mocked: phase-runner (to inspect call patterns) @@ -84,7 +84,7 @@ function buildEngineOptions(projectCwd: string) { }; } -function makeStep( +function makeMovement( name: string, agentPath: string, rules: WorkflowRule[], @@ -124,7 +124,7 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () = rmSync(testDir, { recursive: true, force: true }); }); - it('should only run Phase 1 when step has no report and no tag rules', async () => { + it('should only run Phase 1 when movement has no report and no tag rules', async () => { setMockScenario([ { status: 'done', content: '[STEP:1]\n\nDone.' }, ]); @@ -133,9 +133,9 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () = name: 'it-phase1-only', description: 'Test', maxIterations: 5, - initialStep: 'step', - steps: [ - makeStep('step', agentPath, [ + initialMovement: 'step', + movements: [ + makeMovement('step', agentPath, [ makeRule('Done', 'COMPLETE'), makeRule('Not done', 'ABORT'), ]), @@ -176,7 +176,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should run Phase 1 + Phase 2 when step has report', async () => { + it('should run Phase 1 + Phase 2 when movement has report', async () => { setMockScenario([ { status: 'done', content: '[STEP:1]\n\nDone.' }, ]); @@ -185,9 +185,9 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { name: 'it-phase1-2', description: 'Test', maxIterations: 5, - initialStep: 'step', - steps: [ - makeStep('step', agentPath, [ + initialMovement: 'step', + movements: [ + makeMovement('step', agentPath, [ makeRule('Done', 'COMPLETE'), makeRule('Not done', 'ABORT'), ], { report: 'test-report.md' }), @@ -206,7 +206,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { expect(mockRunStatusJudgmentPhase).not.toHaveBeenCalled(); }); - it('should run Phase 2 for multi-report step', async () => { + it('should run Phase 2 for multi-report movement', async () => { setMockScenario([ { status: 'done', content: '[STEP:1]\n\nDone.' }, ]); @@ -215,9 +215,9 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { name: 'it-phase1-2-multi', description: 'Test', maxIterations: 5, - initialStep: 'step', - steps: [ - makeStep('step', agentPath, [ + initialMovement: 'step', + movements: [ + makeMovement('step', agentPath, [ makeRule('Done', 'COMPLETE'), ], { report: [{ label: 'Scope', path: 'scope.md' }, { label: 'Decisions', path: 'decisions.md' }] }), ], @@ -256,7 +256,7 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () => rmSync(testDir, { recursive: true, force: true }); }); - it('should run Phase 1 + Phase 3 when step has tag-based rules but no report', async () => { + it('should run Phase 1 + Phase 3 when movement has tag-based rules but no report', async () => { setMockScenario([ // Phase 1: main content (no tag — Phase 3 will provide it) { status: 'done', content: 'Agent completed the work.' }, @@ -266,9 +266,9 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () => name: 'it-phase1-3', description: 'Test', maxIterations: 5, - initialStep: 'step', - steps: [ - makeStep('step', agentPath, [ + initialMovement: 'step', + movements: [ + makeMovement('step', agentPath, [ makeRule('Done', 'COMPLETE'), makeRule('Not done', 'ABORT'), ]), @@ -308,7 +308,7 @@ describe('Three-Phase Execution IT: all three phases', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should run Phase 1 → Phase 2 → Phase 3 when step has report and tag rules', async () => { + it('should run Phase 1 → Phase 2 → Phase 3 when movement has report and tag rules', async () => { setMockScenario([ { status: 'done', content: 'Agent completed the work.' }, ]); @@ -317,9 +317,9 @@ describe('Three-Phase Execution IT: all three phases', () => { name: 'it-all-phases', description: 'Test', maxIterations: 5, - initialStep: 'step', - steps: [ - makeStep('step', agentPath, [ + initialMovement: 'step', + movements: [ + makeMovement('step', agentPath, [ makeRule('Done', 'COMPLETE'), makeRule('Not done', 'ABORT'), ], { report: 'test-report.md' }), @@ -377,13 +377,13 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => { name: 'it-phase3-tag', description: 'Test', maxIterations: 5, - initialStep: 'step1', - steps: [ - makeStep('step1', agentPath, [ + initialMovement: 'step1', + movements: [ + makeMovement('step1', agentPath, [ makeRule('Done', 'step2'), makeRule('Not done', 'ABORT'), ]), - makeStep('step2', agentPath, [ + makeMovement('step2', agentPath, [ makeRule('Checked', 'COMPLETE'), ]), ], diff --git a/src/__tests__/it-workflow-execution.test.ts b/src/__tests__/it-workflow-execution.test.ts index 5bf4782..aaba706 100644 --- a/src/__tests__/it-workflow-execution.test.ts +++ b/src/__tests__/it-workflow-execution.test.ts @@ -3,7 +3,7 @@ * * Tests WorkflowEngine with real runAgent + MockProvider + ScenarioQueue. * No vi.mock on runAgent or detectMatchedRule — rules are matched via - * [STEP_NAME:N] tags in scenario content (tag-based detection). + * [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 @@ -62,7 +62,7 @@ function makeRule(condition: string, next: string): WorkflowRule { return { condition, next }; } -function makeStep(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep { +function makeMovement(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep { return { name, agent: `./agents/${name}.md`, @@ -105,17 +105,17 @@ function buildSimpleWorkflow(agentPaths: Record): WorkflowConfig name: 'it-simple', description: 'IT simple workflow', maxIterations: 15, - initialStep: 'plan', - steps: [ - makeStep('plan', agentPaths.planner, [ + initialMovement: 'plan', + movements: [ + makeMovement('plan', agentPaths.planner, [ makeRule('Requirements are clear', 'implement'), makeRule('Requirements unclear', 'ABORT'), ]), - makeStep('implement', agentPaths.coder, [ + makeMovement('implement', agentPaths.coder, [ makeRule('Implementation complete', 'review'), makeRule('Cannot proceed', 'plan'), ]), - makeStep('review', agentPaths.reviewer, [ + makeMovement('review', agentPaths.reviewer, [ makeRule('All checks passed', 'COMPLETE'), makeRule('Issues found', 'implement'), ]), @@ -128,25 +128,25 @@ function buildLoopWorkflow(agentPaths: Record): WorkflowConfig { name: 'it-loop', description: 'IT workflow with fix loop', maxIterations: 20, - initialStep: 'plan', - steps: [ - makeStep('plan', agentPaths.planner, [ + initialMovement: 'plan', + movements: [ + makeMovement('plan', agentPaths.planner, [ makeRule('Requirements are clear', 'implement'), makeRule('Requirements unclear', 'ABORT'), ]), - makeStep('implement', agentPaths.coder, [ + makeMovement('implement', agentPaths.coder, [ makeRule('Implementation complete', 'review'), makeRule('Cannot proceed', 'plan'), ]), - makeStep('review', agentPaths.reviewer, [ + makeMovement('review', agentPaths.reviewer, [ makeRule('Approved', 'supervise'), makeRule('Needs fix', 'fix'), ]), - makeStep('fix', agentPaths.fixer, [ + makeMovement('fix', agentPaths.fixer, [ makeRule('Fix complete', 'review'), makeRule('Cannot fix', 'ABORT'), ]), - makeStep('supervise', agentPaths.supervisor, [ + makeMovement('supervise', agentPaths.supervisor, [ makeRule('All checks passed', 'COMPLETE'), makeRule('Requirements unmet', 'plan'), ]), @@ -308,7 +308,7 @@ describe('Workflow Engine IT: Max Iterations', () => { }); }); -describe('Workflow Engine IT: Step Output Tracking', () => { +describe('Workflow Engine IT: Movement Output Tracking', () => { let testDir: string; let agentPaths: Record; @@ -324,7 +324,7 @@ describe('Workflow Engine IT: Step Output Tracking', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should track step outputs through events', async () => { + it('should track movement outputs through events', async () => { setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nPlan output.' }, { agent: 'implement', status: 'done', content: '[IMPLEMENT:1]\n\nImplement output.' }, @@ -337,14 +337,14 @@ describe('Workflow Engine IT: Step Output Tracking', () => { provider: 'mock', }); - const completedSteps: string[] = []; - engine.on('step:complete', (step) => { - completedSteps.push(step.name); + const completedMovements: string[] = []; + engine.on('movement:complete', (movement) => { + completedMovements.push(movement.name); }); const state = await engine.run(); expect(state.status).toBe('completed'); - expect(completedSteps).toEqual(['plan', 'implement', 'review']); + expect(completedMovements).toEqual(['plan', 'implement', 'review']); }); }); diff --git a/src/__tests__/it-workflow-loader.test.ts b/src/__tests__/it-workflow-loader.test.ts index acb2ed9..ec6fa30 100644 --- a/src/__tests__/it-workflow-loader.test.ts +++ b/src/__tests__/it-workflow-loader.test.ts @@ -53,8 +53,8 @@ describe('Workflow Loader IT: builtin workflow loading', () => { expect(config).not.toBeNull(); expect(config!.name).toBe(name); - expect(config!.steps.length).toBeGreaterThan(0); - expect(config!.initialStep).toBeDefined(); + expect(config!.movements.length).toBeGreaterThan(0); + expect(config!.initialMovement).toBeDefined(); expect(config!.maxIterations).toBeGreaterThan(0); }); } @@ -88,9 +88,9 @@ describe('Workflow Loader IT: project-local workflow override', () => { name: custom-wf description: Custom project workflow max_iterations: 5 -initial_step: start +initial_movement: start -steps: +movements: - name: start agent: ./agents/custom.md rules: @@ -103,8 +103,8 @@ steps: expect(config).not.toBeNull(); expect(config!.name).toBe('custom-wf'); - expect(config!.steps.length).toBe(1); - expect(config!.steps[0]!.name).toBe('start'); + expect(config!.movements.length).toBe(1); + expect(config!.movements[0]!.name).toBe('start'); }); }); @@ -123,15 +123,15 @@ describe('Workflow Loader IT: agent path resolution', () => { const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); - for (const step of config!.steps) { - if (step.agentPath) { + for (const movement of config!.movements) { + if (movement.agentPath) { // Agent paths should be resolved to absolute paths - expect(step.agentPath).toMatch(/^\//); + expect(movement.agentPath).toMatch(/^\//); // Agent files should exist - expect(existsSync(step.agentPath)).toBe(true); + expect(existsSync(movement.agentPath)).toBe(true); } - if (step.parallel) { - for (const sub of step.parallel) { + if (movement.parallel) { + for (const sub of movement.parallel) { if (sub.agentPath) { expect(sub.agentPath).toMatch(/^\//); expect(existsSync(sub.agentPath)).toBe(true); @@ -157,8 +157,8 @@ describe('Workflow Loader IT: rule syntax parsing', () => { const config = loadWorkflow('default', testDir); expect(config).not.toBeNull(); - // Find the parallel reviewers step - const reviewersStep = config!.steps.find( + // Find the parallel reviewers movement + const reviewersStep = config!.movements.find( (s) => s.parallel && s.parallel.length > 0, ); expect(reviewersStep).toBeDefined(); @@ -175,7 +175,7 @@ describe('Workflow Loader IT: rule syntax parsing', () => { const config = loadWorkflow('default', testDir); expect(config).not.toBeNull(); - const reviewersStep = config!.steps.find( + const reviewersStep = config!.movements.find( (s) => s.parallel && s.parallel.length > 0, ); @@ -186,11 +186,11 @@ describe('Workflow Loader IT: rule syntax parsing', () => { expect(anyRule!.aggregateConditionText).toBe('needs_fix'); }); - it('should parse standard rules with next step', () => { + it('should parse standard rules with next movement', () => { const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); - const implementStep = config!.steps.find((s) => s.name === 'implement'); + const implementStep = config!.movements.find((s) => s.name === 'implement'); expect(implementStep).toBeDefined(); expect(implementStep!.rules).toBeDefined(); expect(implementStep!.rules!.length).toBeGreaterThan(0); @@ -221,34 +221,34 @@ describe('Workflow Loader IT: workflow config validation', () => { expect(config!.maxIterations).toBeGreaterThan(0); }); - it('should set initial_step from YAML', () => { + it('should set initial_movement from YAML', () => { const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); - expect(typeof config!.initialStep).toBe('string'); + expect(typeof config!.initialMovement).toBe('string'); - // initial_step should reference an existing step - const stepNames = config!.steps.map((s) => s.name); - expect(stepNames).toContain(config!.initialStep); + // initial_movement should reference an existing movement + const movementNames = config!.movements.map((s) => s.name); + expect(movementNames).toContain(config!.initialMovement); }); - it('should preserve edit property on steps (review-only has no edit: true)', () => { + it('should preserve edit property on movements (review-only has no edit: true)', () => { const config = loadWorkflow('review-only', testDir); expect(config).not.toBeNull(); - // review-only: no step should have edit: true - for (const step of config!.steps) { - expect(step.edit).not.toBe(true); - if (step.parallel) { - for (const sub of step.parallel) { + // review-only: no movement should have edit: true + for (const movement of config!.movements) { + expect(movement.edit).not.toBe(true); + if (movement.parallel) { + for (const sub of movement.parallel) { expect(sub.edit).not.toBe(true); } } } - // expert: implement step should have edit: true + // expert: implement movement should have edit: true const expertConfig = loadWorkflow('expert', testDir); expect(expertConfig).not.toBeNull(); - const implementStep = expertConfig!.steps.find((s) => s.name === 'implement'); + const implementStep = expertConfig!.movements.find((s) => s.name === 'implement'); expect(implementStep).toBeDefined(); expect(implementStep!.edit).toBe(true); }); @@ -257,13 +257,13 @@ describe('Workflow Loader IT: workflow config validation', () => { const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); - // At least some steps should have passPreviousResponse set - const stepsWithPassPrev = config!.steps.filter((s) => s.passPreviousResponse === true); - expect(stepsWithPassPrev.length).toBeGreaterThan(0); + // At least some movements should have passPreviousResponse set + const movementsWithPassPrev = config!.movements.filter((s) => s.passPreviousResponse === true); + expect(movementsWithPassPrev.length).toBeGreaterThan(0); }); }); -describe('Workflow Loader IT: parallel step loading', () => { +describe('Workflow Loader IT: parallel movement loading', () => { let testDir: string; beforeEach(() => { @@ -274,17 +274,17 @@ describe('Workflow Loader IT: parallel step loading', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should load parallel sub-steps from default workflow', () => { + it('should load parallel sub-movements from default workflow', () => { const config = loadWorkflow('default', testDir); expect(config).not.toBeNull(); - const parallelStep = config!.steps.find( + const parallelStep = config!.movements.find( (s) => s.parallel && s.parallel.length > 0, ); expect(parallelStep).toBeDefined(); expect(parallelStep!.parallel!.length).toBeGreaterThanOrEqual(2); - // Each sub-step should have required fields + // Each sub-movement should have required fields for (const sub of parallelStep!.parallel!) { expect(sub.name).toBeDefined(); expect(sub.agent).toBeDefined(); @@ -296,7 +296,7 @@ describe('Workflow Loader IT: parallel step loading', () => { const config = loadWorkflow('expert', testDir); expect(config).not.toBeNull(); - const parallelStep = config!.steps.find( + const parallelStep = config!.movements.find( (s) => s.parallel && s.parallel.length === 4, ); expect(parallelStep).toBeDefined(); @@ -324,8 +324,8 @@ describe('Workflow Loader IT: report config loading', () => { const config = loadWorkflow('default', testDir); expect(config).not.toBeNull(); - // default workflow: plan step has a report config - const planStep = config!.steps.find((s) => s.name === 'plan'); + // default workflow: plan movement has a report config + const planStep = config!.movements.find((s) => s.name === 'plan'); expect(planStep).toBeDefined(); expect(planStep!.report).toBeDefined(); }); @@ -334,8 +334,8 @@ describe('Workflow Loader IT: report config loading', () => { const config = loadWorkflow('expert', testDir); expect(config).not.toBeNull(); - // implement step has multi-report: [Scope, Decisions] - const implementStep = config!.steps.find((s) => s.name === 'implement'); + // implement movement has multi-report: [Scope, Decisions] + const implementStep = config!.movements.find((s) => s.name === 'implement'); expect(implementStep).toBeDefined(); expect(implementStep!.report).toBeDefined(); expect(Array.isArray(implementStep!.report)).toBe(true); @@ -373,7 +373,7 @@ this is not: valid yaml: [[[[ writeFileSync(join(workflowsDir, 'incomplete.yaml'), ` name: incomplete -description: Missing steps +description: Missing movements `); expect(() => loadWorkflow('incomplete', testDir)).toThrow(); diff --git a/src/__tests__/it-workflow-patterns.test.ts b/src/__tests__/it-workflow-patterns.test.ts index 5fe3dd9..944ace2 100644 --- a/src/__tests__/it-workflow-patterns.test.ts +++ b/src/__tests__/it-workflow-patterns.test.ts @@ -304,15 +304,15 @@ describe('Workflow Patterns IT: review-only workflow', () => { expect(state.status).toBe('completed'); }); - it('should verify no steps have edit: true', () => { + it('should verify no movements have edit: true', () => { const config = loadWorkflow('review-only', testDir); expect(config).not.toBeNull(); - for (const step of config!.steps) { - expect(step.edit).not.toBe(true); - if (step.parallel) { - for (const subStep of step.parallel) { - expect(subStep.edit).not.toBe(true); + for (const movement of config!.movements) { + expect(movement.edit).not.toBe(true); + if (movement.parallel) { + for (const subMovement of movement.parallel) { + expect(subMovement.edit).not.toBe(true); } } } diff --git a/src/__tests__/models.test.ts b/src/__tests__/models.test.ts index 707675f..400d879 100644 --- a/src/__tests__/models.test.ts +++ b/src/__tests__/models.test.ts @@ -62,7 +62,7 @@ describe('WorkflowConfigRawSchema', () => { const config = { name: 'test-workflow', description: 'A test workflow', - steps: [ + movements: [ { name: 'step1', agent: 'coder', @@ -77,15 +77,15 @@ describe('WorkflowConfigRawSchema', () => { const result = WorkflowConfigRawSchema.parse(config); expect(result.name).toBe('test-workflow'); - expect(result.steps).toHaveLength(1); - expect(result.steps[0]?.allowed_tools).toEqual(['Read', 'Grep']); + expect(result.movements).toHaveLength(1); + expect(result.movements![0]?.allowed_tools).toEqual(['Read', 'Grep']); expect(result.max_iterations).toBe(10); }); - it('should parse step with permission_mode', () => { + it('should parse movement with permission_mode', () => { const config = { name: 'test-workflow', - steps: [ + movements: [ { name: 'implement', agent: 'coder', @@ -100,13 +100,13 @@ describe('WorkflowConfigRawSchema', () => { }; const result = WorkflowConfigRawSchema.parse(config); - expect(result.steps[0]?.permission_mode).toBe('edit'); + expect(result.movements![0]?.permission_mode).toBe('edit'); }); it('should allow omitting permission_mode', () => { const config = { name: 'test-workflow', - steps: [ + movements: [ { name: 'plan', agent: 'planner', @@ -116,13 +116,13 @@ describe('WorkflowConfigRawSchema', () => { }; const result = WorkflowConfigRawSchema.parse(config); - expect(result.steps[0]?.permission_mode).toBeUndefined(); + expect(result.movements![0]?.permission_mode).toBeUndefined(); }); it('should reject invalid permission_mode', () => { const config = { name: 'test-workflow', - steps: [ + movements: [ { name: 'step1', agent: 'coder', @@ -135,10 +135,10 @@ describe('WorkflowConfigRawSchema', () => { expect(() => WorkflowConfigRawSchema.parse(config)).toThrow(); }); - it('should require at least one step', () => { + it('should require at least one movement', () => { const config = { name: 'empty-workflow', - steps: [], + movements: [], }; expect(() => WorkflowConfigRawSchema.parse(config)).toThrow(); diff --git a/src/__tests__/parallel-and-loader.test.ts b/src/__tests__/parallel-and-loader.test.ts index 1222833..8f4bc56 100644 --- a/src/__tests__/parallel-and-loader.test.ts +++ b/src/__tests__/parallel-and-loader.test.ts @@ -1,17 +1,17 @@ /** - * Tests for parallel step execution and ai() condition loader + * Tests for parallel movement execution and ai() condition loader * * Covers: - * - Schema validation for parallel sub-steps - * - Workflow loader normalization of ai() conditions and parallel steps - * - Engine parallel step aggregation logic + * - Schema validation for parallel sub-movements + * - Workflow loader normalization of ai() conditions and parallel movements + * - Engine parallel movement aggregation logic */ import { describe, it, expect } from 'vitest'; import { WorkflowConfigRawSchema, ParallelSubStepRawSchema, WorkflowStepRawSchema } from '../core/models/index.js'; describe('ParallelSubStepRawSchema', () => { - it('should validate a valid parallel sub-step', () => { + it('should validate a valid parallel sub-movement', () => { const raw = { name: 'arch-review', agent: '~/.takt/agents/default/reviewer.md', @@ -22,7 +22,7 @@ describe('ParallelSubStepRawSchema', () => { expect(result.success).toBe(true); }); - it('should accept a sub-step without agent (instruction_template only)', () => { + it('should accept a sub-movement without agent (instruction_template only)', () => { const raw = { name: 'no-agent-step', instruction_template: 'Do something', @@ -54,7 +54,7 @@ describe('ParallelSubStepRawSchema', () => { } }); - it('should accept rules on sub-steps', () => { + it('should accept rules on sub-movements', () => { const raw = { name: 'reviewed', agent: '~/.takt/agents/default/reviewer.md', @@ -74,7 +74,7 @@ describe('ParallelSubStepRawSchema', () => { }); describe('WorkflowStepRawSchema with parallel', () => { - it('should accept a step with parallel sub-steps (no agent)', () => { + it('should accept a movement with parallel sub-movements (no agent)', () => { const raw = { name: 'parallel-review', parallel: [ @@ -90,7 +90,7 @@ describe('WorkflowStepRawSchema with parallel', () => { expect(result.success).toBe(true); }); - it('should accept a step with neither agent nor parallel (instruction_template only)', () => { + it('should accept a movement with neither agent nor parallel (instruction_template only)', () => { const raw = { name: 'orphan-step', instruction_template: 'Do something', @@ -100,7 +100,7 @@ describe('WorkflowStepRawSchema with parallel', () => { expect(result.success).toBe(true); }); - it('should accept a step with agent (no parallel)', () => { + it('should accept a movement with agent (no parallel)', () => { const raw = { name: 'normal-step', agent: 'coder.md', @@ -111,7 +111,7 @@ describe('WorkflowStepRawSchema with parallel', () => { expect(result.success).toBe(true); }); - it('should accept a step with empty parallel array (no agent, no parallel content)', () => { + it('should accept a movement with empty parallel array (no agent, no parallel content)', () => { const raw = { name: 'empty-parallel', parallel: [], @@ -122,11 +122,11 @@ describe('WorkflowStepRawSchema with parallel', () => { }); }); -describe('WorkflowConfigRawSchema with parallel steps', () => { - it('should validate a workflow with parallel step', () => { +describe('WorkflowConfigRawSchema with parallel movements', () => { + it('should validate a workflow with parallel movement', () => { const raw = { name: 'test-parallel-workflow', - steps: [ + movements: [ { name: 'plan', agent: 'planner.md', @@ -144,22 +144,22 @@ describe('WorkflowConfigRawSchema with parallel steps', () => { ], }, ], - initial_step: 'plan', + initial_movement: 'plan', max_iterations: 10, }; const result = WorkflowConfigRawSchema.safeParse(raw); expect(result.success).toBe(true); if (result.success) { - expect(result.data.steps).toHaveLength(2); - expect(result.data.steps[1].parallel).toHaveLength(2); + expect(result.data.movements).toHaveLength(2); + expect(result.data.movements[1].parallel).toHaveLength(2); } }); - it('should validate a workflow mixing normal and parallel steps', () => { + it('should validate a workflow mixing normal and parallel movements', () => { const raw = { name: 'mixed-workflow', - steps: [ + movements: [ { name: 'plan', agent: 'planner.md', rules: [{ condition: 'Done', next: 'implement' }] }, { name: 'implement', agent: 'coder.md', rules: [{ condition: 'Done', next: 'review' }] }, { @@ -171,14 +171,14 @@ describe('WorkflowConfigRawSchema with parallel steps', () => { rules: [{ condition: 'All pass', next: 'COMPLETE' }], }, ], - initial_step: 'plan', + initial_movement: 'plan', }; const result = WorkflowConfigRawSchema.safeParse(raw); expect(result.success).toBe(true); if (result.success) { - expect(result.data.steps[0].agent).toBe('planner.md'); - expect(result.data.steps[2].parallel).toHaveLength(2); + expect(result.data.movements[0].agent).toBe('planner.md'); + expect(result.data.movements[2].parallel).toHaveLength(2); } }); }); @@ -369,7 +369,7 @@ describe('aggregate condition evaluation logic', () => { { condition: 'rejected' }, ]; - it('all(): true when all sub-steps match', () => { + it('all(): true when all sub-movements match', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 0, rules }, @@ -377,7 +377,7 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('all', 'approved', subs)).toBe(true); }); - it('all(): false when some sub-steps do not match', () => { + it('all(): false when some sub-movements do not match', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 1, rules }, @@ -385,7 +385,7 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('all', 'approved', subs)).toBe(false); }); - it('all(): false when sub-step has no matched rule', () => { + it('all(): false when sub-movement has no matched rule', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: undefined, rules }, @@ -393,7 +393,7 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('all', 'approved', subs)).toBe(false); }); - it('all(): false when sub-step has no rules', () => { + it('all(): false when sub-movement has no rules', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 0, rules: undefined }, @@ -401,11 +401,11 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('all', 'approved', subs)).toBe(false); }); - it('all(): false with zero sub-steps', () => { + it('all(): false with zero sub-movements', () => { expect(evaluateAggregate('all', 'approved', [])).toBe(false); }); - it('any(): true when one sub-step matches', () => { + it('any(): true when one sub-movement matches', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 1, rules }, @@ -413,7 +413,7 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('any', 'rejected', subs)).toBe(true); }); - it('any(): true when all sub-steps match', () => { + it('any(): true when all sub-movements match', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 1, rules }, { name: 'b', matchedRuleIndex: 1, rules }, @@ -421,7 +421,7 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('any', 'rejected', subs)).toBe(true); }); - it('any(): false when no sub-steps match', () => { + it('any(): false when no sub-movements match', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 0, rules }, @@ -429,11 +429,11 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('any', 'rejected', subs)).toBe(false); }); - it('any(): false with zero sub-steps', () => { + it('any(): false with zero sub-movements', () => { expect(evaluateAggregate('any', 'rejected', [])).toBe(false); }); - it('any(): skips sub-steps without matched rule (does not count as match)', () => { + it('any(): skips sub-movements without matched rule (does not count as match)', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: undefined, rules }, { name: 'b', matchedRuleIndex: 1, rules }, @@ -441,7 +441,7 @@ describe('aggregate condition evaluation logic', () => { expect(evaluateAggregate('any', 'rejected', subs)).toBe(true); }); - it('any(): false when only unmatched sub-steps exist', () => { + it('any(): false when only unmatched sub-movements exist', () => { const subs: SubResult[] = [ { name: 'a', matchedRuleIndex: undefined, rules }, { name: 'b', matchedRuleIndex: undefined, rules }, @@ -473,8 +473,8 @@ describe('aggregate condition evaluation logic', () => { }); }); -describe('parallel step aggregation format', () => { - it('should aggregate sub-step outputs in the expected format', () => { +describe('parallel movement aggregation format', () => { + it('should aggregate sub-movement outputs in the expected format', () => { // Mirror the aggregation logic from engine.ts const subResults = [ { name: 'arch-review', content: 'Architecture looks good.\n## Result: APPROVE' }, @@ -492,7 +492,7 @@ describe('parallel step aggregation format', () => { expect(aggregatedContent).toContain('No security issues.'); }); - it('should handle single sub-step', () => { + it('should handle single sub-movement', () => { const subResults = [ { name: 'only-step', content: 'Single result' }, ]; @@ -505,7 +505,7 @@ describe('parallel step aggregation format', () => { expect(aggregatedContent).not.toContain('---'); }); - it('should handle empty content from sub-steps', () => { + it('should handle empty content from sub-movements', () => { const subResults = [ { name: 'step-a', content: '' }, { name: 'step-b', content: 'Has content' }, diff --git a/src/__tests__/parallel-logger.test.ts b/src/__tests__/parallel-logger.test.ts index 04fb359..e401ef4 100644 --- a/src/__tests__/parallel-logger.test.ts +++ b/src/__tests__/parallel-logger.test.ts @@ -18,7 +18,7 @@ describe('ParallelLogger', () => { describe('buildPrefix', () => { it('should build colored prefix with padding', () => { const logger = new ParallelLogger({ - subStepNames: ['arch-review', 'sec'], + subMovementNames: ['arch-review', 'sec'], writeFn, }); @@ -34,7 +34,7 @@ describe('ParallelLogger', () => { it('should cycle colors for index >= 4', () => { const logger = new ParallelLogger({ - subStepNames: ['a', 'b', 'c', 'd', 'e'], + subMovementNames: ['a', 'b', 'c', 'd', 'e'], writeFn, }); @@ -47,7 +47,7 @@ describe('ParallelLogger', () => { it('should assign correct colors in order', () => { const logger = new ParallelLogger({ - subStepNames: ['a', 'b', 'c', 'd'], + subMovementNames: ['a', 'b', 'c', 'd'], writeFn, }); @@ -59,7 +59,7 @@ describe('ParallelLogger', () => { it('should have no extra padding for longest name', () => { const logger = new ParallelLogger({ - subStepNames: ['long-name', 'short'], + subMovementNames: ['long-name', 'short'], writeFn, }); @@ -72,7 +72,7 @@ describe('ParallelLogger', () => { describe('text event line buffering', () => { it('should buffer partial line and output on newline', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a'], + subMovementNames: ['step-a'], writeFn, }); const handler = logger.createStreamHandler('step-a', 0); @@ -91,7 +91,7 @@ describe('ParallelLogger', () => { it('should handle multiple lines in single text event', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a'], + subMovementNames: ['step-a'], writeFn, }); const handler = logger.createStreamHandler('step-a', 0); @@ -104,7 +104,7 @@ describe('ParallelLogger', () => { it('should output empty line without prefix', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a'], + subMovementNames: ['step-a'], writeFn, }); const handler = logger.createStreamHandler('step-a', 0); @@ -118,7 +118,7 @@ describe('ParallelLogger', () => { it('should keep trailing partial in buffer', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a'], + subMovementNames: ['step-a'], writeFn, }); const handler = logger.createStreamHandler('step-a', 0); @@ -137,7 +137,7 @@ describe('ParallelLogger', () => { describe('block events (tool_use, tool_result, tool_output, thinking)', () => { it('should prefix tool_use events', () => { const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], writeFn, }); const handler = logger.createStreamHandler('sub-a', 0); @@ -154,7 +154,7 @@ describe('ParallelLogger', () => { it('should prefix tool_result events', () => { const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], writeFn, }); const handler = logger.createStreamHandler('sub-a', 0); @@ -170,7 +170,7 @@ describe('ParallelLogger', () => { it('should prefix multi-line tool output', () => { const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], writeFn, }); const handler = logger.createStreamHandler('sub-a', 0); @@ -187,7 +187,7 @@ describe('ParallelLogger', () => { it('should prefix thinking events', () => { const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], writeFn, }); const handler = logger.createStreamHandler('sub-a', 0); @@ -206,7 +206,7 @@ describe('ParallelLogger', () => { it('should delegate init event to parent callback', () => { const parentEvents: StreamEvent[] = []; const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], parentOnStream: (event) => parentEvents.push(event), writeFn, }); @@ -226,7 +226,7 @@ describe('ParallelLogger', () => { it('should delegate result event to parent callback', () => { const parentEvents: StreamEvent[] = []; const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], parentOnStream: (event) => parentEvents.push(event), writeFn, }); @@ -245,7 +245,7 @@ describe('ParallelLogger', () => { it('should delegate error event to parent callback', () => { const parentEvents: StreamEvent[] = []; const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], parentOnStream: (event) => parentEvents.push(event), writeFn, }); @@ -263,7 +263,7 @@ describe('ParallelLogger', () => { it('should not crash when no parent callback for delegated events', () => { const logger = new ParallelLogger({ - subStepNames: ['sub-a'], + subMovementNames: ['sub-a'], writeFn, }); const handler = logger.createStreamHandler('sub-a', 0); @@ -280,7 +280,7 @@ describe('ParallelLogger', () => { describe('flush', () => { it('should output remaining buffered content', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a', 'step-b'], + subMovementNames: ['step-a', 'step-b'], writeFn, }); const handlerA = logger.createStreamHandler('step-a', 0); @@ -300,7 +300,7 @@ describe('ParallelLogger', () => { it('should not output empty buffers', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a', 'step-b'], + subMovementNames: ['step-a', 'step-b'], writeFn, }); const handlerA = logger.createStreamHandler('step-a', 0); @@ -316,7 +316,7 @@ describe('ParallelLogger', () => { describe('printSummary', () => { it('should print completion summary', () => { const logger = new ParallelLogger({ - subStepNames: ['arch-review', 'security-review'], + subMovementNames: ['arch-review', 'security-review'], writeFn, }); @@ -337,7 +337,7 @@ describe('ParallelLogger', () => { it('should show (no result) for undefined condition', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a'], + subMovementNames: ['step-a'], writeFn, }); @@ -349,9 +349,9 @@ describe('ParallelLogger', () => { expect(fullOutput).toContain('(no result)'); }); - it('should right-pad sub-step names to align results', () => { + it('should right-pad sub-movement names to align results', () => { const logger = new ParallelLogger({ - subStepNames: ['short', 'very-long-name'], + subMovementNames: ['short', 'very-long-name'], writeFn, }); @@ -372,7 +372,7 @@ describe('ParallelLogger', () => { it('should flush remaining buffers before printing summary', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a'], + subMovementNames: ['step-a'], writeFn, }); const handler = logger.createStreamHandler('step-a', 0); @@ -392,10 +392,10 @@ describe('ParallelLogger', () => { }); }); - describe('interleaved output from multiple sub-steps', () => { + describe('interleaved output from multiple sub-movements', () => { it('should correctly interleave prefixed output', () => { const logger = new ParallelLogger({ - subStepNames: ['step-a', 'step-b'], + subMovementNames: ['step-a', 'step-b'], writeFn, }); const handlerA = logger.createStreamHandler('step-a', 0); diff --git a/src/__tests__/prompts.test.ts b/src/__tests__/prompts.test.ts index 875f652..0b8ba65 100644 --- a/src/__tests__/prompts.test.ts +++ b/src/__tests__/prompts.test.ts @@ -1,178 +1,230 @@ /** - * Tests for prompt loader utility (src/shared/prompts/index.ts) + * Tests for Markdown template loader (src/shared/prompts/index.ts) */ import { describe, it, expect, beforeEach } from 'vitest'; -import { getPrompt, getPromptObject, _resetCache } from '../shared/prompts/index.js'; +import { loadTemplate, renderTemplate, _resetCache } from '../shared/prompts/index.js'; beforeEach(() => { _resetCache(); }); -describe('getPrompt', () => { - it('returns a language-independent prompt by key (defaults to en)', () => { - const result = getPrompt('summarize.slugGenerator'); +describe('loadTemplate', () => { + it('loads an English template', () => { + const result = loadTemplate('score_slug_system_prompt', 'en'); expect(result).toContain('You are a slug generator'); }); - it('returns an English prompt when lang is "en"', () => { - const result = getPrompt('interactive.systemPrompt', 'en'); + it('loads an English interactive template', () => { + const result = loadTemplate('score_interactive_system_prompt', 'en'); expect(result).toContain('You are a task planning assistant'); }); - it('returns a Japanese prompt when lang is "ja"', () => { - const result = getPrompt('interactive.systemPrompt', 'ja'); + it('loads a Japanese template', () => { + const result = loadTemplate('score_interactive_system_prompt', 'ja'); expect(result).toContain('あなたはTAKT'); }); - it('throws for a non-existent key', () => { - expect(() => getPrompt('nonexistent.key')).toThrow('Prompt key not found: nonexistent.key'); - }); - - it('throws for a non-existent key with language', () => { - expect(() => getPrompt('nonexistent.key', 'en')).toThrow('Prompt key not found: nonexistent.key (lang: en)'); - }); - - it('returns prompt from en file when lang is explicitly "en"', () => { - const result = getPrompt('summarize.slugGenerator', 'en'); + it('loads score_slug_system_prompt with explicit lang', () => { + const result = loadTemplate('score_slug_system_prompt', 'en'); expect(result).toContain('You are a slug generator'); }); - describe('template variable substitution', () => { - it('replaces {variableName} placeholders with provided values', () => { - const result = getPrompt('claude.agentDefault', undefined, { agentName: 'test-agent' }); - expect(result).toContain('You are the test-agent agent'); - expect(result).toContain('Follow the standard test-agent workflow'); - }); - - it('leaves unmatched placeholders as-is', () => { - const result = getPrompt('claude.agentDefault', undefined, {}); - expect(result).toContain('{agentName}'); - }); - - it('replaces multiple different variables', () => { - const result = getPrompt('claude.judgePrompt', undefined, { - agentOutput: 'test output', - conditionList: '| 1 | Success |', - }); - expect(result).toContain('test output'); - expect(result).toContain('| 1 | Success |'); - }); + it('throws for a non-existent template with language', () => { + expect(() => loadTemplate('nonexistent_template', 'en')).toThrow('Template not found: nonexistent_template (lang: en)'); }); }); -describe('getPromptObject', () => { - it('returns an object for a given key and language', () => { - const result = getPromptObject<{ heading: string }>('instruction.metadata', 'en'); - expect(result.heading).toBe('## Execution Context'); +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'); }); - it('returns a Japanese object when lang is "ja"', () => { - const result = getPromptObject<{ heading: string }>('instruction.metadata', 'ja'); - expect(result.heading).toBe('## 実行コンテキスト'); + it('replaces undefined variables with empty string', () => { + const result = loadTemplate('perform_builtin_agent_system_prompt', 'en', {}); + expect(result).not.toContain('{{agentName}}'); + expect(result).toContain('You are the agent'); }); - it('throws for a non-existent key', () => { - expect(() => getPromptObject('nonexistent.key')).toThrow('Prompt key not found: nonexistent.key'); + it('replaces multiple different variables', () => { + const result = loadTemplate('perform_judge_message', 'en', { + agentOutput: 'test output', + conditionList: '| 1 | Success |', + }); + expect(result).toContain('test output'); + expect(result).toContain('| 1 | Success |'); }); + + it('replaces workflow info variables in interactive prompt', () => { + const result = loadTemplate('score_interactive_system_prompt', 'en', { + workflowInfo: true, + workflowName: 'my-workflow', + workflowDescription: 'Test description', + }); + expect(result).toContain('"my-workflow"'); + expect(result).toContain('Test description'); + }); +}); + +describe('renderTemplate', () => { + it('processes {{#if}} blocks with truthy value', () => { + const template = 'before{{#if show}}visible{{/if}}after'; + const result = renderTemplate(template, { show: true }); + expect(result).toBe('beforevisibleafter'); + }); + + it('processes {{#if}} blocks with falsy value', () => { + const template = 'before{{#if show}}visible{{/if}}after'; + const result = renderTemplate(template, { show: false }); + expect(result).toBe('beforeafter'); + }); + + it('processes {{#if}}...{{else}}...{{/if}} blocks', () => { + const template = '{{#if flag}}yes{{else}}no{{/if}}'; + expect(renderTemplate(template, { flag: true })).toBe('yes'); + expect(renderTemplate(template, { flag: false })).toBe('no'); + }); + + it('treats empty string as falsy', () => { + const template = '{{#if value}}has value{{else}}empty{{/if}}'; + expect(renderTemplate(template, { value: '' })).toBe('empty'); + }); + + it('treats non-empty string as truthy', () => { + const template = '{{#if value}}has value{{else}}empty{{/if}}'; + expect(renderTemplate(template, { value: 'hello' })).toBe('has value'); + }); + + it('handles undefined variable in condition as falsy', () => { + const template = '{{#if missing}}yes{{else}}no{{/if}}'; + expect(renderTemplate(template, {})).toBe('no'); + }); + + it('replaces boolean true with "true" string', () => { + const template = 'value is {{flag}}'; + expect(renderTemplate(template, { flag: true })).toBe('value is true'); + }); + + it('replaces boolean false with empty string', () => { + const template = 'value is [{{flag}}]'; + expect(renderTemplate(template, { flag: false })).toBe('value is []'); + }); +}); + +describe('template file existence', () => { + const allTemplates = [ + 'score_interactive_system_prompt', + 'score_summary_system_prompt', + 'score_slug_system_prompt', + 'perform_phase1_message', + 'perform_phase2_message', + 'perform_phase3_message', + 'perform_agent_system_prompt', + 'perform_builtin_agent_system_prompt', + 'perform_judge_message', + ]; + + for (const name of allTemplates) { + it(`en/${name}.md exists and is loadable`, () => { + expect(() => loadTemplate(name, 'en')).not.toThrow(); + }); + + it(`ja/${name}.md exists and is loadable`, () => { + expect(() => loadTemplate(name, 'ja')).not.toThrow(); + }); + } }); describe('caching', () => { - it('returns the same data on repeated calls', () => { - const first = getPrompt('summarize.slugGenerator'); - const second = getPrompt('summarize.slugGenerator'); + it('returns consistent results on repeated calls', () => { + const first = loadTemplate('score_slug_system_prompt', 'en'); + const second = loadTemplate('score_slug_system_prompt', 'en'); expect(first).toBe(second); }); it('reloads after cache reset', () => { - const first = getPrompt('summarize.slugGenerator'); + const first = loadTemplate('score_slug_system_prompt', 'en'); _resetCache(); - const second = getPrompt('summarize.slugGenerator'); + const second = loadTemplate('score_slug_system_prompt', 'en'); expect(first).toBe(second); }); }); -describe('YAML content integrity', () => { - it('contains all expected top-level keys in en', () => { - expect(() => getPrompt('interactive.systemPrompt', 'en')).not.toThrow(); - expect(() => getPrompt('interactive.summaryPrompt', 'en')).not.toThrow(); - expect(() => getPrompt('interactive.workflowInfo', 'en')).not.toThrow(); - expect(() => getPrompt('interactive.conversationLabel', 'en')).not.toThrow(); - expect(() => getPrompt('interactive.noTranscript', 'en')).not.toThrow(); - expect(() => getPrompt('summarize.slugGenerator')).not.toThrow(); - expect(() => getPrompt('claude.agentDefault')).not.toThrow(); - expect(() => getPrompt('claude.judgePrompt')).not.toThrow(); - expect(() => getPromptObject('instruction.metadata', 'en')).not.toThrow(); - expect(() => getPromptObject('instruction.sections', 'en')).not.toThrow(); - expect(() => getPromptObject('instruction.reportOutput', 'en')).not.toThrow(); - expect(() => getPromptObject('instruction.reportPhase', 'en')).not.toThrow(); - expect(() => getPromptObject('instruction.reportSections', 'en')).not.toThrow(); - expect(() => getPrompt('instruction.statusJudgment.header', 'en')).not.toThrow(); - expect(() => getPromptObject('instruction.statusRules', 'en')).not.toThrow(); +describe('template content integrity', () => { + it('score_interactive_system_prompt contains core instructions', () => { + const en = loadTemplate('score_interactive_system_prompt', 'en'); + expect(en).toContain('task planning assistant'); + + const ja = loadTemplate('score_interactive_system_prompt', 'ja'); + expect(ja).toContain('あなたはTAKT'); }); - it('contains all expected top-level keys in ja', () => { - expect(() => getPrompt('interactive.systemPrompt', 'ja')).not.toThrow(); - expect(() => getPrompt('interactive.summaryPrompt', 'ja')).not.toThrow(); - expect(() => getPrompt('interactive.workflowInfo', 'ja')).not.toThrow(); - expect(() => getPrompt('interactive.conversationLabel', 'ja')).not.toThrow(); - expect(() => getPrompt('interactive.noTranscript', 'ja')).not.toThrow(); - expect(() => getPrompt('summarize.slugGenerator', 'ja')).not.toThrow(); - expect(() => getPrompt('claude.agentDefault', 'ja')).not.toThrow(); - expect(() => getPrompt('claude.judgePrompt', 'ja')).not.toThrow(); - expect(() => getPromptObject('instruction.metadata', 'ja')).not.toThrow(); - expect(() => getPromptObject('instruction.sections', 'ja')).not.toThrow(); - expect(() => getPromptObject('instruction.reportOutput', 'ja')).not.toThrow(); - expect(() => getPromptObject('instruction.reportPhase', 'ja')).not.toThrow(); - expect(() => getPromptObject('instruction.reportSections', 'ja')).not.toThrow(); - expect(() => getPrompt('instruction.statusJudgment.header', 'ja')).not.toThrow(); - expect(() => getPromptObject('instruction.statusRules', 'ja')).not.toThrow(); + it('score_slug_system_prompt contains format specification', () => { + const result = loadTemplate('score_slug_system_prompt', 'en'); + expect(result).toContain('verb-noun'); + expect(result).toContain('max 30 chars'); }); - it('instruction.metadata has all required fields', () => { - const en = getPromptObject>('instruction.metadata', 'en'); - expect(en).toHaveProperty('heading'); - expect(en).toHaveProperty('workingDirectory'); - expect(en).toHaveProperty('rulesHeading'); - expect(en).toHaveProperty('noCommit'); - expect(en).toHaveProperty('noCd'); - expect(en).toHaveProperty('editEnabled'); - expect(en).toHaveProperty('editDisabled'); - expect(en).toHaveProperty('note'); + it('perform_builtin_agent_system_prompt contains {{agentName}} placeholder', () => { + const result = loadTemplate('perform_builtin_agent_system_prompt', 'en'); + expect(result).toContain('{{agentName}}'); }); - it('instruction.sections has all required fields', () => { - const en = getPromptObject>('instruction.sections', 'en'); - expect(en).toHaveProperty('workflowContext'); - expect(en).toHaveProperty('iteration'); - expect(en).toHaveProperty('step'); - expect(en).toHaveProperty('userRequest'); - expect(en).toHaveProperty('instructions'); + it('perform_agent_system_prompt contains {{agentDefinition}} placeholder', () => { + const result = loadTemplate('perform_agent_system_prompt', 'en'); + expect(result).toContain('{{agentDefinition}}'); }); - it('instruction.statusRules has appendixInstruction with {tag} placeholder', () => { - const en = getPromptObject<{ appendixInstruction: string }>('instruction.statusRules', 'en'); - expect(en.appendixInstruction).toContain('{tag}'); + it('perform_judge_message contains {{agentOutput}} and {{conditionList}} placeholders', () => { + const result = loadTemplate('perform_judge_message', 'en'); + expect(result).toContain('{{agentOutput}}'); + expect(result).toContain('{{conditionList}}'); }); - it('en and ja files have the same key structure', () => { - // Verify a sampling of keys exist in both languages - const stringKeys = [ - 'interactive.systemPrompt', - 'summarize.slugGenerator', - 'claude.agentDefault', + it('perform_phase1_message contains execution context and rules sections', () => { + const en = loadTemplate('perform_phase1_message', 'en'); + expect(en).toContain('## Execution Context'); + 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('## Instructions'); + }); + + it('perform_phase1_message contains workflow context variables', () => { + const en = loadTemplate('perform_phase1_message', 'en'); + expect(en).toContain('{{iteration}}'); + expect(en).toContain('{{movement}}'); + expect(en).toContain('{{workingDirectory}}'); + }); + + it('perform_phase2_message contains report-specific rules', () => { + const en = loadTemplate('perform_phase2_message', 'en'); + expect(en).toContain('Do NOT modify project source files'); + expect(en).toContain('## Instructions'); + + const ja = loadTemplate('perform_phase2_message', 'ja'); + expect(ja).toContain('プロジェクトのソースファイルを変更しないでください'); + }); + + it('perform_phase3_message contains criteria and output variables', () => { + const en = loadTemplate('perform_phase3_message', 'en'); + expect(en).toContain('{{criteriaTable}}'); + expect(en).toContain('{{outputList}}'); + }); + + it('MD files contain only prompt body (no front matter)', () => { + const templates = [ + 'score_interactive_system_prompt', + 'score_summary_system_prompt', + 'perform_phase1_message', + 'perform_phase2_message', ]; - for (const key of stringKeys) { - expect(() => getPrompt(key, 'en')).not.toThrow(); - expect(() => getPrompt(key, 'ja')).not.toThrow(); - } - const objectKeys = [ - 'instruction.metadata', - 'instruction.sections', - ]; - for (const key of objectKeys) { - expect(() => getPromptObject(key, 'en')).not.toThrow(); - expect(() => getPromptObject(key, 'ja')).not.toThrow(); + for (const name of templates) { + const content = loadTemplate(name, 'en'); + expect(content).not.toMatch(/^---\n/); } }); }); diff --git a/src/__tests__/review-only-workflow.test.ts b/src/__tests__/review-only-workflow.test.ts index 9a65074..9e08b9b 100644 --- a/src/__tests__/review-only-workflow.test.ts +++ b/src/__tests__/review-only-workflow.test.ts @@ -4,7 +4,7 @@ * Covers: * - Workflow YAML files (EN/JA) load and pass schema validation * - Workflow structure: plan -> reviewers (parallel) -> supervise -> pr-comment - * - All steps have edit: false + * - All movements have edit: false * - pr-commenter agent has Bash in allowed_tools * - Routing rules for local vs PR comment flows */ @@ -18,7 +18,7 @@ import { WorkflowConfigRawSchema } from '../core/models/index.js'; const RESOURCES_DIR = join(import.meta.dirname, '../../resources/global'); function loadReviewOnlyYaml(lang: 'en' | 'ja') { - const filePath = join(RESOURCES_DIR, lang, 'workflows', 'review-only.yaml'); + const filePath = join(RESOURCES_DIR, lang, 'pieces', 'review-only.yaml'); const content = readFileSync(filePath, 'utf-8'); return parseYaml(content); } @@ -31,27 +31,27 @@ describe('review-only workflow (EN)', () => { expect(result.success).toBe(true); }); - it('should have correct name and initial_step', () => { + it('should have correct name and initial_movement', () => { expect(raw.name).toBe('review-only'); - expect(raw.initial_step).toBe('plan'); + expect(raw.initial_movement).toBe('plan'); }); it('should have max_iterations of 10', () => { expect(raw.max_iterations).toBe(10); }); - it('should have 4 steps: plan, reviewers, supervise, pr-comment', () => { - const stepNames = raw.steps.map((s: { name: string }) => s.name); - expect(stepNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']); + it('should have 4 movements: plan, reviewers, supervise, pr-comment', () => { + const movementNames = raw.movements.map((s: { name: string }) => s.name); + expect(movementNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']); }); - it('should have all steps with edit: false', () => { - for (const step of raw.steps) { - if (step.edit !== undefined) { - expect(step.edit).toBe(false); + it('should have all movements with edit: false', () => { + for (const movement of raw.movements) { + if (movement.edit !== undefined) { + expect(movement.edit).toBe(false); } - if (step.parallel) { - for (const sub of step.parallel) { + if (movement.parallel) { + for (const sub of movement.parallel) { if (sub.edit !== undefined) { expect(sub.edit).toBe(false); } @@ -60,8 +60,8 @@ describe('review-only workflow (EN)', () => { } }); - it('should have reviewers step with 3 parallel sub-steps', () => { - const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); + it('should have reviewers movement with 3 parallel sub-movements', () => { + const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers'); expect(reviewers).toBeDefined(); expect(reviewers.parallel).toHaveLength(3); @@ -69,8 +69,8 @@ describe('review-only workflow (EN)', () => { expect(subNames).toEqual(['arch-review', 'security-review', 'ai-review']); }); - it('should have reviewers step with aggregate rules', () => { - const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); + it('should have reviewers movement with aggregate rules', () => { + const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers'); expect(reviewers.rules).toHaveLength(2); expect(reviewers.rules[0].condition).toBe('all("approved")'); expect(reviewers.rules[0].next).toBe('supervise'); @@ -78,8 +78,8 @@ describe('review-only workflow (EN)', () => { expect(reviewers.rules[1].next).toBe('supervise'); }); - it('should have supervise step with routing rules for local and PR flows', () => { - const supervise = raw.steps.find((s: { name: string }) => s.name === 'supervise'); + it('should have supervise movement with routing rules for local and PR flows', () => { + const supervise = raw.movements.find((s: { name: string }) => s.name === 'supervise'); expect(supervise.rules).toHaveLength(3); const conditions = supervise.rules.map((r: { condition: string }) => r.condition); @@ -97,40 +97,40 @@ describe('review-only workflow (EN)', () => { expect(rejectRule.next).toBe('ABORT'); }); - it('should have pr-comment step with Bash in allowed_tools', () => { - const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); + it('should have pr-comment movement with Bash in allowed_tools', () => { + const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment'); expect(prComment).toBeDefined(); expect(prComment.allowed_tools).toContain('Bash'); }); - it('should have pr-comment step using pr-commenter agent', () => { - const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); + it('should have pr-comment movement using pr-commenter agent', () => { + const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment'); expect(prComment.agent).toContain('review/pr-commenter.md'); }); - it('should have plan step reusing default planner agent', () => { - const plan = raw.steps.find((s: { name: string }) => s.name === 'plan'); + it('should have plan movement reusing default planner agent', () => { + const plan = raw.movements.find((s: { name: string }) => s.name === 'plan'); expect(plan.agent).toContain('default/planner.md'); }); - it('should have supervise step reusing default supervisor agent', () => { - const supervise = raw.steps.find((s: { name: string }) => s.name === 'supervise'); + it('should have supervise movement reusing default supervisor agent', () => { + const supervise = raw.movements.find((s: { name: string }) => s.name === 'supervise'); expect(supervise.agent).toContain('default/supervisor.md'); }); - it('should not have any step with edit: true', () => { - for (const step of raw.steps) { - expect(step.edit).not.toBe(true); - if (step.parallel) { - for (const sub of step.parallel) { + it('should not have any movement with edit: true', () => { + for (const movement of raw.movements) { + expect(movement.edit).not.toBe(true); + if (movement.parallel) { + for (const sub of movement.parallel) { expect(sub.edit).not.toBe(true); } } } }); - it('reviewer sub-steps should not have Bash in allowed_tools', () => { - const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); + it('reviewer sub-movements should not have Bash in allowed_tools', () => { + const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers'); for (const sub of reviewers.parallel) { expect(sub.allowed_tools).not.toContain('Bash'); } @@ -145,42 +145,42 @@ describe('review-only workflow (JA)', () => { expect(result.success).toBe(true); }); - it('should have correct name and initial_step', () => { + it('should have correct name and initial_movement', () => { expect(raw.name).toBe('review-only'); - expect(raw.initial_step).toBe('plan'); + expect(raw.initial_movement).toBe('plan'); }); - it('should have same step structure as EN version', () => { - const stepNames = raw.steps.map((s: { name: string }) => s.name); - expect(stepNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']); + it('should have same movement structure as EN version', () => { + const movementNames = raw.movements.map((s: { name: string }) => s.name); + expect(movementNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']); }); - it('should have reviewers step with 3 parallel sub-steps', () => { - const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); + it('should have reviewers movement with 3 parallel sub-movements', () => { + const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers'); expect(reviewers.parallel).toHaveLength(3); const subNames = reviewers.parallel.map((s: { name: string }) => s.name); expect(subNames).toEqual(['arch-review', 'security-review', 'ai-review']); }); - it('should have all steps with edit: false or undefined', () => { - for (const step of raw.steps) { - expect(step.edit).not.toBe(true); - if (step.parallel) { - for (const sub of step.parallel) { + it('should have all movements with edit: false or undefined', () => { + for (const movement of raw.movements) { + expect(movement.edit).not.toBe(true); + if (movement.parallel) { + for (const sub of movement.parallel) { expect(sub.edit).not.toBe(true); } } } }); - it('should have pr-comment step with Bash in allowed_tools', () => { - const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); + it('should have pr-comment movement with Bash in allowed_tools', () => { + const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment'); expect(prComment.allowed_tools).toContain('Bash'); }); it('should have same aggregate rules on reviewers', () => { - const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); + const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers'); expect(reviewers.rules[0].condition).toBe('all("approved")'); expect(reviewers.rules[1].condition).toBe('any("needs_fix")'); }); @@ -230,7 +230,7 @@ describe('pr-commenter agent files', () => { describe('pr-comment instruction_template contains workflow-specific procedures', () => { it('EN: should reference specific report files', () => { const raw = loadReviewOnlyYaml('en'); - const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); + const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment'); const template = prComment.instruction_template; expect(template).toContain('01-architect-review.md'); expect(template).toContain('02-security-review.md'); @@ -240,7 +240,7 @@ describe('pr-comment instruction_template contains workflow-specific procedures' it('JA: should reference specific report files', () => { const raw = loadReviewOnlyYaml('ja'); - const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); + const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment'); const template = prComment.instruction_template; expect(template).toContain('01-architect-review.md'); expect(template).toContain('02-security-review.md'); diff --git a/src/__tests__/transitions.test.ts b/src/__tests__/transitions.test.ts index 4a0ba2c..6503c82 100644 --- a/src/__tests__/transitions.test.ts +++ b/src/__tests__/transitions.test.ts @@ -1,12 +1,12 @@ /** - * Tests for workflow transitions module + * Tests for workflow transitions module (movement-based) */ import { describe, it, expect } from 'vitest'; -import { determineNextStepByRules } from '../core/workflow/index.js'; -import type { WorkflowStep } from '../core/models/index.js'; +import { determineNextMovementByRules } from '../core/workflow/index.js'; +import type { WorkflowMovement } from '../core/models/index.js'; -function createStepWithRules(rules: { condition: string; next: string }[]): WorkflowStep { +function createMovementWithRules(rules: { condition: string; next: string }[]): WorkflowMovement { return { name: 'test-step', agent: 'test-agent', @@ -20,29 +20,29 @@ function createStepWithRules(rules: { condition: string; next: string }[]): Work }; } -describe('determineNextStepByRules', () => { - it('should return next step for valid rule index', () => { - const step = createStepWithRules([ +describe('determineNextMovementByRules', () => { + it('should return next movement for valid rule index', () => { + const step = createMovementWithRules([ { condition: 'Clear', next: 'implement' }, { condition: 'Blocked', next: 'ABORT' }, ]); - expect(determineNextStepByRules(step, 0)).toBe('implement'); - expect(determineNextStepByRules(step, 1)).toBe('ABORT'); + expect(determineNextMovementByRules(step, 0)).toBe('implement'); + expect(determineNextMovementByRules(step, 1)).toBe('ABORT'); }); it('should return null for out-of-bounds index', () => { - const step = createStepWithRules([ + const step = createMovementWithRules([ { condition: 'Clear', next: 'implement' }, ]); - expect(determineNextStepByRules(step, 1)).toBeNull(); - expect(determineNextStepByRules(step, -1)).toBeNull(); - expect(determineNextStepByRules(step, 100)).toBeNull(); + expect(determineNextMovementByRules(step, 1)).toBeNull(); + expect(determineNextMovementByRules(step, -1)).toBeNull(); + expect(determineNextMovementByRules(step, 100)).toBeNull(); }); - it('should return null when step has no rules', () => { - const step: WorkflowStep = { + it('should return null when movement has no rules', () => { + const step: WorkflowMovement = { name: 'test-step', agent: 'test-agent', agentDisplayName: 'Test Agent', @@ -50,20 +50,20 @@ describe('determineNextStepByRules', () => { passPreviousResponse: false, }; - expect(determineNextStepByRules(step, 0)).toBeNull(); + expect(determineNextMovementByRules(step, 0)).toBeNull(); }); - it('should handle COMPLETE as next step', () => { - const step = createStepWithRules([ + it('should handle COMPLETE as next movement', () => { + const step = createMovementWithRules([ { condition: 'All passed', next: 'COMPLETE' }, ]); - expect(determineNextStepByRules(step, 0)).toBe('COMPLETE'); + expect(determineNextMovementByRules(step, 0)).toBe('COMPLETE'); }); it('should return null when rule exists but next is undefined', () => { - // Parallel sub-step rules may omit `next` (optional field) - const step: WorkflowStep = { + // Parallel sub-movement rules may omit `next` (optional field) + const step: WorkflowMovement = { name: 'sub-step', agent: 'test-agent', agentDisplayName: 'Test Agent', @@ -75,7 +75,7 @@ describe('determineNextStepByRules', () => { ], }; - expect(determineNextStepByRules(step, 0)).toBeNull(); - expect(determineNextStepByRules(step, 1)).toBeNull(); + expect(determineNextMovementByRules(step, 0)).toBeNull(); + expect(determineNextMovementByRules(step, 1)).toBeNull(); }); }); diff --git a/src/__tests__/workflow-builtin-toggle.test.ts b/src/__tests__/workflow-builtin-toggle.test.ts index c872109..581d70d 100644 --- a/src/__tests__/workflow-builtin-toggle.test.ts +++ b/src/__tests__/workflow-builtin-toggle.test.ts @@ -20,7 +20,7 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { const { listWorkflows } = await import('../infra/config/loaders/workflowLoader.js'); const SAMPLE_WORKFLOW = `name: test-workflow -steps: +movements: - name: step1 agent: coder instruction: "{task}" diff --git a/src/__tests__/workflow-categories.test.ts b/src/__tests__/workflow-categories.test.ts index 9cbc6be..1634cdf 100644 --- a/src/__tests__/workflow-categories.test.ts +++ b/src/__tests__/workflow-categories.test.ts @@ -23,10 +23,10 @@ import { const SAMPLE_WORKFLOW = `name: test-workflow description: Test workflow -initial_step: step1 +initial_movement: step1 max_iterations: 1 -steps: +movements: - name: step1 agent: coder instruction: "{task}" diff --git a/src/__tests__/workflow-category-config.test.ts b/src/__tests__/workflow-category-config.test.ts index 2fb7a8b..db08ce3 100644 --- a/src/__tests__/workflow-category-config.test.ts +++ b/src/__tests__/workflow-category-config.test.ts @@ -75,8 +75,8 @@ function createWorkflowMap(entries: { name: string; source: 'builtin' | 'user' | source: entry.source, config: { name: entry.name, - steps: [], - initialStep: 'start', + movements: [], + initialMovement: 'start', maxIterations: 1, }, }); diff --git a/src/__tests__/workflow-expert-parallel.test.ts b/src/__tests__/workflow-expert-parallel.test.ts index 67c7185..6514cae 100644 --- a/src/__tests__/workflow-expert-parallel.test.ts +++ b/src/__tests__/workflow-expert-parallel.test.ts @@ -3,11 +3,11 @@ * * Validates that: * - expert and expert-cqrs workflows load successfully via loadWorkflow - * - The reviewers step is a parallel step with expected sub-steps - * - ai_review routes to reviewers (not individual review steps) - * - fix step routes back to reviewers - * - Aggregate rules (all/any) are configured on the reviewers step - * - Sub-step rules use simple approved/needs_fix conditions + * - 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 + * - Aggregate rules (all/any) are configured on the reviewers movement + * - Sub-movement rules use simple approved/needs_fix conditions */ import { describe, it, expect } from 'vitest'; @@ -21,15 +21,15 @@ describe('expert workflow parallel structure', () => { expect(workflow!.name).toBe('expert'); }); - it('should have a reviewers parallel step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have a reviewers parallel movement', () => { + const reviewers = workflow!.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-steps', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have arch-review, frontend-review, security-review, qa-review as sub-movements', () => { + const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); const subNames = reviewers!.parallel!.map((s) => s.name); expect(subNames).toContain('arch-review'); expect(subNames).toContain('frontend-review'); @@ -37,16 +37,16 @@ describe('expert workflow parallel structure', () => { expect(subNames).toContain('qa-review'); }); - it('should have aggregate rules on reviewers step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have aggregate rules on reviewers movement', () => { + const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); expect(reviewers!.rules).toBeDefined(); const conditions = reviewers!.rules!.map((r) => r.condition); expect(conditions).toContain('all("approved")'); expect(conditions).toContain('any("needs_fix")'); }); - it('should have simple approved/needs_fix rules on each sub-step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have simple approved/needs_fix rules on each sub-movement', () => { + const reviewers = workflow!.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,39 +56,39 @@ describe('expert workflow parallel structure', () => { }); it('should route ai_review to reviewers', () => { - const aiReview = workflow!.steps.find((s) => s.name === 'ai_review'); + const aiReview = workflow!.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 step routing back to reviewers', () => { - const fix = workflow!.steps.find((s) => s.name === 'fix'); + it('should have a unified fix movement routing back to reviewers', () => { + const fix = workflow!.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 steps', () => { - const stepNames = workflow!.steps.map((s) => s.name); - expect(stepNames).not.toContain('architect_review'); - expect(stepNames).not.toContain('fix_architect'); - expect(stepNames).not.toContain('frontend_review'); - expect(stepNames).not.toContain('fix_frontend'); - expect(stepNames).not.toContain('security_review'); - expect(stepNames).not.toContain('fix_security'); - expect(stepNames).not.toContain('qa_review'); - expect(stepNames).not.toContain('fix_qa'); + it('should not have individual review/fix movements', () => { + const movementNames = workflow!.movements.map((s) => s.name); + expect(movementNames).not.toContain('architect_review'); + expect(movementNames).not.toContain('fix_architect'); + expect(movementNames).not.toContain('frontend_review'); + expect(movementNames).not.toContain('fix_frontend'); + expect(movementNames).not.toContain('security_review'); + expect(movementNames).not.toContain('fix_security'); + expect(movementNames).not.toContain('qa_review'); + expect(movementNames).not.toContain('fix_qa'); }); it('should route reviewers all("approved") to supervise', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + const reviewers = workflow!.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!.steps.find((s) => s.name === 'reviewers'); + const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); const needsFixRule = reviewers!.rules!.find((r) => r.condition === 'any("needs_fix")'); expect(needsFixRule!.next).toBe('fix'); }); @@ -102,15 +102,15 @@ describe('expert-cqrs workflow parallel structure', () => { expect(workflow!.name).toBe('expert-cqrs'); }); - it('should have a reviewers parallel step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have a reviewers parallel movement', () => { + const reviewers = workflow!.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!.steps.find((s) => s.name === 'reviewers'); + const reviewers = workflow!.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'); @@ -119,16 +119,16 @@ describe('expert-cqrs workflow parallel structure', () => { expect(subNames).toContain('qa-review'); }); - it('should have aggregate rules on reviewers step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have aggregate rules on reviewers movement', () => { + const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); expect(reviewers!.rules).toBeDefined(); const conditions = reviewers!.rules!.map((r) => r.condition); expect(conditions).toContain('all("approved")'); expect(conditions).toContain('any("needs_fix")'); }); - it('should have simple approved/needs_fix rules on each sub-step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should have simple approved/needs_fix rules on each sub-movement', () => { + const reviewers = workflow!.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,33 +138,33 @@ describe('expert-cqrs workflow parallel structure', () => { }); it('should route ai_review to reviewers', () => { - const aiReview = workflow!.steps.find((s) => s.name === 'ai_review'); + const aiReview = workflow!.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 step routing back to reviewers', () => { - const fix = workflow!.steps.find((s) => s.name === 'fix'); + it('should have a unified fix movement routing back to reviewers', () => { + const fix = workflow!.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 steps', () => { - const stepNames = workflow!.steps.map((s) => s.name); - expect(stepNames).not.toContain('cqrs_es_review'); - expect(stepNames).not.toContain('fix_cqrs_es'); - expect(stepNames).not.toContain('frontend_review'); - expect(stepNames).not.toContain('fix_frontend'); - expect(stepNames).not.toContain('security_review'); - expect(stepNames).not.toContain('fix_security'); - expect(stepNames).not.toContain('qa_review'); - expect(stepNames).not.toContain('fix_qa'); + it('should not have individual review/fix movements', () => { + const movementNames = workflow!.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'); + expect(movementNames).not.toContain('fix_frontend'); + expect(movementNames).not.toContain('security_review'); + expect(movementNames).not.toContain('fix_security'); + expect(movementNames).not.toContain('qa_review'); + expect(movementNames).not.toContain('fix_qa'); }); - it('should use cqrs-es-reviewer agent for the first sub-step', () => { - const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); + it('should use cqrs-es-reviewer agent for the first sub-movement', () => { + const reviewers = workflow!.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__/workflowLoader.test.ts b/src/__tests__/workflowLoader.test.ts index 71c87a2..2649cb0 100644 --- a/src/__tests__/workflowLoader.test.ts +++ b/src/__tests__/workflowLoader.test.ts @@ -15,10 +15,10 @@ import { const SAMPLE_WORKFLOW = `name: test-workflow description: Test workflow -initial_step: step1 +initial_movement: step1 max_iterations: 1 -steps: +movements: - name: step1 agent: coder instruction: "{task}" @@ -172,10 +172,10 @@ describe('loadAllWorkflows with project-local', () => { const overrideWorkflow = `name: project-override description: Project override -initial_step: step1 +initial_movement: step1 max_iterations: 1 -steps: +movements: - name: step1 agent: coder instruction: "{task}" diff --git a/src/agents/runner.ts b/src/agents/runner.ts index b27199b..818d40f 100644 --- a/src/agents/runner.ts +++ b/src/agents/runner.ts @@ -13,6 +13,7 @@ import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js'; import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js'; import { createLogger } from '../shared/utils/index.js'; +import { loadTemplate } from '../shared/prompts/index.js'; import type { RunAgentOptions } from './types.js'; // Re-export for backward compatibility @@ -192,8 +193,11 @@ export class AgentRunner { }); // 1. If agentPath is provided (resolved file exists), load prompt from file + // and wrap it through the perform_agent_system_prompt template if (options.agentPath) { - const systemPrompt = AgentRunner.loadAgentPromptFromPath(options.agentPath); + const agentDefinition = AgentRunner.loadAgentPromptFromPath(options.agentPath); + const language = options.language ?? 'en'; + const systemPrompt = loadTemplate('perform_agent_system_prompt', language, { agentDefinition }); const providerType = AgentRunner.resolveProvider(options.cwd, options); const provider = getProvider(providerType); return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt)); diff --git a/src/agents/types.ts b/src/agents/types.ts index 4addd54..62c2c12 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -3,7 +3,7 @@ */ import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/index.js'; -import type { PermissionMode } from '../core/models/index.js'; +import type { PermissionMode, Language } from '../core/models/index.js'; export type { StreamCallback }; @@ -26,4 +26,6 @@ export interface RunAgentOptions { onAskUserQuestion?: AskUserQuestionHandler; /** Bypass all permission checks (sacrifice-my-pc mode) */ bypassPermissions?: boolean; + /** Language for template resolution */ + language?: Language; } diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index 850675e..a16b9af 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -1,13 +1,14 @@ /** * CLI subcommand definitions * - * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config). + * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config, prompt). */ import { clearAgentSessions, getCurrentWorkflow } 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 { previewPrompts } from '../../features/prompt/index.js'; import { program, resolvedCwd } from './program.js'; import { resolveAgentOverrides } from './helpers.js'; @@ -72,3 +73,11 @@ program .action(async (key?: string) => { await switchConfig(resolvedCwd, key); }); + +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); + }); diff --git a/src/core/models/index.ts b/src/core/models/index.ts index 24b2ecb..554d24e 100644 --- a/src/core/models/index.ts +++ b/src/core/models/index.ts @@ -9,6 +9,7 @@ export type { AgentResponse, SessionState, WorkflowRule, + WorkflowMovement, WorkflowStep, LoopDetectionConfig, WorkflowConfig, diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index 1ec14b5..5262d8c 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -103,7 +103,7 @@ export const ReportFieldSchema = z.union([ export const WorkflowRuleSchema = z.object({ /** Human-readable condition text */ condition: z.string().min(1), - /** Next step name (e.g., implement, COMPLETE, ABORT). Optional for parallel sub-steps (parent handles routing). */ + /** Next movement name (e.g., implement, COMPLETE, ABORT). Optional for parallel sub-movements (parent handles routing). */ next: z.string().min(1).optional(), /** Template for additional AI output */ appendix: z.string().optional(), @@ -113,8 +113,8 @@ export const WorkflowRuleSchema = z.object({ interactive_only: z.boolean().optional(), }); -/** Sub-step schema for parallel execution */ -export const ParallelSubStepRawSchema = z.object({ +/** Sub-movement schema for parallel execution */ +export const ParallelSubMovementRawSchema = z.object({ name: z.string().min(1), agent: z.string().optional(), agent_name: z.string().optional(), @@ -130,43 +130,58 @@ export const ParallelSubStepRawSchema = z.object({ pass_previous_response: z.boolean().optional().default(true), }); -/** Workflow step schema - raw YAML format */ -export const WorkflowStepRawSchema = z.object({ +/** Workflow movement schema - raw YAML format */ +export const WorkflowMovementRawSchema = z.object({ name: z.string().min(1), description: z.string().optional(), - /** Agent is required for normal steps, optional for parallel container steps */ + /** Agent is required for normal movements, optional for parallel container movements */ agent: z.string().optional(), - /** Session handling for this step */ + /** Session handling for this movement */ session: z.enum(['continue', 'refresh']).optional(), /** Display name for the agent (shown in output). Falls back to agent basename if not specified */ agent_name: z.string().optional(), allowed_tools: z.array(z.string()).optional(), provider: z.enum(['claude', 'codex', 'mock']).optional(), model: z.string().optional(), - /** Permission mode for tool execution in this step */ + /** Permission mode for tool execution in this movement */ permission_mode: PermissionModeSchema.optional(), - /** Whether this step is allowed to edit project files */ + /** Whether this movement is allowed to edit project files */ edit: z.boolean().optional(), instruction: z.string().optional(), instruction_template: z.string().optional(), - /** Rules for step routing */ + /** Rules for movement routing */ rules: z.array(WorkflowRuleSchema).optional(), - /** Report file(s) for this step */ + /** Report file(s) for this movement */ report: ReportFieldSchema.optional(), pass_previous_response: z.boolean().optional().default(true), - /** Sub-steps to execute in parallel */ - parallel: z.array(ParallelSubStepRawSchema).optional(), + /** Sub-movements to execute in parallel */ + parallel: z.array(ParallelSubMovementRawSchema).optional(), }); -/** Workflow configuration schema - raw YAML format */ +/** @deprecated Use WorkflowMovementRawSchema instead */ +export const WorkflowStepRawSchema = WorkflowMovementRawSchema; + +/** @deprecated Use ParallelSubMovementRawSchema instead */ +export const ParallelSubStepRawSchema = ParallelSubMovementRawSchema; + +/** Workflow configuration schema - raw YAML format (accepts both `movements` and legacy `steps`) */ export const WorkflowConfigRawSchema = z.object({ name: z.string().min(1), description: z.string().optional(), - steps: z.array(WorkflowStepRawSchema).min(1), + /** Preferred key: movements */ + movements: z.array(WorkflowMovementRawSchema).min(1).optional(), + /** @deprecated Use `movements` instead */ + steps: z.array(WorkflowMovementRawSchema).min(1).optional(), + /** Preferred key: initial_movement */ + initial_movement: z.string().optional(), + /** @deprecated Use `initial_movement` instead */ initial_step: z.string().optional(), max_iterations: z.number().int().positive().optional().default(10), answer_agent: z.string().optional(), -}); +}).refine( + (data) => (data.movements && data.movements.length > 0) || (data.steps && data.steps.length > 0), + { message: 'Workflow must have at least one movement (use `movements` or legacy `steps` key)' } +); /** Custom agent configuration schema */ export const CustomAgentConfigSchema = z.object({ diff --git a/src/core/models/types.ts b/src/core/models/types.ts index 4622fc7..77331be 100644 --- a/src/core/models/types.ts +++ b/src/core/models/types.ts @@ -28,6 +28,7 @@ export type { WorkflowRule, ReportConfig, ReportObjectConfig, + WorkflowMovement, WorkflowStep, LoopDetectionConfig, WorkflowConfig, diff --git a/src/core/models/workflow-types.ts b/src/core/models/workflow-types.ts index 3d3d7f6..5104156 100644 --- a/src/core/models/workflow-types.ts +++ b/src/core/models/workflow-types.ts @@ -9,7 +9,7 @@ import type { AgentResponse } from './response.js'; export interface WorkflowRule { /** Human-readable condition text */ condition: string; - /** Next step name (e.g., implement, COMPLETE, ABORT). Optional for parallel sub-steps. */ + /** Next movement name (e.g., implement, COMPLETE, ABORT). Optional for parallel sub-movements. */ next?: string; /** Template for additional AI output */ appendix?: string; @@ -23,16 +23,16 @@ export interface WorkflowRule { aiConditionText?: string; /** Whether this condition uses all()/any() aggregate expression (set by loader) */ isAggregateCondition?: boolean; - /** Aggregate type: 'all' requires all sub-steps match, 'any' requires at least one (set by loader) */ + /** Aggregate type: 'all' requires all sub-movements match, 'any' requires at least one (set by loader) */ aggregateType?: 'all' | 'any'; - /** The condition text(s) inside all("...")/any("...") to match against sub-step results (set by loader). - * - string: all sub-steps must match this single condition (e.g., all("approved")) - * - string[]: each sub-step must match the corresponding condition by index (e.g., all("A", "B")) + /** The condition text(s) inside all("...")/any("...") to match against sub-movement results (set by loader). + * - string: all sub-movements must match this single condition (e.g., all("approved")) + * - string[]: each sub-movement must match the corresponding condition by index (e.g., all("A", "B")) */ aggregateConditionText?: string | string[]; } -/** Report file configuration for a workflow step (label: path pair) */ +/** Report file configuration for a workflow movement (label: path pair) */ export interface ReportConfig { /** Display label (e.g., "Scope", "Decisions") */ label: string; @@ -50,39 +50,42 @@ export interface ReportObjectConfig { format?: string; } -/** Single step in a workflow */ -export interface WorkflowStep { +/** Single movement in a workflow */ +export interface WorkflowMovement { name: string; - /** Brief description of this step's role in the workflow */ + /** Brief description of this movement's role in the workflow */ description?: string; - /** Agent name, path, or inline prompt as specified in workflow YAML. Undefined when step runs without an agent. */ + /** Agent name, path, or inline prompt as specified in workflow YAML. Undefined when movement runs without an agent. */ agent?: string; - /** Session handling for this step */ + /** Session handling for this movement */ session?: 'continue' | 'refresh'; /** Display name for the agent (shown in output). Falls back to agent basename if not specified */ agentDisplayName: string; - /** Allowed tools for this step (optional, passed to agent execution) */ + /** Allowed tools for this movement (optional, passed to agent execution) */ allowedTools?: string[]; /** Resolved absolute path to agent prompt file (set by loader) */ agentPath?: string; - /** Provider override for this step */ + /** Provider override for this movement */ provider?: 'claude' | 'codex' | 'mock'; - /** Model override for this step */ + /** Model override for this movement */ model?: string; - /** Permission mode for tool execution in this step */ + /** Permission mode for tool execution in this movement */ permissionMode?: PermissionMode; - /** Whether this step is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */ + /** Whether this movement is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */ edit?: boolean; instructionTemplate: string; - /** Rules for step routing */ + /** Rules for movement routing */ rules?: WorkflowRule[]; /** Report file configuration. Single string, array of label:path, or object with order/format. */ report?: string | ReportConfig[] | ReportObjectConfig; passPreviousResponse: boolean; - /** Sub-steps to execute in parallel. When set, this step runs all sub-steps concurrently. */ - parallel?: WorkflowStep[]; + /** Sub-movements to execute in parallel. When set, this movement runs all sub-movements concurrently. */ + parallel?: WorkflowMovement[]; } +/** @deprecated Use WorkflowMovement instead */ +export type WorkflowStep = WorkflowMovement; + /** Loop detection configuration */ export interface LoopDetectionConfig { /** Maximum consecutive runs of the same step before triggering (default: 10) */ @@ -95,8 +98,8 @@ export interface LoopDetectionConfig { export interface WorkflowConfig { name: string; description?: string; - steps: WorkflowStep[]; - initialStep: string; + movements: WorkflowMovement[]; + initialMovement: string; maxIterations: number; /** Loop detection settings */ loopDetection?: LoopDetectionConfig; @@ -111,12 +114,12 @@ export interface WorkflowConfig { /** Runtime state of a workflow execution */ export interface WorkflowState { workflowName: string; - currentStep: string; + currentMovement: string; iteration: number; - stepOutputs: Map; + movementOutputs: Map; userInputs: string[]; agentSessions: Map; - /** Per-step iteration counters (how many times each step has been executed) */ - stepIterations: Map; + /** Per-movement iteration counters (how many times each movement has been executed) */ + movementIterations: Map; status: 'running' | 'completed' | 'aborted'; } diff --git a/src/core/workflow/constants.ts b/src/core/workflow/constants.ts index 272bbb7..136f86a 100644 --- a/src/core/workflow/constants.ts +++ b/src/core/workflow/constants.ts @@ -2,12 +2,17 @@ * Workflow engine constants * * Contains all constants used by the workflow engine including - * special step names, limits, and error messages. + * special movement names, limits, and error messages. */ -/** Special step names for workflow termination */ -export const COMPLETE_STEP = 'COMPLETE'; -export const ABORT_STEP = 'ABORT'; +/** Special movement names for workflow termination */ +export const COMPLETE_MOVEMENT = 'COMPLETE'; +export const ABORT_MOVEMENT = 'ABORT'; + +/** @deprecated Use COMPLETE_MOVEMENT instead */ +export const COMPLETE_STEP = COMPLETE_MOVEMENT; +/** @deprecated Use ABORT_MOVEMENT instead */ +export const ABORT_STEP = ABORT_MOVEMENT; /** Maximum user inputs to store */ export const MAX_USER_INPUTS = 100; @@ -15,9 +20,9 @@ export const MAX_INPUT_LENGTH = 10000; /** Error messages */ export const ERROR_MESSAGES = { - LOOP_DETECTED: (stepName: string, count: number) => - `Loop detected: step "${stepName}" ran ${count} times consecutively without progress.`, - UNKNOWN_STEP: (stepName: string) => `Unknown step: ${stepName}`, - STEP_EXECUTION_FAILED: (message: string) => `Step execution failed: ${message}`, + LOOP_DETECTED: (movementName: string, count: number) => + `Loop detected: movement "${movementName}" ran ${count} times consecutively without progress.`, + UNKNOWN_MOVEMENT: (movementName: string) => `Unknown movement: ${movementName}`, + MOVEMENT_EXECUTION_FAILED: (message: string) => `Movement execution failed: ${message}`, MAX_ITERATIONS_REACHED: 'Max iterations reached', }; diff --git a/src/core/workflow/engine/StepExecutor.ts b/src/core/workflow/engine/MovementExecutor.ts similarity index 66% rename from src/core/workflow/engine/StepExecutor.ts rename to src/core/workflow/engine/MovementExecutor.ts index caa5110..83f92fe 100644 --- a/src/core/workflow/engine/StepExecutor.ts +++ b/src/core/workflow/engine/MovementExecutor.ts @@ -1,5 +1,5 @@ /** - * Executes a single workflow step through the 3-phase model. + * Executes a single workflow movement through the 3-phase model. * * Phase 1: Main agent execution (with tools) * Phase 2: Report output (Write-only, optional) @@ -9,7 +9,7 @@ import { existsSync } from 'node:fs'; import { join } from 'node:path'; import type { - WorkflowStep, + WorkflowMovement, WorkflowState, AgentResponse, Language, @@ -19,49 +19,49 @@ import { runAgent } from '../../../agents/runner.js'; import { InstructionBuilder, isReportObjectConfig } from '../instruction/InstructionBuilder.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { detectMatchedRule } from '../evaluation/index.js'; -import { incrementStepIteration, getPreviousOutput } from './state-manager.js'; +import { incrementMovementIteration, getPreviousOutput } from './state-manager.js'; import { createLogger } from '../../../shared/utils/index.js'; import type { OptionsBuilder } from './OptionsBuilder.js'; -const log = createLogger('step-executor'); +const log = createLogger('movement-executor'); -export interface StepExecutorDeps { +export interface MovementExecutorDeps { readonly optionsBuilder: OptionsBuilder; readonly getCwd: () => string; readonly getProjectCwd: () => string; readonly getReportDir: () => string; readonly getLanguage: () => Language | undefined; readonly getInteractive: () => boolean; - readonly getWorkflowSteps: () => ReadonlyArray<{ name: string; description?: string }>; - readonly detectRuleIndex: (content: string, stepName: string) => number; + readonly getWorkflowMovements: () => 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: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; - readonly onPhaseComplete?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + 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; } -export class StepExecutor { +export class MovementExecutor { constructor( - private readonly deps: StepExecutorDeps, + private readonly deps: MovementExecutorDeps, ) {} /** Build Phase 1 instruction from template */ buildInstruction( - step: WorkflowStep, - stepIteration: number, + step: WorkflowMovement, + movementIteration: number, state: WorkflowState, task: string, maxIterations: number, ): string { - const workflowSteps = this.deps.getWorkflowSteps(); + const workflowMovements = this.deps.getWorkflowMovements(); return new InstructionBuilder(step, { task, iteration: state.iteration, maxIterations, - stepIteration, + movementIteration, cwd: this.deps.getCwd(), projectCwd: this.deps.getProjectCwd(), userInputs: state.userInputs, @@ -69,39 +69,39 @@ export class StepExecutor { reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()), language: this.deps.getLanguage(), interactive: this.deps.getInteractive(), - workflowSteps, - currentStepIndex: workflowSteps.findIndex(s => s.name === step.name), + workflowMovements: workflowMovements, + currentMovementIndex: workflowMovements.findIndex(s => s.name === step.name), }).build(); } /** - * Execute a normal (non-parallel) step through all 3 phases. + * Execute a normal (non-parallel) movement through all 3 phases. * * Returns the final response (with matchedRuleIndex if a rule matched) * and the instruction used for Phase 1. */ - async runNormalStep( - step: WorkflowStep, + async runNormalMovement( + step: WorkflowMovement, state: WorkflowState, task: string, maxIterations: number, updateAgentSession: (agent: string, sessionId: string | undefined) => void, prebuiltInstruction?: string, ): Promise<{ response: AgentResponse; instruction: string }> { - const stepIteration = prebuiltInstruction - ? state.stepIterations.get(step.name) ?? 1 - : incrementStepIteration(state, step.name); - const instruction = prebuiltInstruction ?? this.buildInstruction(step, stepIteration, state, task, maxIterations); + const movementIteration = prebuiltInstruction + ? state.movementIterations.get(step.name) ?? 1 + : incrementMovementIteration(state, step.name); + const instruction = prebuiltInstruction ?? this.buildInstruction(step, movementIteration, state, task, maxIterations); const sessionKey = step.agent ?? step.name; - log.debug('Running step', { - step: step.name, + log.debug('Running movement', { + movement: step.name, agent: step.agent ?? '(none)', - stepIteration, + movementIteration, iteration: state.iteration, sessionId: state.agentSessions.get(sessionKey) ?? 'new', }); - // Phase 1: main execution (Write excluded if step has report) + // Phase 1: main execution (Write excluded if movement has report) this.deps.onPhaseStart?.(step, 1, 'execute', instruction); const agentOptions = this.deps.optionsBuilder.buildAgentOptions(step); let response = await runAgent(step.agent, instruction, agentOptions); @@ -112,7 +112,7 @@ export class StepExecutor { // Phase 2: report output (resume same session, Write only) if (step.report) { - await runReportPhase(step, stepIteration, phaseCtx); + await runReportPhase(step, movementIteration, phaseCtx); } // Phase 3: status judgment (resume session, no tools, output status tag) @@ -129,17 +129,17 @@ export class StepExecutor { callAiJudge: this.deps.callAiJudge, }); if (match) { - log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method }); + log.debug('Rule matched', { movement: step.name, ruleIndex: match.index, method: match.method }); response = { ...response, matchedRuleIndex: match.index, matchedRuleMethod: match.method }; } - state.stepOutputs.set(step.name, response); - this.emitStepReports(step); + state.movementOutputs.set(step.name, response); + this.emitMovementReports(step); return { response, instruction }; } - /** Emit step:report events for each report file that exists */ - emitStepReports(step: WorkflowStep): void { + /** Collect movement:report events for each report file that exists */ + emitMovementReports(step: WorkflowMovement): void { if (!step.report) return; const baseDir = join(this.deps.getProjectCwd(), this.deps.getReportDir()); @@ -156,21 +156,22 @@ export class StepExecutor { } // Collects report file paths that exist (used by WorkflowEngine to emit events) - private reportFiles: Array<{ step: WorkflowStep; filePath: string; fileName: string }> = []; + private reportFiles: Array<{ step: WorkflowMovement; filePath: string; fileName: string }> = []; /** Check if report file exists and collect for emission */ - private checkReportFile(step: WorkflowStep, baseDir: string, fileName: string): void { + private checkReportFile(step: WorkflowMovement, baseDir: string, fileName: string): void { const filePath = join(baseDir, fileName); if (existsSync(filePath)) { this.reportFiles.push({ step, filePath, fileName }); } } - /** Drain collected report files (called by engine after step execution) */ - drainReportFiles(): Array<{ step: WorkflowStep; filePath: string; fileName: string }> { + /** Drain collected report files (called by engine after movement execution) */ + drainReportFiles(): Array<{ step: WorkflowMovement; filePath: string; fileName: string }> { const files = this.reportFiles; this.reportFiles = []; return files; } } + diff --git a/src/core/workflow/engine/OptionsBuilder.ts b/src/core/workflow/engine/OptionsBuilder.ts index 0269693..d688539 100644 --- a/src/core/workflow/engine/OptionsBuilder.ts +++ b/src/core/workflow/engine/OptionsBuilder.ts @@ -6,7 +6,7 @@ */ import { join } from 'node:path'; -import type { WorkflowStep, WorkflowState, Language } from '../../models/types.js'; +import type { WorkflowMovement, WorkflowState, 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'; @@ -22,13 +22,14 @@ export class OptionsBuilder { ) {} /** Build common RunAgentOptions shared by all phases */ - buildBaseOptions(step: WorkflowStep): RunAgentOptions { + buildBaseOptions(step: WorkflowMovement): RunAgentOptions { return { cwd: this.getCwd(), agentPath: step.agentPath, provider: step.provider ?? this.engineOptions.provider, model: step.model ?? this.engineOptions.model, permissionMode: step.permissionMode, + language: this.getLanguage(), onStream: this.engineOptions.onStream, onPermissionRequest: this.engineOptions.onPermissionRequest, onAskUserQuestion: this.engineOptions.onAskUserQuestion, @@ -37,8 +38,8 @@ export class OptionsBuilder { } /** Build RunAgentOptions for Phase 1 (main execution) */ - buildAgentOptions(step: WorkflowStep): RunAgentOptions { - // Phase 1: exclude Write from allowedTools when step has report config AND edit is NOT enabled + buildAgentOptions(step: WorkflowMovement): 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 const allowedTools = step.report && step.edit !== true @@ -57,7 +58,7 @@ export class OptionsBuilder { /** Build RunAgentOptions for session-resume phases (Phase 2, Phase 3) */ buildResumeOptions( - step: WorkflowStep, + step: WorkflowMovement, sessionId: string, overrides: Pick, ): RunAgentOptions { @@ -75,8 +76,8 @@ export class OptionsBuilder { buildPhaseRunnerContext( state: WorkflowState, updateAgentSession: (agent: string, sessionId: string | undefined) => void, - onPhaseStart?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void, - onPhaseComplete?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => 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, ): PhaseRunnerContext { return { cwd: this.getCwd(), diff --git a/src/core/workflow/engine/ParallelRunner.ts b/src/core/workflow/engine/ParallelRunner.ts index 29d9e53..4b833d7 100644 --- a/src/core/workflow/engine/ParallelRunner.ts +++ b/src/core/workflow/engine/ParallelRunner.ts @@ -1,12 +1,12 @@ /** - * Executes parallel workflow steps concurrently and aggregates results. + * Executes parallel workflow movements concurrently and aggregates results. * * When onStream is provided, uses ParallelLogger to prefix each - * sub-step's output with `[name]` for readable interleaved display. + * sub-movement's output with `[name]` for readable interleaved display. */ import type { - WorkflowStep, + WorkflowMovement, WorkflowState, AgentResponse, } from '../../models/types.js'; @@ -14,29 +14,29 @@ import { runAgent } from '../../../agents/runner.js'; import { ParallelLogger } from './parallel-logger.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { detectMatchedRule } from '../evaluation/index.js'; -import { incrementStepIteration } from './state-manager.js'; +import { incrementMovementIteration } from './state-manager.js'; import { createLogger } from '../../../shared/utils/index.js'; import type { OptionsBuilder } from './OptionsBuilder.js'; -import type { StepExecutor } from './StepExecutor.js'; +import type { MovementExecutor } from './MovementExecutor.js'; import type { WorkflowEngineOptions, PhaseName } from '../types.js'; const log = createLogger('parallel-runner'); export interface ParallelRunnerDeps { readonly optionsBuilder: OptionsBuilder; - readonly stepExecutor: StepExecutor; + readonly movementExecutor: MovementExecutor; readonly engineOptions: WorkflowEngineOptions; readonly getCwd: () => string; readonly getReportDir: () => string; readonly getInteractive: () => boolean; - readonly detectRuleIndex: (content: string, stepName: string) => number; + readonly detectRuleIndex: (content: string, movementName: string) => number; readonly callAiJudge: ( agentOutput: string, conditions: Array<{ index: number; text: string }>, options: { cwd: string } ) => Promise; - readonly onPhaseStart?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; - readonly onPhaseComplete?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + 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; } export class ParallelRunner { @@ -45,28 +45,28 @@ export class ParallelRunner { ) {} /** - * Run a parallel step: execute all sub-steps concurrently, then aggregate results. - * The aggregated output becomes the parent step's response for rules evaluation. + * Run a parallel movement: execute all sub-movements concurrently, then aggregate results. + * The aggregated output becomes the parent movement's response for rules evaluation. */ - async runParallelStep( - step: WorkflowStep, + async runParallelMovement( + step: WorkflowMovement, state: WorkflowState, task: string, maxIterations: number, updateAgentSession: (agent: string, sessionId: string | undefined) => void, ): Promise<{ response: AgentResponse; instruction: string }> { - const subSteps = step.parallel!; - const stepIteration = incrementStepIteration(state, step.name); - log.debug('Running parallel step', { - step: step.name, - subSteps: subSteps.map(s => s.name), - stepIteration, + const subMovements = step.parallel!; + const movementIteration = incrementMovementIteration(state, step.name); + log.debug('Running parallel movement', { + movement: step.name, + subMovements: subMovements.map(s => s.name), + movementIteration, }); // Create parallel logger for prefixed output (only when streaming is enabled) const parallelLogger = this.deps.engineOptions.onStream ? new ParallelLogger({ - subStepNames: subSteps.map((s) => s.name), + subMovementNames: subMovements.map((s) => s.name), parentOnStream: this.deps.engineOptions.onStream, }) : undefined; @@ -80,46 +80,46 @@ export class ParallelRunner { callAiJudge: this.deps.callAiJudge, }; - // Run all sub-steps concurrently + // Run all sub-movements concurrently const subResults = await Promise.all( - subSteps.map(async (subStep, index) => { - const subIteration = incrementStepIteration(state, subStep.name); - const subInstruction = this.deps.stepExecutor.buildInstruction(subStep, subIteration, state, task, maxIterations); + subMovements.map(async (subMovement, index) => { + const subIteration = incrementMovementIteration(state, subMovement.name); + const subInstruction = this.deps.movementExecutor.buildInstruction(subMovement, subIteration, state, task, maxIterations); - // Phase 1: main execution (Write excluded if sub-step has report) - const baseOptions = this.deps.optionsBuilder.buildAgentOptions(subStep); + // Phase 1: main execution (Write excluded if sub-movement has report) + const baseOptions = this.deps.optionsBuilder.buildAgentOptions(subMovement); // Override onStream with parallel logger's prefixed handler (immutable) const agentOptions = parallelLogger - ? { ...baseOptions, onStream: parallelLogger.createStreamHandler(subStep.name, index) } + ? { ...baseOptions, onStream: parallelLogger.createStreamHandler(subMovement.name, index) } : baseOptions; - const subSessionKey = subStep.agent ?? subStep.name; - this.deps.onPhaseStart?.(subStep, 1, 'execute', subInstruction); - const subResponse = await runAgent(subStep.agent, subInstruction, agentOptions); + const subSessionKey = subMovement.agent ?? subMovement.name; + this.deps.onPhaseStart?.(subMovement, 1, 'execute', subInstruction); + const subResponse = await runAgent(subMovement.agent, subInstruction, agentOptions); updateAgentSession(subSessionKey, subResponse.sessionId); - this.deps.onPhaseComplete?.(subStep, 1, 'execute', subResponse.content, subResponse.status, subResponse.error); + this.deps.onPhaseComplete?.(subMovement, 1, 'execute', subResponse.content, subResponse.status, subResponse.error); - // Phase 2: report output for sub-step - if (subStep.report) { - await runReportPhase(subStep, subIteration, phaseCtx); + // Phase 2: report output for sub-movement + if (subMovement.report) { + await runReportPhase(subMovement, subIteration, phaseCtx); } - // Phase 3: status judgment for sub-step + // Phase 3: status judgment for sub-movement let subTagContent = ''; - if (needsStatusJudgmentPhase(subStep)) { - subTagContent = await runStatusJudgmentPhase(subStep, phaseCtx); + if (needsStatusJudgmentPhase(subMovement)) { + subTagContent = await runStatusJudgmentPhase(subMovement, phaseCtx); } - const match = await detectMatchedRule(subStep, subResponse.content, subTagContent, ruleCtx); + const match = await detectMatchedRule(subMovement, subResponse.content, subTagContent, ruleCtx); const finalResponse = match ? { ...subResponse, matchedRuleIndex: match.index, matchedRuleMethod: match.method } : subResponse; - state.stepOutputs.set(subStep.name, finalResponse); - this.deps.stepExecutor.emitStepReports(subStep); + state.movementOutputs.set(subMovement.name, finalResponse); + this.deps.movementExecutor.emitMovementReports(subMovement); - return { subStep, response: finalResponse, instruction: subInstruction }; + return { subMovement, response: finalResponse, instruction: subInstruction }; }), ); @@ -128,24 +128,24 @@ export class ParallelRunner { parallelLogger.printSummary( step.name, subResults.map((r) => ({ - name: r.subStep.name, - condition: r.response.matchedRuleIndex != null && r.subStep.rules - ? r.subStep.rules[r.response.matchedRuleIndex]?.condition + name: r.subMovement.name, + condition: r.response.matchedRuleIndex != null && r.subMovement.rules + ? r.subMovement.rules[r.response.matchedRuleIndex]?.condition : undefined, })), ); } - // Aggregate sub-step outputs into parent step's response + // Aggregate sub-movement outputs into parent movement's response const aggregatedContent = subResults - .map((r) => `## ${r.subStep.name}\n${r.response.content}`) + .map((r) => `## ${r.subMovement.name}\n${r.response.content}`) .join('\n\n---\n\n'); const aggregatedInstruction = subResults .map((r) => r.instruction) .join('\n\n'); - // Parent step uses aggregate conditions, so tagContent is empty + // Parent movement uses aggregate conditions, so tagContent is empty const match = await detectMatchedRule(step, aggregatedContent, '', ruleCtx); const aggregatedResponse: AgentResponse = { @@ -156,8 +156,8 @@ export class ParallelRunner { ...(match && { matchedRuleIndex: match.index, matchedRuleMethod: match.method }), }; - state.stepOutputs.set(step.name, aggregatedResponse); - this.deps.stepExecutor.emitStepReports(step); + state.movementOutputs.set(step.name, aggregatedResponse); + this.deps.movementExecutor.emitMovementReports(step); return { response: aggregatedResponse, instruction: aggregatedInstruction }; } diff --git a/src/core/workflow/engine/WorkflowEngine.ts b/src/core/workflow/engine/WorkflowEngine.ts index e47220c..8be2dd9 100644 --- a/src/core/workflow/engine/WorkflowEngine.ts +++ b/src/core/workflow/engine/WorkflowEngine.ts @@ -1,9 +1,9 @@ /** * Workflow execution engine. * - * Orchestrates the main execution loop: step transitions, abort handling, - * loop detection, and iteration limits. Delegates step execution to - * StepExecutor (normal steps) and ParallelRunner (parallel steps). + * Orchestrates the main execution loop: movement transitions, abort handling, + * loop detection, and iteration limits. Delegates movement execution to + * MovementExecutor (normal movements) and ParallelRunner (parallel movements). */ import { EventEmitter } from 'node:events'; @@ -12,22 +12,22 @@ import { join } from 'node:path'; import type { WorkflowConfig, WorkflowState, - WorkflowStep, + WorkflowMovement, AgentResponse, } from '../../models/types.js'; -import { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from '../constants.js'; +import { COMPLETE_MOVEMENT, ABORT_MOVEMENT, ERROR_MESSAGES } from '../constants.js'; import type { WorkflowEngineOptions } from '../types.js'; -import { determineNextStepByRules } from './transitions.js'; +import { determineNextMovementByRules } from './transitions.js'; import { LoopDetector } from './loop-detector.js'; import { handleBlocked } from './blocked-handler.js'; import { createInitialState, addUserInput as addUserInputToState, - incrementStepIteration, + incrementMovementIteration, } from './state-manager.js'; import { generateReportDir, getErrorMessage, createLogger } from '../../../shared/utils/index.js'; import { OptionsBuilder } from './OptionsBuilder.js'; -import { StepExecutor } from './StepExecutor.js'; +import { MovementExecutor } from './MovementExecutor.js'; import { ParallelRunner } from './ParallelRunner.js'; const log = createLogger('engine'); @@ -41,7 +41,7 @@ export type { IterationLimitCallback, WorkflowEngineOptions, } from '../types.js'; -export { COMPLETE_STEP, ABORT_STEP } from '../constants.js'; +export { COMPLETE_MOVEMENT, ABORT_MOVEMENT, COMPLETE_STEP, ABORT_STEP } from '../constants.js'; /** Workflow engine for orchestrating agent execution */ export class WorkflowEngine extends EventEmitter { @@ -56,9 +56,9 @@ export class WorkflowEngine extends EventEmitter { private abortRequested = false; private readonly optionsBuilder: OptionsBuilder; - private readonly stepExecutor: StepExecutor; + private readonly movementExecutor: MovementExecutor; private readonly parallelRunner: ParallelRunner; - private readonly detectRuleIndex: (content: string, stepName: string) => number; + private readonly detectRuleIndex: (content: string, movementName: string) => number; private readonly callAiJudge: ( agentOutput: string, conditions: Array<{ index: number; text: string }>, @@ -94,14 +94,14 @@ export class WorkflowEngine extends EventEmitter { () => this.options.language, ); - this.stepExecutor = new StepExecutor({ + this.movementExecutor = new MovementExecutor({ optionsBuilder: this.optionsBuilder, getCwd: () => this.cwd, getProjectCwd: () => this.projectCwd, getReportDir: () => this.reportDir, getLanguage: () => this.options.language, getInteractive: () => this.options.interactive === true, - getWorkflowSteps: () => this.config.steps.map(s => ({ name: s.name, description: s.description })), + getWorkflowMovements: () => this.config.movements.map(s => ({ name: s.name, description: s.description })), detectRuleIndex: this.detectRuleIndex, callAiJudge: this.callAiJudge, onPhaseStart: (step, phase, phaseName, instruction) => { @@ -114,7 +114,7 @@ export class WorkflowEngine extends EventEmitter { this.parallelRunner = new ParallelRunner({ optionsBuilder: this.optionsBuilder, - stepExecutor: this.stepExecutor, + movementExecutor: this.movementExecutor, engineOptions: this.options, getCwd: () => this.cwd, getReportDir: () => this.reportDir, @@ -131,8 +131,8 @@ export class WorkflowEngine extends EventEmitter { log.debug('WorkflowEngine initialized', { workflow: config.name, - steps: config.steps.map(s => s.name), - initialStep: config.initialStep, + movements: config.movements.map(s => s.name), + initialMovement: config.initialMovement, maxIterations: config.maxIterations, }); } @@ -159,21 +159,21 @@ export class WorkflowEngine extends EventEmitter { /** Validate workflow configuration at construction time */ private validateConfig(): void { - const initialStep = this.config.steps.find((s) => s.name === this.config.initialStep); - if (!initialStep) { - throw new Error(ERROR_MESSAGES.UNKNOWN_STEP(this.config.initialStep)); + const initialMovement = this.config.movements.find((s) => s.name === this.config.initialMovement); + if (!initialMovement) { + throw new Error(ERROR_MESSAGES.UNKNOWN_MOVEMENT(this.config.initialMovement)); } - const stepNames = new Set(this.config.steps.map((s) => s.name)); - stepNames.add(COMPLETE_STEP); - stepNames.add(ABORT_STEP); + const movementNames = new Set(this.config.movements.map((s) => s.name)); + movementNames.add(COMPLETE_MOVEMENT); + movementNames.add(ABORT_MOVEMENT); - for (const step of this.config.steps) { - if (step.rules) { - for (const rule of step.rules) { - if (rule.next && !stepNames.has(rule.next)) { + for (const movement of this.config.movements) { + if (movement.rules) { + for (const rule of movement.rules) { + if (rule.next && !movementNames.has(rule.next)) { throw new Error( - `Invalid rule in step "${step.name}": target step "${rule.next}" does not exist` + `Invalid rule in movement "${movement.name}": target movement "${rule.next}" does not exist` ); } } @@ -206,7 +206,7 @@ export class WorkflowEngine extends EventEmitter { return this.projectCwd; } - /** Request graceful abort: interrupt running queries and stop after current step */ + /** Request graceful abort: interrupt running queries and stop after current movement */ abort(): void { if (this.abortRequested) return; this.abortRequested = true; @@ -218,13 +218,13 @@ export class WorkflowEngine extends EventEmitter { return this.abortRequested; } - /** Get step by name */ - private getStep(name: string): WorkflowStep { - const step = this.config.steps.find((s) => s.name === name); - if (!step) { - throw new Error(ERROR_MESSAGES.UNKNOWN_STEP(name)); + /** Get movement by name */ + private getMovement(name: string): WorkflowMovement { + const movement = this.config.movements.find((s) => s.name === name); + if (!movement) { + throw new Error(ERROR_MESSAGES.UNKNOWN_MOVEMENT(name)); } - return step; + return movement; } /** Update agent session and notify via callback if session changed */ @@ -239,24 +239,24 @@ export class WorkflowEngine extends EventEmitter { } } - /** Emit step:report events collected by StepExecutor */ + /** Emit movement:report events collected by MovementExecutor */ private emitCollectedReports(): void { - for (const { step, filePath, fileName } of this.stepExecutor.drainReportFiles()) { - this.emit('step:report', step, filePath, fileName); + for (const { step, filePath, fileName } of this.movementExecutor.drainReportFiles()) { + this.emit('movement:report', step, filePath, fileName); } } - /** Run a single step (delegates to ParallelRunner if step has parallel sub-steps) */ - private async runStep(step: WorkflowStep, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> { + /** 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 }> { const updateSession = this.updateAgentSession.bind(this); let result: { response: AgentResponse; instruction: string }; if (step.parallel && step.parallel.length > 0) { - result = await this.parallelRunner.runParallelStep( + result = await this.parallelRunner.runParallelMovement( step, this.state, this.task, this.config.maxIterations, updateSession, ); } else { - result = await this.stepExecutor.runNormalStep( + result = await this.movementExecutor.runNormalMovement( step, this.state, this.task, this.config.maxIterations, updateSession, prebuiltInstruction, ); } @@ -266,23 +266,23 @@ export class WorkflowEngine extends EventEmitter { } /** - * Determine next step for a completed step using rules-based routing. + * Determine next movement for a completed movement using rules-based routing. */ - private resolveNextStep(step: WorkflowStep, response: AgentResponse): string { + private resolveNextMovement(step: WorkflowMovement, response: AgentResponse): string { if (response.matchedRuleIndex != null && step.rules) { - const nextByRules = determineNextStepByRules(step, response.matchedRuleIndex); + const nextByRules = determineNextMovementByRules(step, response.matchedRuleIndex); if (nextByRules) { return nextByRules; } } - throw new Error(`No matching rule found for step "${step.name}" (status: ${response.status})`); + 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: WorkflowStep, stepIteration: number): string { - return this.stepExecutor.buildInstruction( - step, stepIteration, this.state, this.task, this.config.maxIterations, + buildInstruction(step: WorkflowMovement, movementIteration: number): string { + return this.movementExecutor.buildInstruction( + step, movementIteration, this.state, this.task, this.config.maxIterations, ); } @@ -302,7 +302,7 @@ export class WorkflowEngine extends EventEmitter { const additionalIterations = await this.options.onIterationLimit({ currentIteration: this.state.iteration, maxIterations: this.config.maxIterations, - currentStep: this.state.currentStep, + currentMovement: this.state.currentMovement, }); if (additionalIterations !== null && additionalIterations > 0) { @@ -319,43 +319,43 @@ export class WorkflowEngine extends EventEmitter { break; } - const step = this.getStep(this.state.currentStep); - const loopCheck = this.loopDetector.check(step.name); + const movement = this.getMovement(this.state.currentMovement); + const loopCheck = this.loopDetector.check(movement.name); if (loopCheck.shouldWarn) { - this.emit('step:loop_detected', step, loopCheck.count); + this.emit('movement:loop_detected', movement, loopCheck.count); } if (loopCheck.shouldAbort) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, ERROR_MESSAGES.LOOP_DETECTED(step.name, loopCheck.count)); + this.emit('workflow:abort', this.state, ERROR_MESSAGES.LOOP_DETECTED(movement.name, loopCheck.count)); break; } this.state.iteration++; - // Build instruction before emitting step:start so listeners can log it - const isParallel = step.parallel && step.parallel.length > 0; + // Build instruction before emitting movement:start so listeners can log it + const isParallel = movement.parallel && movement.parallel.length > 0; let prebuiltInstruction: string | undefined; if (!isParallel) { - const stepIteration = incrementStepIteration(this.state, step.name); - prebuiltInstruction = this.stepExecutor.buildInstruction( - step, stepIteration, this.state, this.task, this.config.maxIterations, + const movementIteration = incrementMovementIteration(this.state, movement.name); + prebuiltInstruction = this.movementExecutor.buildInstruction( + movement, movementIteration, this.state, this.task, this.config.maxIterations, ); } - this.emit('step:start', step, this.state.iteration, prebuiltInstruction ?? ''); + this.emit('movement:start', movement, this.state.iteration, prebuiltInstruction ?? ''); try { - const { response, instruction } = await this.runStep(step, prebuiltInstruction); - this.emit('step:complete', step, response, instruction); + const { response, instruction } = await this.runMovement(movement, prebuiltInstruction); + this.emit('movement:complete', movement, response, instruction); if (response.status === 'blocked') { - this.emit('step:blocked', step, response); - const result = await handleBlocked(step, response, this.options); + this.emit('movement:blocked', movement, response); + const result = await handleBlocked(movement, response, this.options); if (result.shouldContinue && result.userInput) { this.addUserInput(result.userInput); - this.emit('step:user_input', step, result.userInput); + this.emit('movement:user_input', movement, result.userInput); continue; } @@ -364,16 +364,16 @@ export class WorkflowEngine extends EventEmitter { break; } - const nextStep = this.resolveNextStep(step, response); - log.debug('Step transition', { - from: step.name, + const nextMovement = this.resolveNextMovement(movement, response); + log.debug('Movement transition', { + from: movement.name, status: response.status, matchedRuleIndex: response.matchedRuleIndex, - nextStep, + nextMovement, }); - if (response.matchedRuleIndex != null && step.rules) { - const matchedRule = step.rules[response.matchedRuleIndex]; + if (response.matchedRuleIndex != null && movement.rules) { + const matchedRule = movement.rules[response.matchedRuleIndex]; if (matchedRule?.requiresUserInput) { if (!this.options.onUserInput) { this.state.status = 'aborted'; @@ -381,7 +381,7 @@ export class WorkflowEngine extends EventEmitter { break; } const userInput = await this.options.onUserInput({ - step, + movement, response, prompt: response.content, }); @@ -391,32 +391,32 @@ export class WorkflowEngine extends EventEmitter { break; } this.addUserInput(userInput); - this.emit('step:user_input', step, userInput); - this.state.currentStep = step.name; + this.emit('movement:user_input', movement, userInput); + this.state.currentMovement = movement.name; continue; } } - if (nextStep === COMPLETE_STEP) { + if (nextMovement === COMPLETE_MOVEMENT) { this.state.status = 'completed'; this.emit('workflow:complete', this.state); break; } - if (nextStep === ABORT_STEP) { + if (nextMovement === ABORT_MOVEMENT) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, 'Workflow aborted by step transition'); + this.emit('workflow:abort', this.state, 'Workflow aborted by movement transition'); break; } - this.state.currentStep = nextStep; + this.state.currentMovement = nextMovement; } catch (error) { this.state.status = 'aborted'; if (this.abortRequested) { this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)'); } else { const message = getErrorMessage(error); - this.emit('workflow:abort', this.state, ERROR_MESSAGES.STEP_EXECUTION_FAILED(message)); + this.emit('workflow:abort', this.state, ERROR_MESSAGES.MOVEMENT_EXECUTION_FAILED(message)); } break; } @@ -428,62 +428,62 @@ export class WorkflowEngine extends EventEmitter { /** Run a single iteration (for interactive mode) */ async runSingleIteration(): Promise<{ response: AgentResponse; - nextStep: string; + nextMovement: string; isComplete: boolean; loopDetected?: boolean; }> { - const step = this.getStep(this.state.currentStep); - const loopCheck = this.loopDetector.check(step.name); + const movement = this.getMovement(this.state.currentMovement); + const loopCheck = this.loopDetector.check(movement.name); if (loopCheck.shouldAbort) { this.state.status = 'aborted'; return { response: { - agent: step.agent ?? step.name, + agent: movement.agent ?? movement.name, status: 'blocked', - content: ERROR_MESSAGES.LOOP_DETECTED(step.name, loopCheck.count), + content: ERROR_MESSAGES.LOOP_DETECTED(movement.name, loopCheck.count), timestamp: new Date(), }, - nextStep: ABORT_STEP, + nextMovement: ABORT_MOVEMENT, isComplete: true, loopDetected: true, }; } this.state.iteration++; - const { response } = await this.runStep(step); - const nextStep = this.resolveNextStep(step, response); - const isComplete = nextStep === COMPLETE_STEP || nextStep === ABORT_STEP; + const { response } = await this.runMovement(movement); + const nextMovement = this.resolveNextMovement(movement, response); + const isComplete = nextMovement === COMPLETE_MOVEMENT || nextMovement === ABORT_MOVEMENT; - if (response.matchedRuleIndex != null && step.rules) { - const matchedRule = step.rules[response.matchedRuleIndex]; + if (response.matchedRuleIndex != null && movement.rules) { + const matchedRule = movement.rules[response.matchedRuleIndex]; if (matchedRule?.requiresUserInput) { if (!this.options.onUserInput) { this.state.status = 'aborted'; - return { response, nextStep: ABORT_STEP, isComplete: true, loopDetected: loopCheck.isLoop }; + return { response, nextMovement: ABORT_MOVEMENT, isComplete: true, loopDetected: loopCheck.isLoop }; } const userInput = await this.options.onUserInput({ - step, + movement, response, prompt: response.content, }); if (userInput === null) { this.state.status = 'aborted'; - return { response, nextStep: ABORT_STEP, isComplete: true, loopDetected: loopCheck.isLoop }; + return { response, nextMovement: ABORT_MOVEMENT, isComplete: true, loopDetected: loopCheck.isLoop }; } this.addUserInput(userInput); - this.emit('step:user_input', step, userInput); - this.state.currentStep = step.name; - return { response, nextStep: step.name, isComplete: false, loopDetected: loopCheck.isLoop }; + this.emit('movement:user_input', movement, userInput); + this.state.currentMovement = movement.name; + return { response, nextMovement: movement.name, isComplete: false, loopDetected: loopCheck.isLoop }; } } if (!isComplete) { - this.state.currentStep = nextStep; + this.state.currentMovement = nextMovement; } else { - this.state.status = nextStep === COMPLETE_STEP ? 'completed' : 'aborted'; + this.state.status = nextMovement === COMPLETE_MOVEMENT ? 'completed' : 'aborted'; } - return { response, nextStep, isComplete, loopDetected: loopCheck.isLoop }; + return { response, nextMovement, isComplete, loopDetected: loopCheck.isLoop }; } } diff --git a/src/core/workflow/engine/blocked-handler.ts b/src/core/workflow/engine/blocked-handler.ts index d04222c..8930ba8 100644 --- a/src/core/workflow/engine/blocked-handler.ts +++ b/src/core/workflow/engine/blocked-handler.ts @@ -5,7 +5,7 @@ * requesting user input to continue. */ -import type { WorkflowStep, AgentResponse } from '../../models/types.js'; +import type { WorkflowMovement, AgentResponse } from '../../models/types.js'; import type { UserInputRequest, WorkflowEngineOptions } from '../types.js'; import { extractBlockedPrompt } from './transitions.js'; @@ -22,13 +22,13 @@ export interface BlockedHandlerResult { /** * Handle blocked status by requesting user input. * - * @param step - The step that is blocked + * @param step - The movement that is blocked * @param response - The blocked response from the agent * @param options - Workflow engine options containing callbacks * @returns Result indicating whether to continue and any user input */ export async function handleBlocked( - step: WorkflowStep, + step: WorkflowMovement, response: AgentResponse, options: WorkflowEngineOptions ): Promise { @@ -42,7 +42,7 @@ export async function handleBlocked( // Build the request const request: UserInputRequest = { - step, + movement: step, response, prompt, }; diff --git a/src/core/workflow/engine/index.ts b/src/core/workflow/engine/index.ts index 9b6ffdc..9158c88 100644 --- a/src/core/workflow/engine/index.ts +++ b/src/core/workflow/engine/index.ts @@ -5,6 +5,7 @@ */ export { WorkflowEngine } from './WorkflowEngine.js'; -export { StepExecutor } from './StepExecutor.js'; +export { MovementExecutor } from './MovementExecutor.js'; +export type { MovementExecutorDeps } from './MovementExecutor.js'; export { ParallelRunner } from './ParallelRunner.js'; export { OptionsBuilder } from './OptionsBuilder.js'; diff --git a/src/core/workflow/engine/loop-detector.ts b/src/core/workflow/engine/loop-detector.ts index 1f0cd85..684ca26 100644 --- a/src/core/workflow/engine/loop-detector.ts +++ b/src/core/workflow/engine/loop-detector.ts @@ -1,7 +1,7 @@ /** * Loop detection for workflow execution * - * Detects when a workflow step is executed repeatedly without progress, + * Detects when a workflow movement is executed repeatedly without progress, * which may indicate an infinite loop. */ @@ -15,10 +15,10 @@ const DEFAULT_LOOP_DETECTION: Required = { }; /** - * Loop detector for tracking consecutive same-step executions. + * Loop detector for tracking consecutive same-movement executions. */ export class LoopDetector { - private lastStepName: string | null = null; + private lastMovementName: string | null = null; private consecutiveCount = 0; private config: Required; @@ -30,15 +30,15 @@ export class LoopDetector { } /** - * Check if the given step execution would be a loop. + * Check if the given movement execution would be a loop. * Updates internal tracking state. */ - check(stepName: string): LoopCheckResult { - if (this.lastStepName === stepName) { + check(movementName: string): LoopCheckResult { + if (this.lastMovementName === movementName) { this.consecutiveCount++; } else { this.consecutiveCount = 1; - this.lastStepName = stepName; + this.lastMovementName = movementName; } const isLoop = this.consecutiveCount > this.config.maxConsecutiveSameStep; @@ -57,7 +57,7 @@ export class LoopDetector { * Reset the detector state. */ reset(): void { - this.lastStepName = null; + this.lastMovementName = null; this.consecutiveCount = 0; } diff --git a/src/core/workflow/engine/parallel-logger.ts b/src/core/workflow/engine/parallel-logger.ts index f7b28be..d99cd2c 100644 --- a/src/core/workflow/engine/parallel-logger.ts +++ b/src/core/workflow/engine/parallel-logger.ts @@ -1,20 +1,20 @@ /** - * Parallel step log display + * Parallel movement log display * - * Provides prefixed, color-coded interleaved output for parallel sub-steps. - * Each sub-step's stream output gets a `[name]` prefix with right-padding - * aligned to the longest sub-step name. + * Provides prefixed, color-coded interleaved output for parallel sub-movements. + * Each sub-movement's stream output gets a `[name]` prefix with right-padding + * aligned to the longest sub-movement name. */ import type { StreamCallback, StreamEvent } from '../types.js'; -/** ANSI color codes for sub-step prefixes (cycled in order) */ +/** ANSI color codes for sub-movement prefixes (cycled in order) */ const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green const RESET = '\x1b[0m'; export interface ParallelLoggerOptions { - /** Sub-step names (used to calculate prefix width) */ - subStepNames: string[]; + /** Sub-movement names (used to calculate prefix width) */ + subMovementNames: string[]; /** Parent onStream callback to delegate non-prefixed events */ parentOnStream?: StreamCallback; /** Override process.stdout.write for testing */ @@ -22,9 +22,9 @@ export interface ParallelLoggerOptions { } /** - * Logger for parallel step execution. + * Logger for parallel movement execution. * - * Creates per-sub-step StreamCallback wrappers that: + * Creates per-sub-movement StreamCallback wrappers that: * - Buffer partial lines until newline * - Prepend colored `[name]` prefix to each complete line * - Delegate init/result/error events to the parent callback @@ -36,17 +36,17 @@ export class ParallelLogger { private readonly writeFn: (text: string) => void; constructor(options: ParallelLoggerOptions) { - this.maxNameLength = Math.max(...options.subStepNames.map((n) => n.length)); + this.maxNameLength = Math.max(...options.subMovementNames.map((n) => n.length)); this.parentOnStream = options.parentOnStream; this.writeFn = options.writeFn ?? ((text: string) => process.stdout.write(text)); - for (const name of options.subStepNames) { + for (const name of options.subMovementNames) { this.lineBuffers.set(name, ''); } } /** - * Build the colored prefix string for a sub-step. + * Build the colored prefix string for a sub-movement. * Format: `\x1b[COLORm[name]\x1b[0m` + padding spaces */ buildPrefix(name: string, index: number): string { @@ -56,19 +56,19 @@ export class ParallelLogger { } /** - * Create a StreamCallback wrapper for a specific sub-step. + * Create a StreamCallback wrapper for a specific sub-movement. * * - `text`: buffered line-by-line with prefix * - `tool_use`, `tool_result`, `tool_output`, `thinking`: prefixed per-line, no buffering * - `init`, `result`, `error`: delegated to parent callback (no prefix) */ - createStreamHandler(subStepName: string, index: number): StreamCallback { - const prefix = this.buildPrefix(subStepName, index); + createStreamHandler(subMovementName: string, index: number): StreamCallback { + const prefix = this.buildPrefix(subMovementName, index); return (event: StreamEvent) => { switch (event.type) { case 'text': - this.handleTextEvent(subStepName, prefix, event.data.text); + this.handleTextEvent(subMovementName, prefix, event.data.text); break; case 'tool_use': @@ -144,13 +144,13 @@ export class ParallelLogger { } /** - * Flush remaining line buffers for all sub-steps. - * Call after all sub-steps complete to output any trailing partial lines. + * Flush remaining line buffers for all sub-movements. + * Call after all sub-movements complete to output any trailing partial lines. */ flush(): void { // Build prefixes for flush — need index mapping // Since we don't store index, iterate lineBuffers in insertion order - // (Map preserves insertion order, matching subStepNames order) + // (Map preserves insertion order, matching subMovementNames order) let index = 0; for (const [name, buffer] of this.lineBuffers) { if (buffer !== '') { @@ -163,7 +163,7 @@ export class ParallelLogger { } /** - * Print completion summary after all sub-steps finish. + * Print completion summary after all sub-movements finish. * * Format: * ``` @@ -174,7 +174,7 @@ export class ParallelLogger { * ``` */ printSummary( - parentStepName: string, + parentMovementName: string, results: Array<{ name: string; condition: string | undefined }>, ): void { this.flush(); @@ -188,7 +188,7 @@ export class ParallelLogger { }); // Header line: ── name results ── - const headerText = ` ${parentStepName} results `; + const headerText = ` ${parentMovementName} results `; const maxLineLength = Math.max( headerText.length + 4, // 4 for "── " + " ──" ...resultLines.map((l) => l.length), diff --git a/src/core/workflow/engine/state-manager.ts b/src/core/workflow/engine/state-manager.ts index 7716b00..a242517 100644 --- a/src/core/workflow/engine/state-manager.ts +++ b/src/core/workflow/engine/state-manager.ts @@ -36,23 +36,23 @@ export class StateManager { this.state = { workflowName: config.name, - currentStep: config.initialStep, + currentMovement: config.initialMovement, iteration: 0, - stepOutputs: new Map(), + movementOutputs: new Map(), userInputs, agentSessions, - stepIterations: new Map(), + movementIterations: new Map(), status: 'running', }; } /** - * Increment the iteration counter for a step and return the new value. + * Increment the iteration counter for a movement and return the new value. */ - incrementStepIteration(stepName: string): number { - const current = this.state.stepIterations.get(stepName) ?? 0; + incrementMovementIteration(movementName: string): number { + const current = this.state.movementIterations.get(movementName) ?? 0; const next = current + 1; - this.state.stepIterations.set(stepName, next); + this.state.movementIterations.set(movementName, next); return next; } @@ -68,10 +68,10 @@ export class StateManager { } /** - * Get the most recent step output. + * Get the most recent movement output. */ getPreviousOutput(): AgentResponse | undefined { - const outputs = Array.from(this.state.stepOutputs.values()); + const outputs = Array.from(this.state.movementOutputs.values()); return outputs[outputs.length - 1]; } } @@ -89,12 +89,12 @@ export function createInitialState( } /** - * Increment the iteration counter for a step and return the new value. + * Increment the iteration counter for a movement and return the new value. */ -export function incrementStepIteration(state: WorkflowState, stepName: string): number { - const current = state.stepIterations.get(stepName) ?? 0; +export function incrementMovementIteration(state: WorkflowState, movementName: string): number { + const current = state.movementIterations.get(movementName) ?? 0; const next = current + 1; - state.stepIterations.set(stepName, next); + state.movementIterations.set(movementName, next); return next; } @@ -110,9 +110,9 @@ export function addUserInput(state: WorkflowState, input: string): void { } /** - * Get the most recent step output. + * Get the most recent movement output. */ export function getPreviousOutput(state: WorkflowState): AgentResponse | undefined { - const outputs = Array.from(state.stepOutputs.values()); + const outputs = Array.from(state.movementOutputs.values()); return outputs[outputs.length - 1]; } diff --git a/src/core/workflow/engine/transitions.ts b/src/core/workflow/engine/transitions.ts index 22234b4..1b300c8 100644 --- a/src/core/workflow/engine/transitions.ts +++ b/src/core/workflow/engine/transitions.ts @@ -1,19 +1,19 @@ /** * Workflow state transition logic * - * Handles determining the next step based on rules-based routing. + * Handles determining the next movement based on rules-based routing. */ import type { - WorkflowStep, + WorkflowMovement, } from '../../models/types.js'; /** - * Determine next step using rules-based detection. - * Returns the next step name from the matched rule, or null if no rule matched. + * Determine next movement using rules-based detection. + * Returns the next movement name from the matched rule, or null if no rule matched. */ -export function determineNextStepByRules( - step: WorkflowStep, +export function determineNextMovementByRules( + step: WorkflowMovement, ruleIndex: number, ): string | null { const rule = step.rules?.[ruleIndex]; diff --git a/src/core/workflow/evaluation/AggregateEvaluator.ts b/src/core/workflow/evaluation/AggregateEvaluator.ts index daaa9d1..b6a9458 100644 --- a/src/core/workflow/evaluation/AggregateEvaluator.ts +++ b/src/core/workflow/evaluation/AggregateEvaluator.ts @@ -1,38 +1,38 @@ /** - * Aggregate condition evaluator for parallel workflow steps + * Aggregate condition evaluator for parallel workflow movements * - * Evaluates all()/any() aggregate conditions against sub-step results. + * Evaluates all()/any() aggregate conditions against sub-movement results. */ -import type { WorkflowStep, WorkflowState } from '../../models/types.js'; +import type { WorkflowMovement, WorkflowState } from '../../models/types.js'; import { createLogger } from '../../../shared/utils/index.js'; const log = createLogger('aggregate-evaluator'); /** - * Evaluates aggregate conditions (all()/any()) for parallel parent steps. + * Evaluates aggregate conditions (all()/any()) for parallel parent movements. * - * For each aggregate rule, checks the matched condition text of sub-steps: - * - all("X"): true when ALL sub-steps have matched condition === X - * - all("A", "B"): true when 1st sub-step matches "A" AND 2nd sub-step matches "B" (order-based) - * - any("X"): true when at least ONE sub-step has matched condition === X - * - any("A", "B"): true when at least ONE sub-step matches "A" OR "B" + * For each aggregate rule, checks the matched condition text of sub-movements: + * - all("X"): true when ALL sub-movements have matched condition === X + * - all("A", "B"): true when 1st sub-movement matches "A" AND 2nd sub-movement matches "B" (order-based) + * - any("X"): true when at least ONE sub-movement has matched condition === X + * - any("A", "B"): true when at least ONE sub-movement matches "A" OR "B" * * Edge cases per spec: - * - Sub-step with no matched rule: all() → false, any() → skip that sub-step - * - No sub-steps (0 件): both → false - * - Non-parallel step: both → false - * - all("A", "B") with wrong number of sub-steps: false (logged as error) + * - Sub-movement with no matched rule: all() → false, any() → skip that sub-movement + * - No sub-movements (0 件): both → false + * - Non-parallel movement: both → false + * - all("A", "B") with wrong number of sub-movements: false (logged as error) */ export class AggregateEvaluator { constructor( - private readonly step: WorkflowStep, + private readonly step: WorkflowMovement, private readonly state: WorkflowState, ) {} /** * Evaluate aggregate conditions. - * Returns the 0-based rule index in the step's rules array, or -1 if no match. + * Returns the 0-based rule index in the movement's rules array, or -1 if no match. */ evaluate(): number { if (!this.step.rules || !this.step.parallel || this.step.parallel.length === 0) return -1; @@ -43,22 +43,22 @@ export class AggregateEvaluator { continue; } - const subSteps = this.step.parallel; + const subMovements = this.step.parallel; const targetCondition = rule.aggregateConditionText; if (rule.aggregateType === 'all') { - // Multiple conditions: order-based matching (1st sub-step matches 1st condition, etc.) + // Multiple conditions: order-based matching (1st sub-movement matches 1st condition, etc.) if (Array.isArray(targetCondition)) { - if (targetCondition.length !== subSteps.length) { + if (targetCondition.length !== subMovements.length) { log.error('all() condition count mismatch', { - step: this.step.name, + movement: this.step.name, conditionCount: targetCondition.length, - subStepCount: subSteps.length, + subMovementCount: subMovements.length, }); continue; } - const allMatch = subSteps.every((sub, idx) => { - const output = this.state.stepOutputs.get(sub.name); + const allMatch = subMovements.every((sub, idx) => { + const output = this.state.movementOutputs.get(sub.name); if (!output || output.matchedRuleIndex == null || !sub.rules) return false; const matchedRule = sub.rules[output.matchedRuleIndex]; const expectedCondition = targetCondition[idx]; @@ -66,46 +66,46 @@ export class AggregateEvaluator { return matchedRule?.condition === expectedCondition; }); if (allMatch) { - log.debug('Aggregate all() matched (multi-condition)', { step: this.step.name, conditions: targetCondition, ruleIndex: i }); + log.debug('Aggregate all() matched (multi-condition)', { movement: this.step.name, conditions: targetCondition, ruleIndex: i }); return i; } } else { - // Single condition: all sub-steps must match the same condition - const allMatch = subSteps.every((sub) => { - const output = this.state.stepOutputs.get(sub.name); + // Single condition: all sub-movements must match the same condition + const allMatch = subMovements.every((sub) => { + const output = this.state.movementOutputs.get(sub.name); if (!output || output.matchedRuleIndex == null || !sub.rules) return false; const matchedRule = sub.rules[output.matchedRuleIndex]; return matchedRule?.condition === targetCondition; }); if (allMatch) { - log.debug('Aggregate all() matched', { step: this.step.name, condition: targetCondition, ruleIndex: i }); + log.debug('Aggregate all() matched', { movement: this.step.name, condition: targetCondition, ruleIndex: i }); return i; } } } else { // 'any' if (Array.isArray(targetCondition)) { - // Multiple conditions: at least one sub-step matches at least one condition - const anyMatch = subSteps.some((sub) => { - const output = this.state.stepOutputs.get(sub.name); + // Multiple conditions: at least one sub-movement matches at least one condition + const anyMatch = subMovements.some((sub) => { + const output = this.state.movementOutputs.get(sub.name); if (!output || output.matchedRuleIndex == null || !sub.rules) return false; const matchedRule = sub.rules[output.matchedRuleIndex]; return targetCondition.includes(matchedRule?.condition ?? ''); }); if (anyMatch) { - log.debug('Aggregate any() matched (multi-condition)', { step: this.step.name, conditions: targetCondition, ruleIndex: i }); + log.debug('Aggregate any() matched (multi-condition)', { movement: this.step.name, conditions: targetCondition, ruleIndex: i }); return i; } } else { - // Single condition: at least one sub-step matches the condition - const anyMatch = subSteps.some((sub) => { - const output = this.state.stepOutputs.get(sub.name); + // Single condition: at least one sub-movement matches the condition + const anyMatch = subMovements.some((sub) => { + const output = this.state.movementOutputs.get(sub.name); if (!output || output.matchedRuleIndex == null || !sub.rules) return false; const matchedRule = sub.rules[output.matchedRuleIndex]; return matchedRule?.condition === targetCondition; }); if (anyMatch) { - log.debug('Aggregate any() matched', { step: this.step.name, condition: targetCondition, ruleIndex: i }); + log.debug('Aggregate any() matched', { movement: this.step.name, condition: targetCondition, ruleIndex: i }); return i; } } diff --git a/src/core/workflow/evaluation/RuleEvaluator.ts b/src/core/workflow/evaluation/RuleEvaluator.ts index 605f84d..0b85fb8 100644 --- a/src/core/workflow/evaluation/RuleEvaluator.ts +++ b/src/core/workflow/evaluation/RuleEvaluator.ts @@ -1,13 +1,13 @@ /** - * Rule evaluation logic for workflow steps + * Rule evaluation logic for workflow movements * - * Evaluates workflow step rules to determine the matched rule index. + * Evaluates workflow movement rules to determine the matched rule index. * Supports tag-based detection, ai() conditions, aggregate conditions, * and AI judge fallback. */ import type { - WorkflowStep, + WorkflowMovement, WorkflowState, RuleMatchMethod, } from '../../models/types.js'; @@ -23,7 +23,7 @@ export interface RuleMatch { } export interface RuleEvaluatorContext { - /** Workflow state (for accessing stepOutputs in aggregate evaluation) */ + /** Workflow state (for accessing movementOutputs in aggregate evaluation) */ state: WorkflowState; /** Working directory (for AI judge calls) */ cwd: string; @@ -36,21 +36,21 @@ export interface RuleEvaluatorContext { } /** - * Evaluates rules for a workflow step to determine the next transition. + * Evaluates rules for a workflow movement to determine the next transition. * * Evaluation order (first match wins): - * 1. Aggregate conditions: all()/any() — evaluate sub-step results + * 1. Aggregate conditions: all()/any() — evaluate sub-movement results * 2. Tag detection from Phase 3 output * 3. Tag detection from Phase 1 output (fallback) * 4. ai() condition evaluation via AI judge * 5. All-conditions AI judge (final fallback) * - * Returns undefined for steps without rules. + * Returns undefined for movements without rules. * Throws if rules exist but no rule matched (Fail Fast). */ export class RuleEvaluator { constructor( - private readonly step: WorkflowStep, + private readonly step: WorkflowMovement, private readonly ctx: RuleEvaluatorContext, ) {} @@ -58,7 +58,7 @@ export class RuleEvaluator { if (!this.step.rules || this.step.rules.length === 0) return undefined; const interactiveEnabled = this.ctx.interactive === true; - // 1. Aggregate conditions (all/any) — only meaningful for parallel parent steps + // 1. Aggregate conditions (all/any) — only meaningful for parallel parent movements const aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state); const aggIndex = aggEvaluator.evaluate(); if (aggIndex >= 0) { @@ -103,7 +103,7 @@ export class RuleEvaluator { return { index: fallbackIndex, method: 'ai_judge_fallback' }; } - throw new Error(`Status not found for step "${this.step.name}": no rule matched after all detection phases`); + throw new Error(`Status not found for movement "${this.step.name}": no rule matched after all detection phases`); } /** @@ -127,7 +127,7 @@ export class RuleEvaluator { if (aiConditions.length === 0) return -1; log.debug('Evaluating ai() conditions via judge', { - step: this.step.name, + movement: this.step.name, conditionCount: aiConditions.length, }); @@ -137,7 +137,7 @@ export class RuleEvaluator { if (judgeResult >= 0 && judgeResult < aiConditions.length) { const matched = aiConditions[judgeResult]!; log.debug('AI judge matched condition', { - step: this.step.name, + movement: this.step.name, judgeResult, originalRuleIndex: matched.index, condition: matched.text, @@ -145,7 +145,7 @@ export class RuleEvaluator { return matched.index; } - log.debug('AI judge did not match any condition', { step: this.step.name }); + log.debug('AI judge did not match any condition', { movement: this.step.name }); return -1; } @@ -162,7 +162,7 @@ export class RuleEvaluator { .map((rule) => ({ index: rule.index, text: rule.text })); log.debug('Evaluating all conditions via AI judge (final fallback)', { - step: this.step.name, + movement: this.step.name, conditionCount: conditions.length, }); @@ -170,14 +170,14 @@ export class RuleEvaluator { if (judgeResult >= 0 && judgeResult < conditions.length) { log.debug('AI judge (fallback) matched condition', { - step: this.step.name, + movement: this.step.name, ruleIndex: judgeResult, condition: conditions[judgeResult]!.text, }); return judgeResult; } - log.debug('AI judge (fallback) did not match any condition', { step: this.step.name }); + log.debug('AI judge (fallback) did not match any condition', { movement: this.step.name }); return -1; } diff --git a/src/core/workflow/evaluation/index.ts b/src/core/workflow/evaluation/index.ts index 65f5634..3820a49 100644 --- a/src/core/workflow/evaluation/index.ts +++ b/src/core/workflow/evaluation/index.ts @@ -2,7 +2,7 @@ * Rule evaluation - barrel exports */ -import type { WorkflowStep, WorkflowState } from '../../models/types.js'; +import type { WorkflowMovement, WorkflowState } from '../../models/types.js'; import { RuleEvaluator } from './RuleEvaluator.js'; import { AggregateEvaluator } from './AggregateEvaluator.js'; @@ -14,11 +14,11 @@ export { AggregateEvaluator } from './AggregateEvaluator.js'; import type { RuleMatch, RuleEvaluatorContext } from './RuleEvaluator.js'; /** - * Detect matched rule for a step's response. + * Detect matched rule for a movement's response. * Function facade over RuleEvaluator class. */ export async function detectMatchedRule( - step: WorkflowStep, + step: WorkflowMovement, 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: WorkflowStep, state: WorkflowState): number { +export function evaluateAggregateConditions(step: WorkflowMovement, state: WorkflowState): number { return new AggregateEvaluator(step, state).evaluate(); } diff --git a/src/core/workflow/evaluation/rule-utils.ts b/src/core/workflow/evaluation/rule-utils.ts index b2fdf0b..167ab66 100644 --- a/src/core/workflow/evaluation/rule-utils.ts +++ b/src/core/workflow/evaluation/rule-utils.ts @@ -2,16 +2,16 @@ * Shared rule utility functions used by both engine.ts and instruction-builder.ts. */ -import type { WorkflowStep } from '../../models/types.js'; +import type { WorkflowMovement } from '../../models/types.js'; /** - * Check whether a step has tag-based rules (i.e., rules that require - * [STEP:N] tag output for detection). + * Check whether a movement has tag-based rules (i.e., rules that require + * [MOVEMENT:N] tag output for detection). * * Returns false when all rules are ai() or aggregate conditions, * meaning no tag-based status output is needed. */ -export function hasTagBasedRules(step: WorkflowStep): boolean { +export function hasTagBasedRules(step: WorkflowMovement): 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/workflow/index.ts index b895d68..889695c 100644 --- a/src/core/workflow/index.ts +++ b/src/core/workflow/index.ts @@ -9,7 +9,7 @@ export { WorkflowEngine } from './engine/index.js'; // Constants -export { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from './constants.js'; +export { COMPLETE_MOVEMENT, ABORT_MOVEMENT, COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from './constants.js'; // Types export type { @@ -30,7 +30,7 @@ export type { } from './types.js'; // Transitions (engine/) -export { determineNextStepByRules, extractBlockedPrompt } from './engine/transitions.js'; +export { determineNextMovementByRules, extractBlockedPrompt } from './engine/transitions.js'; // Loop detection (engine/) export { LoopDetector } from './engine/loop-detector.js'; @@ -40,6 +40,7 @@ export { createInitialState, addUserInput, getPreviousOutput, + incrementMovementIteration, } from './engine/state-manager.js'; // Blocked handling (engine/) @@ -52,8 +53,8 @@ export { ParallelLogger } from './engine/parallel-logger.js'; export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js'; export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js'; export { StatusJudgmentBuilder, type StatusJudgmentContext } from './instruction/StatusJudgmentBuilder.js'; -export { buildExecutionMetadata, renderExecutionMetadata, type InstructionContext, type ExecutionMetadata } from './instruction/instruction-context.js'; -export { generateStatusRulesFromRules } from './instruction/status-rules.js'; +export { buildEditRule, type InstructionContext } from './instruction/instruction-context.js'; +export { generateStatusRulesComponents, type StatusRulesComponents } from './instruction/status-rules.js'; // Rule evaluation export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js'; diff --git a/src/core/workflow/instruction/InstructionBuilder.ts b/src/core/workflow/instruction/InstructionBuilder.ts index 82abe47..841e00f 100644 --- a/src/core/workflow/instruction/InstructionBuilder.ts +++ b/src/core/workflow/instruction/InstructionBuilder.ts @@ -1,20 +1,15 @@ /** * Phase 1 instruction builder * - * Builds the instruction string for main agent execution by: - * 1. Auto-injecting standard sections (Execution Context, Workflow Context, - * User Request, Previous Response, Additional User Inputs, Instructions header, - * Status Output Rules) - * 2. Replacing template placeholders with actual values + * Builds the instruction string for main agent execution. + * Assembles template variables and renders a single complete template. */ -import type { WorkflowStep, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js'; -import { hasTagBasedRules } from '../evaluation/rule-utils.js'; +import type { WorkflowMovement, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js'; import type { InstructionContext } from './instruction-context.js'; -import { buildExecutionMetadata, renderExecutionMetadata } from './instruction-context.js'; -import { generateStatusRulesFromRules } from './status-rules.js'; +import { buildEditRule } from './instruction-context.js'; import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.js'; -import { getPromptObject } from '../../../shared/prompts/index.js'; +import { loadTemplate } from '../../../shared/prompts/index.js'; /** * Check if a report config is the object form (ReportObjectConfig). @@ -23,162 +18,142 @@ export function isReportObjectConfig(report: string | ReportConfig[] | ReportObj return typeof report === 'object' && !Array.isArray(report) && 'name' in report; } -/** Shape of localized section strings */ -interface SectionStrings { - workflowContext: string; - workflowStructure: string; - currentStepMarker: string; - iteration: string; - iterationWorkflowWide: string; - stepIteration: string; - stepIterationTimes: string; - step: string; - reportDirectory: string; - reportFile: string; - reportFiles: string; - phaseNote: string; - userRequest: string; - previousResponse: string; - additionalUserInputs: string; - instructions: string; -} - -/** Shape of localized report output strings */ -interface ReportOutputStrings { - singleHeading: string; - multiHeading: string; - createRule: string; - appendRule: string; -} - /** * Builds Phase 1 instructions for agent execution. * * Stateless builder — all data is passed via constructor context. + * Renders a single complete template with all variables. */ export class InstructionBuilder { constructor( - private readonly step: WorkflowStep, + private readonly step: WorkflowMovement, private readonly context: InstructionContext, ) {} /** * Build the complete instruction string. * - * Generates a complete instruction by auto-injecting standard sections - * around the step-specific instruction_template content. + * Assembles all template variables and renders the Phase 1 template + * in a single loadTemplate() call. */ build(): string { const language = this.context.language ?? 'en'; - const s = getPromptObject('instruction.sections', language); - const sections: string[] = []; - // 1. Execution context metadata (working directory + rules + edit permission) - const metadata = buildExecutionMetadata(this.context, this.step.edit); - sections.push(renderExecutionMetadata(metadata)); + // Execution context variables + const editRule = buildEditRule(this.step.edit, language); - // 2. Workflow Context (iteration, step, report info) - sections.push(this.renderWorkflowContext(language)); + // Workflow structure (loop expansion done in code) + const workflowStructure = this.buildWorkflowStructure(language); - // Skip auto-injection for sections whose placeholders exist in the template, - // to avoid duplicate content. + // Report info + const hasReport = !!(this.step.report && this.context.reportDir); + let reportInfo = ''; + let phaseNote = ''; + if (hasReport) { + reportInfo = renderReportContext(this.step.report!, this.context.reportDir!); + phaseNote = language === 'ja' + ? '**注意:** これはPhase 1(本来の作業)です。作業完了後、Phase 2で自動的にレポートを生成します。' + : '**Note:** This is Phase 1 (main work). After you complete your work, Phase 2 will automatically generate the report based on your findings.'; + } + + // Skip auto-injection for sections whose placeholders exist in the template const tmpl = this.step.instructionTemplate; const hasTaskPlaceholder = tmpl.includes('{task}'); const hasPreviousResponsePlaceholder = tmpl.includes('{previous_response}'); const hasUserInputsPlaceholder = tmpl.includes('{user_inputs}'); - // 3. User Request (skip if template embeds {task} directly) - if (!hasTaskPlaceholder) { - sections.push(`${s.userRequest}\n${escapeTemplateChars(this.context.task)}`); - } + // User Request + const hasTaskSection = !hasTaskPlaceholder; + const userRequest = hasTaskSection ? escapeTemplateChars(this.context.task) : ''; - // 4. Previous Response (skip if template embeds {previous_response} directly) - if (this.step.passPreviousResponse && this.context.previousOutput && !hasPreviousResponsePlaceholder) { - sections.push( - `${s.previousResponse}\n${escapeTemplateChars(this.context.previousOutput.content)}`, - ); - } + // Previous Response + const hasPreviousResponse = !!( + this.step.passPreviousResponse && + this.context.previousOutput && + !hasPreviousResponsePlaceholder + ); + const previousResponse = hasPreviousResponse + ? escapeTemplateChars(this.context.previousOutput!.content) + : ''; - // 5. Additional User Inputs (skip if template embeds {user_inputs} directly) - if (!hasUserInputsPlaceholder) { - const userInputsStr = this.context.userInputs.join('\n'); - sections.push(`${s.additionalUserInputs}\n${escapeTemplateChars(userInputsStr)}`); - } + // User Inputs + const hasUserInputs = !hasUserInputsPlaceholder; + const userInputs = hasUserInputs + ? escapeTemplateChars(this.context.userInputs.join('\n')) + : ''; - // 6. Instructions header + instruction_template content - const processedTemplate = replaceTemplatePlaceholders( + // Instructions (instruction_template processed) + const instructions = replaceTemplatePlaceholders( this.step.instructionTemplate, this.step, this.context, ); - sections.push(`${s.instructions}\n${processedTemplate}`); - // 7. Status Output Rules (for tag-based detection in Phase 1) - if (hasTagBasedRules(this.step)) { - const statusRulesPrompt = generateStatusRulesFromRules( - this.step.name, - this.step.rules!, - language, - { interactive: this.context.interactive }, - ); - sections.push(statusRulesPrompt); - } - - return sections.join('\n\n'); + return loadTemplate('perform_phase1_message', language, { + workingDirectory: this.context.cwd, + editRule, + workflowStructure, + iteration: `${this.context.iteration}/${this.context.maxIterations}`, + movementIteration: String(this.context.movementIteration), + movement: this.step.name, + hasReport, + reportInfo, + phaseNote, + hasTaskSection, + userRequest, + hasPreviousResponse, + previousResponse, + hasUserInputs, + userInputs, + instructions, + }); } - private renderWorkflowContext(language: Language): string { - const s = getPromptObject('instruction.sections', language); - const lines: string[] = [s.workflowContext]; - - // Workflow structure (if workflow steps info is available) - if (this.context.workflowSteps && this.context.workflowSteps.length > 0) { - lines.push(s.workflowStructure.replace('{count}', String(this.context.workflowSteps.length))); - this.context.workflowSteps.forEach((ws, index) => { - const isCurrent = index === this.context.currentStepIndex; - const marker = isCurrent ? ` ← ${s.currentStepMarker}` : ''; - const desc = ws.description ? `(${ws.description})` : ''; - lines.push(`- Step ${index + 1}: ${ws.name}${desc}${marker}`); - }); - lines.push(''); + /** + * Build the workflow structure display string. + * Returns empty string if no workflow movements are available. + */ + private buildWorkflowStructure(language: Language): string { + if (!this.context.workflowMovements || this.context.workflowMovements.length === 0) { + return ''; } - lines.push(`- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`); - lines.push(`- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`); - lines.push(`- ${s.step}: ${this.step.name}`); - - // If step has report config, include Report Directory path and phase note - if (this.step.report && this.context.reportDir) { - const reportContext = renderReportContext(this.step.report, this.context.reportDir, language); - lines.push(reportContext); - lines.push(''); - lines.push(s.phaseNote); - } - - return lines.join('\n'); + 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) => { + const isCurrent = index === this.context.currentMovementIndex; + const marker = isCurrent ? ` ← ${currentMovementMarker}` : ''; + const desc = ws.description ? `(${ws.description})` : ''; + return `- Movement ${index + 1}: ${ws.name}${desc}${marker}`; + }); + return [structureHeader, ...movementLines].join('\n'); } } /** * Render report context info for Workflow Context section. - * Used by ReportInstructionBuilder. + * Used by InstructionBuilder and ReportInstructionBuilder. */ export function renderReportContext( report: string | ReportConfig[] | ReportObjectConfig, reportDir: string, - language: Language, ): string { - const s = getPromptObject('instruction.sections', language); + const reportDirectory = 'Report Directory'; + const reportFile = 'Report File'; + const reportFiles = 'Report Files'; + const lines: string[] = [ - `- ${s.reportDirectory}: ${reportDir}/`, + `- ${reportDirectory}: ${reportDir}/`, ]; if (typeof report === 'string') { - lines.push(`- ${s.reportFile}: ${reportDir}/${report}`); + lines.push(`- ${reportFile}: ${reportDir}/${report}`); } else if (isReportObjectConfig(report)) { - lines.push(`- ${s.reportFile}: ${reportDir}/${report.name}`); + lines.push(`- ${reportFile}: ${reportDir}/${report.name}`); } else { - lines.push(`- ${s.reportFiles}:`); + lines.push(`- ${reportFiles}:`); for (const file of report) { lines.push(` - ${file.label}: ${reportDir}/${file.path}`); } @@ -188,20 +163,35 @@ export function renderReportContext( } /** - * Generate report output instructions from step.report config. - * Returns undefined if step has no report or no reportDir. + * Generate report output instructions from movement's report config. + * Returns empty string if movement has no report or no reportDir. */ export function renderReportOutputInstruction( - step: WorkflowStep, + step: WorkflowMovement, context: InstructionContext, language: Language, -): string | undefined { - if (!step.report || !context.reportDir) return undefined; +): string { + if (!step.report || !context.reportDir) return ''; - const s = getPromptObject('instruction.reportOutput', language); const isMulti = Array.isArray(step.report); - const heading = isMulti ? s.multiHeading : s.singleHeading; - const appendRule = s.appendRule.replace('{step_iteration}', String(context.stepIteration)); - return [heading, s.createRule, appendRule].join('\n'); + let heading: string; + let createRule: string; + let appendRule: string; + + if (language === 'ja') { + heading = isMulti + ? '**レポート出力:** Report Files に出力してください。' + : '**レポート出力:** `Report File` に出力してください。'; + createRule = '- ファイルが存在しない場合: 新規作成'; + appendRule = `- ファイルが存在する場合: \`## Iteration ${context.movementIteration}\` セクションを追記`; + } else { + heading = isMulti + ? '**Report output:** Output to the `Report Files` specified above.' + : '**Report output:** Output to the `Report File` specified above.'; + createRule = '- If file does not exist: Create new file'; + appendRule = `- If file exists: Append with \`## Iteration ${context.movementIteration}\` section`; + } + + return `${heading}\n${createRule}\n${appendRule}`; } diff --git a/src/core/workflow/instruction/ReportInstructionBuilder.ts b/src/core/workflow/instruction/ReportInstructionBuilder.ts index aac2319..8ce5a96 100644 --- a/src/core/workflow/instruction/ReportInstructionBuilder.ts +++ b/src/core/workflow/instruction/ReportInstructionBuilder.ts @@ -1,37 +1,15 @@ /** * Phase 2 instruction builder (report output) * - * Builds the instruction for the report output phase. Includes: - * - Execution Context (cwd + rules) - * - Workflow Context (report info only) - * - Report output instruction + format - * - * Does NOT include: User Request, Previous Response, User Inputs, - * Status rules, instruction_template. + * Builds the instruction for the report output phase. + * Assembles template variables and renders a single complete template. */ -import type { WorkflowStep, Language } from '../../models/types.js'; +import type { WorkflowMovement, Language } from '../../models/types.js'; import type { InstructionContext } from './instruction-context.js'; -import { getMetadataStrings } from './instruction-context.js'; import { replaceTemplatePlaceholders } from './escape.js'; import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js'; -import { getPromptObject } from '../../../shared/prompts/index.js'; - -/** Shape of localized report phase strings */ -interface ReportPhaseStrings { - noSourceEdit: string; - reportDirOnly: string; - instructionBody: string; - reportJsonFormat: string; - reportPlainAllowed: string; - reportOnlyOutput: string; -} - -/** Shape of localized report section strings */ -interface ReportSectionStrings { - workflowContext: string; - instructions: string; -} +import { loadTemplate } from '../../../shared/prompts/index.js'; /** * Context for building report phase instruction. @@ -41,8 +19,8 @@ export interface ReportInstructionContext { cwd: string; /** Report directory path */ reportDir: string; - /** Step iteration (for {step_iteration} replacement) */ - stepIteration: number; + /** Movement iteration (for {step_iteration} replacement) */ + movementIteration: number; /** Language */ language?: Language; /** Target report file name (when generating a single report) */ @@ -51,73 +29,38 @@ export interface ReportInstructionContext { /** * Builds Phase 2 (report output) instructions. + * + * Renders a single complete template with all variables. */ export class ReportInstructionBuilder { constructor( - private readonly step: WorkflowStep, + private readonly step: WorkflowMovement, private readonly context: ReportInstructionContext, ) {} build(): string { if (!this.step.report) { - throw new Error(`ReportInstructionBuilder called for step "${this.step.name}" which has no report config`); + throw new Error(`ReportInstructionBuilder called for movement "${this.step.name}" which has no report config`); } const language = this.context.language ?? 'en'; - const s = getPromptObject('instruction.reportSections', language); - const r = getPromptObject('instruction.reportPhase', language); - const m = getMetadataStrings(language); - const sections: string[] = []; - // 1. Execution Context - const execLines = [ - m.heading, - `- ${m.workingDirectory}: ${this.context.cwd}`, - '', - m.rulesHeading, - `- ${m.noCommit}`, - `- ${m.noCd}`, - `- ${r.noSourceEdit}`, - `- ${r.reportDirOnly}`, - ]; - if (m.note) { - execLines.push(''); - execLines.push(m.note); - } - execLines.push(''); - sections.push(execLines.join('\n')); - - // 2. Workflow Context (single file info when targetFile is specified) - const workflowLines = [s.workflowContext]; + // Build report context for Workflow Context section + let reportContext: string; if (this.context.targetFile) { - const sectionStr = getPromptObject<{ reportDirectory: string; reportFile: string }>('instruction.sections', language); - workflowLines.push(`- ${sectionStr.reportDirectory}: ${this.context.reportDir}/`); - workflowLines.push(`- ${sectionStr.reportFile}: ${this.context.reportDir}/${this.context.targetFile}`); + reportContext = `- Report Directory: ${this.context.reportDir}/\n- Report File: ${this.context.reportDir}/${this.context.targetFile}`; } else { - workflowLines.push(renderReportContext(this.step.report, this.context.reportDir, language)); - } - sections.push(workflowLines.join('\n')); - - // 3. Instructions (simplified when targetFile is specified) - const instrParts: string[] = [s.instructions]; - - if (this.context.targetFile) { - instrParts.push(r.instructionBody); - instrParts.push(`**このフェーズではツールは使えません。レポート内容をテキストとして直接回答してください。**`); - instrParts.push(`**レポート本文のみを回答してください。** ファイル名やJSON形式は不要です。`); - } else { - instrParts.push(r.instructionBody); - instrParts.push(r.reportJsonFormat); - instrParts.push(r.reportPlainAllowed); - instrParts.push(r.reportOnlyOutput); + reportContext = renderReportContext(this.step.report, this.context.reportDir); } - // Report output instruction (auto-generated or explicit order) - const reportContext: InstructionContext = { + // Build report output instruction + let reportOutput = ''; + let hasReportOutput = false; + const instrContext: InstructionContext = { task: '', iteration: 0, maxIterations: 0, - stepIteration: this.context.stepIteration, + movementIteration: this.context.movementIteration, cwd: this.context.cwd, projectCwd: this.context.cwd, userInputs: [], @@ -126,26 +69,31 @@ export class ReportInstructionBuilder { }; if (isReportObjectConfig(this.step.report) && this.step.report.order) { - const processedOrder = replaceTemplatePlaceholders(this.step.report.order.trimEnd(), this.step, reportContext); - instrParts.push(''); - instrParts.push(processedOrder); + reportOutput = replaceTemplatePlaceholders(this.step.report.order.trimEnd(), this.step, instrContext); + hasReportOutput = true; } else if (!this.context.targetFile) { - const reportInstruction = renderReportOutputInstruction(this.step, reportContext, language); - if (reportInstruction) { - instrParts.push(''); - instrParts.push(reportInstruction); + const output = renderReportOutputInstruction(this.step, instrContext, language); + if (output) { + reportOutput = output; + hasReportOutput = true; } } - // Report format + // Build report format + let reportFormat = ''; + let hasReportFormat = false; if (isReportObjectConfig(this.step.report) && this.step.report.format) { - const processedFormat = replaceTemplatePlaceholders(this.step.report.format.trimEnd(), this.step, reportContext); - instrParts.push(''); - instrParts.push(processedFormat); + reportFormat = replaceTemplatePlaceholders(this.step.report.format.trimEnd(), this.step, instrContext); + hasReportFormat = true; } - sections.push(instrParts.join('\n')); - - return sections.join('\n\n'); + return loadTemplate('perform_phase2_message', language, { + workingDirectory: this.context.cwd, + reportContext, + hasReportOutput, + reportOutput, + hasReportFormat, + reportFormat, + }); } } diff --git a/src/core/workflow/instruction/StatusJudgmentBuilder.ts b/src/core/workflow/instruction/StatusJudgmentBuilder.ts index 4225213..bd4adc9 100644 --- a/src/core/workflow/instruction/StatusJudgmentBuilder.ts +++ b/src/core/workflow/instruction/StatusJudgmentBuilder.ts @@ -4,14 +4,13 @@ * Resumes the agent session and asks it to evaluate its work * and output the appropriate status tag. No tools are allowed. * - * Includes: - * - Header instruction (review and determine status) - * - Status rules (criteria table + output format) + * Renders a single complete template combining the judgment header + * and status rules (criteria table + output format). */ -import type { WorkflowStep, Language } from '../../models/types.js'; -import { generateStatusRulesFromRules } from './status-rules.js'; -import { getPrompt } from '../../../shared/prompts/index.js'; +import type { WorkflowMovement, Language } from '../../models/types.js'; +import { generateStatusRulesComponents } from './status-rules.js'; +import { loadTemplate } from '../../../shared/prompts/index.js'; /** * Context for building status judgment instruction. @@ -25,33 +24,34 @@ export interface StatusJudgmentContext { /** * Builds Phase 3 (status judgment) instructions. + * + * Renders a single complete template with all variables. */ export class StatusJudgmentBuilder { constructor( - private readonly step: WorkflowStep, + private readonly step: WorkflowMovement, private readonly context: StatusJudgmentContext, ) {} build(): string { if (!this.step.rules || this.step.rules.length === 0) { - throw new Error(`StatusJudgmentBuilder called for step "${this.step.name}" which has no rules`); + throw new Error(`StatusJudgmentBuilder called for movement "${this.step.name}" which has no rules`); } const language = this.context.language ?? 'en'; - const sections: string[] = []; - // Header - sections.push(getPrompt('instruction.statusJudgment.header', language)); - - // Status rules (criteria table + output format) - const generatedPrompt = generateStatusRulesFromRules( + const components = generateStatusRulesComponents( this.step.name, this.step.rules, language, { interactive: this.context.interactive }, ); - sections.push(generatedPrompt); - return sections.join('\n\n'); + return loadTemplate('perform_phase3_message', language, { + criteriaTable: components.criteriaTable, + outputList: components.outputList, + hasAppendix: components.hasAppendix, + appendixContent: components.appendixContent, + }); } } diff --git a/src/core/workflow/instruction/escape.ts b/src/core/workflow/instruction/escape.ts index 839618e..94b4e43 100644 --- a/src/core/workflow/instruction/escape.ts +++ b/src/core/workflow/instruction/escape.ts @@ -4,7 +4,7 @@ * Used by instruction builders to process instruction_template content. */ -import type { WorkflowStep } from '../../models/types.js'; +import type { WorkflowMovement } 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: WorkflowStep, + step: WorkflowMovement, context: InstructionContext, ): string { let result = template; @@ -30,10 +30,12 @@ export function replaceTemplatePlaceholders( // Replace {task} result = result.replace(/\{task\}/g, escapeTemplateChars(context.task)); - // Replace {iteration}, {max_iterations}, and {step_iteration} + // Replace {iteration}, {max_iterations}, and {movement_iteration} result = result.replace(/\{iteration\}/g, String(context.iteration)); result = result.replace(/\{max_iterations\}/g, String(context.maxIterations)); - result = result.replace(/\{step_iteration\}/g, String(context.stepIteration)); + result = result.replace(/\{movement_iteration\}/g, String(context.movementIteration)); + // @deprecated Use {movement_iteration} instead + result = result.replace(/\{step_iteration\}/g, String(context.movementIteration)); // Replace {previous_response} if (step.passPreviousResponse) { diff --git a/src/core/workflow/instruction/instruction-context.ts b/src/core/workflow/instruction/instruction-context.ts index 3e51f00..764fa07 100644 --- a/src/core/workflow/instruction/instruction-context.ts +++ b/src/core/workflow/instruction/instruction-context.ts @@ -1,12 +1,10 @@ /** - * Instruction context types and execution metadata rendering + * Instruction context types and edit rule generation * - * Defines the context structures used by instruction builders, - * and renders execution metadata (working directory, rules) as markdown. + * Defines the context structures used by instruction builders. */ import type { AgentResponse, Language } from '../../models/types.js'; -import { getPromptObject } from '../../../shared/prompts/index.js'; /** * Context for building instruction from template. @@ -18,15 +16,15 @@ export interface InstructionContext { iteration: number; /** Maximum iterations allowed */ maxIterations: number; - /** Current step's iteration number (how many times this step has been executed) */ - stepIteration: number; + /** Current movement's iteration number (how many times this movement has been executed) */ + movementIteration: number; /** Working directory (agent work dir, may be a clone) */ cwd: string; /** Project root directory (where .takt/ lives). */ projectCwd: string; /** User inputs accumulated during workflow */ userInputs: string[]; - /** Previous step output if available */ + /** Previous movement output if available */ previousOutput?: AgentResponse; /** Report directory path */ reportDir?: string; @@ -34,78 +32,30 @@ export interface InstructionContext { language?: Language; /** Whether interactive-only rules are enabled */ interactive?: boolean; - /** Top-level workflow steps for workflow structure display */ - workflowSteps?: ReadonlyArray<{ name: string; description?: string }>; - /** Index of the current step in workflowSteps (0-based) */ - currentStepIndex?: number; -} - -/** Execution environment metadata prepended to agent instructions */ -export interface ExecutionMetadata { - /** The agent's working directory (may be a clone) */ - readonly workingDirectory: string; - /** Language for metadata rendering */ - readonly language: Language; - /** Whether file editing is allowed for this step (undefined = no prompt) */ - readonly edit?: boolean; + /** Top-level workflow movements for workflow structure display */ + workflowMovements?: ReadonlyArray<{ name: string; description?: string }>; + /** Index of the current movement in workflowMovements (0-based) */ + currentMovementIndex?: number; } /** - * Build execution metadata from instruction context and step config. + * Build the edit rule string for the execution context section. * - * Pure function: (InstructionContext, edit?) → ExecutionMetadata. + * Returns a localized string describing the edit permission for this movement. + * Returns empty string when edit is undefined (no explicit permission). */ -export function buildExecutionMetadata(context: InstructionContext, edit?: boolean): ExecutionMetadata { - return { - workingDirectory: context.cwd, - language: context.language ?? 'en', - edit, - }; -} - -/** Shape of localized metadata strings from YAML */ -export interface MetadataStrings { - heading: string; - workingDirectory: string; - rulesHeading: string; - noCommit: string; - noCd: string; - editEnabled: string; - editDisabled: string; - note: string; -} - -/** Load metadata strings for the given language from YAML */ -export function getMetadataStrings(language: Language): MetadataStrings { - return getPromptObject('instruction.metadata', language); -} - -/** - * Render execution metadata as a markdown string. - * - * Pure function: ExecutionMetadata → string. - * Always includes heading + Working Directory + Execution Rules. - * Language determines the output language; 'en' includes a note about language consistency. - */ -export function renderExecutionMetadata(metadata: ExecutionMetadata): string { - const strings = getMetadataStrings(metadata.language); - const lines = [ - strings.heading, - `- ${strings.workingDirectory}: ${metadata.workingDirectory}`, - '', - strings.rulesHeading, - `- ${strings.noCommit}`, - `- ${strings.noCd}`, - ]; - if (metadata.edit === true) { - lines.push(`- ${strings.editEnabled}`); - } else if (metadata.edit === false) { - lines.push(`- ${strings.editDisabled}`); +export function buildEditRule(edit: boolean | undefined, language: Language): string { + if (edit === true) { + if (language === 'ja') { + return '**このムーブメントでは編集が許可されています。** ユーザーの要求に応じて、ファイルの作成・変更・削除を行ってください。'; + } + return '**Editing is ENABLED for this movement.** You may create, modify, and delete files as needed to fulfill the user\'s request.'; } - if (strings.note) { - lines.push(''); - lines.push(strings.note); + if (edit === false) { + if (language === 'ja') { + return '**このムーブメントでは編集が禁止されています。** プロジェクトのソースファイルを作成・変更・削除しないでください。コードの読み取り・検索のみ行ってください。レポート出力は後のフェーズで自動的に行われます。'; + } + return '**Editing is DISABLED for this movement.** Do NOT create, modify, or delete any project source files. You may only read and search code. Report output will be handled automatically in a later phase.'; } - lines.push(''); - return lines.join('\n'); + return ''; } diff --git a/src/core/workflow/instruction/status-rules.ts b/src/core/workflow/instruction/status-rules.ts index 68bd50a..d3eb676 100644 --- a/src/core/workflow/instruction/status-rules.ts +++ b/src/core/workflow/instruction/status-rules.ts @@ -1,87 +1,92 @@ /** - * Status rules prompt generation for workflow steps + * Status rules prompt generation for workflow movements * - * Generates structured prompts that tell agents which numbered tags to output - * based on the step's rule configuration. + * Generates structured status rules content that tells agents which + * numbered tags to output based on the movement's rule configuration. + * + * Returns individual components (criteriaTable, outputList, appendix) + * that are passed as template variables to Phase 1/Phase 3 templates. */ import type { WorkflowRule, Language } from '../../models/types.js'; -import { getPromptObject } from '../../../shared/prompts/index.js'; -/** Shape of localized status rules strings */ -interface StatusRulesStrings { - criteriaHeading: string; - headerNum: string; - headerCondition: string; - headerTag: string; - outputHeading: string; - outputInstruction: string; - appendixHeading: string; - appendixInstruction: string; +/** Components of the generated status rules */ +export interface StatusRulesComponents { + criteriaTable: string; + outputList: string; + hasAppendix: boolean; + appendixContent: string; } /** - * Generate status rules prompt from rules configuration. - * Creates a structured prompt that tells the agent which numbered tags to output. + * Generate status rules components from rules configuration. * - * Example output for step "plan" with 3 rules: - * ## 判定基準 - * | # | 状況 | タグ | - * |---|------|------| - * | 1 | 要件が明確で実装可能 | `[PLAN:1]` | - * | 2 | ユーザーが質問をしている | `[PLAN:2]` | - * | 3 | 要件が不明確、情報不足 | `[PLAN:3]` | + * Loop expansion (criteria table rows, output list items, appendix blocks) + * is done in code and returned as individual string components. + * These are passed as template variables to the Phase 1/Phase 3 templates. */ -export function generateStatusRulesFromRules( - stepName: string, +export function generateStatusRulesComponents( + movementName: string, rules: WorkflowRule[], language: Language, options?: { interactive?: boolean }, -): string { - const tag = stepName.toUpperCase(); - const strings = getPromptObject('instruction.statusRules', language); +): StatusRulesComponents { + const tag = movementName.toUpperCase(); const interactiveEnabled = options?.interactive; const visibleRules = rules .map((rule, index) => ({ rule, index })) .filter(({ rule }) => interactiveEnabled !== false || !rule.interactiveOnly); - const lines: string[] = []; + // Build criteria table rows + const headerNum = '#'; + const headerCondition = language === 'ja' ? '状況' : 'Condition'; + const headerTag = language === 'ja' ? 'タグ' : 'Tag'; - // Criteria table - lines.push(strings.criteriaHeading); - lines.push(''); - lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`); - lines.push('|---|------|------|'); - for (const { rule, index } of visibleRules) { - lines.push(`| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`); - } - lines.push(''); + const tableLines = [ + `| ${headerNum} | ${headerCondition} | ${headerTag} |`, + '|---|------|------|', + ...visibleRules.map(({ rule, index }) => + `| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`, + ), + ]; + const criteriaTable = tableLines.join('\n'); - // Output format - lines.push(strings.outputHeading); - lines.push(''); - lines.push(strings.outputInstruction); - lines.push(''); - for (const { rule, index } of visibleRules) { - lines.push(`- \`[${tag}:${index + 1}]\` — ${rule.condition}`); - } + // Build output list + const outputInstruction = language === 'ja' + ? '判定に対応するタグを出力してください:' + : 'Output the tag corresponding to your decision:'; - // Appendix templates (if any rules have appendix) + const outputLines = [ + outputInstruction, + '', + ...visibleRules.map(({ rule, index }) => + `- \`[${tag}:${index + 1}]\` — ${rule.condition}`, + ), + ]; + const outputList = outputLines.join('\n'); + + // Build appendix content const rulesWithAppendix = visibleRules.filter(({ rule }) => rule.appendix); - if (rulesWithAppendix.length > 0) { - lines.push(''); - lines.push(strings.appendixHeading); + const hasAppendix = rulesWithAppendix.length > 0; + let appendixContent = ''; + + if (hasAppendix) { + const appendixInstructionTemplate = language === 'ja' + ? '`[{tag}]` を出力する場合、以下を追記してください:' + : 'When outputting `[{tag}]`, append the following:'; + + const appendixBlocks: string[] = []; for (const { rule, index } of visibleRules) { if (!rule.appendix) continue; const tagStr = `[${tag}:${index + 1}]`; - lines.push(''); - // appendixInstruction contains {tag} as a domain-specific placeholder, not a YAML template variable - lines.push(strings.appendixInstruction.replace('{tag}', tagStr)); - lines.push('```'); - lines.push(rule.appendix.trimEnd()); - lines.push('```'); + appendixBlocks.push(''); + appendixBlocks.push(appendixInstructionTemplate.replace('{tag}', tagStr)); + appendixBlocks.push('```'); + appendixBlocks.push(rule.appendix.trimEnd()); + appendixBlocks.push('```'); } + appendixContent = appendixBlocks.join('\n'); } - return lines.join('\n'); + return { criteriaTable, outputList, hasAppendix, appendixContent }; } diff --git a/src/core/workflow/phase-runner.ts b/src/core/workflow/phase-runner.ts index be6ed3a..6672e89 100644 --- a/src/core/workflow/phase-runner.ts +++ b/src/core/workflow/phase-runner.ts @@ -7,7 +7,7 @@ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve, sep } from 'node:path'; -import type { WorkflowStep, Language } from '../models/types.js'; +import type { WorkflowMovement, 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'; @@ -29,25 +29,25 @@ export interface PhaseRunnerContext { interactive?: boolean; /** Get agent session ID */ getSessionId: (agent: string) => string | undefined; - /** Build resume options for a step */ - buildResumeOptions: (step: WorkflowStep, sessionId: string, overrides: Pick) => RunAgentOptions; + /** Build resume options for a movement */ + buildResumeOptions: (step: WorkflowMovement, 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: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; + onPhaseStart?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; /** Callback for phase completion logging */ - onPhaseComplete?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; } /** - * Check if a step needs Phase 3 (status judgment). + * Check if a movement needs Phase 3 (status judgment). * Returns true when at least one rule requires tag-based detection. */ -export function needsStatusJudgmentPhase(step: WorkflowStep): boolean { +export function needsStatusJudgmentPhase(step: WorkflowMovement): boolean { return hasTagBasedRules(step); } -function getReportFiles(report: WorkflowStep['report']): string[] { +function getReportFiles(report: WorkflowMovement['report']): string[] { if (!report) return []; if (typeof report === 'string') return [report]; if (isReportObjectConfig(report)) return [report.name]; @@ -76,17 +76,17 @@ function writeReportFile(reportDir: string, fileName: string, content: string): * Plain text responses are written directly to files (no JSON parsing). */ export async function runReportPhase( - step: WorkflowStep, - stepIteration: number, + step: WorkflowMovement, + movementIteration: number, ctx: PhaseRunnerContext, ): Promise { const sessionKey = step.agent ?? step.name; let currentSessionId = ctx.getSessionId(sessionKey); if (!currentSessionId) { - throw new Error(`Report phase requires a session to resume, but no sessionId found for agent "${sessionKey}" in step "${step.name}"`); + throw new Error(`Report phase requires a session to resume, but no sessionId found for agent "${sessionKey}" in movement "${step.name}"`); } - log.debug('Running report phase', { step: step.name, sessionId: currentSessionId }); + log.debug('Running report phase', { movement: step.name, sessionId: currentSessionId }); const reportFiles = getReportFiles(step.report); if (reportFiles.length === 0) { @@ -99,12 +99,12 @@ export async function runReportPhase( throw new Error(`Invalid report file name: ${fileName}`); } - log.debug('Generating report file', { step: step.name, fileName }); + log.debug('Generating report file', { movement: step.name, fileName }); const reportInstruction = new ReportInstructionBuilder(step, { cwd: ctx.cwd, reportDir: ctx.reportDir, - stepIteration, + movementIteration: movementIteration, language: ctx.language, targetFile: fileName, }).build(); @@ -144,10 +144,10 @@ export async function runReportPhase( } ctx.onPhaseComplete?.(step, 2, 'report', reportResponse.content, reportResponse.status); - log.debug('Report file generated', { step: step.name, fileName }); + log.debug('Report file generated', { movement: step.name, fileName }); } - log.debug('Report phase complete', { step: step.name, filesGenerated: reportFiles.length }); + log.debug('Report phase complete', { movement: step.name, filesGenerated: reportFiles.length }); } /** @@ -156,16 +156,16 @@ export async function runReportPhase( * Returns the Phase 3 response content (containing the status tag). */ export async function runStatusJudgmentPhase( - step: WorkflowStep, + step: WorkflowMovement, ctx: PhaseRunnerContext, ): Promise { const sessionKey = step.agent ?? step.name; const sessionId = ctx.getSessionId(sessionKey); if (!sessionId) { - throw new Error(`Status judgment phase requires a session to resume, but no sessionId found for agent "${sessionKey}" in step "${step.name}"`); + throw new Error(`Status judgment phase requires a session to resume, but no sessionId found for agent "${sessionKey}" in movement "${step.name}"`); } - log.debug('Running status judgment phase', { step: step.name, sessionId }); + log.debug('Running status judgment phase', { movement: step.name, sessionId }); const judgmentInstruction = new StatusJudgmentBuilder(step, { language: ctx.language, @@ -199,6 +199,6 @@ export async function runStatusJudgmentPhase( ctx.updateAgentSession(sessionKey, judgmentResponse.sessionId); ctx.onPhaseComplete?.(step, 3, 'judge', judgmentResponse.content, judgmentResponse.status); - log.debug('Status judgment phase complete', { step: step.name, status: judgmentResponse.status }); + log.debug('Status judgment phase complete', { movement: step.name, status: judgmentResponse.status }); return judgmentResponse.content; } diff --git a/src/core/workflow/types.ts b/src/core/workflow/types.ts index f59d2f8..f8b4ba4 100644 --- a/src/core/workflow/types.ts +++ b/src/core/workflow/types.ts @@ -6,7 +6,7 @@ */ import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'; -import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; +import type { WorkflowMovement, WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; export type ProviderType = 'claude' | 'codex' | 'mock'; @@ -91,7 +91,7 @@ export type AskUserQuestionHandler = ( input: AskUserQuestionInput ) => Promise>; -export type RuleIndexDetector = (content: string, stepName: string) => number; +export type RuleIndexDetector = (content: string, movementName: string) => number; export interface AiJudgeCondition { index: number; @@ -108,23 +108,23 @@ export type PhaseName = 'execute' | 'report' | 'judge'; /** Events emitted by workflow engine */ export interface WorkflowEvents { - 'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void; - 'step:complete': (step: WorkflowStep, response: AgentResponse, instruction: string) => void; - 'step:report': (step: WorkflowStep, filePath: string, fileName: string) => void; - 'step:blocked': (step: WorkflowStep, response: AgentResponse) => void; - 'step:user_input': (step: WorkflowStep, userInput: string) => void; - 'phase:start': (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; - 'phase:complete': (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + '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; 'iteration:limit': (iteration: number, maxIterations: number) => void; - 'step:loop_detected': (step: WorkflowStep, consecutiveCount: number) => void; + 'movement:loop_detected': (step: WorkflowMovement, consecutiveCount: number) => void; } /** User input request for blocked state */ export interface UserInputRequest { - /** The step that is blocked */ - step: WorkflowStep; + /** The movement that is blocked */ + movement: WorkflowMovement; /** The blocked response from the agent */ response: AgentResponse; /** Prompt for the user (extracted from blocked message) */ @@ -137,8 +137,8 @@ export interface IterationLimitRequest { currentIteration: number; /** Current max iterations */ maxIterations: number; - /** Current step name */ - currentStep: string; + /** Current movement name */ + currentMovement: string; } /** Callback for session updates (when agent session IDs change) */ diff --git a/src/features/config/ejectBuiltin.ts b/src/features/config/ejectBuiltin.ts index 20e20f5..a968208 100644 --- a/src/features/config/ejectBuiltin.ts +++ b/src/features/config/ejectBuiltin.ts @@ -8,9 +8,9 @@ import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { - getGlobalWorkflowsDir, + getGlobalPiecesDir, getGlobalAgentsDir, - getBuiltinWorkflowsDir, + getBuiltinPiecesDir, getBuiltinAgentsDir, getLanguage, } from '../../infra/config/index.js'; @@ -25,7 +25,7 @@ export async function ejectBuiltin(name?: string): Promise { header('Eject Builtin'); const lang = getLanguage(); - const builtinWorkflowsDir = getBuiltinWorkflowsDir(lang); + const builtinWorkflowsDir = getBuiltinPiecesDir(lang); if (!name) { // List available builtins @@ -40,7 +40,7 @@ export async function ejectBuiltin(name?: string): Promise { return; } - const userWorkflowsDir = getGlobalWorkflowsDir(); + const userWorkflowsDir = getGlobalPiecesDir(); const userAgentsDir = getGlobalAgentsDir(); const builtinAgentsDir = getBuiltinAgentsDir(lang); diff --git a/src/features/interactive/interactive.ts b/src/features/interactive/interactive.ts index 109f222..99730ca 100644 --- a/src/features/interactive/interactive.ts +++ b/src/features/interactive/interactive.ts @@ -19,8 +19,8 @@ import { getProvider, type ProviderType } from '../../infra/providers/index.js'; import { selectOption } from '../../shared/prompt/index.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js'; import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js'; -import { getPrompt } from '../../shared/prompts/index.js'; -import { getLabelObject } from '../../shared/i18n/index.js'; +import { loadTemplate } from '../../shared/prompts/index.js'; +import { getLabel, getLabelObject } from '../../shared/i18n/index.js'; const log = createLogger('interactive'); /** Shape of interactive UI text */ @@ -40,25 +40,20 @@ function resolveLanguage(lang?: Language): 'en' | 'ja' { } function getInteractivePrompts(lang: 'en' | 'ja', workflowContext?: WorkflowContext) { - let systemPrompt = getPrompt('interactive.systemPrompt', lang); - let summaryPrompt = getPrompt('interactive.summaryPrompt', lang); + const hasWorkflow = !!workflowContext; - // Add workflow context to prompts if available - if (workflowContext) { - const workflowInfo = getPrompt('interactive.workflowInfo', lang, { - name: workflowContext.name, - description: workflowContext.description, - }); - - systemPrompt += workflowInfo; - summaryPrompt += workflowInfo; - } + const systemPrompt = loadTemplate('score_interactive_system_prompt', lang, { + workflowInfo: hasWorkflow, + workflowName: workflowContext?.name ?? '', + workflowDescription: workflowContext?.description ?? '', + }); return { systemPrompt, - summaryPrompt, - conversationLabel: getPrompt('interactive.conversationLabel', lang), - noTranscript: getPrompt('interactive.noTranscript', lang), + lang, + workflowContext, + conversationLabel: getLabel('interactive.conversationLabel', lang), + noTranscript: getLabel('interactive.noTranscript', lang), ui: getLabelObject('interactive.ui', lang), }; } @@ -83,21 +78,36 @@ function buildTaskFromHistory(history: ConversationMessage[]): string { .join('\n\n'); } +/** + * Build the summary prompt (used as both system prompt and user message). + * Renders the complete score_summary_system_prompt template with conversation data. + * Returns empty string if there is no conversation to summarize. + */ function buildSummaryPrompt( history: ConversationMessage[], hasSession: boolean, - summaryPrompt: string, + lang: 'en' | 'ja', noTranscriptNote: string, conversationLabel: string, + workflowContext?: WorkflowContext, ): string { + let conversation = ''; if (history.length > 0) { const historyText = buildTaskFromHistory(history); - return `${summaryPrompt}\n\n${conversationLabel}\n${historyText}`; + conversation = `${conversationLabel}\n${historyText}`; + } else if (hasSession) { + conversation = `${conversationLabel}\n${noTranscriptNote}`; + } else { + return ''; } - if (hasSession) { - return `${summaryPrompt}\n\n${conversationLabel}\n${noTranscriptNote}`; - } - return ''; + + const hasWorkflow = !!workflowContext; + return loadTemplate('score_summary_system_prompt', lang, { + workflowInfo: hasWorkflow, + workflowName: workflowContext?.name ?? '', + workflowDescription: workflowContext?.description ?? '', + conversation, + }); } async function confirmTask(task: string, message: string, confirmLabel: string, yesLabel: string, noLabel: string): Promise { @@ -305,18 +315,19 @@ export async function interactiveMode( let summaryPrompt = buildSummaryPrompt( history, !!sessionId, - prompts.summaryPrompt, + prompts.lang, prompts.noTranscript, prompts.conversationLabel, + prompts.workflowContext, ); - if (summaryPrompt && userNote) { - summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`; - } if (!summaryPrompt) { info(prompts.ui.noConversation); continue; } - const summaryResult = await callAIWithRetry(summaryPrompt, prompts.summaryPrompt); + if (userNote) { + summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`; + } + const summaryResult = await callAIWithRetry(summaryPrompt, summaryPrompt); if (!summaryResult) { info(prompts.ui.summarizeFailed); continue; diff --git a/src/features/prompt/index.ts b/src/features/prompt/index.ts new file mode 100644 index 0000000..a42a0d9 --- /dev/null +++ b/src/features/prompt/index.ts @@ -0,0 +1,5 @@ +/** + * Prompt feature exports + */ + +export { previewPrompts } from './preview.js'; diff --git a/src/features/prompt/preview.ts b/src/features/prompt/preview.ts new file mode 100644 index 0000000..15d0dce --- /dev/null +++ b/src/features/prompt/preview.ts @@ -0,0 +1,87 @@ +/** + * Prompt preview feature + * + * Loads a workflow 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 type { Language } from '../../core/models/types.js'; +import { header, info, error, blankLine } from '../../shared/ui/index.js'; + +/** + * Preview all prompts for a workflow. + * + * Loads the workflow 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); + + if (!config) { + error(`Workflow "${identifier}" not found.`); + return; + } + + const globalConfig = loadGlobalConfig(); + const language: Language = globalConfig.language ?? 'en'; + + header(`Prompt Preview: ${config.name}`); + info(`Movements: ${config.movements.length}`); + info(`Language: ${language}`); + blankLine(); + + for (const [i, movement] of config.movements.entries()) { + const separator = '='.repeat(60); + + console.log(separator); + console.log(`Movement ${i + 1}: ${movement.name} (agent: ${movement.agentDisplayName})`); + console.log(separator); + + // Phase 1: Main execution + const context: InstructionContext = { + task: '', + iteration: 1, + maxIterations: config.maxIterations, + movementIteration: 1, + cwd, + projectCwd: cwd, + userInputs: [], + workflowMovements: config.movements, + currentMovementIndex: i, + reportDir: movement.report ? '.takt/reports/preview' : undefined, + language, + }; + + const phase1Builder = new InstructionBuilder(movement, context); + console.log('\n--- Phase 1 (Main Execution) ---\n'); + console.log(phase1Builder.build()); + + // Phase 2: Report output (only if movement has report config) + if (movement.report) { + const reportBuilder = new ReportInstructionBuilder(movement, { + cwd, + reportDir: '.takt/reports/preview', + movementIteration: 1, + language, + }); + console.log('\n--- Phase 2 (Report Output) ---\n'); + console.log(reportBuilder.build()); + } + + // Phase 3: Status judgment (only if movement has tag-based rules) + if (needsStatusJudgmentPhase(movement)) { + const judgmentBuilder = new StatusJudgmentBuilder(movement, { language }); + console.log('\n--- Phase 3 (Status Judgment) ---\n'); + console.log(judgmentBuilder.build()); + } + + blankLine(); + } +} diff --git a/src/features/tasks/execute/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts index f782f3a..3b56853 100644 --- a/src/features/tasks/execute/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -41,7 +41,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise log.debug('Running workflow', { name: workflowConfig.name, - steps: workflowConfig.steps.map((s: { name: string }) => s.name), + movements: workflowConfig.movements.map((s: { name: string }) => s.name), }); const globalConfig = loadGlobalConfig(); diff --git a/src/features/tasks/execute/workflowExecution.ts b/src/features/tasks/execute/workflowExecution.ts index 07993ac..225eb09 100644 --- a/src/features/tasks/execute/workflowExecution.ts +++ b/src/features/tasks/execute/workflowExecution.ts @@ -159,7 +159,7 @@ export async function executeWorkflow( maxIterations: String(request.maxIterations), }) ); - info(getLabel('workflow.iterationLimit.currentStep', undefined, { currentStep: request.currentStep })); + info(getLabel('workflow.iterationLimit.currentMovement', undefined, { currentMovement: request.currentMovement })); const action = await selectOption(getLabel('workflow.iterationLimit.continueQuestion'), [ { @@ -248,8 +248,8 @@ export async function executeWorkflow( appendNdjsonLine(ndjsonLogPath, record); }); - engine.on('step:start', (step, iteration, instruction) => { - log.debug('Step starting', { step: step.name, agent: step.agentDisplayName, iteration }); + 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})`); // Log prompt content for debugging @@ -273,8 +273,8 @@ export async function executeWorkflow( }); - engine.on('step:complete', (step, response, instruction) => { - log.debug('Step completed', { + engine.on('movement:complete', (step, response, instruction) => { + log.debug('Movement completed', { step: step.name, status: response.status, matchedRuleIndex: response.matchedRuleIndex, @@ -329,7 +329,7 @@ export async function executeWorkflow( updateLatestPointer(sessionLog, workflowSessionId, projectCwd); }); - engine.on('step:report', (_step, filePath, fileName) => { + engine.on('movement:report', (_step, filePath, fileName) => { const content = readFileSync(filePath, 'utf-8'); console.log(`\n📄 Report: ${fileName}\n`); console.log(content); diff --git a/src/index.ts b/src/index.ts index 72e8376..017c41f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -105,7 +105,7 @@ export { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES, - determineNextStepByRules, + determineNextMovementByRules, extractBlockedPrompt, LoopDetector, createInitialState, @@ -117,8 +117,7 @@ export { isReportObjectConfig, ReportInstructionBuilder, StatusJudgmentBuilder, - buildExecutionMetadata, - renderExecutionMetadata, + buildEditRule, RuleEvaluator, detectMatchedRule, evaluateAggregateConditions, @@ -141,7 +140,7 @@ export type { ReportInstructionContext, StatusJudgmentContext, InstructionContext, - ExecutionMetadata, + StatusRulesComponents, BlockedHandlerResult, } from './core/workflow/index.js'; diff --git a/src/infra/claude/client.ts b/src/infra/claude/client.ts index 84f864d..ae379f5 100644 --- a/src/infra/claude/client.ts +++ b/src/infra/claude/client.ts @@ -8,7 +8,7 @@ import { executeClaudeCli } from './process.js'; import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js'; import type { AgentResponse, Status } from '../../core/models/index.js'; import { createLogger } from '../../shared/utils/index.js'; -import { getPrompt } from '../../shared/prompts/index.js'; +import { loadTemplate } from '../../shared/prompts/index.js'; // Re-export for backward compatibility export type { ClaudeCallOptions } from './types.js'; @@ -21,8 +21,8 @@ const log = createLogger('client'); * * Example: detectRuleIndex("... [PLAN:2] ...", "plan") → 1 */ -export function detectRuleIndex(content: string, stepName: string): number { - const tag = stepName.toUpperCase(); +export function detectRuleIndex(content: string, movementName: string): number { + const tag = movementName.toUpperCase(); const regex = new RegExp(`\\[${tag}:(\\d+)\\]`, 'gi'); const matches = [...content.matchAll(regex)]; const match = matches.at(-1); @@ -153,7 +153,7 @@ export class ClaudeClient { prompt: string, options: ClaudeCallOptions, ): Promise { - const systemPrompt = getPrompt('claude.agentDefault', undefined, { agentName: claudeAgentName }); + const systemPrompt = loadTemplate('perform_builtin_agent_system_prompt', 'en', { agentName: claudeAgentName }); return this.callCustom(claudeAgentName, prompt, systemPrompt, options); } @@ -219,7 +219,7 @@ export class ClaudeClient { .map((c) => `| ${c.index + 1} | ${c.text} |`) .join('\n'); - return getPrompt('claude.judgePrompt', undefined, { agentOutput, conditionList }); + return loadTemplate('perform_judge_message', 'en', { agentOutput, conditionList }); } /** diff --git a/src/infra/config/loaders/agentLoader.ts b/src/infra/config/loaders/agentLoader.ts index a2ef729..309f8b9 100644 --- a/src/infra/config/loaders/agentLoader.ts +++ b/src/infra/config/loaders/agentLoader.ts @@ -11,9 +11,9 @@ import { join, basename } from 'node:path'; import type { CustomAgentConfig } from '../../../core/models/index.js'; import { getGlobalAgentsDir, - getGlobalWorkflowsDir, + getGlobalPiecesDir, getBuiltinAgentsDir, - getBuiltinWorkflowsDir, + getBuiltinPiecesDir, isPathSafe, } from '../paths.js'; import { getLanguage } from '../global/globalConfig.js'; @@ -23,9 +23,9 @@ function getAllowedAgentBases(): string[] { const lang = getLanguage(); return [ getGlobalAgentsDir(), - getGlobalWorkflowsDir(), + getGlobalPiecesDir(), getBuiltinAgentsDir(lang), - getBuiltinWorkflowsDir(lang), + getBuiltinPiecesDir(lang), ]; } diff --git a/src/infra/config/loaders/workflowParser.ts b/src/infra/config/loaders/workflowParser.ts index a2d8ef5..43c97be 100644 --- a/src/infra/config/loaders/workflowParser.ts +++ b/src/infra/config/loaders/workflowParser.ts @@ -9,11 +9,11 @@ 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, WorkflowStepRawSchema } from '../../../core/models/index.js'; -import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } from '../../../core/models/index.js'; +import { WorkflowConfigRawSchema, WorkflowMovementRawSchema } from '../../../core/models/index.js'; +import type { WorkflowConfig, WorkflowMovement, WorkflowRule, ReportConfig, ReportObjectConfig } from '../../../core/models/index.js'; -/** Parsed step type from Zod schema (replaces `any`) */ -type RawStep = z.output; +/** Parsed movement type from Zod schema (replaces `any`) */ +type RawStep = z.output; /** * Resolve agent path from workflow specification. @@ -168,8 +168,8 @@ function normalizeRule(r: { }; } -/** Normalize a raw step into internal WorkflowStep format. */ -function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep { +/** Normalize a raw step into internal WorkflowMovement format. */ +function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowMovement { const rules: WorkflowRule[] | undefined = step.rules?.map(normalizeRule); const agentSpec: string | undefined = step.agent || undefined; @@ -183,7 +183,7 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep } } - const result: WorkflowStep = { + const result: WorkflowMovement = { name: step.name, description: step.description, agent: agentSpec, @@ -215,15 +215,20 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep export function normalizeWorkflowConfig(raw: unknown, workflowDir: string): WorkflowConfig { const parsed = WorkflowConfigRawSchema.parse(raw); - const steps: WorkflowStep[] = parsed.steps.map((step) => + // Prefer `movements` over legacy `steps` + const rawMovements = parsed.movements ?? parsed.steps ?? []; + const movements: WorkflowMovement[] = rawMovements.map((step) => normalizeStepFromRaw(step, workflowDir), ); + // Prefer `initial_movement` over legacy `initial_step` + const initialMovement = parsed.initial_movement ?? parsed.initial_step ?? movements[0]?.name ?? ''; + return { name: parsed.name, description: parsed.description, - steps, - initialStep: parsed.initial_step || steps[0]?.name || '', + movements, + initialMovement, maxIterations: parsed.max_iterations, answerAgent: parsed.answer_agent, }; diff --git a/src/infra/config/loaders/workflowResolver.ts b/src/infra/config/loaders/workflowResolver.ts index a8832eb..6e29f03 100644 --- a/src/infra/config/loaders/workflowResolver.ts +++ b/src/infra/config/loaders/workflowResolver.ts @@ -9,7 +9,7 @@ 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 { getGlobalWorkflowsDir, getBuiltinWorkflowsDir, getProjectConfigDir } from '../paths.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'; @@ -25,7 +25,7 @@ export interface WorkflowWithSource { export function listBuiltinWorkflowNames(options?: { includeDisabled?: boolean }): string[] { const lang = getLanguage(); - const dir = getBuiltinWorkflowsDir(lang); + const dir = getBuiltinPiecesDir(lang); const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins(); const names = new Set(); for (const entry of iterateWorkflowDir(dir, 'builtin', disabled)) { @@ -41,7 +41,7 @@ export function getBuiltinWorkflow(name: string): WorkflowConfig | null { const disabled = getDisabledBuiltins(); if (disabled.includes(name)) return null; - const builtinDir = getBuiltinWorkflowsDir(lang); + const builtinDir = getBuiltinPiecesDir(lang); const yamlPath = join(builtinDir, `${name}.yaml`); if (existsSync(yamlPath)) { return loadWorkflowFromFile(yamlPath); @@ -109,12 +109,20 @@ export function loadWorkflow( return loadWorkflowFromFile(projectMatch); } - const globalWorkflowsDir = getGlobalWorkflowsDir(); - const globalMatch = resolveWorkflowFile(globalWorkflowsDir, name); + const globalPiecesDir = getGlobalPiecesDir(); + const globalMatch = resolveWorkflowFile(globalPiecesDir, name); if (globalMatch) { return loadWorkflowFromFile(globalMatch); } + // Fallback: legacy ~/.takt/workflows/ directory (deprecated) + const legacyGlobalDir = join(homedir(), '.takt', 'workflows'); + const legacyMatch = resolveWorkflowFile(legacyGlobalDir, name); + if (legacyMatch) { + log.info(`Loading workflow from deprecated path ~/.takt/workflows/. Please move to ~/.takt/pieces/.`); + return loadWorkflowFromFile(legacyMatch); + } + return getBuiltinWorkflow(name); } @@ -219,9 +227,15 @@ function getWorkflowDirs(cwd: string): { dir: string; source: WorkflowSource; di const lang = getLanguage(); const dirs: { dir: string; source: WorkflowSource; disabled?: string[] }[] = []; if (getBuiltinWorkflowsEnabled()) { - dirs.push({ dir: getBuiltinWorkflowsDir(lang), disabled, source: 'builtin' }); + dirs.push({ dir: getBuiltinPiecesDir(lang), disabled, source: 'builtin' }); } - dirs.push({ dir: getGlobalWorkflowsDir(), source: 'user' }); + // Legacy fallback: ~/.takt/workflows/ (deprecated, lowest user priority) + const legacyGlobalDir = join(homedir(), '.takt', 'workflows'); + if (existsSync(legacyGlobalDir)) { + log.info(`Scanning deprecated path ~/.takt/workflows/. Please move to ~/.takt/pieces/.`); + dirs.push({ dir: legacyGlobalDir, source: 'user' }); + } + dirs.push({ dir: getGlobalPiecesDir(), source: 'user' }); dirs.push({ dir: join(getProjectConfigDir(cwd), 'workflows'), source: 'project' }); return dirs; } diff --git a/src/infra/config/paths.ts b/src/infra/config/paths.ts index 858355f..444e15e 100644 --- a/src/infra/config/paths.ts +++ b/src/infra/config/paths.ts @@ -21,11 +21,14 @@ export function getGlobalAgentsDir(): string { return join(getGlobalConfigDir(), 'agents'); } -/** Get takt global workflows directory (~/.takt/workflows) */ -export function getGlobalWorkflowsDir(): string { - return join(getGlobalConfigDir(), 'workflows'); +/** Get takt global pieces directory (~/.takt/pieces) */ +export function getGlobalPiecesDir(): string { + return join(getGlobalConfigDir(), 'pieces'); } +/** @deprecated Use getGlobalPiecesDir() instead */ +export const getGlobalWorkflowsDir = getGlobalPiecesDir; + /** Get takt global logs directory */ export function getGlobalLogsDir(): string { return join(getGlobalConfigDir(), 'logs'); @@ -36,11 +39,14 @@ export function getGlobalConfigPath(): string { return join(getGlobalConfigDir(), 'config.yaml'); } -/** Get builtin workflows directory (resources/global/{lang}/workflows) */ -export function getBuiltinWorkflowsDir(lang: Language): string { - return join(getLanguageResourcesDir(lang), 'workflows'); +/** Get builtin pieces directory (resources/global/{lang}/pieces) */ +export function getBuiltinPiecesDir(lang: Language): string { + return join(getLanguageResourcesDir(lang), 'pieces'); } +/** @deprecated Use getBuiltinPiecesDir() instead */ +export const getBuiltinWorkflowsDir = getBuiltinPiecesDir; + /** Get builtin agents directory (resources/global/{lang}/agents) */ export function getBuiltinAgentsDir(lang: Language): string { return join(getLanguageResourcesDir(lang), 'agents'); diff --git a/src/infra/task/summarize.ts b/src/infra/task/summarize.ts index d0fdd88..fb665d6 100644 --- a/src/infra/task/summarize.ts +++ b/src/infra/task/summarize.ts @@ -8,7 +8,7 @@ import * as wanakana from 'wanakana'; import { loadGlobalConfig } from '../config/global/globalConfig.js'; import { getProvider, type ProviderType } from '../providers/index.js'; import { createLogger } from '../../shared/utils/index.js'; -import { getPrompt } from '../../shared/prompts/index.js'; +import { loadTemplate } from '../../shared/prompts/index.js'; import type { SummarizeOptions } from './types.js'; export type { SummarizeOptions }; @@ -70,7 +70,7 @@ export class TaskSummarizer { const response = await provider.call('summarizer', taskName, { cwd: options.cwd, model, - systemPrompt: getPrompt('summarize.slugGenerator'), + systemPrompt: loadTemplate('score_slug_system_prompt', 'en'), allowedTools: [], }); diff --git a/src/shared/i18n/labels_en.yaml b/src/shared/i18n/labels_en.yaml index 5ca9f34..d5e7872 100644 --- a/src/shared/i18n/labels_en.yaml +++ b/src/shared/i18n/labels_en.yaml @@ -7,6 +7,8 @@ # ===== Interactive Mode UI ===== interactive: + conversationLabel: "Conversation:" + noTranscript: "(No local transcript. Summarize the current session context.)" ui: intro: "Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)" resume: "Resuming previous session" @@ -21,7 +23,7 @@ interactive: workflow: iterationLimit: maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})" - currentStep: "現在のステップ: {currentStep}" + currentMovement: "現在のムーブメント: {currentMovement}" continueQuestion: "続行しますか?" continueLabel: "続行する(追加イテレーション数を入力)" continueDescription: "入力した回数だけ上限を増やします" diff --git a/src/shared/i18n/labels_ja.yaml b/src/shared/i18n/labels_ja.yaml index 453c531..939a11a 100644 --- a/src/shared/i18n/labels_ja.yaml +++ b/src/shared/i18n/labels_ja.yaml @@ -7,6 +7,8 @@ # ===== Interactive Mode UI ===== interactive: + conversationLabel: "会話:" + noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)" ui: intro: "対話モード - タスク内容を入力してください。コマンド: /go(実行), /cancel(終了)" resume: "前回のセッションを再開します" @@ -21,7 +23,7 @@ interactive: workflow: iterationLimit: maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})" - currentStep: "現在のステップ: {currentStep}" + currentMovement: "現在のムーブメント: {currentMovement}" continueQuestion: "続行しますか?" continueLabel: "続行する(追加イテレーション数を入力)" continueDescription: "入力した回数だけ上限を増やします" diff --git a/src/shared/prompts/en/perform_agent_system_prompt.md b/src/shared/prompts/en/perform_agent_system_prompt.md new file mode 100644 index 0000000..9145c9c --- /dev/null +++ b/src/shared/prompts/en/perform_agent_system_prompt.md @@ -0,0 +1,7 @@ + +{{agentDefinition}} diff --git a/src/shared/prompts/en/perform_builtin_agent_system_prompt.md b/src/shared/prompts/en/perform_builtin_agent_system_prompt.md new file mode 100644 index 0000000..cfc88bd --- /dev/null +++ b/src/shared/prompts/en/perform_builtin_agent_system_prompt.md @@ -0,0 +1,7 @@ + +You are the {{agentName}} agent. Follow the standard {{agentName}} workflow. diff --git a/src/shared/prompts/en/perform_judge_message.md b/src/shared/prompts/en/perform_judge_message.md new file mode 100644 index 0000000..cc674dc --- /dev/null +++ b/src/shared/prompts/en/perform_judge_message.md @@ -0,0 +1,24 @@ + +# Judge Task + +You are a judge evaluating an agent's output against a set of conditions. +Read the agent output below, then determine which condition best matches. + +## Agent Output +``` +{{agentOutput}} +``` + +## Conditions +| # | Condition | +|---|-----------| +{{conditionList}} + +## Instructions +Output ONLY the tag `[JUDGE:N]` where N is the number of the best matching condition. +Do not output anything else. diff --git a/src/shared/prompts/en/perform_phase1_message.md b/src/shared/prompts/en/perform_phase1_message.md new file mode 100644 index 0000000..24dbdc5 --- /dev/null +++ b/src/shared/prompts/en/perform_phase1_message.md @@ -0,0 +1,45 @@ + +## Execution Context +- Working Directory: {{workingDirectory}} + +## Execution Rules +- **Do NOT run git commit.** Commits are handled automatically by the system after workflow completion. +- **Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories. +{{#if editRule}}- {{editRule}} +{{/if}} +Note: This section is metadata. Follow the language used in the rest of the prompt. + +## Workflow Context +{{#if workflowStructure}}{{workflowStructure}} + +{{/if}}- Iteration: {{iteration}}(workflow-wide) +- Movement Iteration: {{movementIteration}}(times this movement has run) +- Movement: {{movement}} +{{#if hasReport}}{{reportInfo}} + +{{phaseNote}}{{/if}} +{{#if hasTaskSection}} + +## User Request +{{userRequest}} +{{/if}} +{{#if hasPreviousResponse}} + +## Previous Response +{{previousResponse}} +{{/if}} +{{#if hasUserInputs}} + +## Additional User Inputs +{{userInputs}} +{{/if}} + +## Instructions +{{instructions}} diff --git a/src/shared/prompts/en/perform_phase2_message.md b/src/shared/prompts/en/perform_phase2_message.md new file mode 100644 index 0000000..48f8775 --- /dev/null +++ b/src/shared/prompts/en/perform_phase2_message.md @@ -0,0 +1,31 @@ + +## Execution Context +- Working Directory: {{workingDirectory}} + +## Execution Rules +- **Do NOT run git commit.** Commits are handled automatically by the system after workflow completion. +- **Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories. +- **Do NOT modify project source files.** Only respond with the report content. +- **Use only the Report Directory files listed below.** Do not search or open reports outside that directory. +Note: This section is metadata. Follow the language used in the rest of the prompt. + +## Workflow Context +{{reportContext}} + +## Instructions +Respond with the results of the work you just completed as a report. **Tools are not available in this phase. Respond with the report content directly as text.** +**Respond with only the report content (no status tags, no commentary). You cannot use the Write tool or any other tools.** +{{#if hasReportOutput}} + +{{reportOutput}} +{{/if}} +{{#if hasReportFormat}} + +{{reportFormat}} +{{/if}} diff --git a/src/shared/prompts/en/perform_phase3_message.md b/src/shared/prompts/en/perform_phase3_message.md new file mode 100644 index 0000000..f40a080 --- /dev/null +++ b/src/shared/prompts/en/perform_phase3_message.md @@ -0,0 +1,19 @@ + +Review your work results and determine the status. Do NOT perform any additional work. + +## Decision Criteria + +{{criteriaTable}} + +## Output Format + +{{outputList}} +{{#if hasAppendix}} + +### Appendix Template +{{appendixContent}}{{/if}} diff --git a/src/shared/prompts/en/score_interactive_system_prompt.md b/src/shared/prompts/en/score_interactive_system_prompt.md new file mode 100644 index 0000000..ab9e71d --- /dev/null +++ b/src/shared/prompts/en/score_interactive_system_prompt.md @@ -0,0 +1,53 @@ + +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. + +## 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 +- 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). + +## Critical: Understanding user intent +**The user is asking YOU to create a task instruction for the WORKFLOW, 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) + +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. + +## When investigation IS appropriate (rare cases) +Only investigate when the user explicitly asks YOU (the planning assistant) to check something: +- "Check the README to understand the project structure" ✓ +- "Read file X to see what it does" ✓ +- "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) + +## Strict constraints +- You are ONLY refining requirements. Do NOT execute the task. +- Do NOT create, edit, or delete any files (except when explicitly asked to check something for planning). +- 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}} + +## Destination of Your Task Instruction +This task instruction will be passed to the "{{workflowName}}" workflow. +Workflow description: {{workflowDescription}} + +Create the instruction in the format expected by this workflow. +{{/if}} diff --git a/src/shared/prompts/en/score_slug_system_prompt.md b/src/shared/prompts/en/score_slug_system_prompt.md new file mode 100644 index 0000000..92ab569 --- /dev/null +++ b/src/shared/prompts/en/score_slug_system_prompt.md @@ -0,0 +1,19 @@ + +You are a slug generator. Given a task description, output ONLY a slug. + +NEVER output sentences. NEVER start with "this", "the", "i", "we", or "it". +ALWAYS start with a verb: add, fix, update, refactor, implement, remove, etc. + +Format: verb-noun (lowercase, hyphens, max 30 chars) + +Input → Output: +認証機能を追加する → add-auth +Fix the login bug → fix-login-bug +ユーザー登録にメール認証を追加 → add-email-verification +worktreeを作るときブランチ名をAIで生成 → ai-branch-naming +レビュー画面に元の指示を表示する → show-original-instruction diff --git a/src/shared/prompts/en/score_summary_system_prompt.md b/src/shared/prompts/en/score_summary_system_prompt.md new file mode 100644 index 0000000..0455c0c --- /dev/null +++ b/src/shared/prompts/en/score_summary_system_prompt.md @@ -0,0 +1,25 @@ + +You are a task summarizer. Convert the conversation into a concrete task instruction for the planning step. + +Requirements: +- Output only the final task instruction (no preamble). +- Be specific about scope and targets (files/modules) if mentioned. +- Preserve constraints and "do not" instructions. +- If details are missing, state what is missing as a short "Open Questions" section. +{{#if workflowInfo}} + +## Destination of Your Task Instruction +This task instruction will be passed to the "{{workflowName}}" workflow. +Workflow description: {{workflowDescription}} + +Create the instruction in the format expected by this workflow. +{{/if}} +{{#if conversation}} + +{{conversation}} +{{/if}} diff --git a/src/shared/prompts/index.ts b/src/shared/prompts/index.ts index fb0c1d1..696fe67 100644 --- a/src/shared/prompts/index.ts +++ b/src/shared/prompts/index.ts @@ -1,110 +1,144 @@ /** - * Prompt loader utility + * Markdown template loader * - * Loads prompt strings from language-specific YAML files - * (prompts_en.yaml / prompts_ja.yaml) and provides - * key-based access with template variable substitution. + * Loads prompt strings from Markdown template files (.md), + * applies {{variable}} substitution and {{#if}}...{{else}}...{{/if}} + * conditional blocks. + * + * Templates are organized in language subdirectories: + * {lang}/{name}.md — localized templates */ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import type { Language } from '../../core/models/types.js'; -import { DEFAULT_LANGUAGE } from '../constants.js'; -/** Cached YAML data per language */ -const promptCache = new Map>(); +/** Cached raw template text (before variable substitution) */ +const templateCache = new Map(); -function loadPrompts(lang: Language): Record { - const cached = promptCache.get(lang); - if (cached) return cached; +/** + * Resolve template file path. + * + * Loads `{lang}/{name}.md`. + * Throws if the file does not exist. + */ +function resolveTemplatePath(name: string, lang: Language): string { const __dirname = dirname(fileURLToPath(import.meta.url)); - const yamlPath = join(__dirname, `prompts_${lang}.yaml`); - const content = readFileSync(yamlPath, 'utf-8'); - const data = parseYaml(content) as Record; - promptCache.set(lang, data); - return data; -} -/** - * Resolve a dot-separated key path to a value in a nested object. - * Returns undefined if the path does not exist. - */ -function resolveKey(obj: Record, keyPath: string): unknown { - const parts = keyPath.split('.'); - let current: unknown = obj; - for (const part of parts) { - if (current === null || current === undefined || typeof current !== 'object') { - return undefined; - } - current = (current as Record)[part]; + const localizedPath = join(__dirname, lang, `${name}.md`); + if (existsSync(localizedPath)) { + return localizedPath; } - return current; + + throw new Error( + `Template not found: ${name} (lang: ${lang})`, + ); } /** - * Replace {key} placeholders in a template string with values from vars. - * Unmatched placeholders are left as-is. + * Strip HTML meta comments () from template content. */ -function applyVars(template: string, vars: Record): string { - return template.replace(/\{(\w+)\}/g, (match, key: string) => { - if (key in vars) { - const value: string = vars[key] as string; - return value; - } - return match; - }); +function stripMetaComments(content: string): string { + return content.replace(//g, ''); } /** - * Get a prompt string from the language-specific YAML by dot-separated key. - * - * When `lang` is provided, loads the corresponding language file. - * When `lang` is omitted, uses DEFAULT_LANGUAGE. - * - * Template variables in `{name}` format are replaced when `vars` is given. + * Read raw template text with caching. */ -export function getPrompt( - key: string, - lang?: Language, - vars?: Record, +function readTemplate(filePath: string): string { + const cached = templateCache.get(filePath); + if (cached !== undefined) return cached; + + const raw = readFileSync(filePath, 'utf-8'); + const content = stripMetaComments(raw); + templateCache.set(filePath, content); + return content; +} + +/** + * Process {{#if variable}}...{{else}}...{{/if}} conditional blocks. + * + * A variable is truthy when: + * - It is a non-empty string + * - It is boolean true + * + * Nesting is NOT supported (per architecture decision). + */ +function processConditionals( + template: string, + vars: Record, ): string { - const effectiveLang = lang ?? DEFAULT_LANGUAGE; - const data = loadPrompts(effectiveLang); + // Pattern: {{#if varName}}...content...{{else}}...altContent...{{/if}} + // or: {{#if varName}}...content...{{/if}} + return template.replace( + /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, + (_match, varName: string, body: string): string => { + const value = vars[varName]; + const isTruthy = value !== undefined && value !== false && value !== ''; - const value = resolveKey(data, key); - if (typeof value !== 'string') { - throw new Error(`Prompt key not found: ${key}${lang ? ` (lang: ${lang})` : ''}`); - } + const elseIndex = body.indexOf('{{else}}'); + if (isTruthy) { + return elseIndex >= 0 ? body.slice(0, elseIndex) : body; + } + return elseIndex >= 0 ? body.slice(elseIndex + '{{else}}'.length) : ''; + }, + ); +} + +/** + * Replace {{variableName}} placeholders with values from vars. + * Undefined variables are replaced with empty string. + */ +function substituteVariables( + template: string, + vars: Record, +): string { + return template.replace( + /\{\{(\w+)\}\}/g, + (_match, varName: string) => { + const value = vars[varName]; + if (value === undefined || value === false) return ''; + if (value === true) return 'true'; + return value; + }, + ); +} + +/** + * Render a template string by processing conditionals then substituting variables. + */ +export function renderTemplate( + template: string, + vars: Record, +): string { + const afterConditionals = processConditionals(template, vars); + return substituteVariables(afterConditionals, vars); +} + +/** + * Load a Markdown template, apply variable substitution and conditional blocks. + * + * @param name Template name (without extension), e.g. 'score_interactive_system_prompt' + * @param lang Language ('en' | 'ja'). + * @param vars Variable values to substitute + * @returns Final prompt string + */ +export function loadTemplate( + name: string, + lang: Language, + vars?: Record, +): string { + const filePath = resolveTemplatePath(name, lang); + const raw = readTemplate(filePath); if (vars) { - return applyVars(value, vars); + return renderTemplate(raw, vars); } - return value; + return raw; } -/** - * Get a nested object from the language-specific YAML by dot-separated key. - * - * When `lang` is provided, loads the corresponding language file. - * When `lang` is omitted, uses DEFAULT_LANGUAGE. - * - * Useful for structured prompt groups (e.g. UI text objects, metadata strings). - */ -export function getPromptObject(key: string, lang?: Language): T { - const effectiveLang = lang ?? DEFAULT_LANGUAGE; - const data = loadPrompts(effectiveLang); - - const value = resolveKey(data, key); - if (value === undefined || value === null) { - throw new Error(`Prompt key not found: ${key}${lang ? ` (lang: ${lang})` : ''}`); - } - - return value as T; -} - -/** Reset cached data (for testing) */ +/** Reset cache (for tests) */ export function _resetCache(): void { - promptCache.clear(); + templateCache.clear(); } diff --git a/src/shared/prompts/ja/perform_agent_system_prompt.md b/src/shared/prompts/ja/perform_agent_system_prompt.md new file mode 100644 index 0000000..9145c9c --- /dev/null +++ b/src/shared/prompts/ja/perform_agent_system_prompt.md @@ -0,0 +1,7 @@ + +{{agentDefinition}} diff --git a/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md b/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md new file mode 100644 index 0000000..cfc88bd --- /dev/null +++ b/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md @@ -0,0 +1,7 @@ + +You are the {{agentName}} agent. Follow the standard {{agentName}} workflow. diff --git a/src/shared/prompts/ja/perform_judge_message.md b/src/shared/prompts/ja/perform_judge_message.md new file mode 100644 index 0000000..c689a2f --- /dev/null +++ b/src/shared/prompts/ja/perform_judge_message.md @@ -0,0 +1,24 @@ + +# 判定タスク + +あなたはエージェントの出力を条件セットに対して評価する判定者です。 +以下のエージェント出力を読み、最も一致する条件を判定してください。 + +## エージェント出力 +``` +{{agentOutput}} +``` + +## 条件 +| # | 条件 | +|---|------| +{{conditionList}} + +## 指示 +最も一致する条件の番号のタグ `[JUDGE:N]` のみを出力してください。 +それ以外は出力しないでください。 diff --git a/src/shared/prompts/ja/perform_phase1_message.md b/src/shared/prompts/ja/perform_phase1_message.md new file mode 100644 index 0000000..31f89e9 --- /dev/null +++ b/src/shared/prompts/ja/perform_phase1_message.md @@ -0,0 +1,44 @@ + +## 実行コンテキスト +- 作業ディレクトリ: {{workingDirectory}} + +## 実行ルール +- **git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。 +- **Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。 +{{#if editRule}}- {{editRule}} +{{/if}} + +## Workflow Context +{{#if workflowStructure}}{{workflowStructure}} + +{{/if}}- Iteration: {{iteration}}(ワークフロー全体) +- Movement Iteration: {{movementIteration}}(このムーブメントの実行回数) +- Movement: {{movement}} +{{#if hasReport}}{{reportInfo}} + +{{phaseNote}}{{/if}} +{{#if hasTaskSection}} + +## User Request +{{userRequest}} +{{/if}} +{{#if hasPreviousResponse}} + +## Previous Response +{{previousResponse}} +{{/if}} +{{#if hasUserInputs}} + +## Additional User Inputs +{{userInputs}} +{{/if}} + +## Instructions +{{instructions}} diff --git a/src/shared/prompts/ja/perform_phase2_message.md b/src/shared/prompts/ja/perform_phase2_message.md new file mode 100644 index 0000000..6e20f7b --- /dev/null +++ b/src/shared/prompts/ja/perform_phase2_message.md @@ -0,0 +1,30 @@ + +## 実行コンテキスト +- 作業ディレクトリ: {{workingDirectory}} + +## 実行ルール +- **git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。 +- **Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。 +- **プロジェクトのソースファイルを変更しないでください。** レポート内容のみを回答してください。 +- **Report Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。 + +## Workflow Context +{{reportContext}} + +## Instructions +あなたが今行った作業の結果をレポートとして回答してください。**このフェーズではツールは使えません。レポート内容をテキストとして直接回答してください。** +**レポート本文のみを回答してください(ステータスタグやコメントは禁止)。Writeツールやその他のツールは使用できません。** +{{#if hasReportOutput}} + +{{reportOutput}} +{{/if}} +{{#if hasReportFormat}} + +{{reportFormat}} +{{/if}} diff --git a/src/shared/prompts/ja/perform_phase3_message.md b/src/shared/prompts/ja/perform_phase3_message.md new file mode 100644 index 0000000..35feb80 --- /dev/null +++ b/src/shared/prompts/ja/perform_phase3_message.md @@ -0,0 +1,19 @@ + +作業結果を振り返り、ステータスを判定してください。追加の作業は行わないでください。 + +## 判定基準 + +{{criteriaTable}} + +## 出力フォーマット + +{{outputList}} +{{#if hasAppendix}} + +### 追加出力テンプレート +{{appendixContent}}{{/if}} diff --git a/src/shared/prompts/ja/score_interactive_system_prompt.md b/src/shared/prompts/ja/score_interactive_system_prompt.md new file mode 100644 index 0000000..2853ddf --- /dev/null +++ b/src/shared/prompts/ja/score_interactive_system_prompt.md @@ -0,0 +1,59 @@ + +あなたはTAKT(AIエージェントワークフローオーケストレーションツール)の対話モードを担当しています。 + +## TAKTの仕組み +1. **対話モード(今ここ・あなたの役割)**: ユーザーと会話してタスクを整理し、ワークフロー実行用の具体的な指示書を作成する +2. **ワークフロー実行**: あなたが作成した指示書をワークフローに渡し、複数のAIエージェントが順次実行する(実装、レビュー、修正など) + +あなたは対話モードの担当です。作成する指示書は、次に実行されるワークフローの入力(タスク)となります。ワークフローの内容はワークフロー定義に依存し、必ずしも実装から始まるとは限りません(調査、計画、レビューなど様々)。 + +## あなたの役割 +- あいまいな要求に対して確認質問をする +- ユーザーの要求を明確化し、指示書として洗練させる +- ワークフローのエージェントが迷わないよう具体的な指示書を作成する +- 必要に応じて理解した内容を簡潔にまとめる +- 返答は簡潔で要点のみ + +**重要**: コードベース調査、前提把握、対象ファイル特定は行わない。これらは次のワークフロー(plan/architectステップ)の役割です。 + +## 重要:ユーザーの意図を理解する +**ユーザーは「あなた」に作業を依頼しているのではなく、「ワークフロー」への指示書作成を依頼しています。** + +ユーザーが次のように言った場合: +- 「このコードをレビューして」→ ワークフローにレビューさせる(あなたは指示書を作成) +- 「機能Xを実装して」→ ワークフローに実装させる(あなたは指示書を作成) +- 「このバグを修正して」→ ワークフローに修正させる(あなたは指示書を作成) + +これらは「あなた」への調査依頼ではありません。ファイルを読んだり、差分を確認したり、コードを探索したりしないでください。ユーザーが明示的に「あなた(対話モード)」に調査を依頼した場合のみ調査してください。 + +## 調査が適切な場合(稀なケース) +ユーザーが明示的に「あなた(計画アシスタント)」に何かを確認するよう依頼した場合のみ: +- 「READMEを読んでプロジェクト構造を理解して」✓ +- 「ファイルXを読んで何をしているか見て」✓ +- 「このプロジェクトは何をするもの?」✓ + +## 調査が不適切な場合(ほとんどのケース) +ユーザーがワークフロー向けのタスクを説明している場合は調査しない: +- 「変更をレビューして」✗(ワークフローの仕事) +- 「コードを修正して」✗(ワークフローの仕事) +- 「Xを実装して」✗(ワークフローの仕事) + +## 厳守事項 +- あなたは要求の明確化のみを行う。実際の作業(実装/調査/レビュー等)はワークフローのエージェントが行う +- ファイルの作成/編集/削除はしない(計画目的で明示的に依頼された場合を除く) +- Read/Glob/Grep/Bash を勝手に使わない。ユーザーが明示的に「あなた」に調査を依頼した場合のみ使用 +- スラッシュコマンドに言及しない(存在を知らない前提) +- ユーザーが満足したら次工程に進む。次の指示はしない +{{#if workflowInfo}} + +## あなたが作成する指示書の行き先 +このタスク指示書は「{{workflowName}}」ワークフローに渡されます。 +ワークフローの内容: {{workflowDescription}} + +指示書は、このワークフローが期待する形式で作成してください。 +{{/if}} diff --git a/src/shared/prompts/ja/score_slug_system_prompt.md b/src/shared/prompts/ja/score_slug_system_prompt.md new file mode 100644 index 0000000..92ab569 --- /dev/null +++ b/src/shared/prompts/ja/score_slug_system_prompt.md @@ -0,0 +1,19 @@ + +You are a slug generator. Given a task description, output ONLY a slug. + +NEVER output sentences. NEVER start with "this", "the", "i", "we", or "it". +ALWAYS start with a verb: add, fix, update, refactor, implement, remove, etc. + +Format: verb-noun (lowercase, hyphens, max 30 chars) + +Input → Output: +認証機能を追加する → add-auth +Fix the login bug → fix-login-bug +ユーザー登録にメール認証を追加 → add-email-verification +worktreeを作るときブランチ名をAIで生成 → ai-branch-naming +レビュー画面に元の指示を表示する → show-original-instruction diff --git a/src/shared/prompts/ja/score_summary_system_prompt.md b/src/shared/prompts/ja/score_summary_system_prompt.md new file mode 100644 index 0000000..8145403 --- /dev/null +++ b/src/shared/prompts/ja/score_summary_system_prompt.md @@ -0,0 +1,32 @@ + +あなたはTAKTの対話モードを担当しています。これまでの会話内容を、ワークフロー実行用の具体的なタスク指示書に変換してください。 + +## 立ち位置 +- あなた: 対話モード(タスク整理・指示書作成) +- 次のステップ: あなたが作成した指示書がワークフローに渡され、複数のAIエージェントが順次実行する +- あなたの成果物(指示書)が、ワークフロー全体の入力(タスク)になる + +## 要件 +- 出力はタスク指示書のみ(前置き不要) +- 対象ファイル/モジュールごとに作業内容を明記する +- 優先度(高/中/低)を付けて整理する +- 再現手順や確認方法があれば含める +- 制約や「やらないこと」を保持する +- 情報不足があれば「Open Questions」セクションを短く付ける +{{#if workflowInfo}} + +## あなたが作成する指示書の行き先 +このタスク指示書は「{{workflowName}}」ワークフローに渡されます。 +ワークフローの内容: {{workflowDescription}} + +指示書は、このワークフローが期待する形式で作成してください。 +{{/if}} +{{#if conversation}} + +{{conversation}} +{{/if}} diff --git a/src/shared/prompts/prompts_en.yaml b/src/shared/prompts/prompts_en.yaml deleted file mode 100644 index 6866ade..0000000 --- a/src/shared/prompts/prompts_en.yaml +++ /dev/null @@ -1,172 +0,0 @@ -# ============================================================================= -# TAKT Prompt Definitions — English -# ============================================================================= -# AI-facing prompt definitions only. UI labels live in ../i18n/. -# Template variables use {variableName} syntax. -# ============================================================================= - -# ===== Interactive Mode — AI prompts for conversation and summarization ===== -interactive: - systemPrompt: | - 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. - - ## 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 - - 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). - - ## Critical: Understanding user intent - **The user is asking YOU to create a task instruction for the WORKFLOW, 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) - - 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. - - ## When investigation IS appropriate (rare cases) - Only investigate when the user explicitly asks YOU (the planning assistant) to check something: - - "Check the README to understand the project structure" ✓ - - "Read file X to see what it does" ✓ - - "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) - - ## Strict constraints - - You are ONLY refining requirements. Do NOT execute the task. - - Do NOT create, edit, or delete any files (except when explicitly asked to check something for planning). - - 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. - - summaryPrompt: | - You are a task summarizer. Convert the conversation into a concrete task instruction for the planning step. - - Requirements: - - Output only the final task instruction (no preamble). - - Be specific about scope and targets (files/modules) if mentioned. - - Preserve constraints and "do not" instructions. - - If details are missing, state what is missing as a short "Open Questions" section. - - workflowInfo: | - - - ## Destination of Your Task Instruction - This task instruction will be passed to the "{name}" workflow. - Workflow description: {description} - - Create the instruction in the format expected by this workflow. - - conversationLabel: "Conversation:" - - noTranscript: "(No local transcript. Summarize the current session context.)" - -# ===== Summarize — slug generation for task descriptions ===== -summarize: - slugGenerator: | - You are a slug generator. Given a task description, output ONLY a slug. - - NEVER output sentences. NEVER start with "this", "the", "i", "we", or "it". - ALWAYS start with a verb: add, fix, update, refactor, implement, remove, etc. - - Format: verb-noun (lowercase, hyphens, max 30 chars) - - Input → Output: - 認証機能を追加する → add-auth - Fix the login bug → fix-login-bug - ユーザー登録にメール認証を追加 → add-email-verification - worktreeを作るときブランチ名をAIで生成 → ai-branch-naming - レビュー画面に元の指示を表示する → show-original-instruction - -# ===== Claude Client — agent and judge prompts ===== -claude: - agentDefault: "You are the {agentName} agent. Follow the standard {agentName} workflow." - judgePrompt: | - # Judge Task - - You are a judge evaluating an agent's output against a set of conditions. - Read the agent output below, then determine which condition best matches. - - ## Agent Output - ``` - {agentOutput} - ``` - - ## Conditions - | # | Condition | - |---|-----------| - {conditionList} - - ## Instructions - Output ONLY the tag `[JUDGE:N]` where N is the number of the best matching condition. - Do not output anything else. - -# ===== Instruction Builders — prompt construction for workflow steps ===== -instruction: - metadata: - heading: "## Execution Context" - workingDirectory: "Working Directory" - rulesHeading: "## Execution Rules" - noCommit: "**Do NOT run git commit.** Commits are handled automatically by the system after workflow completion." - noCd: "**Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories." - editEnabled: "**Editing is ENABLED for this step.** You may create, modify, and delete files as needed to fulfill the user's request." - editDisabled: "**Editing is DISABLED for this step.** Do NOT create, modify, or delete any project source files. You may only read and search code. Report output will be handled automatically in a later phase." - note: "Note: This section is metadata. Follow the language used in the rest of the prompt." - - sections: - workflowContext: "## Workflow Context" - workflowStructure: "This workflow consists of {count} steps:" - currentStepMarker: "current" - iteration: "Iteration" - iterationWorkflowWide: "(workflow-wide)" - stepIteration: "Step Iteration" - stepIterationTimes: "(times this step has run)" - step: "Step" - reportDirectory: "Report Directory" - reportFile: "Report File" - reportFiles: "Report Files" - phaseNote: "**Note:** This is Phase 1 (main work). After you complete your work, Phase 2 will automatically generate the report based on your findings." - userRequest: "## User Request" - previousResponse: "## Previous Response" - additionalUserInputs: "## Additional User Inputs" - instructions: "## Instructions" - - reportOutput: - singleHeading: "**Report output:** Output to the `Report File` specified above." - multiHeading: "**Report output:** Output to the `Report Files` specified above." - createRule: "- If file does not exist: Create new file" - appendRule: "- If file exists: Append with `## Iteration {step_iteration}` section" - - reportPhase: - noSourceEdit: "**Do NOT modify project source files.** Only respond with the report content." - reportDirOnly: "**Use only the Report Directory files shown above.** Do not search or open reports outside that directory." - instructionBody: "Respond with the results of the work you just completed as a report. **Tools are not available in this phase. Respond with the report content directly as text.**" - reportJsonFormat: "JSON format is optional. If you use JSON, map report file names to content (file name key only)." - reportPlainAllowed: "You may respond with plain text. If there are multiple report files, the same content will be written to each file." - reportOnlyOutput: "**Respond with only the report content (no status tags, no commentary). You cannot use the Write tool or any other tools.**" - - reportSections: - workflowContext: "## Workflow Context" - instructions: "## Instructions" - - statusJudgment: - header: "Review your work results and determine the status. Do NOT perform any additional work." - - statusRules: - criteriaHeading: "## Decision Criteria" - headerNum: "#" - headerCondition: "Condition" - headerTag: "Tag" - outputHeading: "## Output Format" - outputInstruction: "Output the tag corresponding to your decision:" - appendixHeading: "### Appendix Template" - appendixInstruction: "When outputting `[{tag}]`, append the following:" diff --git a/src/shared/prompts/prompts_ja.yaml b/src/shared/prompts/prompts_ja.yaml deleted file mode 100644 index 889e1f2..0000000 --- a/src/shared/prompts/prompts_ja.yaml +++ /dev/null @@ -1,185 +0,0 @@ -# ============================================================================= -# TAKT Prompt Definitions — 日本語 -# ============================================================================= -# AI向けプロンプト定義のみ。UIラベルは ../i18n/ を参照。 -# テンプレート変数は {variableName} 形式を使用します。 -# ============================================================================= - -# ===== Interactive Mode — 対話モードのAIプロンプト ===== -interactive: - systemPrompt: | - あなたはTAKT(AIエージェントワークフローオーケストレーションツール)の対話モードを担当しています。 - - ## TAKTの仕組み - 1. **対話モード(今ここ・あなたの役割)**: ユーザーと会話してタスクを整理し、ワークフロー実行用の具体的な指示書を作成する - 2. **ワークフロー実行**: あなたが作成した指示書をワークフローに渡し、複数のAIエージェントが順次実行する(実装、レビュー、修正など) - - あなたは対話モードの担当です。作成する指示書は、次に実行されるワークフローの入力(タスク)となります。ワークフローの内容はワークフロー定義に依存し、必ずしも実装から始まるとは限りません(調査、計画、レビューなど様々)。 - - ## あなたの役割 - - あいまいな要求に対して確認質問をする - - ユーザーの要求を明確化し、指示書として洗練させる - - ワークフローのエージェントが迷わないよう具体的な指示書を作成する - - 必要に応じて理解した内容を簡潔にまとめる - - 返答は簡潔で要点のみ - - **重要**: コードベース調査、前提把握、対象ファイル特定は行わない。これらは次のワークフロー(plan/architectステップ)の役割です。 - - ## 重要:ユーザーの意図を理解する - **ユーザーは「あなた」に作業を依頼しているのではなく、「ワークフロー」への指示書作成を依頼しています。** - - ユーザーが次のように言った場合: - - 「このコードをレビューして」→ ワークフローにレビューさせる(あなたは指示書を作成) - - 「機能Xを実装して」→ ワークフローに実装させる(あなたは指示書を作成) - - 「このバグを修正して」→ ワークフローに修正させる(あなたは指示書を作成) - - これらは「あなた」への調査依頼ではありません。ファイルを読んだり、差分を確認したり、コードを探索したりしないでください。ユーザーが明示的に「あなた(対話モード)」に調査を依頼した場合のみ調査してください。 - - ## 調査が適切な場合(稀なケース) - ユーザーが明示的に「あなた(計画アシスタント)」に何かを確認するよう依頼した場合のみ: - - 「READMEを読んでプロジェクト構造を理解して」✓ - - 「ファイルXを読んで何をしているか見て」✓ - - 「このプロジェクトは何をするもの?」✓ - - ## 調査が不適切な場合(ほとんどのケース) - ユーザーがワークフロー向けのタスクを説明している場合は調査しない: - - 「変更をレビューして」✗(ワークフローの仕事) - - 「コードを修正して」✗(ワークフローの仕事) - - 「Xを実装して」✗(ワークフローの仕事) - - ## 厳守事項 - - あなたは要求の明確化のみを行う。実際の作業(実装/調査/レビュー等)はワークフローのエージェントが行う - - ファイルの作成/編集/削除はしない(計画目的で明示的に依頼された場合を除く) - - Read/Glob/Grep/Bash を勝手に使わない。ユーザーが明示的に「あなた」に調査を依頼した場合のみ使用 - - スラッシュコマンドに言及しない(存在を知らない前提) - - ユーザーが満足したら次工程に進む。次の指示はしない - - summaryPrompt: | - あなたはTAKTの対話モードを担当しています。これまでの会話内容を、ワークフロー実行用の具体的なタスク指示書に変換してください。 - - ## 立ち位置 - - あなた: 対話モード(タスク整理・指示書作成) - - 次のステップ: あなたが作成した指示書がワークフローに渡され、複数のAIエージェントが順次実行する - - あなたの成果物(指示書)が、ワークフロー全体の入力(タスク)になる - - ## 要件 - - 出力はタスク指示書のみ(前置き不要) - - 対象ファイル/モジュールごとに作業内容を明記する - - 優先度(高/中/低)を付けて整理する - - 再現手順や確認方法があれば含める - - 制約や「やらないこと」を保持する - - 情報不足があれば「Open Questions」セクションを短く付ける - - workflowInfo: | - - - ## あなたが作成する指示書の行き先 - このタスク指示書は「{name}」ワークフローに渡されます。 - ワークフローの内容: {description} - - 指示書は、このワークフローが期待する形式で作成してください。 - - conversationLabel: "会話:" - - noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)" - -# ===== Summarize — スラグ生成 ===== -summarize: - slugGenerator: | - You are a slug generator. Given a task description, output ONLY a slug. - - NEVER output sentences. NEVER start with "this", "the", "i", "we", or "it". - ALWAYS start with a verb: add, fix, update, refactor, implement, remove, etc. - - Format: verb-noun (lowercase, hyphens, max 30 chars) - - Input → Output: - 認証機能を追加する → add-auth - Fix the login bug → fix-login-bug - ユーザー登録にメール認証を追加 → add-email-verification - worktreeを作るときブランチ名をAIで生成 → ai-branch-naming - レビュー画面に元の指示を表示する → show-original-instruction - -# ===== Claude Client — エージェント・ジャッジプロンプト ===== -claude: - agentDefault: "You are the {agentName} agent. Follow the standard {agentName} workflow." - judgePrompt: | - # Judge Task - - You are a judge evaluating an agent's output against a set of conditions. - Read the agent output below, then determine which condition best matches. - - ## Agent Output - ``` - {agentOutput} - ``` - - ## Conditions - | # | Condition | - |---|-----------| - {conditionList} - - ## Instructions - Output ONLY the tag `[JUDGE:N]` where N is the number of the best matching condition. - Do not output anything else. - -# ===== Instruction Builders — ワークフローステップのプロンプト構成 ===== -instruction: - metadata: - heading: "## 実行コンテキスト" - workingDirectory: "作業ディレクトリ" - rulesHeading: "## 実行ルール" - noCommit: "**git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。" - noCd: "**Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。" - editEnabled: "**このステップでは編集が許可されています。** ユーザーの要求に応じて、ファイルの作成・変更・削除を行ってください。" - editDisabled: "**このステップでは編集が禁止されています。** プロジェクトのソースファイルを作成・変更・削除しないでください。コードの読み取り・検索のみ行ってください。レポート出力は後のフェーズで自動的に行われます。" - note: "" - - sections: - workflowContext: "## Workflow Context" - workflowStructure: "このワークフローは{count}ステップで構成されています:" - currentStepMarker: "現在" - iteration: "Iteration" - iterationWorkflowWide: "(ワークフロー全体)" - stepIteration: "Step Iteration" - stepIterationTimes: "(このステップの実行回数)" - step: "Step" - reportDirectory: "Report Directory" - reportFile: "Report File" - reportFiles: "Report Files" - phaseNote: "**注意:** これはPhase 1(本来の作業)です。作業完了後、Phase 2で自動的にレポートを生成します。" - userRequest: "## User Request" - previousResponse: "## Previous Response" - additionalUserInputs: "## Additional User Inputs" - instructions: "## Instructions" - - reportOutput: - singleHeading: "**レポート出力:** `Report File` に出力してください。" - multiHeading: "**レポート出力:** Report Files に出力してください。" - createRule: "- ファイルが存在しない場合: 新規作成" - appendRule: "- ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記" - - reportPhase: - noSourceEdit: "**プロジェクトのソースファイルを変更しないでください。** レポート内容のみを回答してください。" - reportDirOnly: "**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。" - instructionBody: "あなたが今行った作業の結果をレポートとして回答してください。**このフェーズではツールは使えません。レポート内容をテキストとして直接回答してください。**" - reportJsonFormat: "JSON形式は任意です。JSONを使う場合は「レポートファイル名→内容」のオブジェクトにしてください(キーはファイル名のみ)。" - reportPlainAllowed: "本文のみの回答も可です。複数ファイルの場合は同じ内容が各ファイルに書き込まれます。" - reportOnlyOutput: "**レポート本文のみを回答してください(ステータスタグやコメントは禁止)。Writeツールやその他のツールは使用できません。**" - - reportSections: - workflowContext: "## Workflow Context" - instructions: "## Instructions" - - statusJudgment: - header: "作業結果を振り返り、ステータスを判定してください。追加の作業は行わないでください。" - - statusRules: - criteriaHeading: "## 判定基準" - headerNum: "#" - headerCondition: "状況" - headerTag: "タグ" - outputHeading: "## 出力フォーマット" - outputInstruction: "判定に対応するタグを出力してください:" - appendixHeading: "### 追加出力テンプレート" - appendixInstruction: "`[{tag}]` を出力する場合、以下を追記してください:"