プロンプトを見通しやすく変更

This commit is contained in:
nrslib 2026-02-04 03:33:48 +09:00
parent 25fb6c4dfd
commit 6378ee6174
121 changed files with 2597 additions and 2411 deletions

42
docs/plan.md Normal file
View File

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

View File

@ -10,7 +10,7 @@
"takt-cli": "./dist/app/cli/index.js" "takt-cli": "./dist/app/cli/index.js"
}, },
"scripts": { "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", "watch": "tsc --watch",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",

View File

@ -3,17 +3,17 @@
# #
# Boilerplate sections (Workflow Context, User Request, Previous Response, # Boilerplate sections (Workflow Context, User Request, Previous Response,
# Additional User Inputs, Instructions heading) are auto-injected by buildInstruction(). # 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): # 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 # {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)
# {previous_response} - Output from the previous step (only when pass_previous_response: true) # {previous_response} - Output from the previous movement (only when pass_previous_response: true)
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
# #
# Step-level Fields: # Movement-level Fields:
# report: - Report file(s) for the step (auto-injected as Report File/Files in Workflow Context) # report: - Report file(s) for the movement (auto-injected as Report File/Files in Workflow Context)
# Single: report: 00-plan.md # Single: report: 00-plan.md
# Multiple: report: # Multiple: report:
# - Scope: 01-coder-scope.md # - Scope: 01-coder-scope.md
@ -24,9 +24,9 @@ description: Standard development workflow with planning and specialized reviews
max_iterations: 30 max_iterations: 30
initial_step: plan initial_movement: plan
steps: movements:
- name: plan - name: plan
edit: false edit: false
agent: ../agents/default/planner.md agent: ../agents/default/planner.md
@ -75,7 +75,7 @@ steps:
instruction_template: | instruction_template: |
Analyze the task and create an implementation plan. 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). review and revise the plan based on that feedback (replan).
**Tasks (for implementation tasks):** **Tasks (for implementation tasks):**
@ -177,7 +177,7 @@ steps:
requires_user_input: true requires_user_input: true
interactive_only: true interactive_only: true
instruction_template: | 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:** **Reports to reference:**
- Plan: {report:00-plan.md} - 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. 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. Report if you encounter unclear points or need design changes.
**Scope report format (create at implementation start):** **Scope report format (create at implementation start):**
@ -300,7 +300,7 @@ steps:
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: plan next: plan
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**
@ -387,8 +387,8 @@ steps:
- condition: approved - condition: approved
- condition: needs_fix - condition: needs_fix
instruction_template: | instruction_template: |
**Verify that the implementation follows the design from the architect step.** **Verify that the implementation follows the design from the architect movement.**
Do NOT review AI-specific issues (that's the ai_review step). Do NOT review AI-specific issues (that's the ai_review movement).
**Reports to reference:** **Reports to reference:**
- Design: {report:01-architecture.md} (if exists) - Design: {report:01-architecture.md} (if exists)
@ -402,7 +402,7 @@ steps:
- Dead code - Dead code
- Call chain verification - 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 - name: security-review
edit: false edit: false
@ -517,7 +517,7 @@ steps:
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan ({report:00-plan.md}) and design ({report:01-architecture.md}, if exists)? 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? 3. Was the original task objective achieved?
**Review Reports:** Read all reports in Report Directory and **Review Reports:** Read all reports in Report Directory and

View File

@ -10,11 +10,11 @@
# any("needs_fix") → fix → reviewers # any("needs_fix") → fix → reviewers
# #
# Template Variables: # 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 # {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 # {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 # {user_inputs} - Accumulated user inputs during workflow
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 30
initial_step: plan initial_movement: plan
steps: movements:
# =========================================== # ===========================================
# Phase 0: Planning # Movement 0: Planning
# =========================================== # ===========================================
- name: plan - name: plan
edit: false edit: false
@ -65,7 +65,7 @@ steps:
instruction_template: | instruction_template: |
Analyze the task and create an implementation plan. 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). review and revise the plan based on that feedback (replan).
**Tasks:** **Tasks:**
@ -80,7 +80,7 @@ steps:
next: ABORT next: ABORT
# =========================================== # ===========================================
# Phase 1: Implementation # Movement 1: Implementation
# =========================================== # ===========================================
- name: implement - name: implement
edit: true edit: true
@ -99,7 +99,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | 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. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory.
@ -153,7 +153,7 @@ steps:
interactive_only: true interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Movement 2: AI Review
# =========================================== # ===========================================
- name: ai_review - name: ai_review
edit: false edit: false
@ -219,7 +219,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**
@ -271,7 +271,7 @@ steps:
next: plan next: plan
# =========================================== # ===========================================
# Phase 3: Expert Reviews (Parallel) # Movement 3: Expert Reviews (Parallel)
# =========================================== # ===========================================
- name: reviewers - name: reviewers
parallel: parallel:
@ -320,7 +320,7 @@ steps:
- condition: needs_fix - condition: needs_fix
instruction_template: | instruction_template: |
Review the changes from the CQRS (Command Query Responsibility Segregation) 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:** **Review Criteria:**
- Aggregate design validity - Aggregate design validity
@ -382,7 +382,7 @@ steps:
- TypeScript type safety - TypeScript type safety
**Note**: If this project does not include frontend code, **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 - name: security-review
edit: false edit: false
@ -525,7 +525,7 @@ steps:
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
# Phase 4: Supervision # Movement 4: Supervision
# =========================================== # ===========================================
- name: supervise - name: supervise
edit: false edit: false
@ -541,7 +541,7 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
## Previous Reviews Summary ## 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 - AI Review: APPROVED
- CQRS+ES Review: APPROVED - CQRS+ES Review: APPROVED
- Frontend Review: APPROVED - Frontend Review: APPROVED
@ -552,7 +552,7 @@ steps:
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan ({report:00-plan.md})? 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? 3. Was the original task objective achieved?
**Review Reports:** Read all reports in Report Directory and **Review Reports:** Read all reports in Report Directory and

View File

@ -14,17 +14,17 @@
# #
# Boilerplate sections (Workflow Context, User Request, Previous Response, # Boilerplate sections (Workflow Context, User Request, Previous Response,
# Additional User Inputs, Instructions heading) are auto-injected by buildInstruction(). # 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): # 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 # {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)
# {previous_response} - Output from the previous step (only when pass_previous_response: true) # {previous_response} - Output from the previous movement (only when pass_previous_response: true)
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
# #
# Step-level Fields: # Movement-level Fields:
# report: - Report file(s) for the step (auto-injected as Report File/Files in Workflow Context) # report: - Report file(s) for the movement (auto-injected as Report File/Files in Workflow Context)
# Single: report: 00-plan.md # Single: report: 00-plan.md
# Multiple: report: # Multiple: report:
# - Scope: 01-coder-scope.md # - Scope: 01-coder-scope.md
@ -35,11 +35,11 @@ description: Architecture, Frontend, Security, QA Expert Review
max_iterations: 30 max_iterations: 30
initial_step: plan initial_movement: plan
steps: movements:
# =========================================== # ===========================================
# Phase 0: Planning # Movement 0: Planning
# =========================================== # ===========================================
- name: plan - name: plan
edit: false edit: false
@ -77,7 +77,7 @@ steps:
instruction_template: | instruction_template: |
Analyze the task and create an implementation plan. 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). review and revise the plan based on that feedback (replan).
**Tasks:** **Tasks:**
@ -92,7 +92,7 @@ steps:
next: ABORT next: ABORT
# =========================================== # ===========================================
# Phase 1: Implementation # Movement 1: Implementation
# =========================================== # ===========================================
- name: implement - name: implement
edit: true edit: true
@ -111,7 +111,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | 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. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory.
@ -165,7 +165,7 @@ steps:
interactive_only: true interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Movement 2: AI Review
# =========================================== # ===========================================
- name: ai_review - name: ai_review
edit: false edit: false
@ -231,7 +231,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**
@ -284,7 +284,7 @@ steps:
next: plan next: plan
# =========================================== # ===========================================
# Phase 3: Expert Reviews (Parallel) # Movement 3: Expert Reviews (Parallel)
# =========================================== # ===========================================
- name: reviewers - name: reviewers
parallel: parallel:
@ -335,7 +335,7 @@ steps:
- condition: approved - condition: approved
- condition: needs_fix - condition: needs_fix
instruction_template: | 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:** **Review Criteria:**
- Structure/design validity - Structure/design validity
@ -395,7 +395,7 @@ steps:
- TypeScript type safety - TypeScript type safety
**Note**: If this project does not include frontend code, **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 - name: security-review
edit: false edit: false
@ -538,7 +538,7 @@ steps:
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
# Phase 4: Supervision # Movement 4: Supervision
# =========================================== # ===========================================
- name: supervise - name: supervise
edit: false edit: false
@ -554,7 +554,7 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
## Previous Reviews Summary ## 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 - Architecture Review: APPROVED
- Frontend Review: APPROVED - Frontend Review: APPROVED
- AI Review: APPROVED - AI Review: APPROVED
@ -565,7 +565,7 @@ steps:
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan ({report:00-plan.md})? 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? 3. Was the original task objective achieved?
**Review Reports:** Read all reports in Report Directory and **Review Reports:** Read all reports in Report Directory and

View File

@ -3,11 +3,11 @@
# Three personas (scientist, nurturer, pragmatist) analyze from different perspectives and vote # Three personas (scientist, nurturer, pragmatist) analyze from different perspectives and vote
# #
# Template Variables: # 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 # {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 # {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 # {user_inputs} - Accumulated user inputs during workflow
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 5
steps: movements:
- name: melchior - name: melchior
agent: ../agents/magi/melchior.md agent: ../agents/magi/melchior.md
allowed_tools: allowed_tools:

View File

@ -3,11 +3,11 @@
# (Simplest configuration - no plan, no architect review) # (Simplest configuration - no plan, no architect review)
# #
# Template Variables (auto-injected): # 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 # {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) # {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) # {user_inputs} - Accumulated user inputs during workflow (auto-injected)
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 20
initial_step: implement initial_movement: implement
steps: movements:
- name: implement - name: implement
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
@ -248,7 +248,7 @@ steps:
- condition: No fix needed (verified target files/spec) - condition: No fix needed (verified target files/spec)
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**
@ -350,7 +350,7 @@ steps:
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: implement next: implement
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**

View File

@ -7,11 +7,11 @@
# -> plan (rejected: restart from planning) # -> plan (rejected: restart from planning)
# #
# Template Variables: # 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 # {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 # {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 # {user_inputs} - Accumulated user inputs during workflow
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 10
steps: movements:
- name: plan - name: plan
agent: ../agents/research/planner.md agent: ../agents/research/planner.md
allowed_tools: allowed_tools:
@ -32,8 +32,8 @@ steps:
instruction_template: | instruction_template: |
## Workflow Status ## Workflow Status
- Iteration: {iteration}/{max_iterations} (workflow-wide) - Iteration: {iteration}/{max_iterations} (workflow-wide)
- Step Iteration: {step_iteration} (times this step has run) - Movement Iteration: {movement_iteration} (times this movement has run)
- Step: plan - Movement: plan
## Research Request ## Research Request
{task} {task}
@ -69,8 +69,8 @@ steps:
instruction_template: | instruction_template: |
## Workflow Status ## Workflow Status
- Iteration: {iteration}/{max_iterations} (workflow-wide) - Iteration: {iteration}/{max_iterations} (workflow-wide)
- Step Iteration: {step_iteration} (times this step has run) - Movement Iteration: {movement_iteration} (times this movement has run)
- Step: dig - Movement: dig
## Original Research Request ## Original Research Request
{task} {task}
@ -111,8 +111,8 @@ steps:
instruction_template: | instruction_template: |
## Workflow Status ## Workflow Status
- Iteration: {iteration}/{max_iterations} (workflow-wide) - Iteration: {iteration}/{max_iterations} (workflow-wide)
- Step Iteration: {step_iteration} (times this step has run) - Movement Iteration: {movement_iteration} (times this movement has run)
- Step: supervise (research quality evaluation) - Movement: supervise (research quality evaluation)
## Original Research Request ## Original Research Request
{task} {task}
@ -131,4 +131,4 @@ steps:
- condition: Research results are insufficient and replanning is needed - condition: Research results are insufficient and replanning is needed
next: plan next: plan
initial_step: plan initial_movement: plan

View File

@ -1,13 +1,13 @@
# Review-Fix Minimal TAKT Workflow # Review-Fix Minimal TAKT Workflow
# Review -> Fix (if needed) -> Re-review -> Complete # Review -> Fix (if needed) -> Re-review -> Complete
# (Starts with review, no implementation step) # (Starts with review, no implementation movement)
# #
# Template Variables (auto-injected): # 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 # {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) # {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) # {user_inputs} - Accumulated user inputs during workflow (auto-injected)
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 20
initial_step: reviewers initial_movement: reviewers
steps: movements:
- name: implement - name: implement
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
@ -248,7 +248,7 @@ steps:
- condition: No fix needed (verified target files/spec) - condition: No fix needed (verified target files/spec)
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**
@ -350,7 +350,7 @@ steps:
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: implement next: implement
instruction_template: | 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. 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.** **Your belief that you "already fixed it" is wrong.**

View File

@ -8,14 +8,14 @@
# -> COMPLETE (local: console output only) # -> COMPLETE (local: console output only)
# -> ABORT (rejected) # -> ABORT (rejected)
# #
# All steps have edit: false (no file modifications) # All movements have edit: false (no file modifications)
# #
# Template Variables: # Template Variables:
# {iteration} - Workflow-wide turn count # {iteration} - Workflow-wide turn count
# {max_iterations} - Maximum iterations allowed # {max_iterations} - Maximum iterations allowed
# {step_iteration} - Per-step iteration count # {movement_iteration} - Per-movement iteration count
# {task} - Original user request # {task} - Original user request
# {previous_response} - Output from the previous step # {previous_response} - Output from the previous movement
# {user_inputs} - Accumulated user inputs # {user_inputs} - Accumulated user inputs
# {report_dir} - Report directory name # {report_dir} - Report directory name
@ -24,9 +24,9 @@ description: Review-only workflow - reviews code without making edits
max_iterations: 10 max_iterations: 10
initial_step: plan initial_movement: plan
steps: movements:
- name: plan - name: plan
edit: false edit: false
agent: ../agents/default/planner.md agent: ../agents/default/planner.md
@ -108,7 +108,7 @@ steps:
- condition: approved - condition: approved
- condition: needs_fix - condition: needs_fix
instruction_template: | 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. Review the code and provide feedback.
@ -248,7 +248,7 @@ steps:
3. Produce a consolidated review summary with overall verdict 3. Produce a consolidated review summary with overall verdict
4. Determine routing: 4. Determine routing:
- If the task mentions posting to a PR (e.g., "post comments to PR", "comment on PR"), - 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 local review only, route to COMPLETE (condition: "approved")
- If critical issues found, route to ABORT (condition: "rejected") - If critical issues found, route to ABORT (condition: "rejected")

View File

@ -482,8 +482,8 @@ function createOrder(data: OrderData) {
- 既存の設定ファイルが新しいスキーマと整合するか - 既存の設定ファイルが新しいスキーマと整合するか
3. ワークフロー定義を変更した場合: 3. ワークフロー定義を変更した場合:
- ステップ種別(通常/parallelに応じた正しいフィールドが使われているか - ムーブメント種別(通常/parallelに応じた正しいフィールドが使われているか
- 不要なフィールドparallelサブステップのnext等が残っていないか - 不要なフィールドparallelサブムーブメントのnext等が残っていないか
**このパターンを見つけたら REJECT:** **このパターンを見つけたら REJECT:**
@ -492,7 +492,7 @@ function createOrder(data: OrderData) {
| 仕様に存在しないフィールドの使用 | 無視されるか予期しない動作 | | 仕様に存在しないフィールドの使用 | 無視されるか予期しない動作 |
| 仕様上無効な値の設定 | 実行時エラーまたは無視される | | 仕様上無効な値の設定 | 実行時エラーまたは無視される |
| 文書化された制約への違反 | 設計意図に反する | | 文書化された制約への違反 | 設計意図に反する |
| ステップ種別とフィールドの不整合 | コピペミスの兆候 | | ムーブメント種別とフィールドの不整合 | コピペミスの兆候 |
### 9. 呼び出しチェーン検証 ### 9. 呼び出しチェーン検証

View File

@ -98,7 +98,7 @@ Architectが「正しく作られているかVerification」を確認す
確認すること: 確認すること:
- 計画00-plan.mdと実装結果が一致しているか - 計画00-plan.mdと実装結果が一致しているか
- 各レビューステップの指摘が適切に対応されているか - 各レビュームーブメントの指摘が適切に対応されているか
- タスクの本来の目的が達成されているか - タスクの本来の目的が達成されているか
**ワークフロー全体の問題:** **ワークフロー全体の問題:**

View File

@ -34,7 +34,7 @@
### 4. 実装アプローチ ### 4. 実装アプローチ
- 段階的な実装手順を設計する - 段階的な実装手順を設計する
- 各ステップの成果物を明示する - 各ムーブメントの成果物を明示する
- リスクと代替案を記載する - リスクと代替案を記載する
## 重要 ## 重要

View File

@ -2,11 +2,11 @@
# Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + Security) -> Supervisor Approval # Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + Security) -> Supervisor Approval
# #
# Template Variables (auto-injected by buildInstruction): # 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 # {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 # {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 # {user_inputs} - Accumulated user inputs during workflow
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 30
initial_step: plan initial_movement: plan
steps: movements:
- name: plan - name: plan
edit: false edit: false
agent: ../agents/default/planner.md agent: ../agents/default/planner.md
@ -168,7 +168,7 @@ steps:
requires_user_input: true requires_user_input: true
interactive_only: true interactive_only: true
instruction_template: | instruction_template: |
planステップで立てた計画と、architectステップで決定した設計に従って実装してください。 planムーブメントで立てた計画と、architectムーブメントで決定した設計に従って実装してください。
**参照するレポート:** **参照するレポート:**
- 計画: {report:00-plan.md} - 計画: {report:00-plan.md}
@ -176,7 +176,7 @@ steps:
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
**重要:** 設計判断はせず、architectステップで決定された設計に従ってください。 **重要:** 設計判断はせず、architectムーブメントで決定された設計に従ってください。
不明点や設計の変更が必要な場合は報告してください。 不明点や設計の変更が必要な場合は報告してください。
**重要**: 実装と同時に単体テストを追加してください。 **重要**: 実装と同時に単体テストを追加してください。
@ -296,7 +296,7 @@ steps:
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
next: plan next: plan
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**
@ -385,8 +385,8 @@ steps:
- condition: approved - condition: approved
- condition: needs_fix - condition: needs_fix
instruction_template: | instruction_template: |
**実装がarchitectステップの設計に従っているか**を確認してください。 **実装がarchitectムーブメントの設計に従っているか**を確認してください。
AI特有の問題はレビューしないでくださいai_reviewステップで行います)。 AI特有の問題はレビューしないでくださいai_reviewムーブメントで行います)。
**参照するレポート:** **参照するレポート:**
- 設計: {report:01-architecture.md}(存在する場合) - 設計: {report:01-architecture.md}(存在する場合)
@ -400,7 +400,7 @@ steps:
- デッドコード - デッドコード
- 呼び出しチェーン検証 - 呼び出しチェーン検証
**注意:** architectステップをスキップした小規模タスクの場合は、従来通り設計の妥当性も確認してください。 **注意:** architectムーブメントをスキップした小規模タスクの場合は、従来通り設計の妥当性も確認してください。
- name: security-review - name: security-review
edit: false edit: false
@ -514,7 +514,7 @@ steps:
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画({report:00-plan.md})と設計({report:01-architecture.md}、存在する場合)に従った実装か 1. 計画({report:00-plan.md})と設計({report:01-architecture.md}、存在する場合)に従った実装か
2. 各レビューステップの指摘が対応されているか 2. 各レビュームーブメントの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか
**レポートの確認:** Report Directory内の全レポートを読み、 **レポートの確認:** Report Directory内の全レポートを読み、

View File

@ -11,17 +11,17 @@
# #
# ボイラープレートセクションWorkflow Context, User Request, Previous Response, # ボイラープレートセクションWorkflow Context, User Request, Previous Response,
# Additional User Inputs, Instructions headingはbuildInstruction()が自動挿入。 # Additional User Inputs, Instructions headingはbuildInstruction()が自動挿入。
# instruction_templateにはステップ固有の内容のみ記述。 # instruction_templateにはムーブメント固有の内容のみ記述。
# #
# テンプレート変数instruction_template内で使用可能: # テンプレート変数instruction_template内で使用可能:
# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) # {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計)
# {max_iterations} - ワークフローの最大イテレーション数 # {max_iterations} - ワークフローの最大イテレーション数
# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか)
# {previous_response} - 前のステップの出力pass_previous_response: true の場合のみ) # {previous_response} - 前のムーブメントの出力pass_previous_response: true の場合のみ)
# {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary" # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary"
# #
# ステップレベルフィールド: # ムーブメントレベルフィールド:
# report: - ステップのレポートファイルWorkflow ContextにReport File/Filesとして自動挿入 # report: - ムーブメントのレポートファイルWorkflow ContextにReport File/Filesとして自動挿入
# 単一: report: 00-plan.md # 単一: report: 00-plan.md
# 複数: report: # 複数: report:
# - Scope: 01-coder-scope.md # - Scope: 01-coder-scope.md
@ -32,11 +32,11 @@ description: CQRS+ES・フロントエンド・セキュリティ・QA専門家
max_iterations: 30 max_iterations: 30
initial_step: plan initial_movement: plan
steps: movements:
# =========================================== # ===========================================
# Phase 0: Planning # Movement 0: Planning
# =========================================== # ===========================================
- name: plan - name: plan
edit: false edit: false
@ -89,7 +89,7 @@ steps:
next: ABORT next: ABORT
# =========================================== # ===========================================
# Phase 1: Implementation # Movement 1: Implementation
# =========================================== # ===========================================
- name: implement - name: implement
edit: true edit: true
@ -108,7 +108,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planムーブメントで立てた計画に従って実装してください。
計画レポート({report:00-plan.md})を参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
@ -162,7 +162,7 @@ steps:
interactive_only: true interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Movement 2: AI Review
# =========================================== # ===========================================
- name: ai_review - name: ai_review
edit: false edit: false
@ -228,7 +228,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**
@ -279,7 +279,7 @@ steps:
next: plan next: plan
# =========================================== # ===========================================
# Phase 3: Expert Reviews (Parallel) # Movement 3: Expert Reviews (Parallel)
# =========================================== # ===========================================
- name: reviewers - name: reviewers
parallel: parallel:
@ -328,7 +328,7 @@ steps:
- condition: needs_fix - condition: needs_fix
instruction_template: | instruction_template: |
CQRSコマンドクエリ責務分離とEvent Sourcingイベントソーシングの観点から CQRSコマンドクエリ責務分離とEvent Sourcingイベントソーシングの観点から
変更をレビューしてください。AI特有の問題のレビューは不要ですai_reviewステップで実施済み)。 変更をレビューしてください。AI特有の問題のレビューは不要ですai_reviewムーブメントで実施済み)。
**レビュー観点:** **レビュー観点:**
- Aggregate設計の妥当性 - Aggregate設計の妥当性
@ -533,7 +533,7 @@ steps:
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
# Phase 4: Supervision # Movement 4: Supervision
# =========================================== # ===========================================
- name: supervise - name: supervise
edit: false edit: false
@ -549,7 +549,7 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
## Previous Reviews Summary ## Previous Reviews Summary
このステップに到達したということは、以下のレビューがすべてAPPROVEされています このムーブメントに到達したということは、以下のレビューがすべてAPPROVEされています
- AI Review: APPROVED - AI Review: APPROVED
- CQRS+ES Review: APPROVED - CQRS+ES Review: APPROVED
- Frontend Review: APPROVED - Frontend Review: APPROVED
@ -560,7 +560,7 @@ steps:
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画({report:00-plan.md})と実装結果が一致しているか 1. 計画({report:00-plan.md})と実装結果が一致しているか
2. 各レビューステップの指摘が対応されているか 2. 各レビュームーブメントの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか
**レポートの確認:** Report Directory内の全レポートを読み、 **レポートの確認:** Report Directory内の全レポートを読み、

View File

@ -10,11 +10,11 @@
# any("needs_fix") → fix → reviewers # any("needs_fix") → fix → reviewers
# #
# テンプレート変数: # テンプレート変数:
# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) # {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計)
# {max_iterations} - ワークフローの最大イテレーション数 # {max_iterations} - ワークフローの最大イテレーション数
# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか)
# {task} - 元のユーザー要求 # {task} - 元のユーザー要求
# {previous_response} - 前のステップの出力 # {previous_response} - 前のムーブメントの出力
# {user_inputs} - ワークフロー中に蓄積されたユーザー入力 # {user_inputs} - ワークフロー中に蓄積されたユーザー入力
# {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary" # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary"
@ -23,11 +23,11 @@ description: アーキテクチャ・フロントエンド・セキュリティ
max_iterations: 30 max_iterations: 30
initial_step: plan initial_movement: plan
steps: movements:
# =========================================== # ===========================================
# Phase 0: Planning # Movement 0: Planning
# =========================================== # ===========================================
- name: plan - name: plan
edit: false edit: false
@ -80,7 +80,7 @@ steps:
next: ABORT next: ABORT
# =========================================== # ===========================================
# Phase 1: Implementation # Movement 1: Implementation
# =========================================== # ===========================================
- name: implement - name: implement
edit: true edit: true
@ -99,7 +99,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planムーブメントで立てた計画に従って実装してください。
計画レポート({report:00-plan.md})を参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
@ -153,7 +153,7 @@ steps:
interactive_only: true interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Movement 2: AI Review
# =========================================== # ===========================================
- name: ai_review - name: ai_review
edit: false edit: false
@ -219,7 +219,7 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**
@ -270,7 +270,7 @@ steps:
next: plan next: plan
# =========================================== # ===========================================
# Phase 3: Expert Reviews (Parallel) # Movement 3: Expert Reviews (Parallel)
# =========================================== # ===========================================
- name: reviewers - name: reviewers
parallel: parallel:
@ -321,7 +321,7 @@ steps:
- condition: approved - condition: approved
- condition: needs_fix - condition: needs_fix
instruction_template: | instruction_template: |
**アーキテクチャと設計**のレビューに集中してください。AI特有の問題のレビューは不要ですai_reviewステップで実施済み)。 **アーキテクチャと設計**のレビューに集中してください。AI特有の問題のレビューは不要ですai_reviewムーブメントで実施済み)。
**レビュー観点:** **レビュー観点:**
- 構造・設計の妥当性 - 構造・設計の妥当性
@ -524,7 +524,7 @@ steps:
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
# Phase 4: Supervision # Movement 4: Supervision
# =========================================== # ===========================================
- name: supervise - name: supervise
edit: false edit: false
@ -540,7 +540,7 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
## Previous Reviews Summary ## Previous Reviews Summary
このステップに到達したということは、以下のレビューがすべてAPPROVEされています このムーブメントに到達したということは、以下のレビューがすべてAPPROVEされています
- AI Review: APPROVED - AI Review: APPROVED
- Architecture Review: APPROVED - Architecture Review: APPROVED
- Frontend Review: APPROVED - Frontend Review: APPROVED
@ -551,7 +551,7 @@ steps:
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画({report:00-plan.md})と実装結果が一致しているか 1. 計画({report:00-plan.md})と実装結果が一致しているか
2. 各レビューステップの指摘が対応されているか 2. 各レビュームーブメントの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか
**レポートの確認:** Report Directory内の全レポートを読み、 **レポートの確認:** Report Directory内の全レポートを読み、

View File

@ -3,11 +3,11 @@
# 3つの人格科学者・育成者・実務家が異なる観点から分析・投票する # 3つの人格科学者・育成者・実務家が異なる観点から分析・投票する
# #
# テンプレート変数: # テンプレート変数:
# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) # {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計)
# {max_iterations} - ワークフローの最大イテレーション数 # {max_iterations} - ワークフローの最大イテレーション数
# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか)
# {task} - 元のユーザー要求 # {task} - 元のユーザー要求
# {previous_response} - 前のステップの出力 # {previous_response} - 前のムーブメントの出力
# {user_inputs} - ワークフロー中に蓄積されたユーザー入力 # {user_inputs} - ワークフロー中に蓄積されたユーザー入力
# {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary" # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary"
@ -16,7 +16,7 @@ description: MAGI合議システム - 3つの観点から分析し多数決で
max_iterations: 5 max_iterations: 5
steps: movements:
- name: melchior - name: melchior
agent: ../agents/magi/melchior.md agent: ../agents/magi/melchior.md
allowed_tools: allowed_tools:

View File

@ -1,13 +1,13 @@
# Simple TAKT Workflow # Simple TAKT Workflow
# Implement -> AI Review -> Supervisor Approval # Implement -> AI Review -> Supervisor Approval
# (最もシンプルな構成 - plan, architect review, fix ステップなし) # (最もシンプルな構成 - plan, architect review, fix ムーブメントなし)
# #
# Template Variables (auto-injected): # 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 # {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) # {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) # {user_inputs} - Accumulated user inputs during workflow (auto-injected)
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {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 max_iterations: 20
initial_step: implement initial_movement: implement
steps: movements:
- name: implement - name: implement
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
@ -248,7 +248,7 @@ steps:
- condition: 修正不要(指摘対象ファイル/仕様の確認済み) - condition: 修正不要(指摘対象ファイル/仕様の確認済み)
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**
@ -350,7 +350,7 @@ steps:
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
next: implement next: implement
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**

View File

@ -7,11 +7,11 @@
# -> plan (rejected: 計画からやり直し) # -> plan (rejected: 計画からやり直し)
# #
# テンプレート変数: # テンプレート変数:
# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたステップの合計) # {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計)
# {max_iterations} - ワークフローの最大イテレーション数 # {max_iterations} - ワークフローの最大イテレーション数
# {step_iteration} - ステップごとのイテレーション数(このステップが何回実行されたか) # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか)
# {task} - 元のユーザー要求 # {task} - 元のユーザー要求
# {previous_response} - 前のステップの出力 # {previous_response} - 前のムーブメントの出力
# {user_inputs} - ワークフロー中に蓄積されたユーザー入力 # {user_inputs} - ワークフロー中に蓄積されたユーザー入力
# {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary" # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary"
@ -20,7 +20,7 @@ description: 調査ワークフロー - 質問せずに自律的に調査を実
max_iterations: 10 max_iterations: 10
steps: movements:
- name: plan - name: plan
agent: ../agents/research/planner.md agent: ../agents/research/planner.md
allowed_tools: allowed_tools:
@ -32,8 +32,8 @@ steps:
instruction_template: | instruction_template: |
## ワークフロー状況 ## ワークフロー状況
- イテレーション: {iteration}/{max_iterations}(ワークフロー全体) - イテレーション: {iteration}/{max_iterations}(ワークフロー全体)
- ステップ実行回数: {step_iteration}(このステップの実行回数) - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数)
- ステップ: plan - ムーブメント: plan
## 調査依頼 ## 調査依頼
{task} {task}
@ -69,8 +69,8 @@ steps:
instruction_template: | instruction_template: |
## ワークフロー状況 ## ワークフロー状況
- イテレーション: {iteration}/{max_iterations}(ワークフロー全体) - イテレーション: {iteration}/{max_iterations}(ワークフロー全体)
- ステップ実行回数: {step_iteration}(このステップの実行回数) - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数)
- ステップ: dig - ムーブメント: dig
## 元の調査依頼 ## 元の調査依頼
{task} {task}
@ -111,8 +111,8 @@ steps:
instruction_template: | instruction_template: |
## ワークフロー状況 ## ワークフロー状況
- イテレーション: {iteration}/{max_iterations}(ワークフロー全体) - イテレーション: {iteration}/{max_iterations}(ワークフロー全体)
- ステップ実行回数: {step_iteration}(このステップの実行回数) - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数)
- ステップ: supervise (調査品質評価) - ムーブメント: supervise (調査品質評価)
## 元の調査依頼 ## 元の調査依頼
{task} {task}
@ -131,4 +131,4 @@ steps:
- condition: 調査結果が不十分であり、計画からやり直す必要がある - condition: 調査結果が不十分であり、計画からやり直す必要がある
next: plan next: plan
initial_step: plan initial_movement: plan

View File

@ -1,13 +1,13 @@
# Review-Fix Minimal TAKT Workflow # Review-Fix Minimal TAKT Workflow
# Review -> Fix (if needed) -> Re-review -> Complete # Review -> Fix (if needed) -> Re-review -> Complete
# (レビューから開始、実装ステップなし) # (レビューから開始、実装ムーブメントなし)
# #
# Template Variables (auto-injected): # 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 # {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) # {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) # {user_inputs} - Accumulated user inputs during workflow (auto-injected)
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
@ -16,9 +16,9 @@ description: 既存コードのレビューと修正ワークフロー(レビ
max_iterations: 20 max_iterations: 20
initial_step: reviewers initial_movement: reviewers
steps: movements:
- name: implement - name: implement
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
@ -248,7 +248,7 @@ steps:
- condition: 修正不要(指摘対象ファイル/仕様の確認済み) - condition: 修正不要(指摘対象ファイル/仕様の確認済み)
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**
@ -350,7 +350,7 @@ steps:
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
next: implement next: implement
instruction_template: | instruction_template: |
**これは {step_iteration} 回目の AI Review です。** **これは {movement_iteration} 回目の AI Review です。**
2回目以降は、前回の修正が実際には行われていなかったということです。 2回目以降は、前回の修正が実際には行われていなかったということです。
**あなたの「修正済み」という認識が間違っています。** **あなたの「修正済み」という認識が間違っています。**

View File

@ -8,14 +8,14 @@
# -> COMPLETE (ローカル: コンソール出力のみ) # -> COMPLETE (ローカル: コンソール出力のみ)
# -> ABORT (rejected) # -> ABORT (rejected)
# #
# 全ステップ edit: falseファイル変更なし # 全ムーブメント edit: falseファイル変更なし
# #
# テンプレート変数: # テンプレート変数:
# {iteration} - ワークフロー全体のターン数 # {iteration} - ワークフロー全体のターン数
# {max_iterations} - 最大イテレーション数 # {max_iterations} - 最大イテレーション数
# {step_iteration} - ステップごとのイテレーション数 # {movement_iteration} - ムーブメントごとのイテレーション数
# {task} - 元のユーザー要求 # {task} - 元のユーザー要求
# {previous_response} - 前のステップの出力 # {previous_response} - 前のムーブメントの出力
# {user_inputs} - 蓄積されたユーザー入力 # {user_inputs} - 蓄積されたユーザー入力
# {report_dir} - レポートディレクトリ名 # {report_dir} - レポートディレクトリ名
@ -24,9 +24,9 @@ description: レビュー専用ワークフロー - コードをレビューす
max_iterations: 10 max_iterations: 10
initial_step: plan initial_movement: plan
steps: movements:
- name: plan - name: plan
edit: false edit: false
agent: ../agents/default/planner.md agent: ../agents/default/planner.md
@ -108,7 +108,7 @@ steps:
- condition: approved - condition: approved
- condition: needs_fix - condition: needs_fix
instruction_template: | instruction_template: |
**アーキテクチャと設計**のレビューに集中してください。AI特有の問題はレビューしないでくださいai_reviewステップで行います)。 **アーキテクチャと設計**のレビューに集中してください。AI特有の問題はレビューしないでくださいai_reviewムーブメントで行います)。
コードをレビューしてフィードバックを提供してください。 コードをレビューしてフィードバックを提供してください。
@ -249,7 +249,7 @@ steps:
4. ルーティング判断: 4. ルーティング判断:
- タスクにPRへのコメント投稿が含まれている場合 - タスクにPRへのコメント投稿が含まれている場合
(例: "PRにコメントして"、"PRにレビュー結果を投稿" (例: "PRにコメントして"、"PRにレビュー結果を投稿"
→ `pr-comment` ステップcondition: "approved, PR comment requested" → `pr-comment` ムーブメントcondition: "approved, PR comment requested"
- ローカルレビューのみ → COMPLETEcondition: "approved" - ローカルレビューのみ → COMPLETEcondition: "approved"
- 重大な問題が見つかった場合 → ABORTcondition: "rejected" - 重大な問題が見つかった場合 → ABORTcondition: "rejected"

View File

@ -45,7 +45,7 @@ describe('detectRuleIndex', () => {
expect(detectRuleIndex('[Plan:2]', 'plan')).toBe(1); 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('[IMPLEMENT:1]', 'implement')).toBe(0);
expect(detectRuleIndex('[REVIEW:2]', 'review')).toBe(1); expect(detectRuleIndex('[REVIEW:2]', 'review')).toBe(1);
}); });

View File

@ -51,109 +51,109 @@ describe('getBuiltinWorkflow', () => {
}); });
}); });
describe('default workflow parallel reviewers step', () => { describe('default workflow parallel reviewers movement', () => {
it('should have a reviewers step with parallel sub-steps', () => { it('should have a reviewers movement with parallel sub-movements', () => {
const workflow = getBuiltinWorkflow('default'); const workflow = getBuiltinWorkflow('default');
expect(workflow).not.toBeNull(); expect(workflow).not.toBeNull();
const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers');
expect(reviewersStep).toBeDefined(); expect(reviewersMovement).toBeDefined();
expect(reviewersStep!.parallel).toBeDefined(); expect(reviewersMovement!.parallel).toBeDefined();
expect(reviewersStep!.parallel).toHaveLength(2); 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 workflow = getBuiltinWorkflow('default');
const reviewersStep = workflow!.steps.find((s) => s.name === 'reviewers')!; const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!;
const subStepNames = reviewersStep.parallel!.map((s) => s.name); const subMovementNames = reviewersMovement.parallel!.map((s) => s.name);
expect(subStepNames).toContain('arch-review'); expect(subMovementNames).toContain('arch-review');
expect(subStepNames).toContain('security-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 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(reviewersMovement.rules).toBeDefined();
expect(reviewersStep.rules).toHaveLength(2); 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).toBeDefined();
expect(allRule!.aggregateConditionText).toBe('approved'); expect(allRule!.aggregateConditionText).toBe('approved');
expect(allRule!.next).toBe('supervise'); 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).toBeDefined();
expect(anyRule!.aggregateConditionText).toBe('needs_fix'); expect(anyRule!.aggregateConditionText).toBe('needs_fix');
expect(anyRule!.next).toBe('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 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!) { for (const subMovement of reviewersMovement.parallel!) {
expect(subStep.rules).toBeDefined(); expect(subMovement.rules).toBeDefined();
const conditions = subStep.rules!.map((r) => r.condition); const conditions = subMovement.rules!.map((r) => r.condition);
expect(conditions).toContain('approved'); expect(conditions).toContain('approved');
expect(conditions).toContain('needs_fix'); 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 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(); 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 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(); 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 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(); 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 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(movementNames).not.toContain('review');
expect(stepNames).not.toContain('security_review'); expect(movementNames).not.toContain('security_review');
expect(stepNames).not.toContain('improve'); expect(movementNames).not.toContain('improve');
expect(stepNames).not.toContain('security_fix'); 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 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'); 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'); 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 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(); 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(); expect(secReview.report).toBeDefined();
}); });
}); });
@ -180,7 +180,7 @@ describe('loadAllWorkflows', () => {
name: test-workflow name: test-workflow
description: Test workflow description: Test workflow
max_iterations: 10 max_iterations: 10
steps: movements:
- name: step1 - name: step1
agent: coder agent: coder
instruction: "{task}" instruction: "{task}"

View File

@ -3,7 +3,7 @@
* *
* Covers: * Covers:
* - abort() sets state to aborted and emits workflow:abort * - 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 * - isAbortRequested() reflects abort state
* - Double abort() is idempotent * - Double abort() is idempotent
*/ */
@ -39,7 +39,7 @@ import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { import {
makeResponse, makeResponse,
makeStep, makeMovement,
makeRule, makeRule,
mockRunAgentSequence, mockRunAgentSequence,
mockDetectMatchedRuleSequence, mockDetectMatchedRuleSequence,
@ -66,15 +66,15 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
return { return {
name: 'test', name: 'test',
maxIterations: 10, maxIterations: 10,
initialStep: 'step1', initialMovement: 'step1',
steps: [ movements: [
makeStep('step1', { makeMovement('step1', {
rules: [ rules: [
makeRule('done', 'step2'), makeRule('done', 'step2'),
makeRule('fail', 'ABORT'), makeRule('fail', 'ABORT'),
], ],
}), }),
makeStep('step2', { makeMovement('step2', {
rules: [ rules: [
makeRule('done', 'COMPLETE'), makeRule('done', 'COMPLETE'),
], ],
@ -84,7 +84,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
} }
describe('abort() before run loop iteration', () => { 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 config = makeSimpleConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -100,17 +100,17 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
expect(state.status).toBe('aborted'); expect(state.status).toBe('aborted');
expect(abortFn).toHaveBeenCalledOnce(); expect(abortFn).toHaveBeenCalledOnce();
expect(abortFn.mock.calls[0][1]).toContain('SIGINT'); 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(); 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 () => { it('should abort when abort() is called during runAgent', async () => {
const config = makeSimpleConfig(); const config = makeSimpleConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); 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 () => { vi.mocked(runAgent).mockImplementation(async () => {
engine.abort(); engine.abort();
throw new Error('Query interrupted'); throw new Error('Query interrupted');
@ -158,14 +158,14 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
}); });
}); });
describe('abort between steps', () => { describe('abort between movements', () => {
it('should stop after completing current step when abort() is called', async () => { it('should stop after completing current movement when abort() is called', async () => {
const config = makeSimpleConfig(); const config = makeSimpleConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); 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 () => { 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(); engine.abort();
return makeResponse({ agent: 'step1', content: 'Step 1 done' }); return makeResponse({ agent: 'step1', content: 'Step 1 done' });
}); });
@ -181,7 +181,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
expect(state.status).toBe('aborted'); expect(state.status).toBe('aborted');
expect(state.iteration).toBe(1); 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(vi.mocked(runAgent)).toHaveBeenCalledTimes(1);
expect(abortFn).toHaveBeenCalledOnce(); expect(abortFn).toHaveBeenCalledOnce();
}); });

View File

@ -1,8 +1,8 @@
/** /**
* Tests for WorkflowEngine provider/model overrides. * Tests for WorkflowEngine provider/model overrides.
* *
* Verifies that CLI-specified overrides take precedence over workflow step defaults, * Verifies that CLI-specified overrides take precedence over workflow movement defaults,
* and that step-specific values are used when no overrides are present. * and that movement-specific values are used when no overrides are present.
*/ */
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, vi } from 'vitest';
@ -32,7 +32,7 @@ import type { WorkflowConfig } from '../core/models/index.js';
import { import {
makeResponse, makeResponse,
makeRule, makeRule,
makeStep, makeMovement,
mockRunAgentSequence, mockRunAgentSequence,
mockDetectMatchedRuleSequence, mockDetectMatchedRuleSequence,
applyDefaultMocks, applyDefaultMocks,
@ -44,21 +44,21 @@ describe('WorkflowEngine agent overrides', () => {
applyDefaultMocks(); applyDefaultMocks();
}); });
it('respects workflow step provider/model even when CLI overrides are provided', async () => { it('respects workflow movement provider/model even when CLI overrides are provided', async () => {
const step = makeStep('plan', { const movement = makeMovement('plan', {
provider: 'claude', provider: 'claude',
model: 'claude-step', model: 'claude-movement',
rules: [makeRule('done', 'COMPLETE')], rules: [makeRule('done', 'COMPLETE')],
}); });
const config: WorkflowConfig = { const config: WorkflowConfig = {
name: 'override-test', name: 'override-test',
steps: [step], movements: [movement],
initialStep: 'plan', initialMovement: 'plan',
maxIterations: 1, maxIterations: 1,
}; };
mockRunAgentSequence([ mockRunAgentSequence([
makeResponse({ agent: step.agent, content: 'done' }), makeResponse({ agent: movement.agent, content: 'done' }),
]); ]);
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
@ -72,22 +72,22 @@ describe('WorkflowEngine agent overrides', () => {
const options = vi.mocked(runAgent).mock.calls[0][2]; const options = vi.mocked(runAgent).mock.calls[0][2];
expect(options.provider).toBe('claude'); 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 () => { it('allows CLI overrides when workflow movement leaves provider/model undefined', async () => {
const step = makeStep('plan', { const movement = makeMovement('plan', {
rules: [makeRule('done', 'COMPLETE')], rules: [makeRule('done', 'COMPLETE')],
}); });
const config: WorkflowConfig = { const config: WorkflowConfig = {
name: 'override-fallback', name: 'override-fallback',
steps: [step], movements: [movement],
initialStep: 'plan', initialMovement: 'plan',
maxIterations: 1, maxIterations: 1,
}; };
mockRunAgentSequence([ mockRunAgentSequence([
makeResponse({ agent: step.agent, content: 'done' }), makeResponse({ agent: movement.agent, content: 'done' }),
]); ]);
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
@ -104,29 +104,29 @@ describe('WorkflowEngine agent overrides', () => {
expect(options.model).toBe('cli-model'); expect(options.model).toBe('cli-model');
}); });
it('falls back to workflow step provider/model when no overrides supplied', async () => { it('falls back to workflow movement provider/model when no overrides supplied', async () => {
const step = makeStep('plan', { const movement = makeMovement('plan', {
provider: 'claude', provider: 'claude',
model: 'step-model', model: 'movement-model',
rules: [makeRule('done', 'COMPLETE')], rules: [makeRule('done', 'COMPLETE')],
}); });
const config: WorkflowConfig = { const config: WorkflowConfig = {
name: 'step-defaults', name: 'movement-defaults',
steps: [step], movements: [movement],
initialStep: 'plan', initialMovement: 'plan',
maxIterations: 1, maxIterations: 1,
}; };
mockRunAgentSequence([ mockRunAgentSequence([
makeResponse({ agent: step.agent, content: 'done' }), makeResponse({ agent: movement.agent, content: 'done' }),
]); ]);
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); 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(); await engine.run();
const options = vi.mocked(runAgent).mock.calls[0][2]; const options = vi.mocked(runAgent).mock.calls[0][2];
expect(options.provider).toBe('claude'); expect(options.provider).toBe('claude');
expect(options.model).toBe('step-model'); expect(options.model).toBe('movement-model');
}); });
}); });

View File

@ -72,7 +72,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => {
const blockedFn = vi.fn(); const blockedFn = vi.fn();
const abortFn = vi.fn(); const abortFn = vi.fn();
engine.on('step:blocked', blockedFn); engine.on('movement:blocked', blockedFn);
engine.on('workflow:abort', abortFn); engine.on('workflow:abort', abortFn);
const state = await engine.run(); const state = await engine.run();
@ -132,7 +132,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => {
]); ]);
const userInputFn = vi.fn(); const userInputFn = vi.fn();
engine.on('step:user_input', userInputFn); engine.on('movement:user_input', userInputFn);
const state = await engine.run(); const state = await engine.run();

View File

@ -39,7 +39,7 @@ import { runAgent } from '../agents/runner.js';
import { detectMatchedRule } from '../core/workflow/index.js'; import { detectMatchedRule } from '../core/workflow/index.js';
import { import {
makeResponse, makeResponse,
makeStep, makeMovement,
makeRule, makeRule,
buildDefaultWorkflowConfig, buildDefaultWorkflowConfig,
mockRunAgentSequence, mockRunAgentSequence,
@ -119,9 +119,9 @@ describe('WorkflowEngine Integration: Error Handling', () => {
const config = buildDefaultWorkflowConfig({ const config = buildDefaultWorkflowConfig({
maxIterations: 100, maxIterations: 100,
loopDetection: { maxConsecutiveSameStep: 3, action: 'abort' }, loopDetection: { maxConsecutiveSameStep: 3, action: 'abort' },
initialStep: 'loop-step', initialMovement: 'loop-step',
steps: [ movements: [
makeStep('loop-step', { makeMovement('loop-step', {
rules: [makeRule('continue', 'loop-step')], rules: [makeRule('continue', 'loop-step')],
}), }),
], ],

View File

@ -7,13 +7,13 @@
* - AI review reject and fix * - AI review reject and fix
* - ABORT transition * - ABORT transition
* - Event emissions * - Event emissions
* - Step output tracking * - Movement output tracking
* - Config validation * - Config validation
*/ */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, rmSync } from 'node:fs'; 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) --- // --- 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 { runAgent } from '../agents/runner.js';
import { import {
makeResponse, makeResponse,
makeStep, makeMovement,
makeRule, makeRule,
buildDefaultWorkflowConfig, buildDefaultWorkflowConfig,
mockRunAgentSequence, mockRunAgentSequence,
@ -101,7 +101,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
expect(state.status).toBe('completed'); expect(state.status).toBe('completed');
expect(state.iteration).toBe(5); // plan, implement, ai_review, reviewers, supervise expect(state.iteration).toBe(5); // plan, implement, ai_review, reviewers, supervise
expect(completeFn).toHaveBeenCalledOnce(); 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 // 4. ABORT transition
// ===================================================== // =====================================================
describe('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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -219,7 +219,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
// 5. Event emissions // 5. Event emissions
// ===================================================== // =====================================================
describe('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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -244,26 +244,26 @@ describe('WorkflowEngine Integration: Happy Path', () => {
const startFn = vi.fn(); const startFn = vi.fn();
const completeFn = vi.fn(); const completeFn = vi.fn();
engine.on('step:start', startFn); engine.on('movement:start', startFn);
engine.on('step:complete', completeFn); engine.on('movement:complete', completeFn);
await engine.run(); await engine.run();
// 5 steps: plan, implement, ai_review, reviewers, supervise // 5 movements: plan, implement, ai_review, reviewers, supervise
expect(startFn).toHaveBeenCalledTimes(5); expect(startFn).toHaveBeenCalledTimes(5);
expect(completeFn).toHaveBeenCalledTimes(5); expect(completeFn).toHaveBeenCalledTimes(5);
const startedSteps = startFn.mock.calls.map(call => (call[0] as WorkflowStep).name); const startedMovements = startFn.mock.calls.map(call => (call[0] as WorkflowMovement).name);
expect(startedSteps).toEqual(['plan', 'implement', 'ai_review', 'reviewers', 'supervise']); 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 = { const simpleConfig: WorkflowConfig = {
name: 'test', name: 'test',
maxIterations: 10, maxIterations: 10,
initialStep: 'plan', initialMovement: 'plan',
steps: [ movements: [
makeStep('plan', { makeMovement('plan', {
rules: [makeRule('done', 'COMPLETE')], rules: [makeRule('done', 'COMPLETE')],
}), }),
], ],
@ -278,18 +278,18 @@ describe('WorkflowEngine Integration: Happy Path', () => {
]); ]);
const startFn = vi.fn(); const startFn = vi.fn();
engine.on('step:start', startFn); engine.on('movement:start', startFn);
await engine.run(); await engine.run();
expect(startFn).toHaveBeenCalledTimes(1); expect(startFn).toHaveBeenCalledTimes(1);
// step:start should receive (step, iteration, instruction) // movement:start should receive (movement, iteration, instruction)
const [_step, _iteration, instruction] = startFn.mock.calls[0]; const [_movement, _iteration, instruction] = startFn.mock.calls[0];
expect(typeof instruction).toBe('string'); expect(typeof instruction).toBe('string');
expect(instruction.length).toBeGreaterThan(0); 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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -313,16 +313,16 @@ describe('WorkflowEngine Integration: Happy Path', () => {
]); ]);
const startFn = vi.fn(); const startFn = vi.fn();
engine.on('step:start', startFn); engine.on('movement:start', startFn);
await engine.run(); 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( const reviewersCall = startFn.mock.calls.find(
(call) => (call[0] as WorkflowStep).name === 'reviewers' (call) => (call[0] as WorkflowMovement).name === 'reviewers'
); );
expect(reviewersCall).toBeDefined(); expect(reviewersCall).toBeDefined();
// Parallel steps emit empty string for instruction // Parallel movements emit empty string for instruction
const [, , instruction] = reviewersCall!; const [, , instruction] = reviewersCall!;
expect(instruction).toBe(''); expect(instruction).toBe('');
}); });
@ -348,10 +348,10 @@ describe('WorkflowEngine Integration: Happy Path', () => {
}); });
// ===================================================== // =====================================================
// 6. Step output tracking // 6. Movement output tracking
// ===================================================== // =====================================================
describe('Step output tracking', () => { describe('Movement output tracking', () => {
it('should store outputs for all executed steps', async () => { it('should store outputs for all executed movements', async () => {
const config = buildDefaultWorkflowConfig(); const config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -376,10 +376,10 @@ describe('WorkflowEngine Integration: Happy Path', () => {
const state = await engine.run(); const state = await engine.run();
expect(state.stepOutputs.get('plan')!.content).toBe('Plan output'); expect(state.movementOutputs.get('plan')!.content).toBe('Plan output');
expect(state.stepOutputs.get('implement')!.content).toBe('Implement output'); expect(state.movementOutputs.get('implement')!.content).toBe('Implement output');
expect(state.stepOutputs.get('ai_review')!.content).toBe('AI review output'); expect(state.movementOutputs.get('ai_review')!.content).toBe('AI review output');
expect(state.stepOutputs.get('supervise')!.content).toBe('Supervise output'); expect(state.movementOutputs.get('supervise')!.content).toBe('Supervise output');
}); });
}); });
@ -391,9 +391,9 @@ describe('WorkflowEngine Integration: Happy Path', () => {
const simpleConfig: WorkflowConfig = { const simpleConfig: WorkflowConfig = {
name: 'test', name: 'test',
maxIterations: 10, maxIterations: 10,
initialStep: 'plan', initialMovement: 'plan',
steps: [ movements: [
makeStep('plan', { makeMovement('plan', {
rules: [makeRule('done', 'COMPLETE')], 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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -454,7 +454,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
await engine.run(); 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(phaseStartFn).toHaveBeenCalledTimes(6);
expect(phaseCompleteFn).toHaveBeenCalledTimes(6); expect(phaseCompleteFn).toHaveBeenCalledTimes(6);
@ -470,21 +470,21 @@ describe('WorkflowEngine Integration: Happy Path', () => {
// 8. Config validation // 8. Config validation
// ===================================================== // =====================================================
describe('Config validation', () => { describe('Config validation', () => {
it('should throw when initial step does not exist', () => { it('should throw when initial movement does not exist', () => {
const config = buildDefaultWorkflowConfig({ initialStep: 'nonexistent' }); const config = buildDefaultWorkflowConfig({ initialMovement: 'nonexistent' });
expect(() => { expect(() => {
new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); 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 = { const config: WorkflowConfig = {
name: 'test', name: 'test',
maxIterations: 10, maxIterations: 10,
initialStep: 'step1', initialMovement: 'step1',
steps: [ movements: [
makeStep('step1', { makeMovement('step1', {
rules: [makeRule('done', 'nonexistent_step')], rules: [makeRule('done', 'nonexistent_step')],
}), }),
], ],

View File

@ -1,10 +1,10 @@
/** /**
* WorkflowEngine integration tests: parallel step aggregation. * WorkflowEngine integration tests: parallel movement aggregation.
* *
* Covers: * Covers:
* - Aggregated output format (## headers and --- separators) * - Aggregated output format (## headers and --- separators)
* - Individual sub-step output storage * - Individual sub-movement output storage
* - Concurrent execution of sub-steps * - Concurrent execution of sub-movements
*/ */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
@ -44,7 +44,7 @@ import {
applyDefaultMocks, applyDefaultMocks,
} from './engine-test-helpers.js'; } from './engine-test-helpers.js';
describe('WorkflowEngine Integration: Parallel Step Aggregation', () => { describe('WorkflowEngine Integration: Parallel Movement Aggregation', () => {
let tmpDir: string; let tmpDir: string;
beforeEach(() => { 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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); 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'); expect(state.status).toBe('completed');
const reviewersOutput = state.stepOutputs.get('reviewers'); const reviewersOutput = state.movementOutputs.get('reviewers');
expect(reviewersOutput).toBeDefined(); expect(reviewersOutput).toBeDefined();
expect(reviewersOutput!.content).toContain('## arch-review'); expect(reviewersOutput!.content).toContain('## arch-review');
expect(reviewersOutput!.content).toContain('Architecture review content'); expect(reviewersOutput!.content).toContain('Architecture review content');
@ -96,7 +96,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => {
expect(reviewersOutput!.matchedRuleMethod).toBe('aggregate'); 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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); 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(); const state = await engine.run();
expect(state.stepOutputs.has('arch-review')).toBe(true); expect(state.movementOutputs.has('arch-review')).toBe(true);
expect(state.stepOutputs.has('security-review')).toBe(true); expect(state.movementOutputs.has('security-review')).toBe(true);
expect(state.stepOutputs.has('reviewers')).toBe(true); expect(state.movementOutputs.has('reviewers')).toBe(true);
expect(state.stepOutputs.get('arch-review')!.content).toBe('Arch content'); expect(state.movementOutputs.get('arch-review')!.content).toBe('Arch content');
expect(state.stepOutputs.get('security-review')!.content).toBe('Sec 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 config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -153,7 +153,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => {
await engine.run(); 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); expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(6);
const calledAgents = vi.mocked(runAgent).mock.calls.map(call => call[0]); const calledAgents = vi.mocked(runAgent).mock.calls.map(call => call[0]);

View File

@ -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'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
@ -9,50 +9,50 @@ import { tmpdir } from 'node:os';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { isReportObjectConfig } from '../core/workflow/index.js'; 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. * Extracted emitMovementReports logic for unit testing.
* Mirrors engine.ts emitStepReports + emitIfReportExists. * Mirrors engine.ts emitMovementReports + emitIfReportExists.
* *
* reportDir already includes the `.takt/reports/` prefix (set by engine constructor). * reportDir already includes the `.takt/reports/` prefix (set by engine constructor).
*/ */
function emitStepReports( function emitMovementReports(
emitter: EventEmitter, emitter: EventEmitter,
step: WorkflowStep, movement: WorkflowMovement,
reportDir: string, reportDir: string,
projectCwd: string, projectCwd: string,
): void { ): void {
if (!step.report || !reportDir) return; if (!movement.report || !reportDir) return;
const baseDir = join(projectCwd, reportDir); const baseDir = join(projectCwd, reportDir);
if (typeof step.report === 'string') { if (typeof movement.report === 'string') {
emitIfReportExists(emitter, step, baseDir, step.report); emitIfReportExists(emitter, movement, baseDir, movement.report);
} else if (isReportObjectConfig(step.report)) { } else if (isReportObjectConfig(movement.report)) {
emitIfReportExists(emitter, step, baseDir, step.report.name); emitIfReportExists(emitter, movement, baseDir, movement.report.name);
} else { } else {
for (const rc of step.report) { for (const rc of movement.report) {
emitIfReportExists(emitter, step, baseDir, rc.path); emitIfReportExists(emitter, movement, baseDir, rc.path);
} }
} }
} }
function emitIfReportExists( function emitIfReportExists(
emitter: EventEmitter, emitter: EventEmitter,
step: WorkflowStep, movement: WorkflowMovement,
baseDir: string, baseDir: string,
fileName: string, fileName: string,
): void { ): void {
const filePath = join(baseDir, fileName); const filePath = join(baseDir, fileName);
if (existsSync(filePath)) { if (existsSync(filePath)) {
emitter.emit('step:report', step, filePath, fileName); emitter.emit('movement:report', movement, filePath, fileName);
} }
} }
/** Create a minimal WorkflowStep for testing */ /** Create a minimal WorkflowMovement for testing */
function createStep(overrides: Partial<WorkflowStep> = {}): WorkflowStep { function createMovement(overrides: Partial<WorkflowMovement> = {}): WorkflowMovement {
return { return {
name: 'test-step', name: 'test-movement',
agent: 'coder', agent: 'coder',
agentDisplayName: 'Coder', agentDisplayName: 'Coder',
instructionTemplate: '', instructionTemplate: '',
@ -61,7 +61,7 @@ function createStep(overrides: Partial<WorkflowStep> = {}): WorkflowStep {
}; };
} }
describe('emitStepReports', () => { describe('emitMovementReports', () => {
let tmpDir: string; let tmpDir: string;
let reportBaseDir: string; let reportBaseDir: string;
// reportDir now includes .takt/reports/ prefix (matches engine constructor behavior) // reportDir now includes .takt/reports/ prefix (matches engine constructor behavior)
@ -77,100 +77,100 @@ describe('emitStepReports', () => {
rmSync(tmpDir, { recursive: true, force: true }); rmSync(tmpDir, { recursive: true, force: true });
}); });
it('should emit step:report when string report file exists', () => { it('should emit movement:report when string report file exists', () => {
// Given: a step with string report and the file exists // Given: a movement with string report and the file exists
const step = createStep({ report: 'plan.md' }); const movement = createMovement({ report: 'plan.md' });
writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8'); writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8');
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const handler = vi.fn(); const handler = vi.fn();
emitter.on('step:report', handler); emitter.on('movement:report', handler);
// When // When
emitStepReports(emitter, step, reportDirName, tmpDir); emitMovementReports(emitter, movement, reportDirName, tmpDir);
// Then // Then
expect(handler).toHaveBeenCalledOnce(); 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', () => { it('should not emit when string report file does not exist', () => {
// Given: a step with string report but file doesn't exist // Given: a movement with string report but file doesn't exist
const step = createStep({ report: 'missing.md' }); const movement = createMovement({ report: 'missing.md' });
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const handler = vi.fn(); const handler = vi.fn();
emitter.on('step:report', handler); emitter.on('movement:report', handler);
// When // When
emitStepReports(emitter, step, reportDirName, tmpDir); emitMovementReports(emitter, movement, reportDirName, tmpDir);
// Then // Then
expect(handler).not.toHaveBeenCalled(); expect(handler).not.toHaveBeenCalled();
}); });
it('should emit step:report when ReportObjectConfig report file exists', () => { it('should emit movement:report when ReportObjectConfig report file exists', () => {
// Given: a step with ReportObjectConfig and the file exists // Given: a movement with ReportObjectConfig and the file exists
const report: ReportObjectConfig = { name: '03-review.md', format: '# Review' }; 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'); writeFileSync(join(reportBaseDir, '03-review.md'), '# Review\nOK', 'utf-8');
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const handler = vi.fn(); const handler = vi.fn();
emitter.on('step:report', handler); emitter.on('movement:report', handler);
// When // When
emitStepReports(emitter, step, reportDirName, tmpDir); emitMovementReports(emitter, movement, reportDirName, tmpDir);
// Then // Then
expect(handler).toHaveBeenCalledOnce(); 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', () => { 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[] = [ const report: ReportConfig[] = [
{ label: 'Scope', path: '01-scope.md' }, { label: 'Scope', path: '01-scope.md' },
{ label: 'Decisions', path: '02-decisions.md' }, { label: 'Decisions', path: '02-decisions.md' },
{ label: 'Missing', path: '03-missing.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, '01-scope.md'), '# Scope', 'utf-8');
writeFileSync(join(reportBaseDir, '02-decisions.md'), '# Decisions', 'utf-8'); writeFileSync(join(reportBaseDir, '02-decisions.md'), '# Decisions', 'utf-8');
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const handler = vi.fn(); const handler = vi.fn();
emitter.on('step:report', handler); emitter.on('movement:report', handler);
// When // When
emitStepReports(emitter, step, reportDirName, tmpDir); emitMovementReports(emitter, movement, reportDirName, tmpDir);
// Then: emitted for scope and decisions, not for missing // Then: emitted for scope and decisions, not for missing
expect(handler).toHaveBeenCalledTimes(2); expect(handler).toHaveBeenCalledTimes(2);
expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '01-scope.md'), '01-scope.md'); expect(handler).toHaveBeenCalledWith(movement, 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, '02-decisions.md'), '02-decisions.md');
}); });
it('should not emit when step has no report', () => { it('should not emit when movement has no report', () => {
// Given: a step without report // Given: a movement without report
const step = createStep({ report: undefined }); const movement = createMovement({ report: undefined });
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const handler = vi.fn(); const handler = vi.fn();
emitter.on('step:report', handler); emitter.on('movement:report', handler);
// When // When
emitStepReports(emitter, step, reportDirName, tmpDir); emitMovementReports(emitter, movement, reportDirName, tmpDir);
// Then // Then
expect(handler).not.toHaveBeenCalled(); expect(handler).not.toHaveBeenCalled();
}); });
it('should not emit when reportDir is empty', () => { it('should not emit when reportDir is empty', () => {
// Given: a step with report but empty reportDir // Given: a movement with report but empty reportDir
const step = createStep({ report: 'plan.md' }); const movement = createMovement({ report: 'plan.md' });
writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8'); writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8');
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const handler = vi.fn(); const handler = vi.fn();
emitter.on('step:report', handler); emitter.on('movement:report', handler);
// When: empty reportDir // When: empty reportDir
emitStepReports(emitter, step, '', tmpDir); emitMovementReports(emitter, movement, '', tmpDir);
// Then // Then
expect(handler).not.toHaveBeenCalled(); expect(handler).not.toHaveBeenCalled();

View File

@ -10,7 +10,7 @@ import { mkdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto'; 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) --- // --- Mock imports (consumers must call vi.mock before importing this) ---
@ -37,7 +37,7 @@ export function makeRule(condition: string, next: string, extra: Partial<Workflo
return { condition, next, ...extra }; return { condition, next, ...extra };
} }
export function makeStep(name: string, overrides: Partial<WorkflowStep> = {}): WorkflowStep { export function makeMovement(name: string, overrides: Partial<WorkflowMovement> = {}): WorkflowMovement {
return { return {
name, name,
agent: `../agents/${name}.md`, agent: `../agents/${name}.md`,
@ -53,14 +53,14 @@ export function makeStep(name: string, overrides: Partial<WorkflowStep> = {}): W
* plan implement ai_review (ai_fix) reviewers(parallel) (fix) supervise * plan implement ai_review (ai_fix) reviewers(parallel) (fix) supervise
*/ */
export function buildDefaultWorkflowConfig(overrides: Partial<WorkflowConfig> = {}): WorkflowConfig { export function buildDefaultWorkflowConfig(overrides: Partial<WorkflowConfig> = {}): WorkflowConfig {
const archReviewSubStep = makeStep('arch-review', { const archReviewSubMovement = makeMovement('arch-review', {
rules: [ rules: [
makeRule('approved', 'COMPLETE'), makeRule('approved', 'COMPLETE'),
makeRule('needs_fix', 'fix'), makeRule('needs_fix', 'fix'),
], ],
}); });
const securityReviewSubStep = makeStep('security-review', { const securityReviewSubMovement = makeMovement('security-review', {
rules: [ rules: [
makeRule('approved', 'COMPLETE'), makeRule('approved', 'COMPLETE'),
makeRule('needs_fix', 'fix'), makeRule('needs_fix', 'fix'),
@ -71,34 +71,34 @@ export function buildDefaultWorkflowConfig(overrides: Partial<WorkflowConfig> =
name: 'test-default', name: 'test-default',
description: 'Test workflow', description: 'Test workflow',
maxIterations: 30, maxIterations: 30,
initialStep: 'plan', initialMovement: 'plan',
steps: [ movements: [
makeStep('plan', { makeMovement('plan', {
rules: [ rules: [
makeRule('Requirements are clear', 'implement'), makeRule('Requirements are clear', 'implement'),
makeRule('Requirements unclear', 'ABORT'), makeRule('Requirements unclear', 'ABORT'),
], ],
}), }),
makeStep('implement', { makeMovement('implement', {
rules: [ rules: [
makeRule('Implementation complete', 'ai_review'), makeRule('Implementation complete', 'ai_review'),
makeRule('Cannot proceed', 'plan'), makeRule('Cannot proceed', 'plan'),
], ],
}), }),
makeStep('ai_review', { makeMovement('ai_review', {
rules: [ rules: [
makeRule('No AI-specific issues', 'reviewers'), makeRule('No AI-specific issues', 'reviewers'),
makeRule('AI-specific issues found', 'ai_fix'), makeRule('AI-specific issues found', 'ai_fix'),
], ],
}), }),
makeStep('ai_fix', { makeMovement('ai_fix', {
rules: [ rules: [
makeRule('AI issues fixed', 'reviewers'), makeRule('AI issues fixed', 'reviewers'),
makeRule('Cannot proceed', 'plan'), makeRule('Cannot proceed', 'plan'),
], ],
}), }),
makeStep('reviewers', { makeMovement('reviewers', {
parallel: [archReviewSubStep, securityReviewSubStep], parallel: [archReviewSubMovement, securityReviewSubMovement],
rules: [ rules: [
makeRule('all("approved")', 'supervise', { makeRule('all("approved")', 'supervise', {
isAggregateCondition: true, isAggregateCondition: true,
@ -112,13 +112,13 @@ export function buildDefaultWorkflowConfig(overrides: Partial<WorkflowConfig> =
}), }),
], ],
}), }),
makeStep('fix', { makeMovement('fix', {
rules: [ rules: [
makeRule('Fix complete', 'reviewers'), makeRule('Fix complete', 'reviewers'),
makeRule('Cannot proceed', 'plan'), makeRule('Cannot proceed', 'plan'),
], ],
}), }),
makeStep('supervise', { makeMovement('supervise', {
rules: [ rules: [
makeRule('All checks passed', 'COMPLETE'), makeRule('All checks passed', 'COMPLETE'),
makeRule('Requirements unmet', 'plan'), makeRule('Requirements unmet', 'plan'),

View File

@ -38,7 +38,7 @@ import { WorkflowEngine } from '../core/workflow/index.js';
import { runReportPhase } from '../core/workflow/index.js'; import { runReportPhase } from '../core/workflow/index.js';
import { import {
makeResponse, makeResponse,
makeStep, makeMovement,
makeRule, makeRule,
mockRunAgentSequence, mockRunAgentSequence,
mockDetectMatchedRuleSequence, mockDetectMatchedRuleSequence,
@ -69,9 +69,9 @@ function buildSimpleConfig(): WorkflowConfig {
name: 'worktree-test', name: 'worktree-test',
description: 'Test workflow for worktree', description: 'Test workflow for worktree',
maxIterations: 10, maxIterations: 10,
initialStep: 'review', initialMovement: 'review',
steps: [ movements: [
makeStep('review', { makeMovement('review', {
report: '00-review.md', report: '00-review.md',
rules: [ rules: [
makeRule('approved', 'COMPLETE'), 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 () => { 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 = { const config: WorkflowConfig = {
name: 'worktree-test', name: 'worktree-test',
description: 'Test', description: 'Test',
maxIterations: 10, maxIterations: 10,
initialStep: 'review', initialMovement: 'review',
steps: [ movements: [
makeStep('review', { makeMovement('review', {
instructionTemplate: 'Write report to {report_dir}', instructionTemplate: 'Write report to {report_dir}',
report: '00-review.md', report: '00-review.md',
rules: [ rules: [

View File

@ -102,7 +102,7 @@ describe('label integrity', () => {
it('contains all expected workflow keys in en', () => { it('contains all expected workflow keys in en', () => {
expect(() => getLabel('workflow.iterationLimit.maxReached')).not.toThrow(); 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.continueQuestion')).not.toThrow();
expect(() => getLabel('workflow.iterationLimit.continueLabel')).not.toThrow(); expect(() => getLabel('workflow.iterationLimit.continueLabel')).not.toThrow();
expect(() => getLabel('workflow.iterationLimit.continueDescription')).not.toThrow(); expect(() => getLabel('workflow.iterationLimit.continueDescription')).not.toThrow();

View File

@ -8,9 +8,7 @@ import {
isReportObjectConfig, isReportObjectConfig,
ReportInstructionBuilder, ReportInstructionBuilder,
StatusJudgmentBuilder, StatusJudgmentBuilder,
buildExecutionMetadata, generateStatusRulesComponents,
renderExecutionMetadata,
generateStatusRulesFromRules,
type ReportInstructionContext, type ReportInstructionContext,
type StatusJudgmentContext, type StatusJudgmentContext,
type InstructionContext, type InstructionContext,
@ -44,8 +42,9 @@ function createMinimalContext(overrides: Partial<InstructionContext> = {}): Inst
task: 'Test task', task: 'Test task',
iteration: 1, iteration: 1,
maxIterations: 10, maxIterations: 10,
stepIteration: 1, movementIteration: 1,
cwd: '/project', cwd: '/project',
projectCwd: '/project',
userInputs: [], userInputs: [],
...overrides, ...overrides,
}; };
@ -186,127 +185,7 @@ describe('instruction-builder', () => {
}); });
}); });
describe('buildExecutionMetadata', () => { describe('generateStatusRulesComponents', () => {
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', () => {
const rules: WorkflowRule[] = [ const rules: WorkflowRule[] = [
{ condition: '要件が明確で実装可能', next: 'implement' }, { condition: '要件が明確で実装可能', next: 'implement' },
{ condition: 'ユーザーが質問をしている', next: 'COMPLETE' }, { condition: 'ユーザーが質問をしている', next: 'COMPLETE' },
@ -314,12 +193,11 @@ describe('instruction-builder', () => {
]; ];
it('should generate criteria table with numbered tags (ja)', () => { 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.criteriaTable).toContain('| 1 | 要件が明確で実装可能 | `[PLAN:1]` |');
expect(result).toContain('| 1 | 要件が明確で実装可能 | `[PLAN:1]` |'); expect(result.criteriaTable).toContain('| 2 | ユーザーが質問をしている | `[PLAN:2]` |');
expect(result).toContain('| 2 | ユーザーが質問をしている | `[PLAN:2]` |'); expect(result.criteriaTable).toContain('| 3 | 要件が不明確、情報不足 | `[PLAN:3]` |');
expect(result).toContain('| 3 | 要件が不明確、情報不足 | `[PLAN:3]` |');
}); });
it('should generate criteria table with numbered tags (en)', () => { it('should generate criteria table with numbered tags (en)', () => {
@ -327,47 +205,46 @@ describe('instruction-builder', () => {
{ condition: 'Requirements are clear', next: 'implement' }, { condition: 'Requirements are clear', next: 'implement' },
{ condition: 'User is asking a question', next: 'COMPLETE' }, { 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.criteriaTable).toContain('| 1 | Requirements are clear | `[PLAN:1]` |');
expect(result).toContain('| 1 | Requirements are clear | `[PLAN:1]` |'); expect(result.criteriaTable).toContain('| 2 | User is asking a question | `[PLAN:2]` |');
expect(result).toContain('| 2 | User is asking a question | `[PLAN:2]` |');
}); });
it('should generate output format section with condition labels', () => { it('should generate output list with condition labels', () => {
const result = generateStatusRulesFromRules('plan', rules, 'ja'); const result = generateStatusRulesComponents('plan', rules, 'ja');
expect(result).toContain('## 出力フォーマット'); expect(result.outputList).toContain('`[PLAN:1]` — 要件が明確で実装可能');
expect(result).toContain('`[PLAN:1]` — 要件が明確で実装可能'); expect(result.outputList).toContain('`[PLAN:2]` — ユーザーが質問をしている');
expect(result).toContain('`[PLAN:2]` — ユーザーが質問をしている'); expect(result.outputList).toContain('`[PLAN:3]` — 要件が不明確、情報不足');
expect(result).toContain('`[PLAN:3]` — 要件が不明確、情報不足');
}); });
it('should generate appendix template section when rules have appendix', () => { it('should generate appendix content when rules have appendix', () => {
const result = generateStatusRulesFromRules('plan', rules, 'ja'); const result = generateStatusRulesComponents('plan', rules, 'ja');
expect(result).toContain('### 追加出力テンプレート'); expect(result.hasAppendix).toBe(true);
expect(result).toContain('`[PLAN:3]`'); expect(result.appendixContent).toContain('[[PLAN:3]]');
expect(result).toContain('確認事項:'); expect(result.appendixContent).toContain('確認事項:');
expect(result).toContain('- {質問1}'); 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[] = [ const noAppendixRules: WorkflowRule[] = [
{ condition: 'Done', next: 'review' }, { condition: 'Done', next: 'review' },
{ condition: 'Blocked', next: 'plan' }, { 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', () => { it('should uppercase step name in tags', () => {
const result = generateStatusRulesFromRules('ai_review', [ const result = generateStatusRulesComponents('ai_review', [
{ condition: 'No issues', next: 'supervise' }, { condition: 'No issues', next: 'supervise' },
], 'en'); ], 'en');
expect(result).toContain('`[AI_REVIEW:1]`'); expect(result.criteriaTable).toContain('`[AI_REVIEW:1]`');
}); });
it('should omit interactive-only rules when interactive is false', () => { 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: 'User input required', next: 'implement', interactiveOnly: true },
{ condition: 'Blocked', next: 'plan' }, { 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.criteriaTable).toContain('`[IMPLEMENT:1]`');
expect(result).toContain('`[IMPLEMENT:3]`'); expect(result.criteriaTable).toContain('`[IMPLEMENT:3]`');
expect(result).not.toContain('User input required'); expect(result.criteriaTable).not.toContain('User input required');
expect(result).not.toContain('`[IMPLEMENT:2]`'); expect(result.criteriaTable).not.toContain('`[IMPLEMENT:2]`');
}); });
}); });
@ -397,9 +274,8 @@ describe('instruction-builder', () => {
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('Decision Criteria'); // Status rules are no longer injected in Phase 1 (only in Phase 3)
expect(result).toContain('[PLAN:1]'); expect(result).not.toContain('Decision Criteria');
expect(result).toContain('[PLAN:2]');
}); });
it('should not add status rules when rules do not exist', () => { it('should not add status rules when rules do not exist', () => {
@ -429,7 +305,7 @@ describe('instruction-builder', () => {
const context = createMinimalContext({ const context = createMinimalContext({
iteration: 3, iteration: 3,
maxIterations: 20, maxIterations: 20,
stepIteration: 2, movementIteration: 2,
language: 'en', language: 'en',
}); });
@ -437,8 +313,8 @@ describe('instruction-builder', () => {
expect(result).toContain('## Workflow Context'); expect(result).toContain('## Workflow Context');
expect(result).toContain('- Iteration: 3/20'); expect(result).toContain('- Iteration: 3/20');
expect(result).toContain('- Step Iteration: 2'); expect(result).toContain('- Movement Iteration: 2');
expect(result).toContain('- Step: implement'); expect(result).toContain('- Movement: implement');
}); });
it('should include report info in Phase 1 when step has report', () => { 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', () => { it('should render Japanese step iteration suffix', () => {
const step = createMinimalStep('Do work'); const step = createMinimalStep('Do work');
const context = createMinimalContext({ const context = createMinimalContext({
stepIteration: 3, movementIteration: 3,
language: 'ja', language: 'ja',
}); });
const result = buildInstruction(step, context); 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', () => { it('should include workflow structure when workflowSteps is provided', () => {
@ -509,21 +385,21 @@ describe('instruction-builder', () => {
step.name = 'implement'; step.name = 'implement';
const context = createMinimalContext({ const context = createMinimalContext({
language: 'en', language: 'en',
workflowSteps: [ workflowMovements: [
{ name: 'plan' }, { name: 'plan' },
{ name: 'implement' }, { name: 'implement' },
{ name: 'review' }, { name: 'review' },
], ],
currentStepIndex: 1, currentMovementIndex: 1,
}); });
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('This workflow consists of 3 steps:'); expect(result).toContain('This workflow consists of 3 movements:');
expect(result).toContain('- Step 1: plan'); expect(result).toContain('- Movement 1: plan');
expect(result).toContain('- Step 2: implement'); expect(result).toContain('- Movement 2: implement');
expect(result).toContain('← current'); expect(result).toContain('← current');
expect(result).toContain('- Step 3: review'); expect(result).toContain('- Movement 3: review');
}); });
it('should mark current step with marker', () => { it('should mark current step with marker', () => {
@ -531,17 +407,17 @@ describe('instruction-builder', () => {
step.name = 'plan'; step.name = 'plan';
const context = createMinimalContext({ const context = createMinimalContext({
language: 'en', language: 'en',
workflowSteps: [ workflowMovements: [
{ name: 'plan' }, { name: 'plan' },
{ name: 'implement' }, { name: 'implement' },
], ],
currentStepIndex: 0, currentMovementIndex: 0,
}); });
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('- Step 1: plan ← current'); expect(result).toContain('- Movement 1: plan ← current');
expect(result).not.toContain('- Step 2: implement ← current'); expect(result).not.toContain('- Movement 2: implement ← current');
}); });
it('should include description in parentheses when provided', () => { it('should include description in parentheses when provided', () => {
@ -549,16 +425,16 @@ describe('instruction-builder', () => {
step.name = 'plan'; step.name = 'plan';
const context = createMinimalContext({ const context = createMinimalContext({
language: 'ja', language: 'ja',
workflowSteps: [ workflowMovements: [
{ name: 'plan', description: 'タスクを分析し実装計画を作成する' }, { name: 'plan', description: 'タスクを分析し実装計画を作成する' },
{ name: 'implement' }, { name: 'implement' },
], ],
currentStepIndex: 0, currentMovementIndex: 0,
}); });
const result = buildInstruction(step, context); 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', () => { it('should skip workflow structure when workflowSteps is not provided', () => {
@ -574,8 +450,8 @@ describe('instruction-builder', () => {
const step = createMinimalStep('Do work'); const step = createMinimalStep('Do work');
const context = createMinimalContext({ const context = createMinimalContext({
language: 'en', language: 'en',
workflowSteps: [], workflowMovements: [],
currentStepIndex: -1, currentMovementIndex: -1,
}); });
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
@ -588,34 +464,34 @@ describe('instruction-builder', () => {
step.name = 'plan'; step.name = 'plan';
const context = createMinimalContext({ const context = createMinimalContext({
language: 'ja', language: 'ja',
workflowSteps: [ workflowMovements: [
{ name: 'plan' }, { name: 'plan' },
{ name: 'implement' }, { name: 'implement' },
], ],
currentStepIndex: 0, currentMovementIndex: 0,
}); });
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('このワークフローは2ステップで構成されています:'); expect(result).toContain('このワークフローは2ムーブメントで構成されています:');
expect(result).toContain('← 現在'); 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'); const step = createMinimalStep('Do work');
step.name = 'sub-step'; step.name = 'sub-step';
const context = createMinimalContext({ const context = createMinimalContext({
language: 'en', language: 'en',
workflowSteps: [ workflowMovements: [
{ name: 'plan' }, { name: 'plan' },
{ name: 'implement' }, { name: 'implement' },
], ],
currentStepIndex: -1, currentMovementIndex: -1,
}); });
const result = buildInstruction(step, context); 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'); expect(result).not.toContain('← current');
}); });
}); });
@ -689,7 +565,7 @@ describe('instruction-builder', () => {
return { return {
cwd: '/project', cwd: '/project',
reportDir: '/project/.takt/reports/20260129-test', reportDir: '/project/.takt/reports/20260129-test',
stepIteration: 1, movementIteration: 1,
language: 'en', language: 'en',
...overrides, ...overrides,
}; };
@ -803,10 +679,10 @@ describe('instruction-builder', () => {
expect(result).toContain('# Plan'); 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'); const step = createMinimalStep('Do work');
step.report = '00-plan.md'; step.report = '00-plan.md';
const ctx = createReportContext({ stepIteration: 5 }); const ctx = createReportContext({ movementIteration: 5 });
const result = buildReportInstruction(step, ctx); const result = buildReportInstruction(step, ctx);
@ -993,9 +869,9 @@ describe('instruction-builder', () => {
expect(result).toContain('Step 3/20'); expect(result).toContain('Step 3/20');
}); });
it('should replace {step_iteration}', () => { it('should replace {movement_iteration}', () => {
const step = createMinimalStep('Run #{step_iteration}'); const step = createMinimalStep('Run #{movement_iteration}');
const context = createMinimalContext({ stepIteration: 2 }); const context = createMinimalContext({ movementIteration: 2 });
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
@ -1018,7 +894,7 @@ describe('instruction-builder', () => {
expect(result).not.toContain('[TEST-STEP:'); 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'); const step = createMinimalStep('Do work');
step.name = 'review'; step.name = 'review';
step.rules = [ step.rules = [
@ -1029,11 +905,11 @@ describe('instruction-builder', () => {
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('Decision Criteria'); // Status rules are no longer injected in Phase 1 (only in Phase 3)
expect(result).toContain('[REVIEW:1]'); 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'); const step = createMinimalStep('Do work');
step.name = 'plan'; step.name = 'plan';
step.rules = [ step.rules = [
@ -1044,9 +920,8 @@ describe('instruction-builder', () => {
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('Decision Criteria'); // Status rules are no longer injected in Phase 1 (only in Phase 3)
expect(result).toContain('[PLAN:1]'); expect(result).not.toContain('Decision Criteria');
expect(result).toContain('[PLAN:2]');
}); });
it('should NOT include status rules when all rules are aggregate conditions', () => { 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'); 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'); const step = createMinimalStep('Do work');
step.name = 'supervise'; step.name = 'supervise';
step.rules = [ step.rules = [
@ -1087,8 +962,8 @@ describe('instruction-builder', () => {
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain('Decision Criteria'); // Status rules are no longer injected in Phase 1 (only in Phase 3)
expect(result).toContain('[SUPERVISE:1]'); expect(result).not.toContain('Decision Criteria');
}); });
}); });

View File

@ -2,7 +2,7 @@
* Error recovery integration tests. * Error recovery integration tests.
* *
* Tests agent error, blocked responses, max iteration limits, * 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 * Mocked: UI, session, phase-runner, notifications, config, callAiJudge
* Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator
@ -59,7 +59,7 @@ function makeRule(condition: string, next: string): WorkflowRule {
return { condition, next }; return { condition, next };
} }
function makeStep(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep { function makeMovement(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep {
return { return {
name, name,
agent: `./agents/${name}.md`, agent: `./agents/${name}.md`,
@ -78,7 +78,7 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
const agentsDir = join(dir, 'agents'); const agentsDir = join(dir, 'agents');
mkdirSync(agentsDir, { recursive: true }); 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 agents = ['plan', 'implement', 'review', 'supervisor'];
const agentPaths: Record<string, string> = {}; const agentPaths: Record<string, string> = {};
for (const agent of agents) { for (const agent of agents) {
@ -103,17 +103,17 @@ function buildWorkflow(agentPaths: Record<string, string>, maxIterations: number
name: 'it-error', name: 'it-error',
description: 'IT error recovery workflow', description: 'IT error recovery workflow',
maxIterations, maxIterations,
initialStep: 'plan', initialMovement: 'plan',
steps: [ movements: [
makeStep('plan', agentPaths.plan, [ makeMovement('plan', agentPaths.plan, [
makeRule('Requirements are clear', 'implement'), makeRule('Requirements are clear', 'implement'),
makeRule('Requirements unclear', 'ABORT'), makeRule('Requirements unclear', 'ABORT'),
]), ]),
makeStep('implement', agentPaths.implement, [ makeMovement('implement', agentPaths.implement, [
makeRule('Implementation complete', 'review'), makeRule('Implementation complete', 'review'),
makeRule('Cannot proceed', 'plan'), makeRule('Cannot proceed', 'plan'),
]), ]),
makeStep('review', agentPaths.review, [ makeMovement('review', agentPaths.review, [
makeRule('All checks passed', 'COMPLETE'), makeRule('All checks passed', 'COMPLETE'),
makeRule('Issues found', 'implement'), 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 () => { 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([ setMockScenario([
{ agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' },
{ agent: 'implement', status: 'done', content: '[IMPLEMENT:1]\n\nDone.' }, { 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 () => { 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([ setMockScenario([
{ agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, { 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 testDir: string;
let agentPaths: Record<string, string>; let agentPaths: Record<string, string>;
@ -304,7 +304,7 @@ describe('Error Recovery IT: step events on error paths', () => {
expect(abortReason).toBeDefined(); 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([ setMockScenario([
{ agent: 'plan', status: 'done', content: '[PLAN:2]\n\nRequirements unclear.' }, { 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 startedSteps: string[] = [];
const completedSteps: string[] = []; const completedSteps: string[] = [];
engine.on('step:start', (step) => { engine.on('movement:start', (step) => {
startedSteps.push(step.name); startedSteps.push(step.name);
}); });
engine.on('step:complete', (step) => { engine.on('movement:complete', (step) => {
completedSteps.push(step.name); completedSteps.push(step.name);
}); });
@ -362,15 +362,15 @@ describe('Error Recovery IT: programmatic abort', () => {
provider: 'mock', provider: 'mock',
}); });
// Abort after the first step completes // Abort after the first movement completes
engine.on('step:complete', () => { engine.on('movement:complete', () => {
engine.abort(); engine.abort();
}); });
const state = await engine.run(); const state = await engine.run();
expect(state.status).toBe('aborted'); expect(state.status).toBe('aborted');
// Should have aborted after 1 step // Should have aborted after 1 movement
expect(state.iteration).toBeLessThanOrEqual(2); expect(state.iteration).toBeLessThanOrEqual(2);
}); });
}); });

View File

@ -2,13 +2,13 @@
* Instruction builder integration tests. * Instruction builder integration tests.
* *
* Tests template variable expansion and auto-injection in buildInstruction(). * 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 * Not mocked: buildInstruction, buildReportInstruction, buildStatusJudgmentInstruction
*/ */
import { describe, it, expect, vi } from 'vitest'; 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', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
@ -22,14 +22,14 @@ import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../core/workf
import type { InstructionContext } from '../core/workflow/index.js'; import type { InstructionContext } from '../core/workflow/index.js';
// Function wrappers for test readability // Function wrappers for test readability
function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string { function buildInstruction(movement: WorkflowMovement, ctx: InstructionContext): string {
return new InstructionBuilder(step, ctx).build(); return new InstructionBuilder(movement, ctx).build();
} }
function buildReportInstruction(step: WorkflowStep, ctx: ReportInstructionContext): string { function buildReportInstruction(movement: WorkflowMovement, ctx: ReportInstructionContext): string {
return new ReportInstructionBuilder(step, ctx).build(); return new ReportInstructionBuilder(movement, ctx).build();
} }
function buildStatusJudgmentInstruction(step: WorkflowStep, ctx: StatusJudgmentContext): string { function buildStatusJudgmentInstruction(movement: WorkflowMovement, ctx: StatusJudgmentContext): string {
return new StatusJudgmentBuilder(step, ctx).build(); return new StatusJudgmentBuilder(movement, ctx).build();
} }
// --- Test helpers --- // --- Test helpers ---
@ -38,7 +38,7 @@ function makeRule(condition: string, next: string, extra?: Partial<WorkflowRule>
return { condition, next, ...extra }; return { condition, next, ...extra };
} }
function makeStep(overrides: Partial<WorkflowStep> = {}): WorkflowStep { function makeMovement(overrides: Partial<WorkflowMovement> = {}): WorkflowMovement {
return { return {
name: 'test-step', name: 'test-step',
agent: 'test-agent', agent: 'test-agent',
@ -58,7 +58,7 @@ function makeContext(overrides: Partial<InstructionContext> = {}): InstructionCo
task: 'Test task description', task: 'Test task description',
iteration: 3, iteration: 3,
maxIterations: 30, maxIterations: 30,
stepIteration: 2, movementIteration: 2,
cwd: '/tmp/test-project', cwd: '/tmp/test-project',
projectCwd: '/tmp/test-project', projectCwd: '/tmp/test-project',
userInputs: [], userInputs: [],
@ -69,7 +69,7 @@ function makeContext(overrides: Partial<InstructionContext> = {}): InstructionCo
describe('Instruction Builder IT: task auto-injection', () => { describe('Instruction Builder IT: task auto-injection', () => {
it('should auto-inject task as "User Request" section when template has no {task}', () => { 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 ctx = makeContext({ task: 'Build the login page' });
const result = buildInstruction(step, ctx); 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}', () => { 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 ctx = makeContext({ task: 'Build the login page' });
const result = buildInstruction(step, ctx); const result = buildInstruction(step, ctx);
@ -94,7 +94,7 @@ describe('Instruction Builder IT: task auto-injection', () => {
describe('Instruction Builder IT: previous_response auto-injection', () => { describe('Instruction Builder IT: previous_response auto-injection', () => {
it('should auto-inject previous response when passPreviousResponse is true', () => { it('should auto-inject previous response when passPreviousResponse is true', () => {
const step = makeStep({ const step = makeMovement({
passPreviousResponse: true, passPreviousResponse: true,
instructionTemplate: 'Continue the work.', 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', () => { it('should NOT inject previous response when passPreviousResponse is false', () => {
const step = makeStep({ const step = makeMovement({
passPreviousResponse: false, passPreviousResponse: false,
instructionTemplate: 'Do fresh work.', 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}', () => { it('should NOT auto-inject when template contains {previous_response}', () => {
const step = makeStep({ const step = makeMovement({
passPreviousResponse: true, passPreviousResponse: true,
instructionTemplate: '## Context\n{previous_response}\n\nDo work.', 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', () => { describe('Instruction Builder IT: user_inputs auto-injection', () => {
it('should auto-inject user inputs section', () => { it('should auto-inject user inputs section', () => {
const step = makeStep(); const step = makeMovement();
const ctx = makeContext({ userInputs: ['Fix the typo', 'Use TypeScript'] }); const ctx = makeContext({ userInputs: ['Fix the typo', 'Use TypeScript'] });
const result = buildInstruction(step, ctx); 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}', () => { 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 ctx = makeContext({ userInputs: ['Input A'] });
const result = buildInstruction(step, ctx); const result = buildInstruction(step, ctx);
@ -176,31 +176,31 @@ describe('Instruction Builder IT: user_inputs auto-injection', () => {
}); });
describe('Instruction Builder IT: iteration variables', () => { describe('Instruction Builder IT: iteration variables', () => {
it('should replace {iteration}, {max_iterations}, {step_iteration} in template', () => { it('should replace {iteration}, {max_iterations}, {movement_iteration} in template', () => {
const step = makeStep({ const step = makeMovement({
instructionTemplate: 'Iter: {iteration}/{max_iterations}, step iter: {step_iteration}', 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); 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', () => { it('should include iteration in Workflow Context section', () => {
const step = makeStep(); const step = makeMovement();
const ctx = makeContext({ iteration: 7, maxIterations: 20, stepIteration: 3 }); const ctx = makeContext({ iteration: 7, maxIterations: 20, movementIteration: 3 });
const result = buildInstruction(step, ctx); const result = buildInstruction(step, ctx);
expect(result).toContain('Iteration: 7/20'); 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', () => { describe('Instruction Builder IT: report_dir expansion', () => {
it('should replace {report_dir} in template', () => { it('should replace {report_dir} in template', () => {
const step = makeStep({ const step = makeMovement({
instructionTemplate: 'Read the plan from {report_dir}/00-plan.md', instructionTemplate: 'Read the plan from {report_dir}/00-plan.md',
}); });
const ctx = makeContext({ reportDir: '/tmp/test-project/.takt/reports/20250126-task' }); 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', () => { it('should replace {report:filename} with full path', () => {
const step = makeStep({ const step = makeMovement({
instructionTemplate: 'Read {report:00-plan.md} for the plan.', instructionTemplate: 'Read {report:00-plan.md} for the plan.',
}); });
const ctx = makeContext({ reportDir: '/tmp/reports' }); 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', () => { describe('Instruction Builder IT: status output rules injection', () => {
it('should inject status rules for steps with tag-based rules', () => { it('should inject status rules for steps with tag-based rules', () => {
const step = makeStep({ const step = makeMovement({
name: 'plan', name: 'plan',
rules: [ rules: [
makeRule('Requirements clear', 'implement'), makeRule('Requirements clear', 'implement'),
@ -235,14 +235,13 @@ describe('Instruction Builder IT: status output rules injection', () => {
const result = buildInstruction(step, ctx); const result = buildInstruction(step, ctx);
// Should contain status rules section with the tag format // Status rules are NO LONGER injected in Phase 1 (buildInstruction).
expect(result).toContain('[PLAN:'); // They are only injected in Phase 3 (buildStatusJudgmentInstruction).
expect(result).toContain('Requirements clear'); expect(result).not.toContain('[PLAN:');
expect(result).toContain('Requirements unclear');
}); });
it('should NOT inject status rules for steps with only ai() conditions', () => { it('should NOT inject status rules for steps with only ai() conditions', () => {
const step = makeStep({ const step = makeMovement({
name: 'review', name: 'review',
rules: [ rules: [
makeRule('ai("approved")', 'COMPLETE', { isAiCondition: true, aiConditionText: 'approved' }), 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', () => { describe('Instruction Builder IT: edit permission in execution context', () => {
it('should include edit permission rules when edit is true', () => { it('should include edit permission rules when edit is true', () => {
const step = makeStep({ edit: true }); const step = makeMovement({ edit: true });
const ctx = makeContext(); const ctx = makeContext();
const result = buildInstruction(step, ctx); 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', () => { it('should indicate read-only when edit is false', () => {
const step = makeStep({ edit: false }); const step = makeMovement({ edit: false });
const ctx = makeContext(); const ctx = makeContext();
const result = buildInstruction(step, ctx); const result = buildInstruction(step, ctx);
@ -283,15 +282,15 @@ describe('Instruction Builder IT: edit permission in execution context', () => {
describe('Instruction Builder IT: buildReportInstruction', () => { describe('Instruction Builder IT: buildReportInstruction', () => {
it('should build report instruction with report context', () => { it('should build report instruction with report context', () => {
const step = makeStep({ const step = makeMovement({
name: 'plan', 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, { const result = buildReportInstruction(step, {
cwd: '/tmp/test', cwd: '/tmp/test',
reportDir: '/tmp/test/.takt/reports/test-dir', reportDir: '/tmp/test/.takt/reports/test-dir',
stepIteration: 1, movementIteration: 1,
language: 'en', language: 'en',
}); });
@ -301,13 +300,13 @@ describe('Instruction Builder IT: buildReportInstruction', () => {
}); });
it('should throw for step without report config', () => { it('should throw for step without report config', () => {
const step = makeStep({ report: undefined }); const step = makeMovement({ report: undefined });
expect(() => expect(() =>
buildReportInstruction(step, { buildReportInstruction(step, {
cwd: '/tmp', cwd: '/tmp',
reportDir: '/tmp/reports', reportDir: '/tmp/reports',
stepIteration: 1, movementIteration: 1,
}), }),
).toThrow(/no report config/); ).toThrow(/no report config/);
}); });
@ -315,7 +314,7 @@ describe('Instruction Builder IT: buildReportInstruction', () => {
describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => { describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => {
it('should build Phase 3 instruction with status rules', () => { it('should build Phase 3 instruction with status rules', () => {
const step = makeStep({ const step = makeMovement({
name: 'plan', name: 'plan',
rules: [ rules: [
makeRule('Clear', 'implement'), makeRule('Clear', 'implement'),
@ -331,7 +330,7 @@ describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => {
}); });
it('should throw for step without rules', () => { it('should throw for step without rules', () => {
const step = makeStep({ rules: undefined }); const step = makeMovement({ rules: undefined });
expect(() => expect(() =>
buildStatusJudgmentInstruction(step, { language: 'en' }), buildStatusJudgmentInstruction(step, { language: 'en' }),
@ -341,7 +340,7 @@ describe('Instruction Builder IT: buildStatusJudgmentInstruction', () => {
describe('Instruction Builder IT: template injection prevention', () => { describe('Instruction Builder IT: template injection prevention', () => {
it('should escape curly braces in task content', () => { it('should escape curly braces in task content', () => {
const step = makeStep(); const step = makeMovement();
const ctx = makeContext({ task: 'Use {variable} in code' }); const ctx = makeContext({ task: 'Use {variable} in code' });
const result = buildInstruction(step, ctx); 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', () => { it('should escape curly braces in previous response content', () => {
const step = makeStep({ const step = makeMovement({
passPreviousResponse: true, passPreviousResponse: true,
instructionTemplate: 'Continue.', instructionTemplate: 'Continue.',
}); });

View File

@ -172,9 +172,9 @@ function createTestWorkflowDir(): { dir: string; workflowPath: string } {
name: it-pipeline name: it-pipeline
description: Pipeline test workflow description: Pipeline test workflow
max_iterations: 10 max_iterations: 10
initial_step: plan initial_movement: plan
steps: movements:
- name: plan - name: plan
agent: ./agents/planner.md agent: ./agents/planner.md
rules: rules:

View File

@ -154,9 +154,9 @@ function createTestWorkflowDir(): { dir: string; workflowPath: string } {
name: it-simple name: it-simple
description: Integration test workflow description: Integration test workflow
max_iterations: 10 max_iterations: 10
initial_step: plan initial_movement: plan
steps: movements:
- name: plan - name: plan
agent: ./agents/planner.md agent: ./agents/planner.md
rules: rules:
@ -209,7 +209,7 @@ describe('Pipeline Integration Tests', () => {
it('should complete pipeline with workflow path + skip-git + mock scenario', async () => { it('should complete pipeline with workflow path + skip-git + mock scenario', async () => {
// Scenario: plan -> implement -> review -> COMPLETE // 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([ setMockScenario([
{ agent: 'planner', status: 'done', content: '[PLAN:1]\n\nPlan completed. Requirements are clear.' }, { agent: 'planner', status: 'done', content: '[PLAN:1]\n\nPlan completed. Requirements are clear.' },
{ agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' }, { 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 () => { it('should complete pipeline with workflow name + skip-git + mock scenario', async () => {
// Use builtin 'minimal' workflow // Use builtin 'minimal' workflow
// agent field: extractAgentName result (from .md filename) // agent field: extractAgentName result (from .md filename)
// tag in content: [STEP_NAME:N] where STEP_NAME is the step name uppercased // tag in content: [MOVEMENT_NAME:N] where MOVEMENT_NAME is the movement name uppercased
setMockScenario([ setMockScenario([
{ agent: 'coder', status: 'done', content: 'Implementation complete' }, { agent: 'coder', status: 'done', content: 'Implementation complete' },
{ agent: 'ai-antipattern-reviewer', status: 'done', content: 'No AI-specific issues' }, { agent: 'ai-antipattern-reviewer', status: 'done', content: 'No AI-specific issues' },

View File

@ -15,7 +15,7 @@
*/ */
import { describe, it, expect, beforeEach, vi } from 'vitest'; 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 --- // --- Mocks ---
@ -43,11 +43,11 @@ function makeRule(condition: string, next: string, extra?: Partial<WorkflowRule>
return { condition, next, ...extra }; return { condition, next, ...extra };
} }
function makeStep( function makeMovement(
name: string, name: string,
rules: WorkflowRule[], rules: WorkflowRule[],
parallel?: WorkflowStep[], parallel?: WorkflowMovement[],
): WorkflowStep { ): WorkflowMovement {
return { return {
name, name,
agent: 'test-agent', agent: 'test-agent',
@ -59,22 +59,22 @@ function makeStep(
}; };
} }
function makeState(stepOutputs?: Map<string, AgentResponse>): WorkflowState { function makeState(movementOutputs?: Map<string, AgentResponse>): WorkflowState {
return { return {
workflowName: 'it-test', workflowName: 'it-test',
currentStep: 'test', currentMovement: 'test',
iteration: 1, iteration: 1,
status: 'running', status: 'running',
stepOutputs: stepOutputs ?? new Map(), movementOutputs: movementOutputs ?? new Map(),
stepIterations: new Map(), movementIterations: new Map(),
agentSessions: new Map(), agentSessions: new Map(),
userInputs: [], userInputs: [],
}; };
} }
function makeCtx(stepOutputs?: Map<string, AgentResponse>): RuleEvaluatorContext { function makeCtx(movementOutputs?: Map<string, AgentResponse>): RuleEvaluatorContext {
return { return {
state: makeState(stepOutputs), state: makeState(movementOutputs),
cwd: '/tmp/test', cwd: '/tmp/test',
detectRuleIndex, detectRuleIndex,
callAiJudge: mockCallAiJudge, callAiJudge: mockCallAiJudge,
@ -88,7 +88,7 @@ describe('Rule Evaluation IT: Phase 3 tag detection', () => {
}); });
it('should detect rule from Phase 3 tag content', async () => { it('should detect rule from Phase 3 tag content', async () => {
const step = makeStep('plan', [ const step = makeMovement('plan', [
makeRule('Clear', 'implement'), makeRule('Clear', 'implement'),
makeRule('Unclear', 'ABORT'), 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 () => { it('should prefer Phase 3 tag over Phase 1 tag', async () => {
const step = makeStep('plan', [ const step = makeMovement('plan', [
makeRule('Clear', 'implement'), makeRule('Clear', 'implement'),
makeRule('Unclear', 'ABORT'), 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 () => { 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('Clear', 'implement'),
makeRule('Unclear', 'ABORT'), 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 () => { it('should detect last tag when multiple tags in Phase 1', async () => {
const step = makeStep('plan', [ const step = makeMovement('plan', [
makeRule('Clear', 'implement'), makeRule('Clear', 'implement'),
makeRule('Unclear', 'ABORT'), makeRule('Unclear', 'ABORT'),
]); ]);
@ -147,17 +147,17 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => {
mockCallAiJudge.mockResolvedValue(-1); mockCallAiJudge.mockResolvedValue(-1);
}); });
it('should match all("approved") when all sub-steps have "approved"', () => { it('should match all("approved") when all sub-movements have "approved"', () => {
const subStep1 = makeStep('arch-review', [ const subStep1 = makeMovement('arch-review', [
makeRule('approved', ''), makeRule('approved', ''),
makeRule('needs_fix', ''), makeRule('needs_fix', ''),
]); ]);
const subStep2 = makeStep('security-review', [ const subStep2 = makeMovement('security-review', [
makeRule('approved', ''), makeRule('approved', ''),
makeRule('needs_fix', ''), makeRule('needs_fix', ''),
]); ]);
const parentStep = makeStep('reviewers', [ const parentStep = makeMovement('reviewers', [
makeRule('all("approved")', 'supervise', { makeRule('all("approved")', 'supervise', {
isAggregateCondition: true, isAggregateCondition: true,
aggregateType: 'all', aggregateType: 'all',
@ -185,17 +185,17 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => {
expect(result).toBe(0); // all("approved") is rule index 0 expect(result).toBe(0); // all("approved") is rule index 0
}); });
it('should match any("needs_fix") when one sub-step has "needs_fix"', () => { it('should match any("needs_fix") when one sub-movement has "needs_fix"', () => {
const subStep1 = makeStep('arch-review', [ const subStep1 = makeMovement('arch-review', [
makeRule('approved', ''), makeRule('approved', ''),
makeRule('needs_fix', ''), makeRule('needs_fix', ''),
]); ]);
const subStep2 = makeStep('security-review', [ const subStep2 = makeMovement('security-review', [
makeRule('approved', ''), makeRule('approved', ''),
makeRule('needs_fix', ''), makeRule('needs_fix', ''),
]); ]);
const parentStep = makeStep('reviewers', [ const parentStep = makeMovement('reviewers', [
makeRule('all("approved")', 'supervise', { makeRule('all("approved")', 'supervise', {
isAggregateCondition: true, isAggregateCondition: true,
aggregateType: 'all', aggregateType: 'all',
@ -224,16 +224,16 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => {
}); });
it('should return -1 when no aggregate condition matches', () => { it('should return -1 when no aggregate condition matches', () => {
const subStep1 = makeStep('review-a', [ const subStep1 = makeMovement('review-a', [
makeRule('approved', ''), makeRule('approved', ''),
makeRule('needs_fix', ''), makeRule('needs_fix', ''),
]); ]);
const subStep2 = makeStep('review-b', [ const subStep2 = makeMovement('review-b', [
makeRule('approved', ''), makeRule('approved', ''),
makeRule('needs_fix', ''), makeRule('needs_fix', ''),
]); ]);
const parentStep = makeStep('reviews', [ const parentStep = makeMovement('reviews', [
makeRule('all("approved")', 'done', { makeRule('all("approved")', 'done', {
isAggregateCondition: true, isAggregateCondition: true,
aggregateType: 'all', aggregateType: 'all',
@ -256,8 +256,8 @@ describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => {
expect(result).toBe(-1); expect(result).toBe(-1);
}); });
it('should return -1 for non-parallel step', () => { it('should return -1 for non-parallel movement', () => {
const step = makeStep('step', [ const step = makeMovement('step', [
makeRule('all("done")', 'COMPLETE', { makeRule('all("done")', 'COMPLETE', {
isAggregateCondition: true, isAggregateCondition: true,
aggregateType: 'all', 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 () => { it('should call AI judge for ai() conditions when no tag match', async () => {
mockCallAiJudge.mockResolvedValue(0); // Judge says first ai() condition matches mockCallAiJudge.mockResolvedValue(0); // Judge says first ai() condition matches
const step = makeStep('step', [ const step = makeMovement('step', [
makeRule('ai("The code is approved")', 'COMPLETE', { makeRule('ai("The code is approved")', 'COMPLETE', {
isAiCondition: true, isAiCondition: true,
aiConditionText: 'The code is approved', 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 () => { it('should skip AI judge if tag already matched', async () => {
const step = makeStep('plan', [ const step = makeMovement('plan', [
makeRule('ai("Clear")', 'implement', { makeRule('ai("Clear")', 'implement', {
isAiCondition: true, isAiCondition: true,
aiConditionText: 'Clear', aiConditionText: 'Clear',
@ -321,7 +321,7 @@ describe('Rule Evaluation IT: AI judge fallback', () => {
// Second call (all conditions fallback): returns 0 // Second call (all conditions fallback): returns 0
mockCallAiJudge.mockResolvedValue(0); mockCallAiJudge.mockResolvedValue(0);
const step = makeStep('review', [ const step = makeMovement('review', [
makeRule('Approved', 'COMPLETE'), makeRule('Approved', 'COMPLETE'),
makeRule('Rejected', 'fix'), 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 () => { it('should throw when no rule matches (AI judge returns -1 for all phases)', async () => {
mockCallAiJudge.mockResolvedValue(-1); mockCallAiJudge.mockResolvedValue(-1);
const step = makeStep('review', [ const step = makeMovement('review', [
makeRule('Approved', 'COMPLETE'), makeRule('Approved', 'COMPLETE'),
makeRule('Rejected', 'fix'), makeRule('Rejected', 'fix'),
]); ]);
@ -353,8 +353,8 @@ describe('Rule Evaluation IT: RuleMatchMethod tracking', () => {
}); });
it('should record method as "aggregate" for aggregate matches', () => { it('should record method as "aggregate" for aggregate matches', () => {
const subStep = makeStep('sub', [makeRule('ok', '')]); const subStep = makeMovement('sub', [makeRule('ok', '')]);
const parentStep = makeStep('parent', [ const parentStep = makeMovement('parent', [
makeRule('all("ok")', 'COMPLETE', { makeRule('all("ok")', 'COMPLETE', {
isAggregateCondition: true, isAggregateCondition: true,
aggregateType: 'all', aggregateType: 'all',
@ -374,7 +374,7 @@ describe('Rule Evaluation IT: RuleMatchMethod tracking', () => {
}); });
it('should record method as "phase3_tag" for Phase 3 matches', async () => { it('should record method as "phase3_tag" for Phase 3 matches', async () => {
const step = makeStep('step', [ const step = makeMovement('step', [
makeRule('Done', 'COMPLETE'), 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 () => { it('should record method as "phase1_tag" for Phase 1 fallback matches', async () => {
const step = makeStep('step', [ const step = makeMovement('step', [
makeRule('Done', 'COMPLETE'), 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(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it('should return undefined for step with no rules', async () => { it('should return undefined for movement with no rules', async () => {
const step: WorkflowStep = { const step: WorkflowMovement = {
name: 'step', name: 'step',
agent: 'agent', agent: 'agent',
agentDisplayName: 'step', agentDisplayName: 'step',
@ -410,8 +410,8 @@ describe('Rule Evaluation IT: steps without rules', () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('should return undefined for step with empty rules array', async () => { it('should return undefined for movement with empty rules array', async () => {
const step = makeStep('step', []); const step = makeMovement('step', []);
const result = await detectMatchedRule(step, 'content', '', makeCtx()); const result = await detectMatchedRule(step, 'content', '', makeCtx());
expect(result).toBeUndefined(); expect(result).toBeUndefined();

View File

@ -2,7 +2,7 @@
* Three-phase execution integration tests. * Three-phase execution integration tests.
* *
* Tests Phase 1 (main) Phase 2 (report) Phase 3 (status judgment) lifecycle. * 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 * Mocked: UI, session, config, callAiJudge
* Selectively mocked: phase-runner (to inspect call patterns) * Selectively mocked: phase-runner (to inspect call patterns)
@ -84,7 +84,7 @@ function buildEngineOptions(projectCwd: string) {
}; };
} }
function makeStep( function makeMovement(
name: string, name: string,
agentPath: string, agentPath: string,
rules: WorkflowRule[], rules: WorkflowRule[],
@ -124,7 +124,7 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () =
rmSync(testDir, { recursive: true, force: true }); 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([ setMockScenario([
{ status: 'done', content: '[STEP:1]\n\nDone.' }, { 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', name: 'it-phase1-only',
description: 'Test', description: 'Test',
maxIterations: 5, maxIterations: 5,
initialStep: 'step', initialMovement: 'step',
steps: [ movements: [
makeStep('step', agentPath, [ makeMovement('step', agentPath, [
makeRule('Done', 'COMPLETE'), makeRule('Done', 'COMPLETE'),
makeRule('Not done', 'ABORT'), makeRule('Not done', 'ABORT'),
]), ]),
@ -176,7 +176,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
rmSync(testDir, { recursive: true, force: true }); 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([ setMockScenario([
{ status: 'done', content: '[STEP:1]\n\nDone.' }, { status: 'done', content: '[STEP:1]\n\nDone.' },
]); ]);
@ -185,9 +185,9 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
name: 'it-phase1-2', name: 'it-phase1-2',
description: 'Test', description: 'Test',
maxIterations: 5, maxIterations: 5,
initialStep: 'step', initialMovement: 'step',
steps: [ movements: [
makeStep('step', agentPath, [ makeMovement('step', agentPath, [
makeRule('Done', 'COMPLETE'), makeRule('Done', 'COMPLETE'),
makeRule('Not done', 'ABORT'), makeRule('Not done', 'ABORT'),
], { report: 'test-report.md' }), ], { report: 'test-report.md' }),
@ -206,7 +206,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
expect(mockRunStatusJudgmentPhase).not.toHaveBeenCalled(); 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([ setMockScenario([
{ status: 'done', content: '[STEP:1]\n\nDone.' }, { 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', name: 'it-phase1-2-multi',
description: 'Test', description: 'Test',
maxIterations: 5, maxIterations: 5,
initialStep: 'step', initialMovement: 'step',
steps: [ movements: [
makeStep('step', agentPath, [ makeMovement('step', agentPath, [
makeRule('Done', 'COMPLETE'), makeRule('Done', 'COMPLETE'),
], { report: [{ label: 'Scope', path: 'scope.md' }, { label: 'Decisions', path: 'decisions.md' }] }), ], { 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 }); 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([ setMockScenario([
// Phase 1: main content (no tag — Phase 3 will provide it) // Phase 1: main content (no tag — Phase 3 will provide it)
{ status: 'done', content: 'Agent completed the work.' }, { 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', name: 'it-phase1-3',
description: 'Test', description: 'Test',
maxIterations: 5, maxIterations: 5,
initialStep: 'step', initialMovement: 'step',
steps: [ movements: [
makeStep('step', agentPath, [ makeMovement('step', agentPath, [
makeRule('Done', 'COMPLETE'), makeRule('Done', 'COMPLETE'),
makeRule('Not done', 'ABORT'), makeRule('Not done', 'ABORT'),
]), ]),
@ -308,7 +308,7 @@ describe('Three-Phase Execution IT: all three phases', () => {
rmSync(testDir, { recursive: true, force: true }); 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([ setMockScenario([
{ status: 'done', content: 'Agent completed the work.' }, { status: 'done', content: 'Agent completed the work.' },
]); ]);
@ -317,9 +317,9 @@ describe('Three-Phase Execution IT: all three phases', () => {
name: 'it-all-phases', name: 'it-all-phases',
description: 'Test', description: 'Test',
maxIterations: 5, maxIterations: 5,
initialStep: 'step', initialMovement: 'step',
steps: [ movements: [
makeStep('step', agentPath, [ makeMovement('step', agentPath, [
makeRule('Done', 'COMPLETE'), makeRule('Done', 'COMPLETE'),
makeRule('Not done', 'ABORT'), makeRule('Not done', 'ABORT'),
], { report: 'test-report.md' }), ], { report: 'test-report.md' }),
@ -377,13 +377,13 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => {
name: 'it-phase3-tag', name: 'it-phase3-tag',
description: 'Test', description: 'Test',
maxIterations: 5, maxIterations: 5,
initialStep: 'step1', initialMovement: 'step1',
steps: [ movements: [
makeStep('step1', agentPath, [ makeMovement('step1', agentPath, [
makeRule('Done', 'step2'), makeRule('Done', 'step2'),
makeRule('Not done', 'ABORT'), makeRule('Not done', 'ABORT'),
]), ]),
makeStep('step2', agentPath, [ makeMovement('step2', agentPath, [
makeRule('Checked', 'COMPLETE'), makeRule('Checked', 'COMPLETE'),
]), ]),
], ],

View File

@ -3,7 +3,7 @@
* *
* Tests WorkflowEngine with real runAgent + MockProvider + ScenarioQueue. * Tests WorkflowEngine with real runAgent + MockProvider + ScenarioQueue.
* No vi.mock on runAgent or detectMatchedRule rules are matched via * 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 * Mocked: UI, session, phase-runner (report/judgment phases), notifications, config
* Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator
@ -62,7 +62,7 @@ function makeRule(condition: string, next: string): WorkflowRule {
return { condition, next }; return { condition, next };
} }
function makeStep(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep { function makeMovement(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowStep {
return { return {
name, name,
agent: `./agents/${name}.md`, agent: `./agents/${name}.md`,
@ -105,17 +105,17 @@ function buildSimpleWorkflow(agentPaths: Record<string, string>): WorkflowConfig
name: 'it-simple', name: 'it-simple',
description: 'IT simple workflow', description: 'IT simple workflow',
maxIterations: 15, maxIterations: 15,
initialStep: 'plan', initialMovement: 'plan',
steps: [ movements: [
makeStep('plan', agentPaths.planner, [ makeMovement('plan', agentPaths.planner, [
makeRule('Requirements are clear', 'implement'), makeRule('Requirements are clear', 'implement'),
makeRule('Requirements unclear', 'ABORT'), makeRule('Requirements unclear', 'ABORT'),
]), ]),
makeStep('implement', agentPaths.coder, [ makeMovement('implement', agentPaths.coder, [
makeRule('Implementation complete', 'review'), makeRule('Implementation complete', 'review'),
makeRule('Cannot proceed', 'plan'), makeRule('Cannot proceed', 'plan'),
]), ]),
makeStep('review', agentPaths.reviewer, [ makeMovement('review', agentPaths.reviewer, [
makeRule('All checks passed', 'COMPLETE'), makeRule('All checks passed', 'COMPLETE'),
makeRule('Issues found', 'implement'), makeRule('Issues found', 'implement'),
]), ]),
@ -128,25 +128,25 @@ function buildLoopWorkflow(agentPaths: Record<string, string>): WorkflowConfig {
name: 'it-loop', name: 'it-loop',
description: 'IT workflow with fix loop', description: 'IT workflow with fix loop',
maxIterations: 20, maxIterations: 20,
initialStep: 'plan', initialMovement: 'plan',
steps: [ movements: [
makeStep('plan', agentPaths.planner, [ makeMovement('plan', agentPaths.planner, [
makeRule('Requirements are clear', 'implement'), makeRule('Requirements are clear', 'implement'),
makeRule('Requirements unclear', 'ABORT'), makeRule('Requirements unclear', 'ABORT'),
]), ]),
makeStep('implement', agentPaths.coder, [ makeMovement('implement', agentPaths.coder, [
makeRule('Implementation complete', 'review'), makeRule('Implementation complete', 'review'),
makeRule('Cannot proceed', 'plan'), makeRule('Cannot proceed', 'plan'),
]), ]),
makeStep('review', agentPaths.reviewer, [ makeMovement('review', agentPaths.reviewer, [
makeRule('Approved', 'supervise'), makeRule('Approved', 'supervise'),
makeRule('Needs fix', 'fix'), makeRule('Needs fix', 'fix'),
]), ]),
makeStep('fix', agentPaths.fixer, [ makeMovement('fix', agentPaths.fixer, [
makeRule('Fix complete', 'review'), makeRule('Fix complete', 'review'),
makeRule('Cannot fix', 'ABORT'), makeRule('Cannot fix', 'ABORT'),
]), ]),
makeStep('supervise', agentPaths.supervisor, [ makeMovement('supervise', agentPaths.supervisor, [
makeRule('All checks passed', 'COMPLETE'), makeRule('All checks passed', 'COMPLETE'),
makeRule('Requirements unmet', 'plan'), 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 testDir: string;
let agentPaths: Record<string, string>; let agentPaths: Record<string, string>;
@ -324,7 +324,7 @@ describe('Workflow Engine IT: Step Output Tracking', () => {
rmSync(testDir, { recursive: true, force: true }); rmSync(testDir, { recursive: true, force: true });
}); });
it('should track step outputs through events', async () => { it('should track movement outputs through events', async () => {
setMockScenario([ setMockScenario([
{ agent: 'plan', status: 'done', content: '[PLAN:1]\n\nPlan output.' }, { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nPlan output.' },
{ agent: 'implement', status: 'done', content: '[IMPLEMENT:1]\n\nImplement output.' }, { agent: 'implement', status: 'done', content: '[IMPLEMENT:1]\n\nImplement output.' },
@ -337,14 +337,14 @@ describe('Workflow Engine IT: Step Output Tracking', () => {
provider: 'mock', provider: 'mock',
}); });
const completedSteps: string[] = []; const completedMovements: string[] = [];
engine.on('step:complete', (step) => { engine.on('movement:complete', (movement) => {
completedSteps.push(step.name); completedMovements.push(movement.name);
}); });
const state = await engine.run(); const state = await engine.run();
expect(state.status).toBe('completed'); expect(state.status).toBe('completed');
expect(completedSteps).toEqual(['plan', 'implement', 'review']); expect(completedMovements).toEqual(['plan', 'implement', 'review']);
}); });
}); });

View File

@ -53,8 +53,8 @@ describe('Workflow Loader IT: builtin workflow loading', () => {
expect(config).not.toBeNull(); expect(config).not.toBeNull();
expect(config!.name).toBe(name); expect(config!.name).toBe(name);
expect(config!.steps.length).toBeGreaterThan(0); expect(config!.movements.length).toBeGreaterThan(0);
expect(config!.initialStep).toBeDefined(); expect(config!.initialMovement).toBeDefined();
expect(config!.maxIterations).toBeGreaterThan(0); expect(config!.maxIterations).toBeGreaterThan(0);
}); });
} }
@ -88,9 +88,9 @@ describe('Workflow Loader IT: project-local workflow override', () => {
name: custom-wf name: custom-wf
description: Custom project workflow description: Custom project workflow
max_iterations: 5 max_iterations: 5
initial_step: start initial_movement: start
steps: movements:
- name: start - name: start
agent: ./agents/custom.md agent: ./agents/custom.md
rules: rules:
@ -103,8 +103,8 @@ steps:
expect(config).not.toBeNull(); expect(config).not.toBeNull();
expect(config!.name).toBe('custom-wf'); expect(config!.name).toBe('custom-wf');
expect(config!.steps.length).toBe(1); expect(config!.movements.length).toBe(1);
expect(config!.steps[0]!.name).toBe('start'); expect(config!.movements[0]!.name).toBe('start');
}); });
}); });
@ -123,15 +123,15 @@ describe('Workflow Loader IT: agent path resolution', () => {
const config = loadWorkflow('minimal', testDir); const config = loadWorkflow('minimal', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
for (const step of config!.steps) { for (const movement of config!.movements) {
if (step.agentPath) { if (movement.agentPath) {
// Agent paths should be resolved to absolute paths // Agent paths should be resolved to absolute paths
expect(step.agentPath).toMatch(/^\//); expect(movement.agentPath).toMatch(/^\//);
// Agent files should exist // Agent files should exist
expect(existsSync(step.agentPath)).toBe(true); expect(existsSync(movement.agentPath)).toBe(true);
} }
if (step.parallel) { if (movement.parallel) {
for (const sub of step.parallel) { for (const sub of movement.parallel) {
if (sub.agentPath) { if (sub.agentPath) {
expect(sub.agentPath).toMatch(/^\//); expect(sub.agentPath).toMatch(/^\//);
expect(existsSync(sub.agentPath)).toBe(true); expect(existsSync(sub.agentPath)).toBe(true);
@ -157,8 +157,8 @@ describe('Workflow Loader IT: rule syntax parsing', () => {
const config = loadWorkflow('default', testDir); const config = loadWorkflow('default', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
// Find the parallel reviewers step // Find the parallel reviewers movement
const reviewersStep = config!.steps.find( const reviewersStep = config!.movements.find(
(s) => s.parallel && s.parallel.length > 0, (s) => s.parallel && s.parallel.length > 0,
); );
expect(reviewersStep).toBeDefined(); expect(reviewersStep).toBeDefined();
@ -175,7 +175,7 @@ describe('Workflow Loader IT: rule syntax parsing', () => {
const config = loadWorkflow('default', testDir); const config = loadWorkflow('default', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
const reviewersStep = config!.steps.find( const reviewersStep = config!.movements.find(
(s) => s.parallel && s.parallel.length > 0, (s) => s.parallel && s.parallel.length > 0,
); );
@ -186,11 +186,11 @@ describe('Workflow Loader IT: rule syntax parsing', () => {
expect(anyRule!.aggregateConditionText).toBe('needs_fix'); 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); const config = loadWorkflow('minimal', testDir);
expect(config).not.toBeNull(); 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).toBeDefined();
expect(implementStep!.rules).toBeDefined(); expect(implementStep!.rules).toBeDefined();
expect(implementStep!.rules!.length).toBeGreaterThan(0); expect(implementStep!.rules!.length).toBeGreaterThan(0);
@ -221,34 +221,34 @@ describe('Workflow Loader IT: workflow config validation', () => {
expect(config!.maxIterations).toBeGreaterThan(0); expect(config!.maxIterations).toBeGreaterThan(0);
}); });
it('should set initial_step from YAML', () => { it('should set initial_movement from YAML', () => {
const config = loadWorkflow('minimal', testDir); const config = loadWorkflow('minimal', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
expect(typeof config!.initialStep).toBe('string'); expect(typeof config!.initialMovement).toBe('string');
// initial_step should reference an existing step // initial_movement should reference an existing movement
const stepNames = config!.steps.map((s) => s.name); const movementNames = config!.movements.map((s) => s.name);
expect(stepNames).toContain(config!.initialStep); 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); const config = loadWorkflow('review-only', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
// review-only: no step should have edit: true // review-only: no movement should have edit: true
for (const step of config!.steps) { for (const movement of config!.movements) {
expect(step.edit).not.toBe(true); expect(movement.edit).not.toBe(true);
if (step.parallel) { if (movement.parallel) {
for (const sub of step.parallel) { for (const sub of movement.parallel) {
expect(sub.edit).not.toBe(true); 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); const expertConfig = loadWorkflow('expert', testDir);
expect(expertConfig).not.toBeNull(); 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).toBeDefined();
expect(implementStep!.edit).toBe(true); expect(implementStep!.edit).toBe(true);
}); });
@ -257,13 +257,13 @@ describe('Workflow Loader IT: workflow config validation', () => {
const config = loadWorkflow('minimal', testDir); const config = loadWorkflow('minimal', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
// At least some steps should have passPreviousResponse set // At least some movements should have passPreviousResponse set
const stepsWithPassPrev = config!.steps.filter((s) => s.passPreviousResponse === true); const movementsWithPassPrev = config!.movements.filter((s) => s.passPreviousResponse === true);
expect(stepsWithPassPrev.length).toBeGreaterThan(0); expect(movementsWithPassPrev.length).toBeGreaterThan(0);
}); });
}); });
describe('Workflow Loader IT: parallel step loading', () => { describe('Workflow Loader IT: parallel movement loading', () => {
let testDir: string; let testDir: string;
beforeEach(() => { beforeEach(() => {
@ -274,17 +274,17 @@ describe('Workflow Loader IT: parallel step loading', () => {
rmSync(testDir, { recursive: true, force: true }); 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); const config = loadWorkflow('default', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
const parallelStep = config!.steps.find( const parallelStep = config!.movements.find(
(s) => s.parallel && s.parallel.length > 0, (s) => s.parallel && s.parallel.length > 0,
); );
expect(parallelStep).toBeDefined(); expect(parallelStep).toBeDefined();
expect(parallelStep!.parallel!.length).toBeGreaterThanOrEqual(2); 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!) { for (const sub of parallelStep!.parallel!) {
expect(sub.name).toBeDefined(); expect(sub.name).toBeDefined();
expect(sub.agent).toBeDefined(); expect(sub.agent).toBeDefined();
@ -296,7 +296,7 @@ describe('Workflow Loader IT: parallel step loading', () => {
const config = loadWorkflow('expert', testDir); const config = loadWorkflow('expert', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
const parallelStep = config!.steps.find( const parallelStep = config!.movements.find(
(s) => s.parallel && s.parallel.length === 4, (s) => s.parallel && s.parallel.length === 4,
); );
expect(parallelStep).toBeDefined(); expect(parallelStep).toBeDefined();
@ -324,8 +324,8 @@ describe('Workflow Loader IT: report config loading', () => {
const config = loadWorkflow('default', testDir); const config = loadWorkflow('default', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
// default workflow: plan step has a report config // default workflow: plan movement has a report config
const planStep = config!.steps.find((s) => s.name === 'plan'); const planStep = config!.movements.find((s) => s.name === 'plan');
expect(planStep).toBeDefined(); expect(planStep).toBeDefined();
expect(planStep!.report).toBeDefined(); expect(planStep!.report).toBeDefined();
}); });
@ -334,8 +334,8 @@ describe('Workflow Loader IT: report config loading', () => {
const config = loadWorkflow('expert', testDir); const config = loadWorkflow('expert', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
// implement step has multi-report: [Scope, Decisions] // implement movement has multi-report: [Scope, Decisions]
const implementStep = config!.steps.find((s) => s.name === 'implement'); const implementStep = config!.movements.find((s) => s.name === 'implement');
expect(implementStep).toBeDefined(); expect(implementStep).toBeDefined();
expect(implementStep!.report).toBeDefined(); expect(implementStep!.report).toBeDefined();
expect(Array.isArray(implementStep!.report)).toBe(true); expect(Array.isArray(implementStep!.report)).toBe(true);
@ -373,7 +373,7 @@ this is not: valid yaml: [[[[
writeFileSync(join(workflowsDir, 'incomplete.yaml'), ` writeFileSync(join(workflowsDir, 'incomplete.yaml'), `
name: incomplete name: incomplete
description: Missing steps description: Missing movements
`); `);
expect(() => loadWorkflow('incomplete', testDir)).toThrow(); expect(() => loadWorkflow('incomplete', testDir)).toThrow();

View File

@ -304,15 +304,15 @@ describe('Workflow Patterns IT: review-only workflow', () => {
expect(state.status).toBe('completed'); 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); const config = loadWorkflow('review-only', testDir);
expect(config).not.toBeNull(); expect(config).not.toBeNull();
for (const step of config!.steps) { for (const movement of config!.movements) {
expect(step.edit).not.toBe(true); expect(movement.edit).not.toBe(true);
if (step.parallel) { if (movement.parallel) {
for (const subStep of step.parallel) { for (const subMovement of movement.parallel) {
expect(subStep.edit).not.toBe(true); expect(subMovement.edit).not.toBe(true);
} }
} }
} }

View File

@ -62,7 +62,7 @@ describe('WorkflowConfigRawSchema', () => {
const config = { const config = {
name: 'test-workflow', name: 'test-workflow',
description: 'A test workflow', description: 'A test workflow',
steps: [ movements: [
{ {
name: 'step1', name: 'step1',
agent: 'coder', agent: 'coder',
@ -77,15 +77,15 @@ describe('WorkflowConfigRawSchema', () => {
const result = WorkflowConfigRawSchema.parse(config); const result = WorkflowConfigRawSchema.parse(config);
expect(result.name).toBe('test-workflow'); expect(result.name).toBe('test-workflow');
expect(result.steps).toHaveLength(1); expect(result.movements).toHaveLength(1);
expect(result.steps[0]?.allowed_tools).toEqual(['Read', 'Grep']); expect(result.movements![0]?.allowed_tools).toEqual(['Read', 'Grep']);
expect(result.max_iterations).toBe(10); expect(result.max_iterations).toBe(10);
}); });
it('should parse step with permission_mode', () => { it('should parse movement with permission_mode', () => {
const config = { const config = {
name: 'test-workflow', name: 'test-workflow',
steps: [ movements: [
{ {
name: 'implement', name: 'implement',
agent: 'coder', agent: 'coder',
@ -100,13 +100,13 @@ describe('WorkflowConfigRawSchema', () => {
}; };
const result = WorkflowConfigRawSchema.parse(config); 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', () => { it('should allow omitting permission_mode', () => {
const config = { const config = {
name: 'test-workflow', name: 'test-workflow',
steps: [ movements: [
{ {
name: 'plan', name: 'plan',
agent: 'planner', agent: 'planner',
@ -116,13 +116,13 @@ describe('WorkflowConfigRawSchema', () => {
}; };
const result = WorkflowConfigRawSchema.parse(config); 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', () => { it('should reject invalid permission_mode', () => {
const config = { const config = {
name: 'test-workflow', name: 'test-workflow',
steps: [ movements: [
{ {
name: 'step1', name: 'step1',
agent: 'coder', agent: 'coder',
@ -135,10 +135,10 @@ describe('WorkflowConfigRawSchema', () => {
expect(() => WorkflowConfigRawSchema.parse(config)).toThrow(); expect(() => WorkflowConfigRawSchema.parse(config)).toThrow();
}); });
it('should require at least one step', () => { it('should require at least one movement', () => {
const config = { const config = {
name: 'empty-workflow', name: 'empty-workflow',
steps: [], movements: [],
}; };
expect(() => WorkflowConfigRawSchema.parse(config)).toThrow(); expect(() => WorkflowConfigRawSchema.parse(config)).toThrow();

View File

@ -1,17 +1,17 @@
/** /**
* Tests for parallel step execution and ai() condition loader * Tests for parallel movement execution and ai() condition loader
* *
* Covers: * Covers:
* - Schema validation for parallel sub-steps * - Schema validation for parallel sub-movements
* - Workflow loader normalization of ai() conditions and parallel steps * - Workflow loader normalization of ai() conditions and parallel movements
* - Engine parallel step aggregation logic * - Engine parallel movement aggregation logic
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WorkflowConfigRawSchema, ParallelSubStepRawSchema, WorkflowStepRawSchema } from '../core/models/index.js'; import { WorkflowConfigRawSchema, ParallelSubStepRawSchema, WorkflowStepRawSchema } from '../core/models/index.js';
describe('ParallelSubStepRawSchema', () => { describe('ParallelSubStepRawSchema', () => {
it('should validate a valid parallel sub-step', () => { it('should validate a valid parallel sub-movement', () => {
const raw = { const raw = {
name: 'arch-review', name: 'arch-review',
agent: '~/.takt/agents/default/reviewer.md', agent: '~/.takt/agents/default/reviewer.md',
@ -22,7 +22,7 @@ describe('ParallelSubStepRawSchema', () => {
expect(result.success).toBe(true); 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 = { const raw = {
name: 'no-agent-step', name: 'no-agent-step',
instruction_template: 'Do something', 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 = { const raw = {
name: 'reviewed', name: 'reviewed',
agent: '~/.takt/agents/default/reviewer.md', agent: '~/.takt/agents/default/reviewer.md',
@ -74,7 +74,7 @@ describe('ParallelSubStepRawSchema', () => {
}); });
describe('WorkflowStepRawSchema with parallel', () => { 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 = { const raw = {
name: 'parallel-review', name: 'parallel-review',
parallel: [ parallel: [
@ -90,7 +90,7 @@ describe('WorkflowStepRawSchema with parallel', () => {
expect(result.success).toBe(true); 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 = { const raw = {
name: 'orphan-step', name: 'orphan-step',
instruction_template: 'Do something', instruction_template: 'Do something',
@ -100,7 +100,7 @@ describe('WorkflowStepRawSchema with parallel', () => {
expect(result.success).toBe(true); 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 = { const raw = {
name: 'normal-step', name: 'normal-step',
agent: 'coder.md', agent: 'coder.md',
@ -111,7 +111,7 @@ describe('WorkflowStepRawSchema with parallel', () => {
expect(result.success).toBe(true); 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 = { const raw = {
name: 'empty-parallel', name: 'empty-parallel',
parallel: [], parallel: [],
@ -122,11 +122,11 @@ describe('WorkflowStepRawSchema with parallel', () => {
}); });
}); });
describe('WorkflowConfigRawSchema with parallel steps', () => { describe('WorkflowConfigRawSchema with parallel movements', () => {
it('should validate a workflow with parallel step', () => { it('should validate a workflow with parallel movement', () => {
const raw = { const raw = {
name: 'test-parallel-workflow', name: 'test-parallel-workflow',
steps: [ movements: [
{ {
name: 'plan', name: 'plan',
agent: 'planner.md', agent: 'planner.md',
@ -144,22 +144,22 @@ describe('WorkflowConfigRawSchema with parallel steps', () => {
], ],
}, },
], ],
initial_step: 'plan', initial_movement: 'plan',
max_iterations: 10, max_iterations: 10,
}; };
const result = WorkflowConfigRawSchema.safeParse(raw); const result = WorkflowConfigRawSchema.safeParse(raw);
expect(result.success).toBe(true); expect(result.success).toBe(true);
if (result.success) { if (result.success) {
expect(result.data.steps).toHaveLength(2); expect(result.data.movements).toHaveLength(2);
expect(result.data.steps[1].parallel).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 = { const raw = {
name: 'mixed-workflow', name: 'mixed-workflow',
steps: [ movements: [
{ name: 'plan', agent: 'planner.md', rules: [{ condition: 'Done', next: 'implement' }] }, { name: 'plan', agent: 'planner.md', rules: [{ condition: 'Done', next: 'implement' }] },
{ name: 'implement', agent: 'coder.md', rules: [{ condition: 'Done', next: 'review' }] }, { 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' }], rules: [{ condition: 'All pass', next: 'COMPLETE' }],
}, },
], ],
initial_step: 'plan', initial_movement: 'plan',
}; };
const result = WorkflowConfigRawSchema.safeParse(raw); const result = WorkflowConfigRawSchema.safeParse(raw);
expect(result.success).toBe(true); expect(result.success).toBe(true);
if (result.success) { if (result.success) {
expect(result.data.steps[0].agent).toBe('planner.md'); expect(result.data.movements[0].agent).toBe('planner.md');
expect(result.data.steps[2].parallel).toHaveLength(2); expect(result.data.movements[2].parallel).toHaveLength(2);
} }
}); });
}); });
@ -369,7 +369,7 @@ describe('aggregate condition evaluation logic', () => {
{ condition: 'rejected' }, { condition: 'rejected' },
]; ];
it('all(): true when all sub-steps match', () => { it('all(): true when all sub-movements match', () => {
const subs: SubResult[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 0, rules }, { name: 'a', matchedRuleIndex: 0, rules },
{ name: 'b', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 0, rules },
@ -377,7 +377,7 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('all', 'approved', subs)).toBe(true); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 0, rules }, { name: 'a', matchedRuleIndex: 0, rules },
{ name: 'b', matchedRuleIndex: 1, rules }, { name: 'b', matchedRuleIndex: 1, rules },
@ -385,7 +385,7 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('all', 'approved', subs)).toBe(false); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 0, rules }, { name: 'a', matchedRuleIndex: 0, rules },
{ name: 'b', matchedRuleIndex: undefined, rules }, { name: 'b', matchedRuleIndex: undefined, rules },
@ -393,7 +393,7 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('all', 'approved', subs)).toBe(false); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 0, rules }, { name: 'a', matchedRuleIndex: 0, rules },
{ name: 'b', matchedRuleIndex: 0, rules: undefined }, { name: 'b', matchedRuleIndex: 0, rules: undefined },
@ -401,11 +401,11 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('all', 'approved', subs)).toBe(false); 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); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 0, rules }, { name: 'a', matchedRuleIndex: 0, rules },
{ name: 'b', matchedRuleIndex: 1, rules }, { name: 'b', matchedRuleIndex: 1, rules },
@ -413,7 +413,7 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('any', 'rejected', subs)).toBe(true); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 1, rules }, { name: 'a', matchedRuleIndex: 1, rules },
{ name: 'b', matchedRuleIndex: 1, rules }, { name: 'b', matchedRuleIndex: 1, rules },
@ -421,7 +421,7 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('any', 'rejected', subs)).toBe(true); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: 0, rules }, { name: 'a', matchedRuleIndex: 0, rules },
{ name: 'b', matchedRuleIndex: 0, rules }, { name: 'b', matchedRuleIndex: 0, rules },
@ -429,11 +429,11 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('any', 'rejected', subs)).toBe(false); 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); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: undefined, rules }, { name: 'a', matchedRuleIndex: undefined, rules },
{ name: 'b', matchedRuleIndex: 1, rules }, { name: 'b', matchedRuleIndex: 1, rules },
@ -441,7 +441,7 @@ describe('aggregate condition evaluation logic', () => {
expect(evaluateAggregate('any', 'rejected', subs)).toBe(true); 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[] = [ const subs: SubResult[] = [
{ name: 'a', matchedRuleIndex: undefined, rules }, { name: 'a', matchedRuleIndex: undefined, rules },
{ name: 'b', matchedRuleIndex: undefined, rules }, { name: 'b', matchedRuleIndex: undefined, rules },
@ -473,8 +473,8 @@ describe('aggregate condition evaluation logic', () => {
}); });
}); });
describe('parallel step aggregation format', () => { describe('parallel movement aggregation format', () => {
it('should aggregate sub-step outputs in the expected format', () => { it('should aggregate sub-movement outputs in the expected format', () => {
// Mirror the aggregation logic from engine.ts // Mirror the aggregation logic from engine.ts
const subResults = [ const subResults = [
{ name: 'arch-review', content: 'Architecture looks good.\n## Result: APPROVE' }, { 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.'); expect(aggregatedContent).toContain('No security issues.');
}); });
it('should handle single sub-step', () => { it('should handle single sub-movement', () => {
const subResults = [ const subResults = [
{ name: 'only-step', content: 'Single result' }, { name: 'only-step', content: 'Single result' },
]; ];
@ -505,7 +505,7 @@ describe('parallel step aggregation format', () => {
expect(aggregatedContent).not.toContain('---'); expect(aggregatedContent).not.toContain('---');
}); });
it('should handle empty content from sub-steps', () => { it('should handle empty content from sub-movements', () => {
const subResults = [ const subResults = [
{ name: 'step-a', content: '' }, { name: 'step-a', content: '' },
{ name: 'step-b', content: 'Has content' }, { name: 'step-b', content: 'Has content' },

View File

@ -18,7 +18,7 @@ describe('ParallelLogger', () => {
describe('buildPrefix', () => { describe('buildPrefix', () => {
it('should build colored prefix with padding', () => { it('should build colored prefix with padding', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['arch-review', 'sec'], subMovementNames: ['arch-review', 'sec'],
writeFn, writeFn,
}); });
@ -34,7 +34,7 @@ describe('ParallelLogger', () => {
it('should cycle colors for index >= 4', () => { it('should cycle colors for index >= 4', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['a', 'b', 'c', 'd', 'e'], subMovementNames: ['a', 'b', 'c', 'd', 'e'],
writeFn, writeFn,
}); });
@ -47,7 +47,7 @@ describe('ParallelLogger', () => {
it('should assign correct colors in order', () => { it('should assign correct colors in order', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['a', 'b', 'c', 'd'], subMovementNames: ['a', 'b', 'c', 'd'],
writeFn, writeFn,
}); });
@ -59,7 +59,7 @@ describe('ParallelLogger', () => {
it('should have no extra padding for longest name', () => { it('should have no extra padding for longest name', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['long-name', 'short'], subMovementNames: ['long-name', 'short'],
writeFn, writeFn,
}); });
@ -72,7 +72,7 @@ describe('ParallelLogger', () => {
describe('text event line buffering', () => { describe('text event line buffering', () => {
it('should buffer partial line and output on newline', () => { it('should buffer partial line and output on newline', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a'], subMovementNames: ['step-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('step-a', 0); const handler = logger.createStreamHandler('step-a', 0);
@ -91,7 +91,7 @@ describe('ParallelLogger', () => {
it('should handle multiple lines in single text event', () => { it('should handle multiple lines in single text event', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a'], subMovementNames: ['step-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('step-a', 0); const handler = logger.createStreamHandler('step-a', 0);
@ -104,7 +104,7 @@ describe('ParallelLogger', () => {
it('should output empty line without prefix', () => { it('should output empty line without prefix', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a'], subMovementNames: ['step-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('step-a', 0); const handler = logger.createStreamHandler('step-a', 0);
@ -118,7 +118,7 @@ describe('ParallelLogger', () => {
it('should keep trailing partial in buffer', () => { it('should keep trailing partial in buffer', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a'], subMovementNames: ['step-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('step-a', 0); const handler = logger.createStreamHandler('step-a', 0);
@ -137,7 +137,7 @@ describe('ParallelLogger', () => {
describe('block events (tool_use, tool_result, tool_output, thinking)', () => { describe('block events (tool_use, tool_result, tool_output, thinking)', () => {
it('should prefix tool_use events', () => { it('should prefix tool_use events', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('sub-a', 0); const handler = logger.createStreamHandler('sub-a', 0);
@ -154,7 +154,7 @@ describe('ParallelLogger', () => {
it('should prefix tool_result events', () => { it('should prefix tool_result events', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('sub-a', 0); const handler = logger.createStreamHandler('sub-a', 0);
@ -170,7 +170,7 @@ describe('ParallelLogger', () => {
it('should prefix multi-line tool output', () => { it('should prefix multi-line tool output', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('sub-a', 0); const handler = logger.createStreamHandler('sub-a', 0);
@ -187,7 +187,7 @@ describe('ParallelLogger', () => {
it('should prefix thinking events', () => { it('should prefix thinking events', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('sub-a', 0); const handler = logger.createStreamHandler('sub-a', 0);
@ -206,7 +206,7 @@ describe('ParallelLogger', () => {
it('should delegate init event to parent callback', () => { it('should delegate init event to parent callback', () => {
const parentEvents: StreamEvent[] = []; const parentEvents: StreamEvent[] = [];
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
parentOnStream: (event) => parentEvents.push(event), parentOnStream: (event) => parentEvents.push(event),
writeFn, writeFn,
}); });
@ -226,7 +226,7 @@ describe('ParallelLogger', () => {
it('should delegate result event to parent callback', () => { it('should delegate result event to parent callback', () => {
const parentEvents: StreamEvent[] = []; const parentEvents: StreamEvent[] = [];
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
parentOnStream: (event) => parentEvents.push(event), parentOnStream: (event) => parentEvents.push(event),
writeFn, writeFn,
}); });
@ -245,7 +245,7 @@ describe('ParallelLogger', () => {
it('should delegate error event to parent callback', () => { it('should delegate error event to parent callback', () => {
const parentEvents: StreamEvent[] = []; const parentEvents: StreamEvent[] = [];
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
parentOnStream: (event) => parentEvents.push(event), parentOnStream: (event) => parentEvents.push(event),
writeFn, writeFn,
}); });
@ -263,7 +263,7 @@ describe('ParallelLogger', () => {
it('should not crash when no parent callback for delegated events', () => { it('should not crash when no parent callback for delegated events', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['sub-a'], subMovementNames: ['sub-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('sub-a', 0); const handler = logger.createStreamHandler('sub-a', 0);
@ -280,7 +280,7 @@ describe('ParallelLogger', () => {
describe('flush', () => { describe('flush', () => {
it('should output remaining buffered content', () => { it('should output remaining buffered content', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a', 'step-b'], subMovementNames: ['step-a', 'step-b'],
writeFn, writeFn,
}); });
const handlerA = logger.createStreamHandler('step-a', 0); const handlerA = logger.createStreamHandler('step-a', 0);
@ -300,7 +300,7 @@ describe('ParallelLogger', () => {
it('should not output empty buffers', () => { it('should not output empty buffers', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a', 'step-b'], subMovementNames: ['step-a', 'step-b'],
writeFn, writeFn,
}); });
const handlerA = logger.createStreamHandler('step-a', 0); const handlerA = logger.createStreamHandler('step-a', 0);
@ -316,7 +316,7 @@ describe('ParallelLogger', () => {
describe('printSummary', () => { describe('printSummary', () => {
it('should print completion summary', () => { it('should print completion summary', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['arch-review', 'security-review'], subMovementNames: ['arch-review', 'security-review'],
writeFn, writeFn,
}); });
@ -337,7 +337,7 @@ describe('ParallelLogger', () => {
it('should show (no result) for undefined condition', () => { it('should show (no result) for undefined condition', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a'], subMovementNames: ['step-a'],
writeFn, writeFn,
}); });
@ -349,9 +349,9 @@ describe('ParallelLogger', () => {
expect(fullOutput).toContain('(no result)'); 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({ const logger = new ParallelLogger({
subStepNames: ['short', 'very-long-name'], subMovementNames: ['short', 'very-long-name'],
writeFn, writeFn,
}); });
@ -372,7 +372,7 @@ describe('ParallelLogger', () => {
it('should flush remaining buffers before printing summary', () => { it('should flush remaining buffers before printing summary', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a'], subMovementNames: ['step-a'],
writeFn, writeFn,
}); });
const handler = logger.createStreamHandler('step-a', 0); 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', () => { it('should correctly interleave prefixed output', () => {
const logger = new ParallelLogger({ const logger = new ParallelLogger({
subStepNames: ['step-a', 'step-b'], subMovementNames: ['step-a', 'step-b'],
writeFn, writeFn,
}); });
const handlerA = logger.createStreamHandler('step-a', 0); const handlerA = logger.createStreamHandler('step-a', 0);

View File

@ -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 { 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(() => { beforeEach(() => {
_resetCache(); _resetCache();
}); });
describe('getPrompt', () => { describe('loadTemplate', () => {
it('returns a language-independent prompt by key (defaults to en)', () => { it('loads an English template', () => {
const result = getPrompt('summarize.slugGenerator'); const result = loadTemplate('score_slug_system_prompt', 'en');
expect(result).toContain('You are a slug generator'); expect(result).toContain('You are a slug generator');
}); });
it('returns an English prompt when lang is "en"', () => { it('loads an English interactive template', () => {
const result = getPrompt('interactive.systemPrompt', 'en'); const result = loadTemplate('score_interactive_system_prompt', 'en');
expect(result).toContain('You are a task planning assistant'); expect(result).toContain('You are a task planning assistant');
}); });
it('returns a Japanese prompt when lang is "ja"', () => { it('loads a Japanese template', () => {
const result = getPrompt('interactive.systemPrompt', 'ja'); const result = loadTemplate('score_interactive_system_prompt', 'ja');
expect(result).toContain('あなたはTAKT'); expect(result).toContain('あなたはTAKT');
}); });
it('throws for a non-existent key', () => { it('loads score_slug_system_prompt with explicit lang', () => {
expect(() => getPrompt('nonexistent.key')).toThrow('Prompt key not found: nonexistent.key'); const result = loadTemplate('score_slug_system_prompt', 'en');
});
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');
expect(result).toContain('You are a slug generator'); expect(result).toContain('You are a slug generator');
}); });
describe('template variable substitution', () => { it('throws for a non-existent template with language', () => {
it('replaces {variableName} placeholders with provided values', () => { expect(() => loadTemplate('nonexistent_template', 'en')).toThrow('Template not found: nonexistent_template (lang: en)');
const result = getPrompt('claude.agentDefault', undefined, { agentName: 'test-agent' }); });
});
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('You are the test-agent agent');
expect(result).toContain('Follow the standard test-agent workflow'); expect(result).toContain('Follow the standard test-agent workflow');
}); });
it('leaves unmatched placeholders as-is', () => { it('replaces undefined variables with empty string', () => {
const result = getPrompt('claude.agentDefault', undefined, {}); const result = loadTemplate('perform_builtin_agent_system_prompt', 'en', {});
expect(result).toContain('{agentName}'); expect(result).not.toContain('{{agentName}}');
expect(result).toContain('You are the agent');
}); });
it('replaces multiple different variables', () => { it('replaces multiple different variables', () => {
const result = getPrompt('claude.judgePrompt', undefined, { const result = loadTemplate('perform_judge_message', 'en', {
agentOutput: 'test output', agentOutput: 'test output',
conditionList: '| 1 | Success |', conditionList: '| 1 | Success |',
}); });
expect(result).toContain('test output'); expect(result).toContain('test output');
expect(result).toContain('| 1 | Success |'); 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('getPromptObject', () => { describe('renderTemplate', () => {
it('returns an object for a given key and language', () => { it('processes {{#if}} blocks with truthy value', () => {
const result = getPromptObject<{ heading: string }>('instruction.metadata', 'en'); const template = 'before{{#if show}}visible{{/if}}after';
expect(result.heading).toBe('## Execution Context'); const result = renderTemplate(template, { show: true });
expect(result).toBe('beforevisibleafter');
}); });
it('returns a Japanese object when lang is "ja"', () => { it('processes {{#if}} blocks with falsy value', () => {
const result = getPromptObject<{ heading: string }>('instruction.metadata', 'ja'); const template = 'before{{#if show}}visible{{/if}}after';
expect(result.heading).toBe('## 実行コンテキスト'); const result = renderTemplate(template, { show: false });
expect(result).toBe('beforeafter');
}); });
it('throws for a non-existent key', () => { it('processes {{#if}}...{{else}}...{{/if}} blocks', () => {
expect(() => getPromptObject('nonexistent.key')).toThrow('Prompt key not found: nonexistent.key'); 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', () => { describe('caching', () => {
it('returns the same data on repeated calls', () => { it('returns consistent results on repeated calls', () => {
const first = getPrompt('summarize.slugGenerator'); const first = loadTemplate('score_slug_system_prompt', 'en');
const second = getPrompt('summarize.slugGenerator'); const second = loadTemplate('score_slug_system_prompt', 'en');
expect(first).toBe(second); expect(first).toBe(second);
}); });
it('reloads after cache reset', () => { it('reloads after cache reset', () => {
const first = getPrompt('summarize.slugGenerator'); const first = loadTemplate('score_slug_system_prompt', 'en');
_resetCache(); _resetCache();
const second = getPrompt('summarize.slugGenerator'); const second = loadTemplate('score_slug_system_prompt', 'en');
expect(first).toBe(second); expect(first).toBe(second);
}); });
}); });
describe('YAML content integrity', () => { describe('template content integrity', () => {
it('contains all expected top-level keys in en', () => { it('score_interactive_system_prompt contains core instructions', () => {
expect(() => getPrompt('interactive.systemPrompt', 'en')).not.toThrow(); const en = loadTemplate('score_interactive_system_prompt', 'en');
expect(() => getPrompt('interactive.summaryPrompt', 'en')).not.toThrow(); expect(en).toContain('task planning assistant');
expect(() => getPrompt('interactive.workflowInfo', 'en')).not.toThrow();
expect(() => getPrompt('interactive.conversationLabel', 'en')).not.toThrow(); const ja = loadTemplate('score_interactive_system_prompt', 'ja');
expect(() => getPrompt('interactive.noTranscript', 'en')).not.toThrow(); expect(ja).toContain('あなたはTAKT');
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();
}); });
it('contains all expected top-level keys in ja', () => { it('score_slug_system_prompt contains format specification', () => {
expect(() => getPrompt('interactive.systemPrompt', 'ja')).not.toThrow(); const result = loadTemplate('score_slug_system_prompt', 'en');
expect(() => getPrompt('interactive.summaryPrompt', 'ja')).not.toThrow(); expect(result).toContain('verb-noun');
expect(() => getPrompt('interactive.workflowInfo', 'ja')).not.toThrow(); expect(result).toContain('max 30 chars');
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('instruction.metadata has all required fields', () => { it('perform_builtin_agent_system_prompt contains {{agentName}} placeholder', () => {
const en = getPromptObject<Record<string, string>>('instruction.metadata', 'en'); const result = loadTemplate('perform_builtin_agent_system_prompt', 'en');
expect(en).toHaveProperty('heading'); expect(result).toContain('{{agentName}}');
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('instruction.sections has all required fields', () => { it('perform_agent_system_prompt contains {{agentDefinition}} placeholder', () => {
const en = getPromptObject<Record<string, string>>('instruction.sections', 'en'); const result = loadTemplate('perform_agent_system_prompt', 'en');
expect(en).toHaveProperty('workflowContext'); expect(result).toContain('{{agentDefinition}}');
expect(en).toHaveProperty('iteration');
expect(en).toHaveProperty('step');
expect(en).toHaveProperty('userRequest');
expect(en).toHaveProperty('instructions');
}); });
it('instruction.statusRules has appendixInstruction with {tag} placeholder', () => { it('perform_judge_message contains {{agentOutput}} and {{conditionList}} placeholders', () => {
const en = getPromptObject<{ appendixInstruction: string }>('instruction.statusRules', 'en'); const result = loadTemplate('perform_judge_message', 'en');
expect(en.appendixInstruction).toContain('{tag}'); expect(result).toContain('{{agentOutput}}');
expect(result).toContain('{{conditionList}}');
}); });
it('en and ja files have the same key structure', () => { it('perform_phase1_message contains execution context and rules sections', () => {
// Verify a sampling of keys exist in both languages const en = loadTemplate('perform_phase1_message', 'en');
const stringKeys = [ expect(en).toContain('## Execution Context');
'interactive.systemPrompt', expect(en).toContain('## Execution Rules');
'summarize.slugGenerator', expect(en).toContain('Do NOT run git commit');
'claude.agentDefault', 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) { for (const name of templates) {
expect(() => getPrompt(key, 'en')).not.toThrow(); const content = loadTemplate(name, 'en');
expect(() => getPrompt(key, 'ja')).not.toThrow(); expect(content).not.toMatch(/^---\n/);
}
const objectKeys = [
'instruction.metadata',
'instruction.sections',
];
for (const key of objectKeys) {
expect(() => getPromptObject(key, 'en')).not.toThrow();
expect(() => getPromptObject(key, 'ja')).not.toThrow();
} }
}); });
}); });

View File

@ -4,7 +4,7 @@
* Covers: * Covers:
* - Workflow YAML files (EN/JA) load and pass schema validation * - Workflow YAML files (EN/JA) load and pass schema validation
* - Workflow structure: plan -> reviewers (parallel) -> supervise -> pr-comment * - 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 * - pr-commenter agent has Bash in allowed_tools
* - Routing rules for local vs PR comment flows * - 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'); const RESOURCES_DIR = join(import.meta.dirname, '../../resources/global');
function loadReviewOnlyYaml(lang: 'en' | 'ja') { 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'); const content = readFileSync(filePath, 'utf-8');
return parseYaml(content); return parseYaml(content);
} }
@ -31,27 +31,27 @@ describe('review-only workflow (EN)', () => {
expect(result.success).toBe(true); 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.name).toBe('review-only');
expect(raw.initial_step).toBe('plan'); expect(raw.initial_movement).toBe('plan');
}); });
it('should have max_iterations of 10', () => { it('should have max_iterations of 10', () => {
expect(raw.max_iterations).toBe(10); expect(raw.max_iterations).toBe(10);
}); });
it('should have 4 steps: plan, reviewers, supervise, pr-comment', () => { it('should have 4 movements: plan, reviewers, supervise, pr-comment', () => {
const stepNames = raw.steps.map((s: { name: string }) => s.name); const movementNames = raw.movements.map((s: { name: string }) => s.name);
expect(stepNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']); expect(movementNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']);
}); });
it('should have all steps with edit: false', () => { it('should have all movements with edit: false', () => {
for (const step of raw.steps) { for (const movement of raw.movements) {
if (step.edit !== undefined) { if (movement.edit !== undefined) {
expect(step.edit).toBe(false); expect(movement.edit).toBe(false);
} }
if (step.parallel) { if (movement.parallel) {
for (const sub of step.parallel) { for (const sub of movement.parallel) {
if (sub.edit !== undefined) { if (sub.edit !== undefined) {
expect(sub.edit).toBe(false); expect(sub.edit).toBe(false);
} }
@ -60,8 +60,8 @@ describe('review-only workflow (EN)', () => {
} }
}); });
it('should have reviewers step with 3 parallel sub-steps', () => { it('should have reviewers movement with 3 parallel sub-movements', () => {
const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers');
expect(reviewers).toBeDefined(); expect(reviewers).toBeDefined();
expect(reviewers.parallel).toHaveLength(3); expect(reviewers.parallel).toHaveLength(3);
@ -69,8 +69,8 @@ describe('review-only workflow (EN)', () => {
expect(subNames).toEqual(['arch-review', 'security-review', 'ai-review']); expect(subNames).toEqual(['arch-review', 'security-review', 'ai-review']);
}); });
it('should have reviewers step with aggregate rules', () => { it('should have reviewers movement with aggregate rules', () => {
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).toHaveLength(2); expect(reviewers.rules).toHaveLength(2);
expect(reviewers.rules[0].condition).toBe('all("approved")'); expect(reviewers.rules[0].condition).toBe('all("approved")');
expect(reviewers.rules[0].next).toBe('supervise'); expect(reviewers.rules[0].next).toBe('supervise');
@ -78,8 +78,8 @@ describe('review-only workflow (EN)', () => {
expect(reviewers.rules[1].next).toBe('supervise'); expect(reviewers.rules[1].next).toBe('supervise');
}); });
it('should have supervise step with routing rules for local and PR flows', () => { it('should have supervise movement with routing rules for local and PR flows', () => {
const supervise = raw.steps.find((s: { name: string }) => s.name === 'supervise'); const supervise = raw.movements.find((s: { name: string }) => s.name === 'supervise');
expect(supervise.rules).toHaveLength(3); expect(supervise.rules).toHaveLength(3);
const conditions = supervise.rules.map((r: { condition: string }) => r.condition); const conditions = supervise.rules.map((r: { condition: string }) => r.condition);
@ -97,40 +97,40 @@ describe('review-only workflow (EN)', () => {
expect(rejectRule.next).toBe('ABORT'); expect(rejectRule.next).toBe('ABORT');
}); });
it('should have pr-comment step with Bash in allowed_tools', () => { it('should have pr-comment movement with Bash in allowed_tools', () => {
const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment');
expect(prComment).toBeDefined(); expect(prComment).toBeDefined();
expect(prComment.allowed_tools).toContain('Bash'); expect(prComment.allowed_tools).toContain('Bash');
}); });
it('should have pr-comment step using pr-commenter agent', () => { it('should have pr-comment movement using pr-commenter agent', () => {
const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment');
expect(prComment.agent).toContain('review/pr-commenter.md'); expect(prComment.agent).toContain('review/pr-commenter.md');
}); });
it('should have plan step reusing default planner agent', () => { it('should have plan movement reusing default planner agent', () => {
const plan = raw.steps.find((s: { name: string }) => s.name === 'plan'); const plan = raw.movements.find((s: { name: string }) => s.name === 'plan');
expect(plan.agent).toContain('default/planner.md'); expect(plan.agent).toContain('default/planner.md');
}); });
it('should have supervise step reusing default supervisor agent', () => { it('should have supervise movement reusing default supervisor agent', () => {
const supervise = raw.steps.find((s: { name: string }) => s.name === 'supervise'); const supervise = raw.movements.find((s: { name: string }) => s.name === 'supervise');
expect(supervise.agent).toContain('default/supervisor.md'); expect(supervise.agent).toContain('default/supervisor.md');
}); });
it('should not have any step with edit: true', () => { it('should not have any movement with edit: true', () => {
for (const step of raw.steps) { for (const movement of raw.movements) {
expect(step.edit).not.toBe(true); expect(movement.edit).not.toBe(true);
if (step.parallel) { if (movement.parallel) {
for (const sub of step.parallel) { for (const sub of movement.parallel) {
expect(sub.edit).not.toBe(true); expect(sub.edit).not.toBe(true);
} }
} }
} }
}); });
it('reviewer sub-steps should not have Bash in allowed_tools', () => { it('reviewer sub-movements should not have Bash in allowed_tools', () => {
const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers');
for (const sub of reviewers.parallel) { for (const sub of reviewers.parallel) {
expect(sub.allowed_tools).not.toContain('Bash'); expect(sub.allowed_tools).not.toContain('Bash');
} }
@ -145,42 +145,42 @@ describe('review-only workflow (JA)', () => {
expect(result.success).toBe(true); 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.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', () => { it('should have same movement structure as EN version', () => {
const stepNames = raw.steps.map((s: { name: string }) => s.name); const movementNames = raw.movements.map((s: { name: string }) => s.name);
expect(stepNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']); expect(movementNames).toEqual(['plan', 'reviewers', 'supervise', 'pr-comment']);
}); });
it('should have reviewers step with 3 parallel sub-steps', () => { it('should have reviewers movement with 3 parallel sub-movements', () => {
const reviewers = raw.steps.find((s: { name: string }) => s.name === 'reviewers'); const reviewers = raw.movements.find((s: { name: string }) => s.name === 'reviewers');
expect(reviewers.parallel).toHaveLength(3); expect(reviewers.parallel).toHaveLength(3);
const subNames = reviewers.parallel.map((s: { name: string }) => s.name); const subNames = reviewers.parallel.map((s: { name: string }) => s.name);
expect(subNames).toEqual(['arch-review', 'security-review', 'ai-review']); expect(subNames).toEqual(['arch-review', 'security-review', 'ai-review']);
}); });
it('should have all steps with edit: false or undefined', () => { it('should have all movements with edit: false or undefined', () => {
for (const step of raw.steps) { for (const movement of raw.movements) {
expect(step.edit).not.toBe(true); expect(movement.edit).not.toBe(true);
if (step.parallel) { if (movement.parallel) {
for (const sub of step.parallel) { for (const sub of movement.parallel) {
expect(sub.edit).not.toBe(true); expect(sub.edit).not.toBe(true);
} }
} }
} }
}); });
it('should have pr-comment step with Bash in allowed_tools', () => { it('should have pr-comment movement with Bash in allowed_tools', () => {
const prComment = raw.steps.find((s: { name: string }) => s.name === 'pr-comment'); const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment');
expect(prComment.allowed_tools).toContain('Bash'); expect(prComment.allowed_tools).toContain('Bash');
}); });
it('should have same aggregate rules on reviewers', () => { 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[0].condition).toBe('all("approved")');
expect(reviewers.rules[1].condition).toBe('any("needs_fix")'); 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', () => { describe('pr-comment instruction_template contains workflow-specific procedures', () => {
it('EN: should reference specific report files', () => { it('EN: should reference specific report files', () => {
const raw = loadReviewOnlyYaml('en'); 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; const template = prComment.instruction_template;
expect(template).toContain('01-architect-review.md'); expect(template).toContain('01-architect-review.md');
expect(template).toContain('02-security-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', () => { it('JA: should reference specific report files', () => {
const raw = loadReviewOnlyYaml('ja'); 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; const template = prComment.instruction_template;
expect(template).toContain('01-architect-review.md'); expect(template).toContain('01-architect-review.md');
expect(template).toContain('02-security-review.md'); expect(template).toContain('02-security-review.md');

View File

@ -1,12 +1,12 @@
/** /**
* Tests for workflow transitions module * Tests for workflow transitions module (movement-based)
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { determineNextStepByRules } from '../core/workflow/index.js'; import { determineNextMovementByRules } from '../core/workflow/index.js';
import type { WorkflowStep } from '../core/models/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 { return {
name: 'test-step', name: 'test-step',
agent: 'test-agent', agent: 'test-agent',
@ -20,29 +20,29 @@ function createStepWithRules(rules: { condition: string; next: string }[]): Work
}; };
} }
describe('determineNextStepByRules', () => { describe('determineNextMovementByRules', () => {
it('should return next step for valid rule index', () => { it('should return next movement for valid rule index', () => {
const step = createStepWithRules([ const step = createMovementWithRules([
{ condition: 'Clear', next: 'implement' }, { condition: 'Clear', next: 'implement' },
{ condition: 'Blocked', next: 'ABORT' }, { condition: 'Blocked', next: 'ABORT' },
]); ]);
expect(determineNextStepByRules(step, 0)).toBe('implement'); expect(determineNextMovementByRules(step, 0)).toBe('implement');
expect(determineNextStepByRules(step, 1)).toBe('ABORT'); expect(determineNextMovementByRules(step, 1)).toBe('ABORT');
}); });
it('should return null for out-of-bounds index', () => { it('should return null for out-of-bounds index', () => {
const step = createStepWithRules([ const step = createMovementWithRules([
{ condition: 'Clear', next: 'implement' }, { condition: 'Clear', next: 'implement' },
]); ]);
expect(determineNextStepByRules(step, 1)).toBeNull(); expect(determineNextMovementByRules(step, 1)).toBeNull();
expect(determineNextStepByRules(step, -1)).toBeNull(); expect(determineNextMovementByRules(step, -1)).toBeNull();
expect(determineNextStepByRules(step, 100)).toBeNull(); expect(determineNextMovementByRules(step, 100)).toBeNull();
}); });
it('should return null when step has no rules', () => { it('should return null when movement has no rules', () => {
const step: WorkflowStep = { const step: WorkflowMovement = {
name: 'test-step', name: 'test-step',
agent: 'test-agent', agent: 'test-agent',
agentDisplayName: 'Test Agent', agentDisplayName: 'Test Agent',
@ -50,20 +50,20 @@ describe('determineNextStepByRules', () => {
passPreviousResponse: false, passPreviousResponse: false,
}; };
expect(determineNextStepByRules(step, 0)).toBeNull(); expect(determineNextMovementByRules(step, 0)).toBeNull();
}); });
it('should handle COMPLETE as next step', () => { it('should handle COMPLETE as next movement', () => {
const step = createStepWithRules([ const step = createMovementWithRules([
{ condition: 'All passed', next: 'COMPLETE' }, { 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', () => { it('should return null when rule exists but next is undefined', () => {
// Parallel sub-step rules may omit `next` (optional field) // Parallel sub-movement rules may omit `next` (optional field)
const step: WorkflowStep = { const step: WorkflowMovement = {
name: 'sub-step', name: 'sub-step',
agent: 'test-agent', agent: 'test-agent',
agentDisplayName: 'Test Agent', agentDisplayName: 'Test Agent',
@ -75,7 +75,7 @@ describe('determineNextStepByRules', () => {
], ],
}; };
expect(determineNextStepByRules(step, 0)).toBeNull(); expect(determineNextMovementByRules(step, 0)).toBeNull();
expect(determineNextStepByRules(step, 1)).toBeNull(); expect(determineNextMovementByRules(step, 1)).toBeNull();
}); });
}); });

View File

@ -20,7 +20,7 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
const { listWorkflows } = await import('../infra/config/loaders/workflowLoader.js'); const { listWorkflows } = await import('../infra/config/loaders/workflowLoader.js');
const SAMPLE_WORKFLOW = `name: test-workflow const SAMPLE_WORKFLOW = `name: test-workflow
steps: movements:
- name: step1 - name: step1
agent: coder agent: coder
instruction: "{task}" instruction: "{task}"

View File

@ -23,10 +23,10 @@ import {
const SAMPLE_WORKFLOW = `name: test-workflow const SAMPLE_WORKFLOW = `name: test-workflow
description: Test workflow description: Test workflow
initial_step: step1 initial_movement: step1
max_iterations: 1 max_iterations: 1
steps: movements:
- name: step1 - name: step1
agent: coder agent: coder
instruction: "{task}" instruction: "{task}"

View File

@ -75,8 +75,8 @@ function createWorkflowMap(entries: { name: string; source: 'builtin' | 'user' |
source: entry.source, source: entry.source,
config: { config: {
name: entry.name, name: entry.name,
steps: [], movements: [],
initialStep: 'start', initialMovement: 'start',
maxIterations: 1, maxIterations: 1,
}, },
}); });

View File

@ -3,11 +3,11 @@
* *
* Validates that: * Validates that:
* - expert and expert-cqrs workflows load successfully via loadWorkflow * - expert and expert-cqrs workflows load successfully via loadWorkflow
* - The reviewers step is a parallel step with expected sub-steps * - The reviewers movement is a parallel movement with expected sub-movements
* - ai_review routes to reviewers (not individual review steps) * - ai_review routes to reviewers (not individual review movements)
* - fix step routes back to reviewers * - fix movement routes back to reviewers
* - Aggregate rules (all/any) are configured on the reviewers step * - Aggregate rules (all/any) are configured on the reviewers movement
* - Sub-step rules use simple approved/needs_fix conditions * - Sub-movement rules use simple approved/needs_fix conditions
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
@ -21,15 +21,15 @@ describe('expert workflow parallel structure', () => {
expect(workflow!.name).toBe('expert'); expect(workflow!.name).toBe('expert');
}); });
it('should have a reviewers parallel step', () => { it('should have a reviewers parallel movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
expect(reviewers).toBeDefined(); expect(reviewers).toBeDefined();
expect(reviewers!.parallel).toBeDefined(); expect(reviewers!.parallel).toBeDefined();
expect(reviewers!.parallel!.length).toBe(4); expect(reviewers!.parallel!.length).toBe(4);
}); });
it('should have arch-review, frontend-review, security-review, qa-review as sub-steps', () => { it('should have arch-review, frontend-review, security-review, qa-review as sub-movements', () => {
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); const subNames = reviewers!.parallel!.map((s) => s.name);
expect(subNames).toContain('arch-review'); expect(subNames).toContain('arch-review');
expect(subNames).toContain('frontend-review'); expect(subNames).toContain('frontend-review');
@ -37,16 +37,16 @@ describe('expert workflow parallel structure', () => {
expect(subNames).toContain('qa-review'); expect(subNames).toContain('qa-review');
}); });
it('should have aggregate rules on reviewers step', () => { it('should have aggregate rules on reviewers movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
expect(reviewers!.rules).toBeDefined(); expect(reviewers!.rules).toBeDefined();
const conditions = reviewers!.rules!.map((r) => r.condition); const conditions = reviewers!.rules!.map((r) => r.condition);
expect(conditions).toContain('all("approved")'); expect(conditions).toContain('all("approved")');
expect(conditions).toContain('any("needs_fix")'); expect(conditions).toContain('any("needs_fix")');
}); });
it('should have simple approved/needs_fix rules on each sub-step', () => { it('should have simple approved/needs_fix rules on each sub-movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
for (const sub of reviewers!.parallel!) { for (const sub of reviewers!.parallel!) {
expect(sub.rules).toBeDefined(); expect(sub.rules).toBeDefined();
const conditions = sub.rules!.map((r) => r.condition); const conditions = sub.rules!.map((r) => r.condition);
@ -56,39 +56,39 @@ describe('expert workflow parallel structure', () => {
}); });
it('should route ai_review to reviewers', () => { 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(); expect(aiReview).toBeDefined();
const approvedRule = aiReview!.rules!.find((r) => r.next === 'reviewers'); const approvedRule = aiReview!.rules!.find((r) => r.next === 'reviewers');
expect(approvedRule).toBeDefined(); expect(approvedRule).toBeDefined();
}); });
it('should have a unified fix step routing back to reviewers', () => { it('should have a unified fix movement routing back to reviewers', () => {
const fix = workflow!.steps.find((s) => s.name === 'fix'); const fix = workflow!.movements.find((s) => s.name === 'fix');
expect(fix).toBeDefined(); expect(fix).toBeDefined();
const fixComplete = fix!.rules!.find((r) => r.next === 'reviewers'); const fixComplete = fix!.rules!.find((r) => r.next === 'reviewers');
expect(fixComplete).toBeDefined(); expect(fixComplete).toBeDefined();
}); });
it('should not have individual review/fix steps', () => { it('should not have individual review/fix movements', () => {
const stepNames = workflow!.steps.map((s) => s.name); const movementNames = workflow!.movements.map((s) => s.name);
expect(stepNames).not.toContain('architect_review'); expect(movementNames).not.toContain('architect_review');
expect(stepNames).not.toContain('fix_architect'); expect(movementNames).not.toContain('fix_architect');
expect(stepNames).not.toContain('frontend_review'); expect(movementNames).not.toContain('frontend_review');
expect(stepNames).not.toContain('fix_frontend'); expect(movementNames).not.toContain('fix_frontend');
expect(stepNames).not.toContain('security_review'); expect(movementNames).not.toContain('security_review');
expect(stepNames).not.toContain('fix_security'); expect(movementNames).not.toContain('fix_security');
expect(stepNames).not.toContain('qa_review'); expect(movementNames).not.toContain('qa_review');
expect(stepNames).not.toContain('fix_qa'); expect(movementNames).not.toContain('fix_qa');
}); });
it('should route reviewers all("approved") to supervise', () => { 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")'); const approvedRule = reviewers!.rules!.find((r) => r.condition === 'all("approved")');
expect(approvedRule!.next).toBe('supervise'); expect(approvedRule!.next).toBe('supervise');
}); });
it('should route reviewers any("needs_fix") to fix', () => { 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")'); const needsFixRule = reviewers!.rules!.find((r) => r.condition === 'any("needs_fix")');
expect(needsFixRule!.next).toBe('fix'); expect(needsFixRule!.next).toBe('fix');
}); });
@ -102,15 +102,15 @@ describe('expert-cqrs workflow parallel structure', () => {
expect(workflow!.name).toBe('expert-cqrs'); expect(workflow!.name).toBe('expert-cqrs');
}); });
it('should have a reviewers parallel step', () => { it('should have a reviewers parallel movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
expect(reviewers).toBeDefined(); expect(reviewers).toBeDefined();
expect(reviewers!.parallel).toBeDefined(); expect(reviewers!.parallel).toBeDefined();
expect(reviewers!.parallel!.length).toBe(4); expect(reviewers!.parallel!.length).toBe(4);
}); });
it('should have cqrs-es-review instead of arch-review', () => { 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); const subNames = reviewers!.parallel!.map((s) => s.name);
expect(subNames).toContain('cqrs-es-review'); expect(subNames).toContain('cqrs-es-review');
expect(subNames).not.toContain('arch-review'); expect(subNames).not.toContain('arch-review');
@ -119,16 +119,16 @@ describe('expert-cqrs workflow parallel structure', () => {
expect(subNames).toContain('qa-review'); expect(subNames).toContain('qa-review');
}); });
it('should have aggregate rules on reviewers step', () => { it('should have aggregate rules on reviewers movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
expect(reviewers!.rules).toBeDefined(); expect(reviewers!.rules).toBeDefined();
const conditions = reviewers!.rules!.map((r) => r.condition); const conditions = reviewers!.rules!.map((r) => r.condition);
expect(conditions).toContain('all("approved")'); expect(conditions).toContain('all("approved")');
expect(conditions).toContain('any("needs_fix")'); expect(conditions).toContain('any("needs_fix")');
}); });
it('should have simple approved/needs_fix rules on each sub-step', () => { it('should have simple approved/needs_fix rules on each sub-movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
for (const sub of reviewers!.parallel!) { for (const sub of reviewers!.parallel!) {
expect(sub.rules).toBeDefined(); expect(sub.rules).toBeDefined();
const conditions = sub.rules!.map((r) => r.condition); 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', () => { 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(); expect(aiReview).toBeDefined();
const approvedRule = aiReview!.rules!.find((r) => r.next === 'reviewers'); const approvedRule = aiReview!.rules!.find((r) => r.next === 'reviewers');
expect(approvedRule).toBeDefined(); expect(approvedRule).toBeDefined();
}); });
it('should have a unified fix step routing back to reviewers', () => { it('should have a unified fix movement routing back to reviewers', () => {
const fix = workflow!.steps.find((s) => s.name === 'fix'); const fix = workflow!.movements.find((s) => s.name === 'fix');
expect(fix).toBeDefined(); expect(fix).toBeDefined();
const fixComplete = fix!.rules!.find((r) => r.next === 'reviewers'); const fixComplete = fix!.rules!.find((r) => r.next === 'reviewers');
expect(fixComplete).toBeDefined(); expect(fixComplete).toBeDefined();
}); });
it('should not have individual review/fix steps', () => { it('should not have individual review/fix movements', () => {
const stepNames = workflow!.steps.map((s) => s.name); const movementNames = workflow!.movements.map((s) => s.name);
expect(stepNames).not.toContain('cqrs_es_review'); expect(movementNames).not.toContain('cqrs_es_review');
expect(stepNames).not.toContain('fix_cqrs_es'); expect(movementNames).not.toContain('fix_cqrs_es');
expect(stepNames).not.toContain('frontend_review'); expect(movementNames).not.toContain('frontend_review');
expect(stepNames).not.toContain('fix_frontend'); expect(movementNames).not.toContain('fix_frontend');
expect(stepNames).not.toContain('security_review'); expect(movementNames).not.toContain('security_review');
expect(stepNames).not.toContain('fix_security'); expect(movementNames).not.toContain('fix_security');
expect(stepNames).not.toContain('qa_review'); expect(movementNames).not.toContain('qa_review');
expect(stepNames).not.toContain('fix_qa'); expect(movementNames).not.toContain('fix_qa');
}); });
it('should use cqrs-es-reviewer agent for the first sub-step', () => { it('should use cqrs-es-reviewer agent for the first sub-movement', () => {
const reviewers = workflow!.steps.find((s) => s.name === 'reviewers'); const reviewers = workflow!.movements.find((s) => s.name === 'reviewers');
const cqrsReview = reviewers!.parallel!.find((s) => s.name === 'cqrs-es-review'); const cqrsReview = reviewers!.parallel!.find((s) => s.name === 'cqrs-es-review');
expect(cqrsReview!.agent).toContain('cqrs-es-reviewer'); expect(cqrsReview!.agent).toContain('cqrs-es-reviewer');
}); });

View File

@ -15,10 +15,10 @@ import {
const SAMPLE_WORKFLOW = `name: test-workflow const SAMPLE_WORKFLOW = `name: test-workflow
description: Test workflow description: Test workflow
initial_step: step1 initial_movement: step1
max_iterations: 1 max_iterations: 1
steps: movements:
- name: step1 - name: step1
agent: coder agent: coder
instruction: "{task}" instruction: "{task}"
@ -172,10 +172,10 @@ describe('loadAllWorkflows with project-local', () => {
const overrideWorkflow = `name: project-override const overrideWorkflow = `name: project-override
description: Project override description: Project override
initial_step: step1 initial_movement: step1
max_iterations: 1 max_iterations: 1
steps: movements:
- name: step1 - name: step1
agent: coder agent: coder
instruction: "{task}" instruction: "{task}"

View File

@ -13,6 +13,7 @@ import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js'; import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js'; import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
import { createLogger } from '../shared/utils/index.js'; import { createLogger } from '../shared/utils/index.js';
import { loadTemplate } from '../shared/prompts/index.js';
import type { RunAgentOptions } from './types.js'; import type { RunAgentOptions } from './types.js';
// Re-export for backward compatibility // Re-export for backward compatibility
@ -192,8 +193,11 @@ export class AgentRunner {
}); });
// 1. If agentPath is provided (resolved file exists), load prompt from file // 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) { 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 providerType = AgentRunner.resolveProvider(options.cwd, options);
const provider = getProvider(providerType); const provider = getProvider(providerType);
return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt)); return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt));

View File

@ -3,7 +3,7 @@
*/ */
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/index.js'; 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 }; export type { StreamCallback };
@ -26,4 +26,6 @@ export interface RunAgentOptions {
onAskUserQuestion?: AskUserQuestionHandler; onAskUserQuestion?: AskUserQuestionHandler;
/** Bypass all permission checks (sacrifice-my-pc mode) */ /** Bypass all permission checks (sacrifice-my-pc mode) */
bypassPermissions?: boolean; bypassPermissions?: boolean;
/** Language for template resolution */
language?: Language;
} }

View File

@ -1,13 +1,14 @@
/** /**
* CLI subcommand definitions * 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 { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/index.js';
import { success } from '../../shared/ui/index.js'; import { success } from '../../shared/ui/index.js';
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js'; import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/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 { program, resolvedCwd } from './program.js';
import { resolveAgentOverrides } from './helpers.js'; import { resolveAgentOverrides } from './helpers.js';
@ -72,3 +73,11 @@ program
.action(async (key?: string) => { .action(async (key?: string) => {
await switchConfig(resolvedCwd, key); 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);
});

View File

@ -9,6 +9,7 @@ export type {
AgentResponse, AgentResponse,
SessionState, SessionState,
WorkflowRule, WorkflowRule,
WorkflowMovement,
WorkflowStep, WorkflowStep,
LoopDetectionConfig, LoopDetectionConfig,
WorkflowConfig, WorkflowConfig,

View File

@ -103,7 +103,7 @@ export const ReportFieldSchema = z.union([
export const WorkflowRuleSchema = z.object({ export const WorkflowRuleSchema = z.object({
/** Human-readable condition text */ /** Human-readable condition text */
condition: z.string().min(1), 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(), next: z.string().min(1).optional(),
/** Template for additional AI output */ /** Template for additional AI output */
appendix: z.string().optional(), appendix: z.string().optional(),
@ -113,8 +113,8 @@ export const WorkflowRuleSchema = z.object({
interactive_only: z.boolean().optional(), interactive_only: z.boolean().optional(),
}); });
/** Sub-step schema for parallel execution */ /** Sub-movement schema for parallel execution */
export const ParallelSubStepRawSchema = z.object({ export const ParallelSubMovementRawSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
agent: z.string().optional(), agent: z.string().optional(),
agent_name: 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), pass_previous_response: z.boolean().optional().default(true),
}); });
/** Workflow step schema - raw YAML format */ /** Workflow movement schema - raw YAML format */
export const WorkflowStepRawSchema = z.object({ export const WorkflowMovementRawSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(), 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(), agent: z.string().optional(),
/** Session handling for this step */ /** Session handling for this movement */
session: z.enum(['continue', 'refresh']).optional(), session: z.enum(['continue', 'refresh']).optional(),
/** Display name for the agent (shown in output). Falls back to agent basename if not specified */ /** Display name for the agent (shown in output). Falls back to agent basename if not specified */
agent_name: z.string().optional(), agent_name: z.string().optional(),
allowed_tools: z.array(z.string()).optional(), allowed_tools: z.array(z.string()).optional(),
provider: z.enum(['claude', 'codex', 'mock']).optional(), provider: z.enum(['claude', 'codex', 'mock']).optional(),
model: z.string().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(), 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(), edit: z.boolean().optional(),
instruction: z.string().optional(), instruction: z.string().optional(),
instruction_template: z.string().optional(), instruction_template: z.string().optional(),
/** Rules for step routing */ /** Rules for movement routing */
rules: z.array(WorkflowRuleSchema).optional(), rules: z.array(WorkflowRuleSchema).optional(),
/** Report file(s) for this step */ /** Report file(s) for this movement */
report: ReportFieldSchema.optional(), report: ReportFieldSchema.optional(),
pass_previous_response: z.boolean().optional().default(true), pass_previous_response: z.boolean().optional().default(true),
/** Sub-steps to execute in parallel */ /** Sub-movements to execute in parallel */
parallel: z.array(ParallelSubStepRawSchema).optional(), 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({ export const WorkflowConfigRawSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(), 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(), initial_step: z.string().optional(),
max_iterations: z.number().int().positive().optional().default(10), max_iterations: z.number().int().positive().optional().default(10),
answer_agent: z.string().optional(), 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 */ /** Custom agent configuration schema */
export const CustomAgentConfigSchema = z.object({ export const CustomAgentConfigSchema = z.object({

View File

@ -28,6 +28,7 @@ export type {
WorkflowRule, WorkflowRule,
ReportConfig, ReportConfig,
ReportObjectConfig, ReportObjectConfig,
WorkflowMovement,
WorkflowStep, WorkflowStep,
LoopDetectionConfig, LoopDetectionConfig,
WorkflowConfig, WorkflowConfig,

View File

@ -9,7 +9,7 @@ import type { AgentResponse } from './response.js';
export interface WorkflowRule { export interface WorkflowRule {
/** Human-readable condition text */ /** Human-readable condition text */
condition: string; 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; next?: string;
/** Template for additional AI output */ /** Template for additional AI output */
appendix?: string; appendix?: string;
@ -23,16 +23,16 @@ export interface WorkflowRule {
aiConditionText?: string; aiConditionText?: string;
/** Whether this condition uses all()/any() aggregate expression (set by loader) */ /** Whether this condition uses all()/any() aggregate expression (set by loader) */
isAggregateCondition?: boolean; 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'; aggregateType?: 'all' | 'any';
/** The condition text(s) inside all("...")/any("...") to match against sub-step results (set by loader). /** The condition text(s) inside all("...")/any("...") to match against sub-movement results (set by loader).
* - string: all sub-steps must match this single condition (e.g., all("approved")) * - string: all sub-movements 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")) * - string[]: each sub-movement must match the corresponding condition by index (e.g., all("A", "B"))
*/ */
aggregateConditionText?: string | string[]; 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 { export interface ReportConfig {
/** Display label (e.g., "Scope", "Decisions") */ /** Display label (e.g., "Scope", "Decisions") */
label: string; label: string;
@ -50,39 +50,42 @@ export interface ReportObjectConfig {
format?: string; format?: string;
} }
/** Single step in a workflow */ /** Single movement in a workflow */
export interface WorkflowStep { export interface WorkflowMovement {
name: string; name: string;
/** Brief description of this step's role in the workflow */ /** Brief description of this movement's role in the workflow */
description?: string; 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; agent?: string;
/** Session handling for this step */ /** Session handling for this movement */
session?: 'continue' | 'refresh'; session?: 'continue' | 'refresh';
/** Display name for the agent (shown in output). Falls back to agent basename if not specified */ /** Display name for the agent (shown in output). Falls back to agent basename if not specified */
agentDisplayName: string; agentDisplayName: string;
/** Allowed tools for this step (optional, passed to agent execution) */ /** Allowed tools for this movement (optional, passed to agent execution) */
allowedTools?: string[]; allowedTools?: string[];
/** Resolved absolute path to agent prompt file (set by loader) */ /** Resolved absolute path to agent prompt file (set by loader) */
agentPath?: string; agentPath?: string;
/** Provider override for this step */ /** Provider override for this movement */
provider?: 'claude' | 'codex' | 'mock'; provider?: 'claude' | 'codex' | 'mock';
/** Model override for this step */ /** Model override for this movement */
model?: string; model?: string;
/** Permission mode for tool execution in this step */ /** Permission mode for tool execution in this movement */
permissionMode?: PermissionMode; 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; edit?: boolean;
instructionTemplate: string; instructionTemplate: string;
/** Rules for step routing */ /** Rules for movement routing */
rules?: WorkflowRule[]; rules?: WorkflowRule[];
/** Report file configuration. Single string, array of label:path, or object with order/format. */ /** Report file configuration. Single string, array of label:path, or object with order/format. */
report?: string | ReportConfig[] | ReportObjectConfig; report?: string | ReportConfig[] | ReportObjectConfig;
passPreviousResponse: boolean; passPreviousResponse: boolean;
/** Sub-steps to execute in parallel. When set, this step runs all sub-steps concurrently. */ /** Sub-movements to execute in parallel. When set, this movement runs all sub-movements concurrently. */
parallel?: WorkflowStep[]; parallel?: WorkflowMovement[];
} }
/** @deprecated Use WorkflowMovement instead */
export type WorkflowStep = WorkflowMovement;
/** Loop detection configuration */ /** Loop detection configuration */
export interface LoopDetectionConfig { export interface LoopDetectionConfig {
/** Maximum consecutive runs of the same step before triggering (default: 10) */ /** Maximum consecutive runs of the same step before triggering (default: 10) */
@ -95,8 +98,8 @@ export interface LoopDetectionConfig {
export interface WorkflowConfig { export interface WorkflowConfig {
name: string; name: string;
description?: string; description?: string;
steps: WorkflowStep[]; movements: WorkflowMovement[];
initialStep: string; initialMovement: string;
maxIterations: number; maxIterations: number;
/** Loop detection settings */ /** Loop detection settings */
loopDetection?: LoopDetectionConfig; loopDetection?: LoopDetectionConfig;
@ -111,12 +114,12 @@ export interface WorkflowConfig {
/** Runtime state of a workflow execution */ /** Runtime state of a workflow execution */
export interface WorkflowState { export interface WorkflowState {
workflowName: string; workflowName: string;
currentStep: string; currentMovement: string;
iteration: number; iteration: number;
stepOutputs: Map<string, AgentResponse>; movementOutputs: Map<string, AgentResponse>;
userInputs: string[]; userInputs: string[];
agentSessions: Map<string, string>; agentSessions: Map<string, string>;
/** Per-step iteration counters (how many times each step has been executed) */ /** Per-movement iteration counters (how many times each movement has been executed) */
stepIterations: Map<string, number>; movementIterations: Map<string, number>;
status: 'running' | 'completed' | 'aborted'; status: 'running' | 'completed' | 'aborted';
} }

View File

@ -2,12 +2,17 @@
* Workflow engine constants * Workflow engine constants
* *
* Contains all constants used by the workflow engine including * 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 */ /** Special movement names for workflow termination */
export const COMPLETE_STEP = 'COMPLETE'; export const COMPLETE_MOVEMENT = 'COMPLETE';
export const ABORT_STEP = 'ABORT'; 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 */ /** Maximum user inputs to store */
export const MAX_USER_INPUTS = 100; export const MAX_USER_INPUTS = 100;
@ -15,9 +20,9 @@ export const MAX_INPUT_LENGTH = 10000;
/** Error messages */ /** Error messages */
export const ERROR_MESSAGES = { export const ERROR_MESSAGES = {
LOOP_DETECTED: (stepName: string, count: number) => LOOP_DETECTED: (movementName: string, count: number) =>
`Loop detected: step "${stepName}" ran ${count} times consecutively without progress.`, `Loop detected: movement "${movementName}" ran ${count} times consecutively without progress.`,
UNKNOWN_STEP: (stepName: string) => `Unknown step: ${stepName}`, UNKNOWN_MOVEMENT: (movementName: string) => `Unknown movement: ${movementName}`,
STEP_EXECUTION_FAILED: (message: string) => `Step execution failed: ${message}`, MOVEMENT_EXECUTION_FAILED: (message: string) => `Movement execution failed: ${message}`,
MAX_ITERATIONS_REACHED: 'Max iterations reached', MAX_ITERATIONS_REACHED: 'Max iterations reached',
}; };

View File

@ -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 1: Main agent execution (with tools)
* Phase 2: Report output (Write-only, optional) * Phase 2: Report output (Write-only, optional)
@ -9,7 +9,7 @@
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import type { import type {
WorkflowStep, WorkflowMovement,
WorkflowState, WorkflowState,
AgentResponse, AgentResponse,
Language, Language,
@ -19,49 +19,49 @@ import { runAgent } from '../../../agents/runner.js';
import { InstructionBuilder, isReportObjectConfig } from '../instruction/InstructionBuilder.js'; import { InstructionBuilder, isReportObjectConfig } from '../instruction/InstructionBuilder.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
import { detectMatchedRule } from '../evaluation/index.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 { createLogger } from '../../../shared/utils/index.js';
import type { OptionsBuilder } from './OptionsBuilder.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 optionsBuilder: OptionsBuilder;
readonly getCwd: () => string; readonly getCwd: () => string;
readonly getProjectCwd: () => string; readonly getProjectCwd: () => string;
readonly getReportDir: () => string; readonly getReportDir: () => string;
readonly getLanguage: () => Language | undefined; readonly getLanguage: () => Language | undefined;
readonly getInteractive: () => boolean; readonly getInteractive: () => boolean;
readonly getWorkflowSteps: () => ReadonlyArray<{ name: string; description?: string }>; readonly getWorkflowMovements: () => ReadonlyArray<{ name: string; description?: string }>;
readonly detectRuleIndex: (content: string, stepName: string) => number; readonly detectRuleIndex: (content: string, movementName: string) => number;
readonly callAiJudge: ( readonly callAiJudge: (
agentOutput: string, agentOutput: string,
conditions: Array<{ index: number; text: string }>, conditions: Array<{ index: number; text: string }>,
options: { cwd: string } options: { cwd: string }
) => Promise<number>; ) => Promise<number>;
readonly onPhaseStart?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; readonly onPhaseStart?: (step: WorkflowMovement, 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 onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void;
} }
export class StepExecutor { export class MovementExecutor {
constructor( constructor(
private readonly deps: StepExecutorDeps, private readonly deps: MovementExecutorDeps,
) {} ) {}
/** Build Phase 1 instruction from template */ /** Build Phase 1 instruction from template */
buildInstruction( buildInstruction(
step: WorkflowStep, step: WorkflowMovement,
stepIteration: number, movementIteration: number,
state: WorkflowState, state: WorkflowState,
task: string, task: string,
maxIterations: number, maxIterations: number,
): string { ): string {
const workflowSteps = this.deps.getWorkflowSteps(); const workflowMovements = this.deps.getWorkflowMovements();
return new InstructionBuilder(step, { return new InstructionBuilder(step, {
task, task,
iteration: state.iteration, iteration: state.iteration,
maxIterations, maxIterations,
stepIteration, movementIteration,
cwd: this.deps.getCwd(), cwd: this.deps.getCwd(),
projectCwd: this.deps.getProjectCwd(), projectCwd: this.deps.getProjectCwd(),
userInputs: state.userInputs, userInputs: state.userInputs,
@ -69,39 +69,39 @@ export class StepExecutor {
reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()), reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()),
language: this.deps.getLanguage(), language: this.deps.getLanguage(),
interactive: this.deps.getInteractive(), interactive: this.deps.getInteractive(),
workflowSteps, workflowMovements: workflowMovements,
currentStepIndex: workflowSteps.findIndex(s => s.name === step.name), currentMovementIndex: workflowMovements.findIndex(s => s.name === step.name),
}).build(); }).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) * Returns the final response (with matchedRuleIndex if a rule matched)
* and the instruction used for Phase 1. * and the instruction used for Phase 1.
*/ */
async runNormalStep( async runNormalMovement(
step: WorkflowStep, step: WorkflowMovement,
state: WorkflowState, state: WorkflowState,
task: string, task: string,
maxIterations: number, maxIterations: number,
updateAgentSession: (agent: string, sessionId: string | undefined) => void, updateAgentSession: (agent: string, sessionId: string | undefined) => void,
prebuiltInstruction?: string, prebuiltInstruction?: string,
): Promise<{ response: AgentResponse; instruction: string }> { ): Promise<{ response: AgentResponse; instruction: string }> {
const stepIteration = prebuiltInstruction const movementIteration = prebuiltInstruction
? state.stepIterations.get(step.name) ?? 1 ? state.movementIterations.get(step.name) ?? 1
: incrementStepIteration(state, step.name); : incrementMovementIteration(state, step.name);
const instruction = prebuiltInstruction ?? this.buildInstruction(step, stepIteration, state, task, maxIterations); const instruction = prebuiltInstruction ?? this.buildInstruction(step, movementIteration, state, task, maxIterations);
const sessionKey = step.agent ?? step.name; const sessionKey = step.agent ?? step.name;
log.debug('Running step', { log.debug('Running movement', {
step: step.name, movement: step.name,
agent: step.agent ?? '(none)', agent: step.agent ?? '(none)',
stepIteration, movementIteration,
iteration: state.iteration, iteration: state.iteration,
sessionId: state.agentSessions.get(sessionKey) ?? 'new', 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); this.deps.onPhaseStart?.(step, 1, 'execute', instruction);
const agentOptions = this.deps.optionsBuilder.buildAgentOptions(step); const agentOptions = this.deps.optionsBuilder.buildAgentOptions(step);
let response = await runAgent(step.agent, instruction, agentOptions); let response = await runAgent(step.agent, instruction, agentOptions);
@ -112,7 +112,7 @@ export class StepExecutor {
// Phase 2: report output (resume same session, Write only) // Phase 2: report output (resume same session, Write only)
if (step.report) { if (step.report) {
await runReportPhase(step, stepIteration, phaseCtx); await runReportPhase(step, movementIteration, phaseCtx);
} }
// Phase 3: status judgment (resume session, no tools, output status tag) // Phase 3: status judgment (resume session, no tools, output status tag)
@ -129,17 +129,17 @@ export class StepExecutor {
callAiJudge: this.deps.callAiJudge, callAiJudge: this.deps.callAiJudge,
}); });
if (match) { 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 }; response = { ...response, matchedRuleIndex: match.index, matchedRuleMethod: match.method };
} }
state.stepOutputs.set(step.name, response); state.movementOutputs.set(step.name, response);
this.emitStepReports(step); this.emitMovementReports(step);
return { response, instruction }; return { response, instruction };
} }
/** Emit step:report events for each report file that exists */ /** Collect movement:report events for each report file that exists */
emitStepReports(step: WorkflowStep): void { emitMovementReports(step: WorkflowMovement): void {
if (!step.report) return; if (!step.report) return;
const baseDir = join(this.deps.getProjectCwd(), this.deps.getReportDir()); 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) // 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 */ /** 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); const filePath = join(baseDir, fileName);
if (existsSync(filePath)) { if (existsSync(filePath)) {
this.reportFiles.push({ step, filePath, fileName }); this.reportFiles.push({ step, filePath, fileName });
} }
} }
/** Drain collected report files (called by engine after step execution) */ /** Drain collected report files (called by engine after movement execution) */
drainReportFiles(): Array<{ step: WorkflowStep; filePath: string; fileName: string }> { drainReportFiles(): Array<{ step: WorkflowMovement; filePath: string; fileName: string }> {
const files = this.reportFiles; const files = this.reportFiles;
this.reportFiles = []; this.reportFiles = [];
return files; return files;
} }
} }

View File

@ -6,7 +6,7 @@
*/ */
import { join } from 'node:path'; 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 { RunAgentOptions } from '../../../agents/runner.js';
import type { PhaseRunnerContext } from '../phase-runner.js'; import type { PhaseRunnerContext } from '../phase-runner.js';
import type { WorkflowEngineOptions, PhaseName } from '../types.js'; import type { WorkflowEngineOptions, PhaseName } from '../types.js';
@ -22,13 +22,14 @@ export class OptionsBuilder {
) {} ) {}
/** Build common RunAgentOptions shared by all phases */ /** Build common RunAgentOptions shared by all phases */
buildBaseOptions(step: WorkflowStep): RunAgentOptions { buildBaseOptions(step: WorkflowMovement): RunAgentOptions {
return { return {
cwd: this.getCwd(), cwd: this.getCwd(),
agentPath: step.agentPath, agentPath: step.agentPath,
provider: step.provider ?? this.engineOptions.provider, provider: step.provider ?? this.engineOptions.provider,
model: step.model ?? this.engineOptions.model, model: step.model ?? this.engineOptions.model,
permissionMode: step.permissionMode, permissionMode: step.permissionMode,
language: this.getLanguage(),
onStream: this.engineOptions.onStream, onStream: this.engineOptions.onStream,
onPermissionRequest: this.engineOptions.onPermissionRequest, onPermissionRequest: this.engineOptions.onPermissionRequest,
onAskUserQuestion: this.engineOptions.onAskUserQuestion, onAskUserQuestion: this.engineOptions.onAskUserQuestion,
@ -37,8 +38,8 @@ export class OptionsBuilder {
} }
/** Build RunAgentOptions for Phase 1 (main execution) */ /** Build RunAgentOptions for Phase 1 (main execution) */
buildAgentOptions(step: WorkflowStep): RunAgentOptions { buildAgentOptions(step: WorkflowMovement): RunAgentOptions {
// Phase 1: exclude Write from allowedTools when step has report config AND edit is NOT enabled // 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) // (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 // Note: edit defaults to undefined, so check !== true to catch both false and undefined
const allowedTools = step.report && step.edit !== true const allowedTools = step.report && step.edit !== true
@ -57,7 +58,7 @@ export class OptionsBuilder {
/** Build RunAgentOptions for session-resume phases (Phase 2, Phase 3) */ /** Build RunAgentOptions for session-resume phases (Phase 2, Phase 3) */
buildResumeOptions( buildResumeOptions(
step: WorkflowStep, step: WorkflowMovement,
sessionId: string, sessionId: string,
overrides: Pick<RunAgentOptions, 'allowedTools' | 'maxTurns'>, overrides: Pick<RunAgentOptions, 'allowedTools' | 'maxTurns'>,
): RunAgentOptions { ): RunAgentOptions {
@ -75,8 +76,8 @@ export class OptionsBuilder {
buildPhaseRunnerContext( buildPhaseRunnerContext(
state: WorkflowState, state: WorkflowState,
updateAgentSession: (agent: string, sessionId: string | undefined) => void, updateAgentSession: (agent: string, sessionId: string | undefined) => void,
onPhaseStart?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void, onPhaseStart?: (step: WorkflowMovement, 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, onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void,
): PhaseRunnerContext { ): PhaseRunnerContext {
return { return {
cwd: this.getCwd(), cwd: this.getCwd(),

View File

@ -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 * 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 { import type {
WorkflowStep, WorkflowMovement,
WorkflowState, WorkflowState,
AgentResponse, AgentResponse,
} from '../../models/types.js'; } from '../../models/types.js';
@ -14,29 +14,29 @@ import { runAgent } from '../../../agents/runner.js';
import { ParallelLogger } from './parallel-logger.js'; import { ParallelLogger } from './parallel-logger.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
import { detectMatchedRule } from '../evaluation/index.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 { createLogger } from '../../../shared/utils/index.js';
import type { OptionsBuilder } from './OptionsBuilder.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'; import type { WorkflowEngineOptions, PhaseName } from '../types.js';
const log = createLogger('parallel-runner'); const log = createLogger('parallel-runner');
export interface ParallelRunnerDeps { export interface ParallelRunnerDeps {
readonly optionsBuilder: OptionsBuilder; readonly optionsBuilder: OptionsBuilder;
readonly stepExecutor: StepExecutor; readonly movementExecutor: MovementExecutor;
readonly engineOptions: WorkflowEngineOptions; readonly engineOptions: WorkflowEngineOptions;
readonly getCwd: () => string; readonly getCwd: () => string;
readonly getReportDir: () => string; readonly getReportDir: () => string;
readonly getInteractive: () => boolean; readonly getInteractive: () => boolean;
readonly detectRuleIndex: (content: string, stepName: string) => number; readonly detectRuleIndex: (content: string, movementName: string) => number;
readonly callAiJudge: ( readonly callAiJudge: (
agentOutput: string, agentOutput: string,
conditions: Array<{ index: number; text: string }>, conditions: Array<{ index: number; text: string }>,
options: { cwd: string } options: { cwd: string }
) => Promise<number>; ) => Promise<number>;
readonly onPhaseStart?: (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; readonly onPhaseStart?: (step: WorkflowMovement, 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 onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void;
} }
export class ParallelRunner { export class ParallelRunner {
@ -45,28 +45,28 @@ export class ParallelRunner {
) {} ) {}
/** /**
* Run a parallel step: execute all sub-steps concurrently, then aggregate results. * Run a parallel movement: execute all sub-movements concurrently, then aggregate results.
* The aggregated output becomes the parent step's response for rules evaluation. * The aggregated output becomes the parent movement's response for rules evaluation.
*/ */
async runParallelStep( async runParallelMovement(
step: WorkflowStep, step: WorkflowMovement,
state: WorkflowState, state: WorkflowState,
task: string, task: string,
maxIterations: number, maxIterations: number,
updateAgentSession: (agent: string, sessionId: string | undefined) => void, updateAgentSession: (agent: string, sessionId: string | undefined) => void,
): Promise<{ response: AgentResponse; instruction: string }> { ): Promise<{ response: AgentResponse; instruction: string }> {
const subSteps = step.parallel!; const subMovements = step.parallel!;
const stepIteration = incrementStepIteration(state, step.name); const movementIteration = incrementMovementIteration(state, step.name);
log.debug('Running parallel step', { log.debug('Running parallel movement', {
step: step.name, movement: step.name,
subSteps: subSteps.map(s => s.name), subMovements: subMovements.map(s => s.name),
stepIteration, movementIteration,
}); });
// Create parallel logger for prefixed output (only when streaming is enabled) // Create parallel logger for prefixed output (only when streaming is enabled)
const parallelLogger = this.deps.engineOptions.onStream const parallelLogger = this.deps.engineOptions.onStream
? new ParallelLogger({ ? new ParallelLogger({
subStepNames: subSteps.map((s) => s.name), subMovementNames: subMovements.map((s) => s.name),
parentOnStream: this.deps.engineOptions.onStream, parentOnStream: this.deps.engineOptions.onStream,
}) })
: undefined; : undefined;
@ -80,46 +80,46 @@ export class ParallelRunner {
callAiJudge: this.deps.callAiJudge, callAiJudge: this.deps.callAiJudge,
}; };
// Run all sub-steps concurrently // Run all sub-movements concurrently
const subResults = await Promise.all( const subResults = await Promise.all(
subSteps.map(async (subStep, index) => { subMovements.map(async (subMovement, index) => {
const subIteration = incrementStepIteration(state, subStep.name); const subIteration = incrementMovementIteration(state, subMovement.name);
const subInstruction = this.deps.stepExecutor.buildInstruction(subStep, subIteration, state, task, maxIterations); const subInstruction = this.deps.movementExecutor.buildInstruction(subMovement, subIteration, state, task, maxIterations);
// Phase 1: main execution (Write excluded if sub-step has report) // Phase 1: main execution (Write excluded if sub-movement has report)
const baseOptions = this.deps.optionsBuilder.buildAgentOptions(subStep); const baseOptions = this.deps.optionsBuilder.buildAgentOptions(subMovement);
// Override onStream with parallel logger's prefixed handler (immutable) // Override onStream with parallel logger's prefixed handler (immutable)
const agentOptions = parallelLogger const agentOptions = parallelLogger
? { ...baseOptions, onStream: parallelLogger.createStreamHandler(subStep.name, index) } ? { ...baseOptions, onStream: parallelLogger.createStreamHandler(subMovement.name, index) }
: baseOptions; : baseOptions;
const subSessionKey = subStep.agent ?? subStep.name; const subSessionKey = subMovement.agent ?? subMovement.name;
this.deps.onPhaseStart?.(subStep, 1, 'execute', subInstruction); this.deps.onPhaseStart?.(subMovement, 1, 'execute', subInstruction);
const subResponse = await runAgent(subStep.agent, subInstruction, agentOptions); const subResponse = await runAgent(subMovement.agent, subInstruction, agentOptions);
updateAgentSession(subSessionKey, subResponse.sessionId); 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 // Phase 2: report output for sub-movement
if (subStep.report) { if (subMovement.report) {
await runReportPhase(subStep, subIteration, phaseCtx); await runReportPhase(subMovement, subIteration, phaseCtx);
} }
// Phase 3: status judgment for sub-step // Phase 3: status judgment for sub-movement
let subTagContent = ''; let subTagContent = '';
if (needsStatusJudgmentPhase(subStep)) { if (needsStatusJudgmentPhase(subMovement)) {
subTagContent = await runStatusJudgmentPhase(subStep, phaseCtx); 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 const finalResponse = match
? { ...subResponse, matchedRuleIndex: match.index, matchedRuleMethod: match.method } ? { ...subResponse, matchedRuleIndex: match.index, matchedRuleMethod: match.method }
: subResponse; : subResponse;
state.stepOutputs.set(subStep.name, finalResponse); state.movementOutputs.set(subMovement.name, finalResponse);
this.deps.stepExecutor.emitStepReports(subStep); 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( parallelLogger.printSummary(
step.name, step.name,
subResults.map((r) => ({ subResults.map((r) => ({
name: r.subStep.name, name: r.subMovement.name,
condition: r.response.matchedRuleIndex != null && r.subStep.rules condition: r.response.matchedRuleIndex != null && r.subMovement.rules
? r.subStep.rules[r.response.matchedRuleIndex]?.condition ? r.subMovement.rules[r.response.matchedRuleIndex]?.condition
: undefined, : undefined,
})), })),
); );
} }
// Aggregate sub-step outputs into parent step's response // Aggregate sub-movement outputs into parent movement's response
const aggregatedContent = subResults 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'); .join('\n\n---\n\n');
const aggregatedInstruction = subResults const aggregatedInstruction = subResults
.map((r) => r.instruction) .map((r) => r.instruction)
.join('\n\n'); .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 match = await detectMatchedRule(step, aggregatedContent, '', ruleCtx);
const aggregatedResponse: AgentResponse = { const aggregatedResponse: AgentResponse = {
@ -156,8 +156,8 @@ export class ParallelRunner {
...(match && { matchedRuleIndex: match.index, matchedRuleMethod: match.method }), ...(match && { matchedRuleIndex: match.index, matchedRuleMethod: match.method }),
}; };
state.stepOutputs.set(step.name, aggregatedResponse); state.movementOutputs.set(step.name, aggregatedResponse);
this.deps.stepExecutor.emitStepReports(step); this.deps.movementExecutor.emitMovementReports(step);
return { response: aggregatedResponse, instruction: aggregatedInstruction }; return { response: aggregatedResponse, instruction: aggregatedInstruction };
} }

View File

@ -1,9 +1,9 @@
/** /**
* Workflow execution engine. * Workflow execution engine.
* *
* Orchestrates the main execution loop: step transitions, abort handling, * Orchestrates the main execution loop: movement transitions, abort handling,
* loop detection, and iteration limits. Delegates step execution to * loop detection, and iteration limits. Delegates movement execution to
* StepExecutor (normal steps) and ParallelRunner (parallel steps). * MovementExecutor (normal movements) and ParallelRunner (parallel movements).
*/ */
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
@ -12,22 +12,22 @@ import { join } from 'node:path';
import type { import type {
WorkflowConfig, WorkflowConfig,
WorkflowState, WorkflowState,
WorkflowStep, WorkflowMovement,
AgentResponse, AgentResponse,
} from '../../models/types.js'; } 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 type { WorkflowEngineOptions } from '../types.js';
import { determineNextStepByRules } from './transitions.js'; import { determineNextMovementByRules } from './transitions.js';
import { LoopDetector } from './loop-detector.js'; import { LoopDetector } from './loop-detector.js';
import { handleBlocked } from './blocked-handler.js'; import { handleBlocked } from './blocked-handler.js';
import { import {
createInitialState, createInitialState,
addUserInput as addUserInputToState, addUserInput as addUserInputToState,
incrementStepIteration, incrementMovementIteration,
} from './state-manager.js'; } from './state-manager.js';
import { generateReportDir, getErrorMessage, createLogger } from '../../../shared/utils/index.js'; import { generateReportDir, getErrorMessage, createLogger } from '../../../shared/utils/index.js';
import { OptionsBuilder } from './OptionsBuilder.js'; import { OptionsBuilder } from './OptionsBuilder.js';
import { StepExecutor } from './StepExecutor.js'; import { MovementExecutor } from './MovementExecutor.js';
import { ParallelRunner } from './ParallelRunner.js'; import { ParallelRunner } from './ParallelRunner.js';
const log = createLogger('engine'); const log = createLogger('engine');
@ -41,7 +41,7 @@ export type {
IterationLimitCallback, IterationLimitCallback,
WorkflowEngineOptions, WorkflowEngineOptions,
} from '../types.js'; } 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 */ /** Workflow engine for orchestrating agent execution */
export class WorkflowEngine extends EventEmitter { export class WorkflowEngine extends EventEmitter {
@ -56,9 +56,9 @@ export class WorkflowEngine extends EventEmitter {
private abortRequested = false; private abortRequested = false;
private readonly optionsBuilder: OptionsBuilder; private readonly optionsBuilder: OptionsBuilder;
private readonly stepExecutor: StepExecutor; private readonly movementExecutor: MovementExecutor;
private readonly parallelRunner: ParallelRunner; private readonly parallelRunner: ParallelRunner;
private readonly detectRuleIndex: (content: string, stepName: string) => number; private readonly detectRuleIndex: (content: string, movementName: string) => number;
private readonly callAiJudge: ( private readonly callAiJudge: (
agentOutput: string, agentOutput: string,
conditions: Array<{ index: number; text: string }>, conditions: Array<{ index: number; text: string }>,
@ -94,14 +94,14 @@ export class WorkflowEngine extends EventEmitter {
() => this.options.language, () => this.options.language,
); );
this.stepExecutor = new StepExecutor({ this.movementExecutor = new MovementExecutor({
optionsBuilder: this.optionsBuilder, optionsBuilder: this.optionsBuilder,
getCwd: () => this.cwd, getCwd: () => this.cwd,
getProjectCwd: () => this.projectCwd, getProjectCwd: () => this.projectCwd,
getReportDir: () => this.reportDir, getReportDir: () => this.reportDir,
getLanguage: () => this.options.language, getLanguage: () => this.options.language,
getInteractive: () => this.options.interactive === true, 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, detectRuleIndex: this.detectRuleIndex,
callAiJudge: this.callAiJudge, callAiJudge: this.callAiJudge,
onPhaseStart: (step, phase, phaseName, instruction) => { onPhaseStart: (step, phase, phaseName, instruction) => {
@ -114,7 +114,7 @@ export class WorkflowEngine extends EventEmitter {
this.parallelRunner = new ParallelRunner({ this.parallelRunner = new ParallelRunner({
optionsBuilder: this.optionsBuilder, optionsBuilder: this.optionsBuilder,
stepExecutor: this.stepExecutor, movementExecutor: this.movementExecutor,
engineOptions: this.options, engineOptions: this.options,
getCwd: () => this.cwd, getCwd: () => this.cwd,
getReportDir: () => this.reportDir, getReportDir: () => this.reportDir,
@ -131,8 +131,8 @@ export class WorkflowEngine extends EventEmitter {
log.debug('WorkflowEngine initialized', { log.debug('WorkflowEngine initialized', {
workflow: config.name, workflow: config.name,
steps: config.steps.map(s => s.name), movements: config.movements.map(s => s.name),
initialStep: config.initialStep, initialMovement: config.initialMovement,
maxIterations: config.maxIterations, maxIterations: config.maxIterations,
}); });
} }
@ -159,21 +159,21 @@ export class WorkflowEngine extends EventEmitter {
/** Validate workflow configuration at construction time */ /** Validate workflow configuration at construction time */
private validateConfig(): void { private validateConfig(): void {
const initialStep = this.config.steps.find((s) => s.name === this.config.initialStep); const initialMovement = this.config.movements.find((s) => s.name === this.config.initialMovement);
if (!initialStep) { if (!initialMovement) {
throw new Error(ERROR_MESSAGES.UNKNOWN_STEP(this.config.initialStep)); throw new Error(ERROR_MESSAGES.UNKNOWN_MOVEMENT(this.config.initialMovement));
} }
const stepNames = new Set(this.config.steps.map((s) => s.name)); const movementNames = new Set(this.config.movements.map((s) => s.name));
stepNames.add(COMPLETE_STEP); movementNames.add(COMPLETE_MOVEMENT);
stepNames.add(ABORT_STEP); movementNames.add(ABORT_MOVEMENT);
for (const step of this.config.steps) { for (const movement of this.config.movements) {
if (step.rules) { if (movement.rules) {
for (const rule of step.rules) { for (const rule of movement.rules) {
if (rule.next && !stepNames.has(rule.next)) { if (rule.next && !movementNames.has(rule.next)) {
throw new Error( 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; 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 { abort(): void {
if (this.abortRequested) return; if (this.abortRequested) return;
this.abortRequested = true; this.abortRequested = true;
@ -218,13 +218,13 @@ export class WorkflowEngine extends EventEmitter {
return this.abortRequested; return this.abortRequested;
} }
/** Get step by name */ /** Get movement by name */
private getStep(name: string): WorkflowStep { private getMovement(name: string): WorkflowMovement {
const step = this.config.steps.find((s) => s.name === name); const movement = this.config.movements.find((s) => s.name === name);
if (!step) { if (!movement) {
throw new Error(ERROR_MESSAGES.UNKNOWN_STEP(name)); throw new Error(ERROR_MESSAGES.UNKNOWN_MOVEMENT(name));
} }
return step; return movement;
} }
/** Update agent session and notify via callback if session changed */ /** 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 { private emitCollectedReports(): void {
for (const { step, filePath, fileName } of this.stepExecutor.drainReportFiles()) { for (const { step, filePath, fileName } of this.movementExecutor.drainReportFiles()) {
this.emit('step:report', step, filePath, fileName); this.emit('movement:report', step, filePath, fileName);
} }
} }
/** Run a single step (delegates to ParallelRunner if step has parallel sub-steps) */ /** Run a single movement (delegates to ParallelRunner if movement has parallel sub-movements) */
private async runStep(step: WorkflowStep, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> { private async runMovement(step: WorkflowMovement, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> {
const updateSession = this.updateAgentSession.bind(this); const updateSession = this.updateAgentSession.bind(this);
let result: { response: AgentResponse; instruction: string }; let result: { response: AgentResponse; instruction: string };
if (step.parallel && step.parallel.length > 0) { 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, step, this.state, this.task, this.config.maxIterations, updateSession,
); );
} else { } else {
result = await this.stepExecutor.runNormalStep( result = await this.movementExecutor.runNormalMovement(
step, this.state, this.task, this.config.maxIterations, updateSession, prebuiltInstruction, 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) { if (response.matchedRuleIndex != null && step.rules) {
const nextByRules = determineNextStepByRules(step, response.matchedRuleIndex); const nextByRules = determineNextMovementByRules(step, response.matchedRuleIndex);
if (nextByRules) { if (nextByRules) {
return 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) */ /** Build instruction (public, used by workflowExecution.ts for logging) */
buildInstruction(step: WorkflowStep, stepIteration: number): string { buildInstruction(step: WorkflowMovement, movementIteration: number): string {
return this.stepExecutor.buildInstruction( return this.movementExecutor.buildInstruction(
step, stepIteration, this.state, this.task, this.config.maxIterations, step, movementIteration, this.state, this.task, this.config.maxIterations,
); );
} }
@ -302,7 +302,7 @@ export class WorkflowEngine extends EventEmitter {
const additionalIterations = await this.options.onIterationLimit({ const additionalIterations = await this.options.onIterationLimit({
currentIteration: this.state.iteration, currentIteration: this.state.iteration,
maxIterations: this.config.maxIterations, maxIterations: this.config.maxIterations,
currentStep: this.state.currentStep, currentMovement: this.state.currentMovement,
}); });
if (additionalIterations !== null && additionalIterations > 0) { if (additionalIterations !== null && additionalIterations > 0) {
@ -319,43 +319,43 @@ export class WorkflowEngine extends EventEmitter {
break; break;
} }
const step = this.getStep(this.state.currentStep); const movement = this.getMovement(this.state.currentMovement);
const loopCheck = this.loopDetector.check(step.name); const loopCheck = this.loopDetector.check(movement.name);
if (loopCheck.shouldWarn) { if (loopCheck.shouldWarn) {
this.emit('step:loop_detected', step, loopCheck.count); this.emit('movement:loop_detected', movement, loopCheck.count);
} }
if (loopCheck.shouldAbort) { if (loopCheck.shouldAbort) {
this.state.status = 'aborted'; 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; break;
} }
this.state.iteration++; this.state.iteration++;
// Build instruction before emitting step:start so listeners can log it // Build instruction before emitting movement:start so listeners can log it
const isParallel = step.parallel && step.parallel.length > 0; const isParallel = movement.parallel && movement.parallel.length > 0;
let prebuiltInstruction: string | undefined; let prebuiltInstruction: string | undefined;
if (!isParallel) { if (!isParallel) {
const stepIteration = incrementStepIteration(this.state, step.name); const movementIteration = incrementMovementIteration(this.state, movement.name);
prebuiltInstruction = this.stepExecutor.buildInstruction( prebuiltInstruction = this.movementExecutor.buildInstruction(
step, stepIteration, this.state, this.task, this.config.maxIterations, 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 { try {
const { response, instruction } = await this.runStep(step, prebuiltInstruction); const { response, instruction } = await this.runMovement(movement, prebuiltInstruction);
this.emit('step:complete', step, response, instruction); this.emit('movement:complete', movement, response, instruction);
if (response.status === 'blocked') { if (response.status === 'blocked') {
this.emit('step:blocked', step, response); this.emit('movement:blocked', movement, response);
const result = await handleBlocked(step, response, this.options); const result = await handleBlocked(movement, response, this.options);
if (result.shouldContinue && result.userInput) { if (result.shouldContinue && result.userInput) {
this.addUserInput(result.userInput); this.addUserInput(result.userInput);
this.emit('step:user_input', step, result.userInput); this.emit('movement:user_input', movement, result.userInput);
continue; continue;
} }
@ -364,16 +364,16 @@ export class WorkflowEngine extends EventEmitter {
break; break;
} }
const nextStep = this.resolveNextStep(step, response); const nextMovement = this.resolveNextMovement(movement, response);
log.debug('Step transition', { log.debug('Movement transition', {
from: step.name, from: movement.name,
status: response.status, status: response.status,
matchedRuleIndex: response.matchedRuleIndex, matchedRuleIndex: response.matchedRuleIndex,
nextStep, nextMovement,
}); });
if (response.matchedRuleIndex != null && step.rules) { if (response.matchedRuleIndex != null && movement.rules) {
const matchedRule = step.rules[response.matchedRuleIndex]; const matchedRule = movement.rules[response.matchedRuleIndex];
if (matchedRule?.requiresUserInput) { if (matchedRule?.requiresUserInput) {
if (!this.options.onUserInput) { if (!this.options.onUserInput) {
this.state.status = 'aborted'; this.state.status = 'aborted';
@ -381,7 +381,7 @@ export class WorkflowEngine extends EventEmitter {
break; break;
} }
const userInput = await this.options.onUserInput({ const userInput = await this.options.onUserInput({
step, movement,
response, response,
prompt: response.content, prompt: response.content,
}); });
@ -391,32 +391,32 @@ export class WorkflowEngine extends EventEmitter {
break; break;
} }
this.addUserInput(userInput); this.addUserInput(userInput);
this.emit('step:user_input', step, userInput); this.emit('movement:user_input', movement, userInput);
this.state.currentStep = step.name; this.state.currentMovement = movement.name;
continue; continue;
} }
} }
if (nextStep === COMPLETE_STEP) { if (nextMovement === COMPLETE_MOVEMENT) {
this.state.status = 'completed'; this.state.status = 'completed';
this.emit('workflow:complete', this.state); this.emit('workflow:complete', this.state);
break; break;
} }
if (nextStep === ABORT_STEP) { if (nextMovement === ABORT_MOVEMENT) {
this.state.status = 'aborted'; 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; break;
} }
this.state.currentStep = nextStep; this.state.currentMovement = nextMovement;
} catch (error) { } catch (error) {
this.state.status = 'aborted'; this.state.status = 'aborted';
if (this.abortRequested) { if (this.abortRequested) {
this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)'); this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)');
} else { } else {
const message = getErrorMessage(error); 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; break;
} }
@ -428,62 +428,62 @@ export class WorkflowEngine extends EventEmitter {
/** Run a single iteration (for interactive mode) */ /** Run a single iteration (for interactive mode) */
async runSingleIteration(): Promise<{ async runSingleIteration(): Promise<{
response: AgentResponse; response: AgentResponse;
nextStep: string; nextMovement: string;
isComplete: boolean; isComplete: boolean;
loopDetected?: boolean; loopDetected?: boolean;
}> { }> {
const step = this.getStep(this.state.currentStep); const movement = this.getMovement(this.state.currentMovement);
const loopCheck = this.loopDetector.check(step.name); const loopCheck = this.loopDetector.check(movement.name);
if (loopCheck.shouldAbort) { if (loopCheck.shouldAbort) {
this.state.status = 'aborted'; this.state.status = 'aborted';
return { return {
response: { response: {
agent: step.agent ?? step.name, agent: movement.agent ?? movement.name,
status: 'blocked', status: 'blocked',
content: ERROR_MESSAGES.LOOP_DETECTED(step.name, loopCheck.count), content: ERROR_MESSAGES.LOOP_DETECTED(movement.name, loopCheck.count),
timestamp: new Date(), timestamp: new Date(),
}, },
nextStep: ABORT_STEP, nextMovement: ABORT_MOVEMENT,
isComplete: true, isComplete: true,
loopDetected: true, loopDetected: true,
}; };
} }
this.state.iteration++; this.state.iteration++;
const { response } = await this.runStep(step); const { response } = await this.runMovement(movement);
const nextStep = this.resolveNextStep(step, response); const nextMovement = this.resolveNextMovement(movement, response);
const isComplete = nextStep === COMPLETE_STEP || nextStep === ABORT_STEP; const isComplete = nextMovement === COMPLETE_MOVEMENT || nextMovement === ABORT_MOVEMENT;
if (response.matchedRuleIndex != null && step.rules) { if (response.matchedRuleIndex != null && movement.rules) {
const matchedRule = step.rules[response.matchedRuleIndex]; const matchedRule = movement.rules[response.matchedRuleIndex];
if (matchedRule?.requiresUserInput) { if (matchedRule?.requiresUserInput) {
if (!this.options.onUserInput) { if (!this.options.onUserInput) {
this.state.status = 'aborted'; 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({ const userInput = await this.options.onUserInput({
step, movement,
response, response,
prompt: response.content, prompt: response.content,
}); });
if (userInput === null) { if (userInput === null) {
this.state.status = 'aborted'; 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.addUserInput(userInput);
this.emit('step:user_input', step, userInput); this.emit('movement:user_input', movement, userInput);
this.state.currentStep = step.name; this.state.currentMovement = movement.name;
return { response, nextStep: step.name, isComplete: false, loopDetected: loopCheck.isLoop }; return { response, nextMovement: movement.name, isComplete: false, loopDetected: loopCheck.isLoop };
} }
} }
if (!isComplete) { if (!isComplete) {
this.state.currentStep = nextStep; this.state.currentMovement = nextMovement;
} else { } 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 };
} }
} }

View File

@ -5,7 +5,7 @@
* requesting user input to continue. * 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 type { UserInputRequest, WorkflowEngineOptions } from '../types.js';
import { extractBlockedPrompt } from './transitions.js'; import { extractBlockedPrompt } from './transitions.js';
@ -22,13 +22,13 @@ export interface BlockedHandlerResult {
/** /**
* Handle blocked status by requesting user input. * 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 response - The blocked response from the agent
* @param options - Workflow engine options containing callbacks * @param options - Workflow engine options containing callbacks
* @returns Result indicating whether to continue and any user input * @returns Result indicating whether to continue and any user input
*/ */
export async function handleBlocked( export async function handleBlocked(
step: WorkflowStep, step: WorkflowMovement,
response: AgentResponse, response: AgentResponse,
options: WorkflowEngineOptions options: WorkflowEngineOptions
): Promise<BlockedHandlerResult> { ): Promise<BlockedHandlerResult> {
@ -42,7 +42,7 @@ export async function handleBlocked(
// Build the request // Build the request
const request: UserInputRequest = { const request: UserInputRequest = {
step, movement: step,
response, response,
prompt, prompt,
}; };

View File

@ -5,6 +5,7 @@
*/ */
export { WorkflowEngine } from './WorkflowEngine.js'; 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 { ParallelRunner } from './ParallelRunner.js';
export { OptionsBuilder } from './OptionsBuilder.js'; export { OptionsBuilder } from './OptionsBuilder.js';

View File

@ -1,7 +1,7 @@
/** /**
* Loop detection for workflow execution * 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. * which may indicate an infinite loop.
*/ */
@ -15,10 +15,10 @@ const DEFAULT_LOOP_DETECTION: Required<LoopDetectionConfig> = {
}; };
/** /**
* Loop detector for tracking consecutive same-step executions. * Loop detector for tracking consecutive same-movement executions.
*/ */
export class LoopDetector { export class LoopDetector {
private lastStepName: string | null = null; private lastMovementName: string | null = null;
private consecutiveCount = 0; private consecutiveCount = 0;
private config: Required<LoopDetectionConfig>; private config: Required<LoopDetectionConfig>;
@ -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. * Updates internal tracking state.
*/ */
check(stepName: string): LoopCheckResult { check(movementName: string): LoopCheckResult {
if (this.lastStepName === stepName) { if (this.lastMovementName === movementName) {
this.consecutiveCount++; this.consecutiveCount++;
} else { } else {
this.consecutiveCount = 1; this.consecutiveCount = 1;
this.lastStepName = stepName; this.lastMovementName = movementName;
} }
const isLoop = this.consecutiveCount > this.config.maxConsecutiveSameStep; const isLoop = this.consecutiveCount > this.config.maxConsecutiveSameStep;
@ -57,7 +57,7 @@ export class LoopDetector {
* Reset the detector state. * Reset the detector state.
*/ */
reset(): void { reset(): void {
this.lastStepName = null; this.lastMovementName = null;
this.consecutiveCount = 0; this.consecutiveCount = 0;
} }

View File

@ -1,20 +1,20 @@
/** /**
* Parallel step log display * Parallel movement log display
* *
* Provides prefixed, color-coded interleaved output for parallel sub-steps. * Provides prefixed, color-coded interleaved output for parallel sub-movements.
* Each sub-step's stream output gets a `[name]` prefix with right-padding * Each sub-movement's stream output gets a `[name]` prefix with right-padding
* aligned to the longest sub-step name. * aligned to the longest sub-movement name.
*/ */
import type { StreamCallback, StreamEvent } from '../types.js'; 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 COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green
const RESET = '\x1b[0m'; const RESET = '\x1b[0m';
export interface ParallelLoggerOptions { export interface ParallelLoggerOptions {
/** Sub-step names (used to calculate prefix width) */ /** Sub-movement names (used to calculate prefix width) */
subStepNames: string[]; subMovementNames: string[];
/** Parent onStream callback to delegate non-prefixed events */ /** Parent onStream callback to delegate non-prefixed events */
parentOnStream?: StreamCallback; parentOnStream?: StreamCallback;
/** Override process.stdout.write for testing */ /** 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 * - Buffer partial lines until newline
* - Prepend colored `[name]` prefix to each complete line * - Prepend colored `[name]` prefix to each complete line
* - Delegate init/result/error events to the parent callback * - Delegate init/result/error events to the parent callback
@ -36,17 +36,17 @@ export class ParallelLogger {
private readonly writeFn: (text: string) => void; private readonly writeFn: (text: string) => void;
constructor(options: ParallelLoggerOptions) { 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.parentOnStream = options.parentOnStream;
this.writeFn = options.writeFn ?? ((text: string) => process.stdout.write(text)); 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, ''); 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 * Format: `\x1b[COLORm[name]\x1b[0m` + padding spaces
*/ */
buildPrefix(name: string, index: number): string { 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 * - `text`: buffered line-by-line with prefix
* - `tool_use`, `tool_result`, `tool_output`, `thinking`: prefixed per-line, no buffering * - `tool_use`, `tool_result`, `tool_output`, `thinking`: prefixed per-line, no buffering
* - `init`, `result`, `error`: delegated to parent callback (no prefix) * - `init`, `result`, `error`: delegated to parent callback (no prefix)
*/ */
createStreamHandler(subStepName: string, index: number): StreamCallback { createStreamHandler(subMovementName: string, index: number): StreamCallback {
const prefix = this.buildPrefix(subStepName, index); const prefix = this.buildPrefix(subMovementName, index);
return (event: StreamEvent) => { return (event: StreamEvent) => {
switch (event.type) { switch (event.type) {
case 'text': case 'text':
this.handleTextEvent(subStepName, prefix, event.data.text); this.handleTextEvent(subMovementName, prefix, event.data.text);
break; break;
case 'tool_use': case 'tool_use':
@ -144,13 +144,13 @@ export class ParallelLogger {
} }
/** /**
* Flush remaining line buffers for all sub-steps. * Flush remaining line buffers for all sub-movements.
* Call after all sub-steps complete to output any trailing partial lines. * Call after all sub-movements complete to output any trailing partial lines.
*/ */
flush(): void { flush(): void {
// Build prefixes for flush — need index mapping // Build prefixes for flush — need index mapping
// Since we don't store index, iterate lineBuffers in insertion order // 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; let index = 0;
for (const [name, buffer] of this.lineBuffers) { for (const [name, buffer] of this.lineBuffers) {
if (buffer !== '') { 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: * Format:
* ``` * ```
@ -174,7 +174,7 @@ export class ParallelLogger {
* ``` * ```
*/ */
printSummary( printSummary(
parentStepName: string, parentMovementName: string,
results: Array<{ name: string; condition: string | undefined }>, results: Array<{ name: string; condition: string | undefined }>,
): void { ): void {
this.flush(); this.flush();
@ -188,7 +188,7 @@ export class ParallelLogger {
}); });
// Header line: ── name results ── // Header line: ── name results ──
const headerText = ` ${parentStepName} results `; const headerText = ` ${parentMovementName} results `;
const maxLineLength = Math.max( const maxLineLength = Math.max(
headerText.length + 4, // 4 for "── " + " ──" headerText.length + 4, // 4 for "── " + " ──"
...resultLines.map((l) => l.length), ...resultLines.map((l) => l.length),

View File

@ -36,23 +36,23 @@ export class StateManager {
this.state = { this.state = {
workflowName: config.name, workflowName: config.name,
currentStep: config.initialStep, currentMovement: config.initialMovement,
iteration: 0, iteration: 0,
stepOutputs: new Map(), movementOutputs: new Map(),
userInputs, userInputs,
agentSessions, agentSessions,
stepIterations: new Map(), movementIterations: new Map(),
status: 'running', 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 { incrementMovementIteration(movementName: string): number {
const current = this.state.stepIterations.get(stepName) ?? 0; const current = this.state.movementIterations.get(movementName) ?? 0;
const next = current + 1; const next = current + 1;
this.state.stepIterations.set(stepName, next); this.state.movementIterations.set(movementName, next);
return 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 { getPreviousOutput(): AgentResponse | undefined {
const outputs = Array.from(this.state.stepOutputs.values()); const outputs = Array.from(this.state.movementOutputs.values());
return outputs[outputs.length - 1]; 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 { export function incrementMovementIteration(state: WorkflowState, movementName: string): number {
const current = state.stepIterations.get(stepName) ?? 0; const current = state.movementIterations.get(movementName) ?? 0;
const next = current + 1; const next = current + 1;
state.stepIterations.set(stepName, next); state.movementIterations.set(movementName, next);
return 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 { 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]; return outputs[outputs.length - 1];
} }

View File

@ -1,19 +1,19 @@
/** /**
* Workflow state transition logic * 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 { import type {
WorkflowStep, WorkflowMovement,
} from '../../models/types.js'; } from '../../models/types.js';
/** /**
* Determine next step using rules-based detection. * Determine next movement using rules-based detection.
* Returns the next step name from the matched rule, or null if no rule matched. * Returns the next movement name from the matched rule, or null if no rule matched.
*/ */
export function determineNextStepByRules( export function determineNextMovementByRules(
step: WorkflowStep, step: WorkflowMovement,
ruleIndex: number, ruleIndex: number,
): string | null { ): string | null {
const rule = step.rules?.[ruleIndex]; const rule = step.rules?.[ruleIndex];

View File

@ -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'; import { createLogger } from '../../../shared/utils/index.js';
const log = createLogger('aggregate-evaluator'); 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: * For each aggregate rule, checks the matched condition text of sub-movements:
* - all("X"): true when ALL sub-steps have matched condition === X * - all("X"): true when ALL sub-movements have matched condition === X
* - all("A", "B"): true when 1st sub-step matches "A" AND 2nd sub-step matches "B" (order-based) * - 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-step has matched condition === X * - any("X"): true when at least ONE sub-movement has matched condition === X
* - any("A", "B"): true when at least ONE sub-step matches "A" OR "B" * - any("A", "B"): true when at least ONE sub-movement matches "A" OR "B"
* *
* Edge cases per spec: * Edge cases per spec:
* - Sub-step with no matched rule: all() false, any() skip that sub-step * - Sub-movement with no matched rule: all() false, any() skip that sub-movement
* - No sub-steps (0 ): both false * - No sub-movements (0 ): both false
* - Non-parallel step: both false * - Non-parallel movement: both false
* - all("A", "B") with wrong number of sub-steps: false (logged as error) * - all("A", "B") with wrong number of sub-movements: false (logged as error)
*/ */
export class AggregateEvaluator { export class AggregateEvaluator {
constructor( constructor(
private readonly step: WorkflowStep, private readonly step: WorkflowMovement,
private readonly state: WorkflowState, private readonly state: WorkflowState,
) {} ) {}
/** /**
* Evaluate aggregate conditions. * 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 { evaluate(): number {
if (!this.step.rules || !this.step.parallel || this.step.parallel.length === 0) return -1; if (!this.step.rules || !this.step.parallel || this.step.parallel.length === 0) return -1;
@ -43,22 +43,22 @@ export class AggregateEvaluator {
continue; continue;
} }
const subSteps = this.step.parallel; const subMovements = this.step.parallel;
const targetCondition = rule.aggregateConditionText; const targetCondition = rule.aggregateConditionText;
if (rule.aggregateType === 'all') { 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 (Array.isArray(targetCondition)) {
if (targetCondition.length !== subSteps.length) { if (targetCondition.length !== subMovements.length) {
log.error('all() condition count mismatch', { log.error('all() condition count mismatch', {
step: this.step.name, movement: this.step.name,
conditionCount: targetCondition.length, conditionCount: targetCondition.length,
subStepCount: subSteps.length, subMovementCount: subMovements.length,
}); });
continue; continue;
} }
const allMatch = subSteps.every((sub, idx) => { const allMatch = subMovements.every((sub, idx) => {
const output = this.state.stepOutputs.get(sub.name); const output = this.state.movementOutputs.get(sub.name);
if (!output || output.matchedRuleIndex == null || !sub.rules) return false; if (!output || output.matchedRuleIndex == null || !sub.rules) return false;
const matchedRule = sub.rules[output.matchedRuleIndex]; const matchedRule = sub.rules[output.matchedRuleIndex];
const expectedCondition = targetCondition[idx]; const expectedCondition = targetCondition[idx];
@ -66,46 +66,46 @@ export class AggregateEvaluator {
return matchedRule?.condition === expectedCondition; return matchedRule?.condition === expectedCondition;
}); });
if (allMatch) { 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; return i;
} }
} else { } else {
// Single condition: all sub-steps must match the same condition // Single condition: all sub-movements must match the same condition
const allMatch = subSteps.every((sub) => { const allMatch = subMovements.every((sub) => {
const output = this.state.stepOutputs.get(sub.name); const output = this.state.movementOutputs.get(sub.name);
if (!output || output.matchedRuleIndex == null || !sub.rules) return false; if (!output || output.matchedRuleIndex == null || !sub.rules) return false;
const matchedRule = sub.rules[output.matchedRuleIndex]; const matchedRule = sub.rules[output.matchedRuleIndex];
return matchedRule?.condition === targetCondition; return matchedRule?.condition === targetCondition;
}); });
if (allMatch) { 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; return i;
} }
} }
} else { } else {
// 'any' // 'any'
if (Array.isArray(targetCondition)) { if (Array.isArray(targetCondition)) {
// Multiple conditions: at least one sub-step matches at least one condition // Multiple conditions: at least one sub-movement matches at least one condition
const anyMatch = subSteps.some((sub) => { const anyMatch = subMovements.some((sub) => {
const output = this.state.stepOutputs.get(sub.name); const output = this.state.movementOutputs.get(sub.name);
if (!output || output.matchedRuleIndex == null || !sub.rules) return false; if (!output || output.matchedRuleIndex == null || !sub.rules) return false;
const matchedRule = sub.rules[output.matchedRuleIndex]; const matchedRule = sub.rules[output.matchedRuleIndex];
return targetCondition.includes(matchedRule?.condition ?? ''); return targetCondition.includes(matchedRule?.condition ?? '');
}); });
if (anyMatch) { 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; return i;
} }
} else { } else {
// Single condition: at least one sub-step matches the condition // Single condition: at least one sub-movement matches the condition
const anyMatch = subSteps.some((sub) => { const anyMatch = subMovements.some((sub) => {
const output = this.state.stepOutputs.get(sub.name); const output = this.state.movementOutputs.get(sub.name);
if (!output || output.matchedRuleIndex == null || !sub.rules) return false; if (!output || output.matchedRuleIndex == null || !sub.rules) return false;
const matchedRule = sub.rules[output.matchedRuleIndex]; const matchedRule = sub.rules[output.matchedRuleIndex];
return matchedRule?.condition === targetCondition; return matchedRule?.condition === targetCondition;
}); });
if (anyMatch) { 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; return i;
} }
} }

View File

@ -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, * Supports tag-based detection, ai() conditions, aggregate conditions,
* and AI judge fallback. * and AI judge fallback.
*/ */
import type { import type {
WorkflowStep, WorkflowMovement,
WorkflowState, WorkflowState,
RuleMatchMethod, RuleMatchMethod,
} from '../../models/types.js'; } from '../../models/types.js';
@ -23,7 +23,7 @@ export interface RuleMatch {
} }
export interface RuleEvaluatorContext { export interface RuleEvaluatorContext {
/** Workflow state (for accessing stepOutputs in aggregate evaluation) */ /** Workflow state (for accessing movementOutputs in aggregate evaluation) */
state: WorkflowState; state: WorkflowState;
/** Working directory (for AI judge calls) */ /** Working directory (for AI judge calls) */
cwd: string; 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): * 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 * 2. Tag detection from Phase 3 output
* 3. Tag detection from Phase 1 output (fallback) * 3. Tag detection from Phase 1 output (fallback)
* 4. ai() condition evaluation via AI judge * 4. ai() condition evaluation via AI judge
* 5. All-conditions AI judge (final fallback) * 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). * Throws if rules exist but no rule matched (Fail Fast).
*/ */
export class RuleEvaluator { export class RuleEvaluator {
constructor( constructor(
private readonly step: WorkflowStep, private readonly step: WorkflowMovement,
private readonly ctx: RuleEvaluatorContext, private readonly ctx: RuleEvaluatorContext,
) {} ) {}
@ -58,7 +58,7 @@ export class RuleEvaluator {
if (!this.step.rules || this.step.rules.length === 0) return undefined; if (!this.step.rules || this.step.rules.length === 0) return undefined;
const interactiveEnabled = this.ctx.interactive === true; 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 aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state);
const aggIndex = aggEvaluator.evaluate(); const aggIndex = aggEvaluator.evaluate();
if (aggIndex >= 0) { if (aggIndex >= 0) {
@ -103,7 +103,7 @@ export class RuleEvaluator {
return { index: fallbackIndex, method: 'ai_judge_fallback' }; 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; if (aiConditions.length === 0) return -1;
log.debug('Evaluating ai() conditions via judge', { log.debug('Evaluating ai() conditions via judge', {
step: this.step.name, movement: this.step.name,
conditionCount: aiConditions.length, conditionCount: aiConditions.length,
}); });
@ -137,7 +137,7 @@ export class RuleEvaluator {
if (judgeResult >= 0 && judgeResult < aiConditions.length) { if (judgeResult >= 0 && judgeResult < aiConditions.length) {
const matched = aiConditions[judgeResult]!; const matched = aiConditions[judgeResult]!;
log.debug('AI judge matched condition', { log.debug('AI judge matched condition', {
step: this.step.name, movement: this.step.name,
judgeResult, judgeResult,
originalRuleIndex: matched.index, originalRuleIndex: matched.index,
condition: matched.text, condition: matched.text,
@ -145,7 +145,7 @@ export class RuleEvaluator {
return matched.index; 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; return -1;
} }
@ -162,7 +162,7 @@ export class RuleEvaluator {
.map((rule) => ({ index: rule.index, text: rule.text })); .map((rule) => ({ index: rule.index, text: rule.text }));
log.debug('Evaluating all conditions via AI judge (final fallback)', { log.debug('Evaluating all conditions via AI judge (final fallback)', {
step: this.step.name, movement: this.step.name,
conditionCount: conditions.length, conditionCount: conditions.length,
}); });
@ -170,14 +170,14 @@ export class RuleEvaluator {
if (judgeResult >= 0 && judgeResult < conditions.length) { if (judgeResult >= 0 && judgeResult < conditions.length) {
log.debug('AI judge (fallback) matched condition', { log.debug('AI judge (fallback) matched condition', {
step: this.step.name, movement: this.step.name,
ruleIndex: judgeResult, ruleIndex: judgeResult,
condition: conditions[judgeResult]!.text, condition: conditions[judgeResult]!.text,
}); });
return judgeResult; 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; return -1;
} }

View File

@ -2,7 +2,7 @@
* Rule evaluation - barrel exports * 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 { RuleEvaluator } from './RuleEvaluator.js';
import { AggregateEvaluator } from './AggregateEvaluator.js'; import { AggregateEvaluator } from './AggregateEvaluator.js';
@ -14,11 +14,11 @@ export { AggregateEvaluator } from './AggregateEvaluator.js';
import type { RuleMatch, RuleEvaluatorContext } from './RuleEvaluator.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. * Function facade over RuleEvaluator class.
*/ */
export async function detectMatchedRule( export async function detectMatchedRule(
step: WorkflowStep, step: WorkflowMovement,
agentContent: string, agentContent: string,
tagContent: string, tagContent: string,
ctx: RuleEvaluatorContext, ctx: RuleEvaluatorContext,
@ -30,6 +30,6 @@ export async function detectMatchedRule(
* Evaluate aggregate conditions. * Evaluate aggregate conditions.
* Function facade over AggregateEvaluator class. * 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(); return new AggregateEvaluator(step, state).evaluate();
} }

View File

@ -2,16 +2,16 @@
* Shared rule utility functions used by both engine.ts and instruction-builder.ts. * 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 * Check whether a movement has tag-based rules (i.e., rules that require
* [STEP:N] tag output for detection). * [MOVEMENT:N] tag output for detection).
* *
* Returns false when all rules are ai() or aggregate conditions, * Returns false when all rules are ai() or aggregate conditions,
* meaning no tag-based status output is needed. * 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; if (!step.rules || step.rules.length === 0) return false;
const allNonTagConditions = step.rules.every((r) => r.isAiCondition || r.isAggregateCondition); const allNonTagConditions = step.rules.every((r) => r.isAiCondition || r.isAggregateCondition);
return !allNonTagConditions; return !allNonTagConditions;

View File

@ -9,7 +9,7 @@
export { WorkflowEngine } from './engine/index.js'; export { WorkflowEngine } from './engine/index.js';
// Constants // 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 // Types
export type { export type {
@ -30,7 +30,7 @@ export type {
} from './types.js'; } from './types.js';
// Transitions (engine/) // Transitions (engine/)
export { determineNextStepByRules, extractBlockedPrompt } from './engine/transitions.js'; export { determineNextMovementByRules, extractBlockedPrompt } from './engine/transitions.js';
// Loop detection (engine/) // Loop detection (engine/)
export { LoopDetector } from './engine/loop-detector.js'; export { LoopDetector } from './engine/loop-detector.js';
@ -40,6 +40,7 @@ export {
createInitialState, createInitialState,
addUserInput, addUserInput,
getPreviousOutput, getPreviousOutput,
incrementMovementIteration,
} from './engine/state-manager.js'; } from './engine/state-manager.js';
// Blocked handling (engine/) // Blocked handling (engine/)
@ -52,8 +53,8 @@ export { ParallelLogger } from './engine/parallel-logger.js';
export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js'; export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js';
export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js'; export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js';
export { StatusJudgmentBuilder, type StatusJudgmentContext } from './instruction/StatusJudgmentBuilder.js'; export { StatusJudgmentBuilder, type StatusJudgmentContext } from './instruction/StatusJudgmentBuilder.js';
export { buildExecutionMetadata, renderExecutionMetadata, type InstructionContext, type ExecutionMetadata } from './instruction/instruction-context.js'; export { buildEditRule, type InstructionContext } from './instruction/instruction-context.js';
export { generateStatusRulesFromRules } from './instruction/status-rules.js'; export { generateStatusRulesComponents, type StatusRulesComponents } from './instruction/status-rules.js';
// Rule evaluation // Rule evaluation
export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js'; export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js';

View File

@ -1,20 +1,15 @@
/** /**
* Phase 1 instruction builder * Phase 1 instruction builder
* *
* Builds the instruction string for main agent execution by: * Builds the instruction string for main agent execution.
* 1. Auto-injecting standard sections (Execution Context, Workflow Context, * Assembles template variables and renders a single complete template.
* User Request, Previous Response, Additional User Inputs, Instructions header,
* Status Output Rules)
* 2. Replacing template placeholders with actual values
*/ */
import type { WorkflowStep, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js'; import type { WorkflowMovement, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js';
import { hasTagBasedRules } from '../evaluation/rule-utils.js';
import type { InstructionContext } from './instruction-context.js'; import type { InstructionContext } from './instruction-context.js';
import { buildExecutionMetadata, renderExecutionMetadata } from './instruction-context.js'; import { buildEditRule } from './instruction-context.js';
import { generateStatusRulesFromRules } from './status-rules.js';
import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.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). * 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; 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. * Builds Phase 1 instructions for agent execution.
* *
* Stateless builder all data is passed via constructor context. * Stateless builder all data is passed via constructor context.
* Renders a single complete template with all variables.
*/ */
export class InstructionBuilder { export class InstructionBuilder {
constructor( constructor(
private readonly step: WorkflowStep, private readonly step: WorkflowMovement,
private readonly context: InstructionContext, private readonly context: InstructionContext,
) {} ) {}
/** /**
* Build the complete instruction string. * Build the complete instruction string.
* *
* Generates a complete instruction by auto-injecting standard sections * Assembles all template variables and renders the Phase 1 template
* around the step-specific instruction_template content. * in a single loadTemplate() call.
*/ */
build(): string { build(): string {
const language = this.context.language ?? 'en'; const language = this.context.language ?? 'en';
const s = getPromptObject<SectionStrings>('instruction.sections', language);
const sections: string[] = [];
// 1. Execution context metadata (working directory + rules + edit permission) // Execution context variables
const metadata = buildExecutionMetadata(this.context, this.step.edit); const editRule = buildEditRule(this.step.edit, language);
sections.push(renderExecutionMetadata(metadata));
// 2. Workflow Context (iteration, step, report info) // Workflow structure (loop expansion done in code)
sections.push(this.renderWorkflowContext(language)); const workflowStructure = this.buildWorkflowStructure(language);
// Skip auto-injection for sections whose placeholders exist in the template, // Report info
// to avoid duplicate content. 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 tmpl = this.step.instructionTemplate;
const hasTaskPlaceholder = tmpl.includes('{task}'); const hasTaskPlaceholder = tmpl.includes('{task}');
const hasPreviousResponsePlaceholder = tmpl.includes('{previous_response}'); const hasPreviousResponsePlaceholder = tmpl.includes('{previous_response}');
const hasUserInputsPlaceholder = tmpl.includes('{user_inputs}'); const hasUserInputsPlaceholder = tmpl.includes('{user_inputs}');
// 3. User Request (skip if template embeds {task} directly) // User Request
if (!hasTaskPlaceholder) { const hasTaskSection = !hasTaskPlaceholder;
sections.push(`${s.userRequest}\n${escapeTemplateChars(this.context.task)}`); const userRequest = hasTaskSection ? escapeTemplateChars(this.context.task) : '';
}
// 4. Previous Response (skip if template embeds {previous_response} directly) // Previous Response
if (this.step.passPreviousResponse && this.context.previousOutput && !hasPreviousResponsePlaceholder) { const hasPreviousResponse = !!(
sections.push( this.step.passPreviousResponse &&
`${s.previousResponse}\n${escapeTemplateChars(this.context.previousOutput.content)}`, this.context.previousOutput &&
!hasPreviousResponsePlaceholder
); );
} const previousResponse = hasPreviousResponse
? escapeTemplateChars(this.context.previousOutput!.content)
: '';
// 5. Additional User Inputs (skip if template embeds {user_inputs} directly) // User Inputs
if (!hasUserInputsPlaceholder) { const hasUserInputs = !hasUserInputsPlaceholder;
const userInputsStr = this.context.userInputs.join('\n'); const userInputs = hasUserInputs
sections.push(`${s.additionalUserInputs}\n${escapeTemplateChars(userInputsStr)}`); ? escapeTemplateChars(this.context.userInputs.join('\n'))
} : '';
// 6. Instructions header + instruction_template content // Instructions (instruction_template processed)
const processedTemplate = replaceTemplatePlaceholders( const instructions = replaceTemplatePlaceholders(
this.step.instructionTemplate, this.step.instructionTemplate,
this.step, this.step,
this.context, this.context,
); );
sections.push(`${s.instructions}\n${processedTemplate}`);
// 7. Status Output Rules (for tag-based detection in Phase 1) return loadTemplate('perform_phase1_message', language, {
if (hasTagBasedRules(this.step)) { workingDirectory: this.context.cwd,
const statusRulesPrompt = generateStatusRulesFromRules( editRule,
this.step.name, workflowStructure,
this.step.rules!, iteration: `${this.context.iteration}/${this.context.maxIterations}`,
language, movementIteration: String(this.context.movementIteration),
{ interactive: this.context.interactive }, movement: this.step.name,
); hasReport,
sections.push(statusRulesPrompt); reportInfo,
} phaseNote,
hasTaskSection,
return sections.join('\n\n'); userRequest,
} hasPreviousResponse,
previousResponse,
private renderWorkflowContext(language: Language): string { hasUserInputs,
const s = getPromptObject<SectionStrings>('instruction.sections', language); userInputs,
const lines: string[] = [s.workflowContext]; instructions,
// 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('');
} }
lines.push(`- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`); /**
lines.push(`- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`); * Build the workflow structure display string.
lines.push(`- ${s.step}: ${this.step.name}`); * Returns empty string if no workflow movements are available.
*/
// If step has report config, include Report Directory path and phase note private buildWorkflowStructure(language: Language): string {
if (this.step.report && this.context.reportDir) { if (!this.context.workflowMovements || this.context.workflowMovements.length === 0) {
const reportContext = renderReportContext(this.step.report, this.context.reportDir, language); return '';
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. * Render report context info for Workflow Context section.
* Used by ReportInstructionBuilder. * Used by InstructionBuilder and ReportInstructionBuilder.
*/ */
export function renderReportContext( export function renderReportContext(
report: string | ReportConfig[] | ReportObjectConfig, report: string | ReportConfig[] | ReportObjectConfig,
reportDir: string, reportDir: string,
language: Language,
): string { ): string {
const s = getPromptObject<SectionStrings>('instruction.sections', language); const reportDirectory = 'Report Directory';
const reportFile = 'Report File';
const reportFiles = 'Report Files';
const lines: string[] = [ const lines: string[] = [
`- ${s.reportDirectory}: ${reportDir}/`, `- ${reportDirectory}: ${reportDir}/`,
]; ];
if (typeof report === 'string') { if (typeof report === 'string') {
lines.push(`- ${s.reportFile}: ${reportDir}/${report}`); lines.push(`- ${reportFile}: ${reportDir}/${report}`);
} else if (isReportObjectConfig(report)) { } else if (isReportObjectConfig(report)) {
lines.push(`- ${s.reportFile}: ${reportDir}/${report.name}`); lines.push(`- ${reportFile}: ${reportDir}/${report.name}`);
} else { } else {
lines.push(`- ${s.reportFiles}:`); lines.push(`- ${reportFiles}:`);
for (const file of report) { for (const file of report) {
lines.push(` - ${file.label}: ${reportDir}/${file.path}`); lines.push(` - ${file.label}: ${reportDir}/${file.path}`);
} }
@ -188,20 +163,35 @@ export function renderReportContext(
} }
/** /**
* Generate report output instructions from step.report config. * Generate report output instructions from movement's report config.
* Returns undefined if step has no report or no reportDir. * Returns empty string if movement has no report or no reportDir.
*/ */
export function renderReportOutputInstruction( export function renderReportOutputInstruction(
step: WorkflowStep, step: WorkflowMovement,
context: InstructionContext, context: InstructionContext,
language: Language, language: Language,
): string | undefined { ): string {
if (!step.report || !context.reportDir) return undefined; if (!step.report || !context.reportDir) return '';
const s = getPromptObject<ReportOutputStrings>('instruction.reportOutput', language);
const isMulti = Array.isArray(step.report); 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}`;
} }

View File

@ -1,37 +1,15 @@
/** /**
* Phase 2 instruction builder (report output) * Phase 2 instruction builder (report output)
* *
* Builds the instruction for the report output phase. Includes: * Builds the instruction for the report output phase.
* - Execution Context (cwd + rules) * Assembles template variables and renders a single complete template.
* - Workflow Context (report info only)
* - Report output instruction + format
*
* Does NOT include: User Request, Previous Response, User Inputs,
* Status rules, instruction_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 type { InstructionContext } from './instruction-context.js';
import { getMetadataStrings } from './instruction-context.js';
import { replaceTemplatePlaceholders } from './escape.js'; import { replaceTemplatePlaceholders } from './escape.js';
import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js'; import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js';
import { getPromptObject } from '../../../shared/prompts/index.js'; import { loadTemplate } 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;
}
/** /**
* Context for building report phase instruction. * Context for building report phase instruction.
@ -41,8 +19,8 @@ export interface ReportInstructionContext {
cwd: string; cwd: string;
/** Report directory path */ /** Report directory path */
reportDir: string; reportDir: string;
/** Step iteration (for {step_iteration} replacement) */ /** Movement iteration (for {step_iteration} replacement) */
stepIteration: number; movementIteration: number;
/** Language */ /** Language */
language?: Language; language?: Language;
/** Target report file name (when generating a single report) */ /** Target report file name (when generating a single report) */
@ -51,73 +29,38 @@ export interface ReportInstructionContext {
/** /**
* Builds Phase 2 (report output) instructions. * Builds Phase 2 (report output) instructions.
*
* Renders a single complete template with all variables.
*/ */
export class ReportInstructionBuilder { export class ReportInstructionBuilder {
constructor( constructor(
private readonly step: WorkflowStep, private readonly step: WorkflowMovement,
private readonly context: ReportInstructionContext, private readonly context: ReportInstructionContext,
) {} ) {}
build(): string { build(): string {
if (!this.step.report) { 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 language = this.context.language ?? 'en';
const s = getPromptObject<ReportSectionStrings>('instruction.reportSections', language);
const r = getPromptObject<ReportPhaseStrings>('instruction.reportPhase', language);
const m = getMetadataStrings(language);
const sections: string[] = [];
// 1. Execution Context // Build report context for Workflow Context section
const execLines = [ let reportContext: string;
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];
if (this.context.targetFile) { if (this.context.targetFile) {
const sectionStr = getPromptObject<{ reportDirectory: string; reportFile: string }>('instruction.sections', language); reportContext = `- Report Directory: ${this.context.reportDir}/\n- Report File: ${this.context.reportDir}/${this.context.targetFile}`;
workflowLines.push(`- ${sectionStr.reportDirectory}: ${this.context.reportDir}/`);
workflowLines.push(`- ${sectionStr.reportFile}: ${this.context.reportDir}/${this.context.targetFile}`);
} else { } else {
workflowLines.push(renderReportContext(this.step.report, this.context.reportDir, language)); reportContext = renderReportContext(this.step.report, this.context.reportDir);
}
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);
} }
// Report output instruction (auto-generated or explicit order) // Build report output instruction
const reportContext: InstructionContext = { let reportOutput = '';
let hasReportOutput = false;
const instrContext: InstructionContext = {
task: '', task: '',
iteration: 0, iteration: 0,
maxIterations: 0, maxIterations: 0,
stepIteration: this.context.stepIteration, movementIteration: this.context.movementIteration,
cwd: this.context.cwd, cwd: this.context.cwd,
projectCwd: this.context.cwd, projectCwd: this.context.cwd,
userInputs: [], userInputs: [],
@ -126,26 +69,31 @@ export class ReportInstructionBuilder {
}; };
if (isReportObjectConfig(this.step.report) && this.step.report.order) { if (isReportObjectConfig(this.step.report) && this.step.report.order) {
const processedOrder = replaceTemplatePlaceholders(this.step.report.order.trimEnd(), this.step, reportContext); reportOutput = replaceTemplatePlaceholders(this.step.report.order.trimEnd(), this.step, instrContext);
instrParts.push(''); hasReportOutput = true;
instrParts.push(processedOrder);
} else if (!this.context.targetFile) { } else if (!this.context.targetFile) {
const reportInstruction = renderReportOutputInstruction(this.step, reportContext, language); const output = renderReportOutputInstruction(this.step, instrContext, language);
if (reportInstruction) { if (output) {
instrParts.push(''); reportOutput = output;
instrParts.push(reportInstruction); hasReportOutput = true;
} }
} }
// Report format // Build report format
let reportFormat = '';
let hasReportFormat = false;
if (isReportObjectConfig(this.step.report) && this.step.report.format) { if (isReportObjectConfig(this.step.report) && this.step.report.format) {
const processedFormat = replaceTemplatePlaceholders(this.step.report.format.trimEnd(), this.step, reportContext); reportFormat = replaceTemplatePlaceholders(this.step.report.format.trimEnd(), this.step, instrContext);
instrParts.push(''); hasReportFormat = true;
instrParts.push(processedFormat);
} }
sections.push(instrParts.join('\n')); return loadTemplate('perform_phase2_message', language, {
workingDirectory: this.context.cwd,
return sections.join('\n\n'); reportContext,
hasReportOutput,
reportOutput,
hasReportFormat,
reportFormat,
});
} }
} }

View File

@ -4,14 +4,13 @@
* Resumes the agent session and asks it to evaluate its work * Resumes the agent session and asks it to evaluate its work
* and output the appropriate status tag. No tools are allowed. * and output the appropriate status tag. No tools are allowed.
* *
* Includes: * Renders a single complete template combining the judgment header
* - Header instruction (review and determine status) * and status rules (criteria table + output format).
* - Status rules (criteria table + output format)
*/ */
import type { WorkflowStep, Language } from '../../models/types.js'; import type { WorkflowMovement, Language } from '../../models/types.js';
import { generateStatusRulesFromRules } from './status-rules.js'; import { generateStatusRulesComponents } from './status-rules.js';
import { getPrompt } from '../../../shared/prompts/index.js'; import { loadTemplate } from '../../../shared/prompts/index.js';
/** /**
* Context for building status judgment instruction. * Context for building status judgment instruction.
@ -25,33 +24,34 @@ export interface StatusJudgmentContext {
/** /**
* Builds Phase 3 (status judgment) instructions. * Builds Phase 3 (status judgment) instructions.
*
* Renders a single complete template with all variables.
*/ */
export class StatusJudgmentBuilder { export class StatusJudgmentBuilder {
constructor( constructor(
private readonly step: WorkflowStep, private readonly step: WorkflowMovement,
private readonly context: StatusJudgmentContext, private readonly context: StatusJudgmentContext,
) {} ) {}
build(): string { build(): string {
if (!this.step.rules || this.step.rules.length === 0) { 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 language = this.context.language ?? 'en';
const sections: string[] = [];
// Header const components = generateStatusRulesComponents(
sections.push(getPrompt('instruction.statusJudgment.header', language));
// Status rules (criteria table + output format)
const generatedPrompt = generateStatusRulesFromRules(
this.step.name, this.step.name,
this.step.rules, this.step.rules,
language, language,
{ interactive: this.context.interactive }, { 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,
});
} }
} }

View File

@ -4,7 +4,7 @@
* Used by instruction builders to process instruction_template content. * 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'; import type { InstructionContext } from './instruction-context.js';
/** /**
@ -22,7 +22,7 @@ export function escapeTemplateChars(str: string): string {
*/ */
export function replaceTemplatePlaceholders( export function replaceTemplatePlaceholders(
template: string, template: string,
step: WorkflowStep, step: WorkflowMovement,
context: InstructionContext, context: InstructionContext,
): string { ): string {
let result = template; let result = template;
@ -30,10 +30,12 @@ export function replaceTemplatePlaceholders(
// Replace {task} // Replace {task}
result = result.replace(/\{task\}/g, escapeTemplateChars(context.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(/\{iteration\}/g, String(context.iteration));
result = result.replace(/\{max_iterations\}/g, String(context.maxIterations)); 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} // Replace {previous_response}
if (step.passPreviousResponse) { if (step.passPreviousResponse) {

View File

@ -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, * Defines the context structures used by instruction builders.
* and renders execution metadata (working directory, rules) as markdown.
*/ */
import type { AgentResponse, Language } from '../../models/types.js'; import type { AgentResponse, Language } from '../../models/types.js';
import { getPromptObject } from '../../../shared/prompts/index.js';
/** /**
* Context for building instruction from template. * Context for building instruction from template.
@ -18,15 +16,15 @@ export interface InstructionContext {
iteration: number; iteration: number;
/** Maximum iterations allowed */ /** Maximum iterations allowed */
maxIterations: number; maxIterations: number;
/** Current step's iteration number (how many times this step has been executed) */ /** Current movement's iteration number (how many times this movement has been executed) */
stepIteration: number; movementIteration: number;
/** Working directory (agent work dir, may be a clone) */ /** Working directory (agent work dir, may be a clone) */
cwd: string; cwd: string;
/** Project root directory (where .takt/ lives). */ /** Project root directory (where .takt/ lives). */
projectCwd: string; projectCwd: string;
/** User inputs accumulated during workflow */ /** User inputs accumulated during workflow */
userInputs: string[]; userInputs: string[];
/** Previous step output if available */ /** Previous movement output if available */
previousOutput?: AgentResponse; previousOutput?: AgentResponse;
/** Report directory path */ /** Report directory path */
reportDir?: string; reportDir?: string;
@ -34,78 +32,30 @@ export interface InstructionContext {
language?: Language; language?: Language;
/** Whether interactive-only rules are enabled */ /** Whether interactive-only rules are enabled */
interactive?: boolean; interactive?: boolean;
/** Top-level workflow steps for workflow structure display */ /** Top-level workflow movements for workflow structure display */
workflowSteps?: ReadonlyArray<{ name: string; description?: string }>; workflowMovements?: ReadonlyArray<{ name: string; description?: string }>;
/** Index of the current step in workflowSteps (0-based) */ /** Index of the current movement in workflowMovements (0-based) */
currentStepIndex?: number; currentMovementIndex?: 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;
} }
/** /**
* 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 { export function buildEditRule(edit: boolean | undefined, language: Language): string {
return { if (edit === true) {
workingDirectory: context.cwd, if (language === 'ja') {
language: context.language ?? 'en', return '**このムーブメントでは編集が許可されています。** ユーザーの要求に応じて、ファイルの作成・変更・削除を行ってください。';
edit,
};
} }
return '**Editing is ENABLED for this movement.** You may create, modify, and delete files as needed to fulfill the user\'s request.';
/** 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;
} }
if (edit === false) {
/** Load metadata strings for the given language from YAML */ if (language === 'ja') {
export function getMetadataStrings(language: Language): MetadataStrings { return '**このムーブメントでは編集が禁止されています。** プロジェクトのソースファイルを作成・変更・削除しないでください。コードの読み取り・検索のみ行ってください。レポート出力は後のフェーズで自動的に行われます。';
return getPromptObject<MetadataStrings>('instruction.metadata', language);
} }
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.';
/**
* 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}`);
} }
if (strings.note) { return '';
lines.push('');
lines.push(strings.note);
}
lines.push('');
return lines.join('\n');
} }

View File

@ -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 * Generates structured status rules content that tells agents which
* based on the step's rule configuration. * 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 type { WorkflowRule, Language } from '../../models/types.js';
import { getPromptObject } from '../../../shared/prompts/index.js';
/** Shape of localized status rules strings */ /** Components of the generated status rules */
interface StatusRulesStrings { export interface StatusRulesComponents {
criteriaHeading: string; criteriaTable: string;
headerNum: string; outputList: string;
headerCondition: string; hasAppendix: boolean;
headerTag: string; appendixContent: string;
outputHeading: string;
outputInstruction: string;
appendixHeading: string;
appendixInstruction: string;
} }
/** /**
* Generate status rules prompt from rules configuration. * Generate status rules components from rules configuration.
* Creates a structured prompt that tells the agent which numbered tags to output.
* *
* Example output for step "plan" with 3 rules: * 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.
* |---|------|------|
* | 1 | | `[PLAN:1]` |
* | 2 | | `[PLAN:2]` |
* | 3 | | `[PLAN:3]` |
*/ */
export function generateStatusRulesFromRules( export function generateStatusRulesComponents(
stepName: string, movementName: string,
rules: WorkflowRule[], rules: WorkflowRule[],
language: Language, language: Language,
options?: { interactive?: boolean }, options?: { interactive?: boolean },
): string { ): StatusRulesComponents {
const tag = stepName.toUpperCase(); const tag = movementName.toUpperCase();
const strings = getPromptObject<StatusRulesStrings>('instruction.statusRules', language);
const interactiveEnabled = options?.interactive; const interactiveEnabled = options?.interactive;
const visibleRules = rules const visibleRules = rules
.map((rule, index) => ({ rule, index })) .map((rule, index) => ({ rule, index }))
.filter(({ rule }) => interactiveEnabled !== false || !rule.interactiveOnly); .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 const tableLines = [
lines.push(strings.criteriaHeading); `| ${headerNum} | ${headerCondition} | ${headerTag} |`,
lines.push(''); '|---|------|------|',
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`); ...visibleRules.map(({ rule, index }) =>
lines.push('|---|------|------|'); `| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`,
for (const { rule, index } of visibleRules) { ),
lines.push(`| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`); ];
} const criteriaTable = tableLines.join('\n');
lines.push('');
// Output format // Build output list
lines.push(strings.outputHeading); const outputInstruction = language === 'ja'
lines.push(''); ? '判定に対応するタグを出力してください:'
lines.push(strings.outputInstruction); : 'Output the tag corresponding to your decision:';
lines.push('');
for (const { rule, index } of visibleRules) {
lines.push(`- \`[${tag}:${index + 1}]\`${rule.condition}`);
}
// 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); const rulesWithAppendix = visibleRules.filter(({ rule }) => rule.appendix);
if (rulesWithAppendix.length > 0) { const hasAppendix = rulesWithAppendix.length > 0;
lines.push(''); let appendixContent = '';
lines.push(strings.appendixHeading);
if (hasAppendix) {
const appendixInstructionTemplate = language === 'ja'
? '`[{tag}]` を出力する場合、以下を追記してください:'
: 'When outputting `[{tag}]`, append the following:';
const appendixBlocks: string[] = [];
for (const { rule, index } of visibleRules) { for (const { rule, index } of visibleRules) {
if (!rule.appendix) continue; if (!rule.appendix) continue;
const tagStr = `[${tag}:${index + 1}]`; const tagStr = `[${tag}:${index + 1}]`;
lines.push(''); appendixBlocks.push('');
// appendixInstruction contains {tag} as a domain-specific placeholder, not a YAML template variable appendixBlocks.push(appendixInstructionTemplate.replace('{tag}', tagStr));
lines.push(strings.appendixInstruction.replace('{tag}', tagStr)); appendixBlocks.push('```');
lines.push('```'); appendixBlocks.push(rule.appendix.trimEnd());
lines.push(rule.appendix.trimEnd()); appendixBlocks.push('```');
lines.push('```');
} }
appendixContent = appendixBlocks.join('\n');
} }
return lines.join('\n'); return { criteriaTable, outputList, hasAppendix, appendixContent };
} }

View File

@ -7,7 +7,7 @@
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve, sep } from 'node:path'; 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 type { PhaseName } from './types.js';
import { runAgent, type RunAgentOptions } from '../../agents/runner.js'; import { runAgent, type RunAgentOptions } from '../../agents/runner.js';
import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js'; import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js';
@ -29,25 +29,25 @@ export interface PhaseRunnerContext {
interactive?: boolean; interactive?: boolean;
/** Get agent session ID */ /** Get agent session ID */
getSessionId: (agent: string) => string | undefined; getSessionId: (agent: string) => string | undefined;
/** Build resume options for a step */ /** Build resume options for a movement */
buildResumeOptions: (step: WorkflowStep, sessionId: string, overrides: Pick<RunAgentOptions, 'allowedTools' | 'maxTurns'>) => RunAgentOptions; buildResumeOptions: (step: WorkflowMovement, sessionId: string, overrides: Pick<RunAgentOptions, 'allowedTools' | 'maxTurns'>) => RunAgentOptions;
/** Update agent session after a phase run */ /** Update agent session after a phase run */
updateAgentSession: (agent: string, sessionId: string | undefined) => void; updateAgentSession: (agent: string, sessionId: string | undefined) => void;
/** Callback for phase lifecycle logging */ /** 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 */ /** 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. * 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); return hasTagBasedRules(step);
} }
function getReportFiles(report: WorkflowStep['report']): string[] { function getReportFiles(report: WorkflowMovement['report']): string[] {
if (!report) return []; if (!report) return [];
if (typeof report === 'string') return [report]; if (typeof report === 'string') return [report];
if (isReportObjectConfig(report)) return [report.name]; 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). * Plain text responses are written directly to files (no JSON parsing).
*/ */
export async function runReportPhase( export async function runReportPhase(
step: WorkflowStep, step: WorkflowMovement,
stepIteration: number, movementIteration: number,
ctx: PhaseRunnerContext, ctx: PhaseRunnerContext,
): Promise<void> { ): Promise<void> {
const sessionKey = step.agent ?? step.name; const sessionKey = step.agent ?? step.name;
let currentSessionId = ctx.getSessionId(sessionKey); let currentSessionId = ctx.getSessionId(sessionKey);
if (!currentSessionId) { 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); const reportFiles = getReportFiles(step.report);
if (reportFiles.length === 0) { if (reportFiles.length === 0) {
@ -99,12 +99,12 @@ export async function runReportPhase(
throw new Error(`Invalid report file name: ${fileName}`); 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, { const reportInstruction = new ReportInstructionBuilder(step, {
cwd: ctx.cwd, cwd: ctx.cwd,
reportDir: ctx.reportDir, reportDir: ctx.reportDir,
stepIteration, movementIteration: movementIteration,
language: ctx.language, language: ctx.language,
targetFile: fileName, targetFile: fileName,
}).build(); }).build();
@ -144,10 +144,10 @@ export async function runReportPhase(
} }
ctx.onPhaseComplete?.(step, 2, 'report', reportResponse.content, reportResponse.status); 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). * Returns the Phase 3 response content (containing the status tag).
*/ */
export async function runStatusJudgmentPhase( export async function runStatusJudgmentPhase(
step: WorkflowStep, step: WorkflowMovement,
ctx: PhaseRunnerContext, ctx: PhaseRunnerContext,
): Promise<string> { ): Promise<string> {
const sessionKey = step.agent ?? step.name; const sessionKey = step.agent ?? step.name;
const sessionId = ctx.getSessionId(sessionKey); const sessionId = ctx.getSessionId(sessionKey);
if (!sessionId) { 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, { const judgmentInstruction = new StatusJudgmentBuilder(step, {
language: ctx.language, language: ctx.language,
@ -199,6 +199,6 @@ export async function runStatusJudgmentPhase(
ctx.updateAgentSession(sessionKey, judgmentResponse.sessionId); ctx.updateAgentSession(sessionKey, judgmentResponse.sessionId);
ctx.onPhaseComplete?.(step, 3, 'judge', judgmentResponse.content, judgmentResponse.status); 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; return judgmentResponse.content;
} }

View File

@ -6,7 +6,7 @@
*/ */
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'; 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'; export type ProviderType = 'claude' | 'codex' | 'mock';
@ -91,7 +91,7 @@ export type AskUserQuestionHandler = (
input: AskUserQuestionInput input: AskUserQuestionInput
) => Promise<Record<string, string>>; ) => Promise<Record<string, string>>;
export type RuleIndexDetector = (content: string, stepName: string) => number; export type RuleIndexDetector = (content: string, movementName: string) => number;
export interface AiJudgeCondition { export interface AiJudgeCondition {
index: number; index: number;
@ -108,23 +108,23 @@ export type PhaseName = 'execute' | 'report' | 'judge';
/** Events emitted by workflow engine */ /** Events emitted by workflow engine */
export interface WorkflowEvents { export interface WorkflowEvents {
'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void; 'movement:start': (step: WorkflowMovement, iteration: number, instruction: string) => void;
'step:complete': (step: WorkflowStep, response: AgentResponse, instruction: string) => void; 'movement:complete': (step: WorkflowMovement, response: AgentResponse, instruction: string) => void;
'step:report': (step: WorkflowStep, filePath: string, fileName: string) => void; 'movement:report': (step: WorkflowMovement, filePath: string, fileName: string) => void;
'step:blocked': (step: WorkflowStep, response: AgentResponse) => void; 'movement:blocked': (step: WorkflowMovement, response: AgentResponse) => void;
'step:user_input': (step: WorkflowStep, userInput: string) => void; 'movement:user_input': (step: WorkflowMovement, userInput: string) => void;
'phase:start': (step: WorkflowStep, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; 'phase:start': (step: WorkflowMovement, 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; 'phase:complete': (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void;
'workflow:complete': (state: WorkflowState) => void; 'workflow:complete': (state: WorkflowState) => void;
'workflow:abort': (state: WorkflowState, reason: string) => void; 'workflow:abort': (state: WorkflowState, reason: string) => void;
'iteration:limit': (iteration: number, maxIterations: number) => 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 */ /** User input request for blocked state */
export interface UserInputRequest { export interface UserInputRequest {
/** The step that is blocked */ /** The movement that is blocked */
step: WorkflowStep; movement: WorkflowMovement;
/** The blocked response from the agent */ /** The blocked response from the agent */
response: AgentResponse; response: AgentResponse;
/** Prompt for the user (extracted from blocked message) */ /** Prompt for the user (extracted from blocked message) */
@ -137,8 +137,8 @@ export interface IterationLimitRequest {
currentIteration: number; currentIteration: number;
/** Current max iterations */ /** Current max iterations */
maxIterations: number; maxIterations: number;
/** Current step name */ /** Current movement name */
currentStep: string; currentMovement: string;
} }
/** Callback for session updates (when agent session IDs change) */ /** Callback for session updates (when agent session IDs change) */

View File

@ -8,9 +8,9 @@
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { import {
getGlobalWorkflowsDir, getGlobalPiecesDir,
getGlobalAgentsDir, getGlobalAgentsDir,
getBuiltinWorkflowsDir, getBuiltinPiecesDir,
getBuiltinAgentsDir, getBuiltinAgentsDir,
getLanguage, getLanguage,
} from '../../infra/config/index.js'; } from '../../infra/config/index.js';
@ -25,7 +25,7 @@ export async function ejectBuiltin(name?: string): Promise<void> {
header('Eject Builtin'); header('Eject Builtin');
const lang = getLanguage(); const lang = getLanguage();
const builtinWorkflowsDir = getBuiltinWorkflowsDir(lang); const builtinWorkflowsDir = getBuiltinPiecesDir(lang);
if (!name) { if (!name) {
// List available builtins // List available builtins
@ -40,7 +40,7 @@ export async function ejectBuiltin(name?: string): Promise<void> {
return; return;
} }
const userWorkflowsDir = getGlobalWorkflowsDir(); const userWorkflowsDir = getGlobalPiecesDir();
const userAgentsDir = getGlobalAgentsDir(); const userAgentsDir = getGlobalAgentsDir();
const builtinAgentsDir = getBuiltinAgentsDir(lang); const builtinAgentsDir = getBuiltinAgentsDir(lang);

View File

@ -19,8 +19,8 @@ import { getProvider, type ProviderType } from '../../infra/providers/index.js';
import { selectOption } from '../../shared/prompt/index.js'; import { selectOption } from '../../shared/prompt/index.js';
import { createLogger, getErrorMessage } from '../../shared/utils/index.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js'; import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
import { getPrompt } from '../../shared/prompts/index.js'; import { loadTemplate } from '../../shared/prompts/index.js';
import { getLabelObject } from '../../shared/i18n/index.js'; import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
const log = createLogger('interactive'); const log = createLogger('interactive');
/** Shape of interactive UI text */ /** Shape of interactive UI text */
@ -40,25 +40,20 @@ function resolveLanguage(lang?: Language): 'en' | 'ja' {
} }
function getInteractivePrompts(lang: 'en' | 'ja', workflowContext?: WorkflowContext) { function getInteractivePrompts(lang: 'en' | 'ja', workflowContext?: WorkflowContext) {
let systemPrompt = getPrompt('interactive.systemPrompt', lang); const hasWorkflow = !!workflowContext;
let summaryPrompt = getPrompt('interactive.summaryPrompt', lang);
// Add workflow context to prompts if available const systemPrompt = loadTemplate('score_interactive_system_prompt', lang, {
if (workflowContext) { workflowInfo: hasWorkflow,
const workflowInfo = getPrompt('interactive.workflowInfo', lang, { workflowName: workflowContext?.name ?? '',
name: workflowContext.name, workflowDescription: workflowContext?.description ?? '',
description: workflowContext.description,
}); });
systemPrompt += workflowInfo;
summaryPrompt += workflowInfo;
}
return { return {
systemPrompt, systemPrompt,
summaryPrompt, lang,
conversationLabel: getPrompt('interactive.conversationLabel', lang), workflowContext,
noTranscript: getPrompt('interactive.noTranscript', lang), conversationLabel: getLabel('interactive.conversationLabel', lang),
noTranscript: getLabel('interactive.noTranscript', lang),
ui: getLabelObject<InteractiveUIText>('interactive.ui', lang), ui: getLabelObject<InteractiveUIText>('interactive.ui', lang),
}; };
} }
@ -83,23 +78,38 @@ function buildTaskFromHistory(history: ConversationMessage[]): string {
.join('\n\n'); .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( function buildSummaryPrompt(
history: ConversationMessage[], history: ConversationMessage[],
hasSession: boolean, hasSession: boolean,
summaryPrompt: string, lang: 'en' | 'ja',
noTranscriptNote: string, noTranscriptNote: string,
conversationLabel: string, conversationLabel: string,
workflowContext?: WorkflowContext,
): string { ): string {
let conversation = '';
if (history.length > 0) { if (history.length > 0) {
const historyText = buildTaskFromHistory(history); const historyText = buildTaskFromHistory(history);
return `${summaryPrompt}\n\n${conversationLabel}\n${historyText}`; conversation = `${conversationLabel}\n${historyText}`;
} } else if (hasSession) {
if (hasSession) { conversation = `${conversationLabel}\n${noTranscriptNote}`;
return `${summaryPrompt}\n\n${conversationLabel}\n${noTranscriptNote}`; } else {
}
return ''; 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<boolean> { async function confirmTask(task: string, message: string, confirmLabel: string, yesLabel: string, noLabel: string): Promise<boolean> {
blankLine(); blankLine();
info(message); info(message);
@ -305,18 +315,19 @@ export async function interactiveMode(
let summaryPrompt = buildSummaryPrompt( let summaryPrompt = buildSummaryPrompt(
history, history,
!!sessionId, !!sessionId,
prompts.summaryPrompt, prompts.lang,
prompts.noTranscript, prompts.noTranscript,
prompts.conversationLabel, prompts.conversationLabel,
prompts.workflowContext,
); );
if (summaryPrompt && userNote) {
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
}
if (!summaryPrompt) { if (!summaryPrompt) {
info(prompts.ui.noConversation); info(prompts.ui.noConversation);
continue; 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) { if (!summaryResult) {
info(prompts.ui.summarizeFailed); info(prompts.ui.summarizeFailed);
continue; continue;

View File

@ -0,0 +1,5 @@
/**
* Prompt feature exports
*/
export { previewPrompts } from './preview.js';

View File

@ -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<void> {
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: '<task content>',
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();
}
}

View File

@ -41,7 +41,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<boolean>
log.debug('Running workflow', { log.debug('Running workflow', {
name: workflowConfig.name, name: workflowConfig.name,
steps: workflowConfig.steps.map((s: { name: string }) => s.name), movements: workflowConfig.movements.map((s: { name: string }) => s.name),
}); });
const globalConfig = loadGlobalConfig(); const globalConfig = loadGlobalConfig();

View File

@ -159,7 +159,7 @@ export async function executeWorkflow(
maxIterations: String(request.maxIterations), 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'), [ const action = await selectOption(getLabel('workflow.iterationLimit.continueQuestion'), [
{ {
@ -248,8 +248,8 @@ export async function executeWorkflow(
appendNdjsonLine(ndjsonLogPath, record); appendNdjsonLine(ndjsonLogPath, record);
}); });
engine.on('step:start', (step, iteration, instruction) => { engine.on('movement:start', (step, iteration, instruction) => {
log.debug('Step starting', { step: step.name, agent: step.agentDisplayName, iteration }); log.debug('Movement starting', { step: step.name, agent: step.agentDisplayName, iteration });
info(`[${iteration}/${workflowConfig.maxIterations}] ${step.name} (${step.agentDisplayName})`); info(`[${iteration}/${workflowConfig.maxIterations}] ${step.name} (${step.agentDisplayName})`);
// Log prompt content for debugging // Log prompt content for debugging
@ -273,8 +273,8 @@ export async function executeWorkflow(
}); });
engine.on('step:complete', (step, response, instruction) => { engine.on('movement:complete', (step, response, instruction) => {
log.debug('Step completed', { log.debug('Movement completed', {
step: step.name, step: step.name,
status: response.status, status: response.status,
matchedRuleIndex: response.matchedRuleIndex, matchedRuleIndex: response.matchedRuleIndex,
@ -329,7 +329,7 @@ export async function executeWorkflow(
updateLatestPointer(sessionLog, workflowSessionId, projectCwd); updateLatestPointer(sessionLog, workflowSessionId, projectCwd);
}); });
engine.on('step:report', (_step, filePath, fileName) => { engine.on('movement:report', (_step, filePath, fileName) => {
const content = readFileSync(filePath, 'utf-8'); const content = readFileSync(filePath, 'utf-8');
console.log(`\n📄 Report: ${fileName}\n`); console.log(`\n📄 Report: ${fileName}\n`);
console.log(content); console.log(content);

View File

@ -105,7 +105,7 @@ export {
COMPLETE_STEP, COMPLETE_STEP,
ABORT_STEP, ABORT_STEP,
ERROR_MESSAGES, ERROR_MESSAGES,
determineNextStepByRules, determineNextMovementByRules,
extractBlockedPrompt, extractBlockedPrompt,
LoopDetector, LoopDetector,
createInitialState, createInitialState,
@ -117,8 +117,7 @@ export {
isReportObjectConfig, isReportObjectConfig,
ReportInstructionBuilder, ReportInstructionBuilder,
StatusJudgmentBuilder, StatusJudgmentBuilder,
buildExecutionMetadata, buildEditRule,
renderExecutionMetadata,
RuleEvaluator, RuleEvaluator,
detectMatchedRule, detectMatchedRule,
evaluateAggregateConditions, evaluateAggregateConditions,
@ -141,7 +140,7 @@ export type {
ReportInstructionContext, ReportInstructionContext,
StatusJudgmentContext, StatusJudgmentContext,
InstructionContext, InstructionContext,
ExecutionMetadata, StatusRulesComponents,
BlockedHandlerResult, BlockedHandlerResult,
} from './core/workflow/index.js'; } from './core/workflow/index.js';

View File

@ -8,7 +8,7 @@ import { executeClaudeCli } from './process.js';
import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js'; import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js';
import type { AgentResponse, Status } from '../../core/models/index.js'; import type { AgentResponse, Status } from '../../core/models/index.js';
import { createLogger } from '../../shared/utils/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 // Re-export for backward compatibility
export type { ClaudeCallOptions } from './types.js'; export type { ClaudeCallOptions } from './types.js';
@ -21,8 +21,8 @@ const log = createLogger('client');
* *
* Example: detectRuleIndex("... [PLAN:2] ...", "plan") 1 * Example: detectRuleIndex("... [PLAN:2] ...", "plan") 1
*/ */
export function detectRuleIndex(content: string, stepName: string): number { export function detectRuleIndex(content: string, movementName: string): number {
const tag = stepName.toUpperCase(); const tag = movementName.toUpperCase();
const regex = new RegExp(`\\[${tag}:(\\d+)\\]`, 'gi'); const regex = new RegExp(`\\[${tag}:(\\d+)\\]`, 'gi');
const matches = [...content.matchAll(regex)]; const matches = [...content.matchAll(regex)];
const match = matches.at(-1); const match = matches.at(-1);
@ -153,7 +153,7 @@ export class ClaudeClient {
prompt: string, prompt: string,
options: ClaudeCallOptions, options: ClaudeCallOptions,
): Promise<AgentResponse> { ): Promise<AgentResponse> {
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); return this.callCustom(claudeAgentName, prompt, systemPrompt, options);
} }
@ -219,7 +219,7 @@ export class ClaudeClient {
.map((c) => `| ${c.index + 1} | ${c.text} |`) .map((c) => `| ${c.index + 1} | ${c.text} |`)
.join('\n'); .join('\n');
return getPrompt('claude.judgePrompt', undefined, { agentOutput, conditionList }); return loadTemplate('perform_judge_message', 'en', { agentOutput, conditionList });
} }
/** /**

View File

@ -11,9 +11,9 @@ import { join, basename } from 'node:path';
import type { CustomAgentConfig } from '../../../core/models/index.js'; import type { CustomAgentConfig } from '../../../core/models/index.js';
import { import {
getGlobalAgentsDir, getGlobalAgentsDir,
getGlobalWorkflowsDir, getGlobalPiecesDir,
getBuiltinAgentsDir, getBuiltinAgentsDir,
getBuiltinWorkflowsDir, getBuiltinPiecesDir,
isPathSafe, isPathSafe,
} from '../paths.js'; } from '../paths.js';
import { getLanguage } from '../global/globalConfig.js'; import { getLanguage } from '../global/globalConfig.js';
@ -23,9 +23,9 @@ function getAllowedAgentBases(): string[] {
const lang = getLanguage(); const lang = getLanguage();
return [ return [
getGlobalAgentsDir(), getGlobalAgentsDir(),
getGlobalWorkflowsDir(), getGlobalPiecesDir(),
getBuiltinAgentsDir(lang), getBuiltinAgentsDir(lang),
getBuiltinWorkflowsDir(lang), getBuiltinPiecesDir(lang),
]; ];
} }

View File

@ -9,11 +9,11 @@ import { readFileSync, existsSync } from 'node:fs';
import { join, dirname, basename } from 'node:path'; import { join, dirname, basename } from 'node:path';
import { parse as parseYaml } from 'yaml'; import { parse as parseYaml } from 'yaml';
import type { z } from 'zod'; import type { z } from 'zod';
import { WorkflowConfigRawSchema, WorkflowStepRawSchema } from '../../../core/models/index.js'; import { WorkflowConfigRawSchema, WorkflowMovementRawSchema } from '../../../core/models/index.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } 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`) */ /** Parsed movement type from Zod schema (replaces `any`) */
type RawStep = z.output<typeof WorkflowStepRawSchema>; type RawStep = z.output<typeof WorkflowMovementRawSchema>;
/** /**
* Resolve agent path from workflow specification. * Resolve agent path from workflow specification.
@ -168,8 +168,8 @@ function normalizeRule(r: {
}; };
} }
/** Normalize a raw step into internal WorkflowStep format. */ /** Normalize a raw step into internal WorkflowMovement format. */
function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep { function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowMovement {
const rules: WorkflowRule[] | undefined = step.rules?.map(normalizeRule); const rules: WorkflowRule[] | undefined = step.rules?.map(normalizeRule);
const agentSpec: string | undefined = step.agent || undefined; 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, name: step.name,
description: step.description, description: step.description,
agent: agentSpec, agent: agentSpec,
@ -215,15 +215,20 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep
export function normalizeWorkflowConfig(raw: unknown, workflowDir: string): WorkflowConfig { export function normalizeWorkflowConfig(raw: unknown, workflowDir: string): WorkflowConfig {
const parsed = WorkflowConfigRawSchema.parse(raw); 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), normalizeStepFromRaw(step, workflowDir),
); );
// Prefer `initial_movement` over legacy `initial_step`
const initialMovement = parsed.initial_movement ?? parsed.initial_step ?? movements[0]?.name ?? '';
return { return {
name: parsed.name, name: parsed.name,
description: parsed.description, description: parsed.description,
steps, movements,
initialStep: parsed.initial_step || steps[0]?.name || '', initialMovement,
maxIterations: parsed.max_iterations, maxIterations: parsed.max_iterations,
answerAgent: parsed.answer_agent, answerAgent: parsed.answer_agent,
}; };

View File

@ -9,7 +9,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs';
import { join, resolve, isAbsolute } from 'node:path'; import { join, resolve, isAbsolute } from 'node:path';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import type { WorkflowConfig } from '../../../core/models/index.js'; 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 { getLanguage, getDisabledBuiltins, getBuiltinWorkflowsEnabled } from '../global/globalConfig.js';
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { loadWorkflowFromFile } from './workflowParser.js'; import { loadWorkflowFromFile } from './workflowParser.js';
@ -25,7 +25,7 @@ export interface WorkflowWithSource {
export function listBuiltinWorkflowNames(options?: { includeDisabled?: boolean }): string[] { export function listBuiltinWorkflowNames(options?: { includeDisabled?: boolean }): string[] {
const lang = getLanguage(); const lang = getLanguage();
const dir = getBuiltinWorkflowsDir(lang); const dir = getBuiltinPiecesDir(lang);
const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins(); const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins();
const names = new Set<string>(); const names = new Set<string>();
for (const entry of iterateWorkflowDir(dir, 'builtin', disabled)) { for (const entry of iterateWorkflowDir(dir, 'builtin', disabled)) {
@ -41,7 +41,7 @@ export function getBuiltinWorkflow(name: string): WorkflowConfig | null {
const disabled = getDisabledBuiltins(); const disabled = getDisabledBuiltins();
if (disabled.includes(name)) return null; if (disabled.includes(name)) return null;
const builtinDir = getBuiltinWorkflowsDir(lang); const builtinDir = getBuiltinPiecesDir(lang);
const yamlPath = join(builtinDir, `${name}.yaml`); const yamlPath = join(builtinDir, `${name}.yaml`);
if (existsSync(yamlPath)) { if (existsSync(yamlPath)) {
return loadWorkflowFromFile(yamlPath); return loadWorkflowFromFile(yamlPath);
@ -109,12 +109,20 @@ export function loadWorkflow(
return loadWorkflowFromFile(projectMatch); return loadWorkflowFromFile(projectMatch);
} }
const globalWorkflowsDir = getGlobalWorkflowsDir(); const globalPiecesDir = getGlobalPiecesDir();
const globalMatch = resolveWorkflowFile(globalWorkflowsDir, name); const globalMatch = resolveWorkflowFile(globalPiecesDir, name);
if (globalMatch) { if (globalMatch) {
return loadWorkflowFromFile(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); return getBuiltinWorkflow(name);
} }
@ -219,9 +227,15 @@ function getWorkflowDirs(cwd: string): { dir: string; source: WorkflowSource; di
const lang = getLanguage(); const lang = getLanguage();
const dirs: { dir: string; source: WorkflowSource; disabled?: string[] }[] = []; const dirs: { dir: string; source: WorkflowSource; disabled?: string[] }[] = [];
if (getBuiltinWorkflowsEnabled()) { 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' }); dirs.push({ dir: join(getProjectConfigDir(cwd), 'workflows'), source: 'project' });
return dirs; return dirs;
} }

View File

@ -21,11 +21,14 @@ export function getGlobalAgentsDir(): string {
return join(getGlobalConfigDir(), 'agents'); return join(getGlobalConfigDir(), 'agents');
} }
/** Get takt global workflows directory (~/.takt/workflows) */ /** Get takt global pieces directory (~/.takt/pieces) */
export function getGlobalWorkflowsDir(): string { export function getGlobalPiecesDir(): string {
return join(getGlobalConfigDir(), 'workflows'); return join(getGlobalConfigDir(), 'pieces');
} }
/** @deprecated Use getGlobalPiecesDir() instead */
export const getGlobalWorkflowsDir = getGlobalPiecesDir;
/** Get takt global logs directory */ /** Get takt global logs directory */
export function getGlobalLogsDir(): string { export function getGlobalLogsDir(): string {
return join(getGlobalConfigDir(), 'logs'); return join(getGlobalConfigDir(), 'logs');
@ -36,11 +39,14 @@ export function getGlobalConfigPath(): string {
return join(getGlobalConfigDir(), 'config.yaml'); return join(getGlobalConfigDir(), 'config.yaml');
} }
/** Get builtin workflows directory (resources/global/{lang}/workflows) */ /** Get builtin pieces directory (resources/global/{lang}/pieces) */
export function getBuiltinWorkflowsDir(lang: Language): string { export function getBuiltinPiecesDir(lang: Language): string {
return join(getLanguageResourcesDir(lang), 'workflows'); return join(getLanguageResourcesDir(lang), 'pieces');
} }
/** @deprecated Use getBuiltinPiecesDir() instead */
export const getBuiltinWorkflowsDir = getBuiltinPiecesDir;
/** Get builtin agents directory (resources/global/{lang}/agents) */ /** Get builtin agents directory (resources/global/{lang}/agents) */
export function getBuiltinAgentsDir(lang: Language): string { export function getBuiltinAgentsDir(lang: Language): string {
return join(getLanguageResourcesDir(lang), 'agents'); return join(getLanguageResourcesDir(lang), 'agents');

View File

@ -8,7 +8,7 @@ import * as wanakana from 'wanakana';
import { loadGlobalConfig } from '../config/global/globalConfig.js'; import { loadGlobalConfig } from '../config/global/globalConfig.js';
import { getProvider, type ProviderType } from '../providers/index.js'; import { getProvider, type ProviderType } from '../providers/index.js';
import { createLogger } from '../../shared/utils/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'; import type { SummarizeOptions } from './types.js';
export type { SummarizeOptions }; export type { SummarizeOptions };
@ -70,7 +70,7 @@ export class TaskSummarizer {
const response = await provider.call('summarizer', taskName, { const response = await provider.call('summarizer', taskName, {
cwd: options.cwd, cwd: options.cwd,
model, model,
systemPrompt: getPrompt('summarize.slugGenerator'), systemPrompt: loadTemplate('score_slug_system_prompt', 'en'),
allowedTools: [], allowedTools: [],
}); });

View File

@ -7,6 +7,8 @@
# ===== Interactive Mode UI ===== # ===== Interactive Mode UI =====
interactive: interactive:
conversationLabel: "Conversation:"
noTranscript: "(No local transcript. Summarize the current session context.)"
ui: ui:
intro: "Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)" intro: "Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)"
resume: "Resuming previous session" resume: "Resuming previous session"
@ -21,7 +23,7 @@ interactive:
workflow: workflow:
iterationLimit: iterationLimit:
maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})" maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})"
currentStep: "現在のステップ: {currentStep}" currentMovement: "現在のムーブメント: {currentMovement}"
continueQuestion: "続行しますか?" continueQuestion: "続行しますか?"
continueLabel: "続行する(追加イテレーション数を入力)" continueLabel: "続行する(追加イテレーション数を入力)"
continueDescription: "入力した回数だけ上限を増やします" continueDescription: "入力した回数だけ上限を増やします"

View File

@ -7,6 +7,8 @@
# ===== Interactive Mode UI ===== # ===== Interactive Mode UI =====
interactive: interactive:
conversationLabel: "会話:"
noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)"
ui: ui:
intro: "対話モード - タスク内容を入力してください。コマンド: /go実行, /cancel終了" intro: "対話モード - タスク内容を入力してください。コマンド: /go実行, /cancel終了"
resume: "前回のセッションを再開します" resume: "前回のセッションを再開します"
@ -21,7 +23,7 @@ interactive:
workflow: workflow:
iterationLimit: iterationLimit:
maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})" maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})"
currentStep: "現在のステップ: {currentStep}" currentMovement: "現在のムーブメント: {currentMovement}"
continueQuestion: "続行しますか?" continueQuestion: "続行しますか?"
continueLabel: "続行する(追加イテレーション数を入力)" continueLabel: "続行する(追加イテレーション数を入力)"
continueDescription: "入力した回数だけ上限を増やします" continueDescription: "入力した回数だけ上限を増やします"

Some files were not shown because too many files have changed in this diff Show More