From 706a59d3b65cef99807ad18e2b5ee67f9c89e74d Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:33:56 +0900 Subject: [PATCH] =?UTF-8?q?edit=20=E3=83=97=E3=83=AD=E3=83=91=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=81=AB=E3=82=88=E3=82=8B=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E7=B7=A8=E9=9B=86=E5=88=B6=E5=BE=A1=E3=80=81=E3=82=B9?= =?UTF-8?q?=E3=83=86=E3=83=83=E3=83=97=E5=AE=8C=E4=BA=86=E6=99=82=E3=81=AE?= =?UTF-8?q?=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88=E3=83=AD=E3=82=B0=E5=87=BA?= =?UTF-8?q?=E5=8A=9B=E3=80=81resolveContentPath=20=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - edit: true/false をワークフローステップに追加し、エージェントへの編集許可/禁止プロンプトを自動注入 - ステップ完了時に step:report イベントを発火し、レポート内容をコンソール出力 - resolveContentPath() で format/instruction_template の .md ファイル参照に対応 - writeStepReport() を削除し、レポート出力はエージェント責務に統一 - 全8ワークフローYAMLに edit フィールドを付与 resolves #6, resolves #21, resolves #22 --- resources/global/en/workflows/default.yaml | 10 + .../global/en/workflows/expert-cqrs.yaml | 14 ++ resources/global/en/workflows/expert.yaml | 14 ++ resources/global/en/workflows/simple.yaml | 5 + resources/global/ja/workflows/default.yaml | 10 + .../global/ja/workflows/expert-cqrs.yaml | 18 +- resources/global/ja/workflows/expert.yaml | 18 +- resources/global/ja/workflows/simple.yaml | 13 ++ src/__tests__/engine-report.test.ts | 175 ++++++++++++++++++ src/__tests__/instructionBuilder.test.ts | 98 ++++++++++ src/commands/workflowExecution.ts | 7 + src/config/workflowLoader.ts | 37 +++- src/models/schemas.ts | 2 + src/models/types.ts | 2 + src/workflow/engine.ts | 31 +++- src/workflow/instruction-builder.ts | 24 ++- src/workflow/types.ts | 1 + 17 files changed, 465 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/engine-report.test.ts diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/workflows/default.yaml index c998a4f..17bd774 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/workflows/default.yaml @@ -28,6 +28,7 @@ initial_step: plan steps: - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -91,6 +92,7 @@ steps: 3. Decide implementation approach - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -149,6 +151,7 @@ steps: ``` - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -202,6 +205,7 @@ steps: - Scope creep detection - name: ai_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -231,6 +235,7 @@ steps: pass_previous_response: true - name: review + edit: false agent: ~/.takt/agents/default/architecture-reviewer.md report: name: 04-architect-review.md @@ -285,6 +290,7 @@ steps: Review the changes and provide feedback. - name: improve + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -316,6 +322,7 @@ steps: pass_previous_response: true - name: fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -342,6 +349,7 @@ steps: pass_previous_response: true - name: security_review + edit: false agent: ~/.takt/agents/default/security-reviewer.md report: name: 05-security-review.md @@ -398,6 +406,7 @@ steps: - Cryptographic weaknesses - name: security_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -423,6 +432,7 @@ steps: pass_previous_response: true - name: supervise + edit: false agent: ~/.takt/agents/default/supervisor.md report: - Validation: 06-supervisor-validation.md diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/workflows/expert-cqrs.yaml index 1a25a71..b649605 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/workflows/expert-cqrs.yaml @@ -31,6 +31,7 @@ steps: # Phase 0: Planning # =========================================== - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -91,6 +92,7 @@ steps: # Phase 1: Implementation # =========================================== - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -151,6 +153,7 @@ steps: # Phase 2: AI Review # =========================================== - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -204,6 +207,7 @@ steps: next: ai_fix - name: ai_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -235,6 +239,7 @@ steps: # Phase 3: CQRS+ES Review # =========================================== - name: cqrs_es_review + edit: false agent: ~/.takt/agents/expert-cqrs/cqrs-es-reviewer.md report: name: 04-cqrs-es-review.md @@ -292,6 +297,7 @@ steps: next: fix_cqrs_es - name: fix_cqrs_es + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -325,6 +331,7 @@ steps: # Phase 4: Frontend Review # =========================================== - name: frontend_review + edit: false agent: ~/.takt/agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md @@ -382,6 +389,7 @@ steps: next: fix_frontend - name: fix_frontend + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -415,6 +423,7 @@ steps: # Phase 5: Security Review # =========================================== - name: security_review + edit: false agent: ~/.takt/agents/expert/security-reviewer.md report: name: 06-security-review.md @@ -469,6 +478,7 @@ steps: next: fix_security - name: fix_security + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -512,6 +522,7 @@ steps: # Phase 6: QA Review # =========================================== - name: qa_review + edit: false agent: ~/.takt/agents/expert/qa-reviewer.md report: name: 07-qa-review.md @@ -566,6 +577,7 @@ steps: next: fix_qa - name: fix_qa + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -613,6 +625,7 @@ steps: # Phase 7: Supervision # =========================================== - name: supervise + edit: false agent: ~/.takt/agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md @@ -709,6 +722,7 @@ steps: next: fix_supervisor - name: fix_supervisor + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/workflows/expert.yaml index ad880c4..d5affbb 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/workflows/expert.yaml @@ -43,6 +43,7 @@ steps: # Phase 0: Planning # =========================================== - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -103,6 +104,7 @@ steps: # Phase 1: Implementation # =========================================== - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -163,6 +165,7 @@ steps: # Phase 2: AI Review # =========================================== - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -216,6 +219,7 @@ steps: next: ai_fix - name: ai_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -247,6 +251,7 @@ steps: # Phase 3: Architecture Review # =========================================== - name: architect_review + edit: false agent: ~/.takt/agents/default/architecture-reviewer.md report: name: 04-architect-review.md @@ -310,6 +315,7 @@ steps: next: fix_architect - name: fix_architect + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -339,6 +345,7 @@ steps: # Phase 4: Frontend Review # =========================================== - name: frontend_review + edit: false agent: ~/.takt/agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md @@ -396,6 +403,7 @@ steps: next: fix_frontend - name: fix_frontend + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -429,6 +437,7 @@ steps: # Phase 5: Security Review # =========================================== - name: security_review + edit: false agent: ~/.takt/agents/expert/security-reviewer.md report: name: 06-security-review.md @@ -483,6 +492,7 @@ steps: next: fix_security - name: fix_security + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -526,6 +536,7 @@ steps: # Phase 6: QA Review # =========================================== - name: qa_review + edit: false agent: ~/.takt/agents/expert/qa-reviewer.md report: name: 07-qa-review.md @@ -580,6 +591,7 @@ steps: next: fix_qa - name: fix_qa + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -627,6 +639,7 @@ steps: # Phase 7: Supervision # =========================================== - name: supervise + edit: false agent: ~/.takt/agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md @@ -723,6 +736,7 @@ steps: next: fix_supervisor - name: fix_supervisor + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read diff --git a/resources/global/en/workflows/simple.yaml b/resources/global/en/workflows/simple.yaml index 67d906a..0ed418c 100644 --- a/resources/global/en/workflows/simple.yaml +++ b/resources/global/en/workflows/simple.yaml @@ -25,6 +25,7 @@ initial_step: plan steps: - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -84,6 +85,7 @@ steps: 3. Decide implementation approach - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -142,6 +144,7 @@ steps: ``` - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -195,6 +198,7 @@ steps: - Scope creep detection - name: review + edit: false agent: ~/.takt/agents/default/architecture-reviewer.md report: name: 04-architect-review.md @@ -250,6 +254,7 @@ steps: If there are minor suggestions, use APPROVE + comments. - name: supervise + edit: false agent: ~/.takt/agents/default/supervisor.md report: - Validation: 05-supervisor-validation.md diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index 0f534e9..0d35bbc 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -19,6 +19,7 @@ initial_step: plan steps: - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -82,6 +83,7 @@ steps: 3. 実装アプローチを決める - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -145,6 +147,7 @@ steps: ``` - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -198,6 +201,7 @@ steps: - スコープクリープの検出 - name: ai_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -227,6 +231,7 @@ steps: pass_previous_response: true - name: review + edit: false agent: ~/.takt/agents/default/architecture-reviewer.md report: name: 04-architect-review.md @@ -292,6 +297,7 @@ steps: - 呼び出しチェーン検証 - name: improve + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -323,6 +329,7 @@ steps: pass_previous_response: true - name: fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -348,6 +355,7 @@ steps: pass_previous_response: true - name: security_review + edit: false agent: ~/.takt/agents/default/security-reviewer.md report: name: 05-security-review.md @@ -404,6 +412,7 @@ steps: - 暗号化の弱点 - name: security_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -429,6 +438,7 @@ steps: pass_previous_response: true - name: supervise + edit: false agent: ~/.takt/agents/default/supervisor.md report: - Validation: 06-supervisor-validation.md diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/workflows/expert-cqrs.yaml index 3116daa..d2f7286 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/workflows/expert-cqrs.yaml @@ -40,6 +40,7 @@ steps: # Phase 0: Planning # =========================================== - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -100,6 +101,7 @@ steps: # Phase 1: Implementation # =========================================== - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -117,7 +119,7 @@ steps: planステップで立てた計画に従って実装してください。 計画レポート(00-plan.md)を参照し、実装を進めてください。 - **レポート出力:** 上記の `Report Files` に出力してください。 + **レポート出力:** Report Files に出力してください。 - ファイルが存在しない場合: 新規作成 - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 @@ -160,6 +162,7 @@ steps: # Phase 2: AI Review # =========================================== - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -213,6 +216,7 @@ steps: next: ai_fix - name: ai_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -244,6 +248,7 @@ steps: # Phase 3: CQRS+ES Review # =========================================== - name: cqrs_es_review + edit: false agent: ~/.takt/agents/expert-cqrs/cqrs-es-reviewer.md report: name: 04-cqrs-es-review.md @@ -301,6 +306,7 @@ steps: next: fix_cqrs_es - name: fix_cqrs_es + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -334,6 +340,7 @@ steps: # Phase 4: Frontend Review # =========================================== - name: frontend_review + edit: false agent: ~/.takt/agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md @@ -391,6 +398,7 @@ steps: next: fix_frontend - name: fix_frontend + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -424,6 +432,7 @@ steps: # Phase 5: Security Review # =========================================== - name: security_review + edit: false agent: ~/.takt/agents/expert/security-reviewer.md report: name: 06-security-review.md @@ -478,6 +487,7 @@ steps: next: fix_security - name: fix_security + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -521,6 +531,7 @@ steps: # Phase 6: QA Review # =========================================== - name: qa_review + edit: false agent: ~/.takt/agents/expert/qa-reviewer.md report: name: 07-qa-review.md @@ -575,6 +586,7 @@ steps: next: fix_qa - name: fix_qa + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -622,6 +634,7 @@ steps: # Phase 7: Supervision # =========================================== - name: supervise + edit: false agent: ~/.takt/agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md @@ -652,7 +665,7 @@ steps: **レポートの確認:** Report Directory内の全レポートを読み、 未対応の改善提案がないか確認してください。 - **レポート出力:** 上記の `Report Files` に出力してください。 + **レポート出力:** Report Files に出力してください。 - ファイルが存在しない場合: 新規作成 - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 @@ -718,6 +731,7 @@ steps: next: fix_supervisor - name: fix_supervisor + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/workflows/expert.yaml index a537eaf..9dd0909 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/workflows/expert.yaml @@ -31,6 +31,7 @@ steps: # Phase 0: Planning # =========================================== - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -91,6 +92,7 @@ steps: # Phase 1: Implementation # =========================================== - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -108,7 +110,7 @@ steps: planステップで立てた計画に従って実装してください。 計画レポート(00-plan.md)を参照し、実装を進めてください。 - **レポート出力:** 上記の `Report Files` に出力してください。 + **レポート出力:** Report Files に出力してください。 - ファイルが存在しない場合: 新規作成 - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 @@ -151,6 +153,7 @@ steps: # Phase 2: AI Review # =========================================== - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -204,6 +207,7 @@ steps: next: ai_fix - name: ai_fix + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -235,6 +239,7 @@ steps: # Phase 3: Architecture Review # =========================================== - name: architect_review + edit: false agent: ~/.takt/agents/default/architecture-reviewer.md report: name: 04-architect-review.md @@ -298,6 +303,7 @@ steps: next: fix_architect - name: fix_architect + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -327,6 +333,7 @@ steps: # Phase 4: Frontend Review # =========================================== - name: frontend_review + edit: false agent: ~/.takt/agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md @@ -384,6 +391,7 @@ steps: next: fix_frontend - name: fix_frontend + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -417,6 +425,7 @@ steps: # Phase 5: Security Review # =========================================== - name: security_review + edit: false agent: ~/.takt/agents/expert/security-reviewer.md report: name: 06-security-review.md @@ -471,6 +480,7 @@ steps: next: fix_security - name: fix_security + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -514,6 +524,7 @@ steps: # Phase 6: QA Review # =========================================== - name: qa_review + edit: false agent: ~/.takt/agents/expert/qa-reviewer.md report: name: 07-qa-review.md @@ -568,6 +579,7 @@ steps: next: fix_qa - name: fix_qa + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read @@ -615,6 +627,7 @@ steps: # Phase 7: Supervision # =========================================== - name: supervise + edit: false agent: ~/.takt/agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md @@ -645,7 +658,7 @@ steps: **レポートの確認:** Report Directory内の全レポートを読み、 未対応の改善提案がないか確認してください。 - **レポート出力:** 上記の `Report Files` に出力してください。 + **レポート出力:** Report Files に出力してください。 - ファイルが存在しない場合: 新規作成 - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 @@ -711,6 +724,7 @@ steps: next: fix_supervisor - name: fix_supervisor + edit: true agent: ~/.takt/agents/default/coder.md allowed_tools: - Read diff --git a/resources/global/ja/workflows/simple.yaml b/resources/global/ja/workflows/simple.yaml index 552331a..dd74e49 100644 --- a/resources/global/ja/workflows/simple.yaml +++ b/resources/global/ja/workflows/simple.yaml @@ -20,6 +20,7 @@ initial_step: plan steps: - name: plan + edit: false agent: ~/.takt/agents/default/planner.md report: name: 00-plan.md @@ -83,6 +84,7 @@ steps: - {質問2} - name: implement + edit: true agent: ~/.takt/agents/default/coder.md report: - Scope: 01-coder-scope.md @@ -101,6 +103,10 @@ steps: planステップで立てた計画に従って実装してください。 計画レポート(00-plan.md)を参照し、実装を進めてください。 + **レポート出力:** Report Files に出力してください。 + - ファイルが存在しない場合: 新規作成 + - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 + **Scopeレポートフォーマット(実装開始時に作成):** ```markdown # 変更スコープ宣言 @@ -137,6 +143,7 @@ steps: next: plan - name: ai_review + edit: false agent: ~/.takt/agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md @@ -190,6 +197,7 @@ steps: next: plan - name: review + edit: false agent: ~/.takt/agents/default/architecture-reviewer.md report: name: 04-architect-review.md @@ -245,6 +253,7 @@ steps: next: plan - name: supervise + edit: false agent: ~/.takt/agents/default/supervisor.md report: - Validation: 05-supervisor-validation.md @@ -268,6 +277,10 @@ steps: **レポートの確認:** Report Directory内の全レポートを読み、 未対応の改善提案がないか確認してください。 + **レポート出力:** Report Files に出力してください。 + - ファイルが存在しない場合: 新規作成 + - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 + **Validationレポートフォーマット:** ```markdown # 最終検証結果 diff --git a/src/__tests__/engine-report.test.ts b/src/__tests__/engine-report.test.ts new file mode 100644 index 0000000..2bd0a09 --- /dev/null +++ b/src/__tests__/engine-report.test.ts @@ -0,0 +1,175 @@ +/** + * Tests for engine report event emission (step:report) + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { EventEmitter } from 'node:events'; +import { existsSync } from 'node:fs'; +import { isReportObjectConfig } from '../workflow/instruction-builder.js'; +import type { WorkflowStep, ReportObjectConfig, ReportConfig } from '../models/types.js'; + +/** + * Extracted emitStepReports logic for unit testing. + * Mirrors engine.ts emitStepReports + emitIfReportExists. + */ +function emitStepReports( + emitter: EventEmitter, + step: WorkflowStep, + reportDir: string, + projectCwd: string, +): void { + if (!step.report || !reportDir) return; + const baseDir = join(projectCwd, '.takt', 'reports', reportDir); + + if (typeof step.report === 'string') { + emitIfReportExists(emitter, step, baseDir, step.report); + } else if (isReportObjectConfig(step.report)) { + emitIfReportExists(emitter, step, baseDir, step.report.name); + } else { + for (const rc of step.report) { + emitIfReportExists(emitter, step, baseDir, rc.path); + } + } +} + +function emitIfReportExists( + emitter: EventEmitter, + step: WorkflowStep, + baseDir: string, + fileName: string, +): void { + const filePath = join(baseDir, fileName); + if (existsSync(filePath)) { + emitter.emit('step:report', step, filePath, fileName); + } +} + +/** Create a minimal WorkflowStep for testing */ +function createStep(overrides: Partial = {}): WorkflowStep { + return { + name: 'test-step', + agent: 'coder', + agentDisplayName: 'Coder', + instructionTemplate: '', + passPreviousResponse: false, + ...overrides, + }; +} + +describe('emitStepReports', () => { + let tmpDir: string; + let reportBaseDir: string; + const reportDirName = 'test-report-dir'; + + beforeEach(() => { + tmpDir = join(tmpdir(), `takt-report-test-${Date.now()}`); + reportBaseDir = join(tmpDir, '.takt', 'reports', reportDirName); + mkdirSync(reportBaseDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('should emit step:report when string report file exists', () => { + // Given: a step with string report and the file exists + const step = createStep({ report: 'plan.md' }); + writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8'); + const emitter = new EventEmitter(); + const handler = vi.fn(); + emitter.on('step:report', handler); + + // When + emitStepReports(emitter, step, reportDirName, tmpDir); + + // Then + expect(handler).toHaveBeenCalledOnce(); + expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, 'plan.md'), 'plan.md'); + }); + + it('should not emit when string report file does not exist', () => { + // Given: a step with string report but file doesn't exist + const step = createStep({ report: 'missing.md' }); + const emitter = new EventEmitter(); + const handler = vi.fn(); + emitter.on('step:report', handler); + + // When + emitStepReports(emitter, step, reportDirName, tmpDir); + + // Then + expect(handler).not.toHaveBeenCalled(); + }); + + it('should emit step:report when ReportObjectConfig report file exists', () => { + // Given: a step with ReportObjectConfig and the file exists + const report: ReportObjectConfig = { name: '03-review.md', format: '# Review' }; + const step = createStep({ report }); + writeFileSync(join(reportBaseDir, '03-review.md'), '# Review\nOK', 'utf-8'); + const emitter = new EventEmitter(); + const handler = vi.fn(); + emitter.on('step:report', handler); + + // When + emitStepReports(emitter, step, reportDirName, tmpDir); + + // Then + expect(handler).toHaveBeenCalledOnce(); + expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '03-review.md'), '03-review.md'); + }); + + it('should emit for each existing file in ReportConfig[] array', () => { + // Given: a step with array report, two files exist, one missing + const report: ReportConfig[] = [ + { label: 'Scope', path: '01-scope.md' }, + { label: 'Decisions', path: '02-decisions.md' }, + { label: 'Missing', path: '03-missing.md' }, + ]; + const step = createStep({ report }); + writeFileSync(join(reportBaseDir, '01-scope.md'), '# Scope', 'utf-8'); + writeFileSync(join(reportBaseDir, '02-decisions.md'), '# Decisions', 'utf-8'); + const emitter = new EventEmitter(); + const handler = vi.fn(); + emitter.on('step:report', handler); + + // When + emitStepReports(emitter, step, reportDirName, tmpDir); + + // Then: emitted for scope and decisions, not for missing + expect(handler).toHaveBeenCalledTimes(2); + expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '01-scope.md'), '01-scope.md'); + expect(handler).toHaveBeenCalledWith(step, join(reportBaseDir, '02-decisions.md'), '02-decisions.md'); + }); + + it('should not emit when step has no report', () => { + // Given: a step without report + const step = createStep({ report: undefined }); + const emitter = new EventEmitter(); + const handler = vi.fn(); + emitter.on('step:report', handler); + + // When + emitStepReports(emitter, step, reportDirName, tmpDir); + + // Then + expect(handler).not.toHaveBeenCalled(); + }); + + it('should not emit when reportDir is empty', () => { + // Given: a step with report but empty reportDir + const step = createStep({ report: 'plan.md' }); + writeFileSync(join(reportBaseDir, 'plan.md'), '# Plan', 'utf-8'); + const emitter = new EventEmitter(); + const handler = vi.fn(); + emitter.on('step:report', handler); + + // When: empty reportDir + emitStepReports(emitter, step, '', tmpDir); + + // Then + expect(handler).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index b2e10a1..afa3048 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -9,6 +9,7 @@ import { renderExecutionMetadata, renderStatusRulesHeader, generateStatusRulesFromRules, + isReportObjectConfig, type InstructionContext, } from '../workflow/instruction-builder.js'; import type { WorkflowStep, WorkflowRule } from '../models/types.js'; @@ -75,6 +76,34 @@ describe('instruction-builder', () => { expect(metadataIndex).toBeLessThan(bodyIndex); }); + + it('should include edit enabled prompt when step.edit is true', () => { + const step = { ...createMinimalStep('Implement feature'), edit: true as const }; + const context = createMinimalContext({ cwd: '/project' }); + + const result = buildInstruction(step, context); + + expect(result).toContain('Editing is ENABLED'); + }); + + it('should include edit disabled prompt when step.edit is false', () => { + const step = { ...createMinimalStep('Review code'), edit: false as const }; + const context = createMinimalContext({ cwd: '/project' }); + + const result = buildInstruction(step, context); + + expect(result).toContain('Editing is DISABLED'); + }); + + it('should not include edit prompt when step.edit is undefined', () => { + const step = createMinimalStep('Do some work'); + const context = createMinimalContext({ cwd: '/project' }); + + const result = buildInstruction(step, context); + + expect(result).not.toContain('Editing is ENABLED'); + expect(result).not.toContain('Editing is DISABLED'); + }); }); describe('report_dir replacement', () => { @@ -176,6 +205,20 @@ describe('instruction-builder', () => { 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', () => { @@ -212,6 +255,43 @@ describe('instruction-builder', () => { 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('renderStatusRulesHeader', () => { @@ -746,4 +826,22 @@ describe('instruction-builder', () => { expect(result).toContain('Run #2'); }); }); + + describe('isReportObjectConfig', () => { + it('should return true for ReportObjectConfig', () => { + expect(isReportObjectConfig({ name: '00-plan.md' })).toBe(true); + }); + + it('should return true for ReportObjectConfig with order/format', () => { + expect(isReportObjectConfig({ name: '00-plan.md', order: 'output to...', format: '# Plan' })).toBe(true); + }); + + it('should return false for string', () => { + expect(isReportObjectConfig('00-plan.md')).toBe(false); + }); + + it('should return false for ReportConfig[] (array)', () => { + expect(isReportObjectConfig([{ label: 'Scope', path: '01-scope.md' }])).toBe(false); + }); + }); }); diff --git a/src/commands/workflowExecution.ts b/src/commands/workflowExecution.ts index 85879ef..0c7be9f 100644 --- a/src/commands/workflowExecution.ts +++ b/src/commands/workflowExecution.ts @@ -2,6 +2,7 @@ * Workflow execution logic */ +import { readFileSync } from 'node:fs'; import { WorkflowEngine } from '../workflow/engine.js'; import type { WorkflowConfig, Language } from '../models/types.js'; import type { IterationLimitRequest } from '../workflow/types.js'; @@ -223,6 +224,12 @@ export async function executeWorkflow( updateLatestPointer(sessionLog, workflowSessionId, projectCwd); }); + engine.on('step:report', (_step, filePath, fileName) => { + const content = readFileSync(filePath, 'utf-8'); + console.log(`\n📄 Report: ${fileName}\n`); + console.log(content); + }); + engine.on('workflow:complete', (state) => { log.info('Workflow completed successfully', { iterations: state.iteration }); finalizeSessionLog(sessionLog, 'completed'); diff --git a/src/config/workflowLoader.ts b/src/config/workflowLoader.ts index b0a1956..93c9f9a 100644 --- a/src/config/workflowLoader.ts +++ b/src/config/workflowLoader.ts @@ -54,6 +54,31 @@ function extractAgentDisplayName(agentPath: string): string { return filename; } +/** + * Resolve a string value that may be a file path. + * If the value ends with .md and the file exists (resolved relative to workflowDir), + * read and return the file contents. Otherwise return the value as-is. + */ +function resolveContentPath(value: string | undefined, workflowDir: string): string | undefined { + if (value == null) return undefined; + if (value.endsWith('.md')) { + // Resolve path relative to workflow directory + let resolvedPath = value; + if (value.startsWith('./')) { + resolvedPath = join(workflowDir, value.slice(2)); + } else if (value.startsWith('~')) { + const homedir = process.env.HOME || process.env.USERPROFILE || ''; + resolvedPath = join(homedir, value.slice(1)); + } else if (!value.startsWith('/')) { + resolvedPath = join(workflowDir, value); + } + if (existsSync(resolvedPath)) { + return readFileSync(resolvedPath, 'utf-8'); + } + } + return value; +} + /** * Check if a raw report value is the object form (has 'name' property). */ @@ -78,11 +103,16 @@ function isReportObject(raw: unknown): raw is { name: string; order?: string; fo */ function normalizeReport( raw: string | Record[] | { name: string; order?: string; format?: string } | undefined, + workflowDir: string, ): string | ReportConfig[] | ReportObjectConfig | undefined { if (raw == null) return undefined; if (typeof raw === 'string') return raw; if (isReportObject(raw)) { - return { name: raw.name, order: raw.order, format: raw.format }; + return { + name: raw.name, + order: resolveContentPath(raw.order, workflowDir), + format: resolveContentPath(raw.format, workflowDir), + }; } // Convert [{Scope: "01-scope.md"}, ...] to [{label: "Scope", path: "01-scope.md"}, ...] return (raw as Record[]).flatMap((entry) => @@ -113,9 +143,10 @@ function normalizeWorkflowConfig(raw: unknown, workflowDir: string): WorkflowCon provider: step.provider, model: step.model, permissionMode: step.permission_mode, - instructionTemplate: step.instruction_template || step.instruction || '{task}', + edit: step.edit, + instructionTemplate: resolveContentPath(step.instruction_template, workflowDir) || step.instruction || '{task}', rules, - report: normalizeReport(step.report), + report: normalizeReport(step.report, workflowDir), passPreviousResponse: step.pass_previous_response, }; }); diff --git a/src/models/schemas.ts b/src/models/schemas.ts index 7da615e..e1d7aab 100644 --- a/src/models/schemas.ts +++ b/src/models/schemas.ts @@ -92,6 +92,8 @@ export const WorkflowStepRawSchema = z.object({ model: z.string().optional(), /** Permission mode for tool execution in this step */ permission_mode: PermissionModeSchema.optional(), + /** Whether this step is allowed to edit project files */ + edit: z.boolean().optional(), instruction: z.string().optional(), instruction_template: z.string().optional(), /** Rules for step routing */ diff --git a/src/models/types.ts b/src/models/types.ts index 0390d1d..8bce74c 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -88,6 +88,8 @@ export interface WorkflowStep { model?: string; /** Permission mode for tool execution in this step */ permissionMode?: PermissionMode; + /** Whether this step is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */ + edit?: boolean; instructionTemplate: string; /** Rules for step routing */ rules?: WorkflowRule[]; diff --git a/src/workflow/engine.ts b/src/workflow/engine.ts index 7cd8d4a..6e311e8 100644 --- a/src/workflow/engine.ts +++ b/src/workflow/engine.ts @@ -16,7 +16,7 @@ import { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from './constants.js'; import type { WorkflowEngineOptions } from './types.js'; import { determineNextStepByRules } from './transitions.js'; import { detectRuleIndex } from '../claude/client.js'; -import { buildInstruction as buildInstructionFromTemplate } from './instruction-builder.js'; +import { buildInstruction as buildInstructionFromTemplate, isReportObjectConfig } from './instruction-builder.js'; import { LoopDetector } from './loop-detector.js'; import { handleBlocked } from './blocked-handler.js'; import { @@ -168,6 +168,34 @@ export class WorkflowEngine extends EventEmitter { return step; } + /** + * Emit step:report events for each report file that exists after step completion. + * The UI layer (workflowExecution.ts) listens and displays the content. + */ + private emitStepReports(step: WorkflowStep): void { + if (!step.report || !this.reportDir) return; + const baseDir = join(this.projectCwd, '.takt', 'reports', this.reportDir); + + if (typeof step.report === 'string') { + this.emitIfReportExists(step, baseDir, step.report); + } else if (isReportObjectConfig(step.report)) { + this.emitIfReportExists(step, baseDir, step.report.name); + } else { + // ReportConfig[] (array) + for (const rc of step.report) { + this.emitIfReportExists(step, baseDir, rc.path); + } + } + } + + /** Emit step:report if the report file exists */ + private emitIfReportExists(step: WorkflowStep, baseDir: string, fileName: string): void { + const filePath = join(baseDir, fileName); + if (existsSync(filePath)) { + this.emit('step:report', step, filePath, fileName); + } + } + /** Run a single step */ private async runStep(step: WorkflowStep): Promise<{ response: AgentResponse; instruction: string }> { const stepIteration = incrementStepIteration(this.state, step.name); @@ -214,6 +242,7 @@ export class WorkflowEngine extends EventEmitter { } this.state.stepOutputs.set(step.name, response); + this.emitStepReports(step); return { response, instruction }; } diff --git a/src/workflow/instruction-builder.ts b/src/workflow/instruction-builder.ts index 12a2724..ab034df 100644 --- a/src/workflow/instruction-builder.ts +++ b/src/workflow/instruction-builder.ts @@ -43,17 +43,20 @@ export interface ExecutionMetadata { 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. + * Build execution metadata from instruction context and step config. * - * Pure function: InstructionContext → ExecutionMetadata. + * Pure function: (InstructionContext, edit?) → ExecutionMetadata. */ -export function buildExecutionMetadata(context: InstructionContext): ExecutionMetadata { +export function buildExecutionMetadata(context: InstructionContext, edit?: boolean): ExecutionMetadata { return { workingDirectory: context.cwd, language: context.language ?? 'en', + edit, }; } @@ -172,6 +175,8 @@ const METADATA_STRINGS = { rulesHeading: '## Execution Rules', noCommit: '**Do NOT run git commit.** Commits are handled automatically by the system after workflow completion.', noCd: '**Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories.', + editEnabled: '**Editing is ENABLED for this step.** You may create, modify, and delete files as needed to fulfill the user\'s request.', + editDisabled: '**Editing is DISABLED for this step.** Do NOT create, modify, or delete any project source files. You may only read/search code and write to report files in the Report Directory.', note: 'Note: This section is metadata. Follow the language used in the rest of the prompt.', }, ja: { @@ -180,6 +185,8 @@ const METADATA_STRINGS = { rulesHeading: '## 実行ルール', noCommit: '**git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。', noCd: '**Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。', + editEnabled: '**このステップでは編集が許可されています。** ユーザーの要求に応じて、ファイルの作成・変更・削除を行ってください。', + editDisabled: '**このステップでは編集が禁止されています。** プロジェクトのソースファイルを作成・変更・削除しないでください。コードの読み取り・検索と、Report Directoryへのレポート出力のみ行えます。', note: '', }, } as const; @@ -201,6 +208,11 @@ export function renderExecutionMetadata(metadata: ExecutionMetadata): string { `- ${strings.noCommit}`, `- ${strings.noCd}`, ]; + if (metadata.edit === true) { + lines.push(`- ${strings.editEnabled}`); + } else if (metadata.edit === false) { + lines.push(`- ${strings.editDisabled}`); + } if (strings.note) { lines.push(''); lines.push(strings.note); @@ -219,7 +231,7 @@ function escapeTemplateChars(str: string): string { /** * Check if a report config is the object form (ReportObjectConfig). */ -function isReportObjectConfig(report: string | ReportConfig[] | ReportObjectConfig): report is ReportObjectConfig { +export function isReportObjectConfig(report: string | ReportConfig[] | ReportObjectConfig): report is ReportObjectConfig { return typeof report === 'object' && !Array.isArray(report) && 'name' in report; } @@ -384,8 +396,8 @@ export function buildInstruction( const s = SECTION_STRINGS[language]; const sections: string[] = []; - // 1. Execution context metadata (working directory + rules) - const metadata = buildExecutionMetadata(context); + // 1. Execution context metadata (working directory + rules + edit permission) + const metadata = buildExecutionMetadata(context, step.edit); sections.push(renderExecutionMetadata(metadata)); // 2. Workflow Context (iteration, step, report info) diff --git a/src/workflow/types.ts b/src/workflow/types.ts index c1b2246..d41f82e 100644 --- a/src/workflow/types.ts +++ b/src/workflow/types.ts @@ -13,6 +13,7 @@ import type { PermissionHandler, AskUserQuestionHandler } from '../claude/proces export interface WorkflowEvents { 'step:start': (step: WorkflowStep, iteration: number) => void; 'step:complete': (step: WorkflowStep, response: AgentResponse, instruction: string) => void; + 'step:report': (step: WorkflowStep, filePath: string, fileName: string) => void; 'step:blocked': (step: WorkflowStep, response: AgentResponse) => void; 'step:user_input': (step: WorkflowStep, userInput: string) => void; 'workflow:complete': (state: WorkflowState) => void;