From 9159f071f402f3b13a20fde307ac3bd079ed1d0b Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:24:50 +0900 Subject: [PATCH] =?UTF-8?q?hotfix:=20=E9=81=8E=E5=8E=BB=E3=81=AEprevious?= =?UTF-8?q?=5Fresponse=E3=81=8C=E3=83=90=E3=82=A4=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F?= =?UTF-8?q?=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/en/prompts/interactive-summary.md | 4 +- .../global/ja/prompts/interactive-summary.md | 2 + src/__tests__/engine-happy-path.test.ts | 122 ++++++++++++++++++ src/__tests__/it-rule-evaluation.test.ts | 1 + src/core/models/workflow-types.ts | 2 + src/core/workflow/engine/MovementExecutor.ts | 2 +- src/core/workflow/engine/ParallelRunner.ts | 1 + src/core/workflow/engine/state-manager.ts | 2 + .../tasks/execute/workflowExecution.ts | 2 +- .../prompts/en/score_summary_system_prompt.md | 4 +- .../prompts/ja/score_summary_system_prompt.md | 4 +- 11 files changed, 141 insertions(+), 5 deletions(-) diff --git a/resources/global/en/prompts/interactive-summary.md b/resources/global/en/prompts/interactive-summary.md index 2be3122..3b1d8bf 100644 --- a/resources/global/en/prompts/interactive-summary.md +++ b/resources/global/en/prompts/interactive-summary.md @@ -8,7 +8,9 @@ You are responsible for instruction creation in TAKT's interactive mode. Convert ## Requirements - Output only the final task instruction (no preamble). - Be specific about scope and targets (files/modules) if mentioned. -- Preserve user-provided constraints and "do not" instructions. +- Preserve user-provided constraints and "do not" instructions **only if explicitly stated by the user**. +- If the source of a constraint is unclear, do not include it; add it to Open Questions if needed. +- Do not include constraints proposed or inferred by the assistant. - Do NOT include assistant/system operational constraints (tool limits, execution prohibitions). - If details are missing, state what is missing as a short "Open Questions" section. - Clearly specify the concrete work that the workflow will execute. diff --git a/resources/global/ja/prompts/interactive-summary.md b/resources/global/ja/prompts/interactive-summary.md index 2ef6436..74b913b 100644 --- a/resources/global/ja/prompts/interactive-summary.md +++ b/resources/global/ja/prompts/interactive-summary.md @@ -9,6 +9,8 @@ - 出力はタスク指示書のみ(前置き不要) - スコープや対象(ファイル/モジュール)が出ている場合は明確に書く - ユーザー由来の制約や「やらないこと」は保持する +- 制約の出所が不明な場合は保持せず、必要なら Open Questions に回す +- アシスタントが提案・推測した制約は指示書に含めない - アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない - 情報不足があれば「Open Questions」セクションを短く付ける - ワークフローが実行する具体的な作業内容を明記する diff --git a/src/__tests__/engine-happy-path.test.ts b/src/__tests__/engine-happy-path.test.ts index f6a2dc3..dd49c36 100644 --- a/src/__tests__/engine-happy-path.test.ts +++ b/src/__tests__/engine-happy-path.test.ts @@ -149,6 +149,128 @@ describe('WorkflowEngine Integration: Happy Path', () => { // plan, implement, ai_review, reviewers(1st), fix, reviewers(2nd), supervise = 7 expect(state.iteration).toBe(7); }); + + it('should inject latest reviewers output as Previous Response for repeated fix steps', async () => { + const config = buildDefaultWorkflowConfig(); + const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + + mockRunAgentSequence([ + makeResponse({ agent: 'plan', content: 'Plan done' }), + makeResponse({ agent: 'implement', content: 'Impl done' }), + makeResponse({ agent: 'ai_review', content: 'No issues' }), + // Round 1 reviewers + makeResponse({ agent: 'arch-review', content: 'Arch R1 OK' }), + makeResponse({ agent: 'security-review', content: 'Sec R1 needs fix' }), + // fix round 1 + makeResponse({ agent: 'fix', content: 'Fix R1' }), + // Round 2 reviewers + makeResponse({ agent: 'arch-review', content: 'Arch R2 OK' }), + makeResponse({ agent: 'security-review', content: 'Sec R2 still failing' }), + // fix round 2 + makeResponse({ agent: 'fix', content: 'Fix R2' }), + // Round 3 reviewers (approved) + makeResponse({ agent: 'arch-review', content: 'Arch R3 OK' }), + makeResponse({ agent: 'security-review', content: 'Sec R3 OK' }), + // supervise + makeResponse({ agent: 'supervise', content: 'All passed' }), + ]); + + mockDetectMatchedRuleSequence([ + { index: 0, method: 'phase1_tag' }, // plan → implement + { index: 0, method: 'phase1_tag' }, // implement → ai_review + { index: 0, method: 'phase1_tag' }, // ai_review → reviewers + { index: 0, method: 'phase1_tag' }, // arch-review → approved + { index: 1, method: 'phase1_tag' }, // security-review → needs_fix + { index: 1, method: 'aggregate' }, // reviewers: any(needs_fix) → fix + { index: 0, method: 'phase1_tag' }, // fix → reviewers + { index: 0, method: 'phase1_tag' }, // arch-review → approved + { index: 1, method: 'phase1_tag' }, // security-review → needs_fix + { index: 1, method: 'aggregate' }, // reviewers: any(needs_fix) → fix + { index: 0, method: 'phase1_tag' }, // fix → reviewers + { index: 0, method: 'phase1_tag' }, // arch-review → approved + { index: 0, method: 'phase1_tag' }, // security-review → approved + { index: 0, method: 'aggregate' }, // reviewers: all(approved) → supervise + { index: 0, method: 'phase1_tag' }, // supervise → COMPLETE + ]); + + const fixInstructions: string[] = []; + engine.on('movement:start', (step, _iteration, instruction) => { + if (step.name === 'fix') { + fixInstructions.push(instruction); + } + }); + + await engine.run(); + + expect(fixInstructions).toHaveLength(2); + + const fix1 = fixInstructions[0]!; + expect(fix1).toContain('## Previous Response'); + expect(fix1).toContain('Arch R1 OK'); + expect(fix1).toContain('Sec R1 needs fix'); + expect(fix1).not.toContain('Arch R2 OK'); + expect(fix1).not.toContain('Sec R2 still failing'); + + const fix2 = fixInstructions[1]!; + expect(fix2).toContain('## Previous Response'); + expect(fix2).toContain('Arch R2 OK'); + expect(fix2).toContain('Sec R2 still failing'); + expect(fix2).not.toContain('Arch R1 OK'); + expect(fix2).not.toContain('Sec R1 needs fix'); + }); + + it('should use the latest movement output across different steps for Previous Response', async () => { + const config = buildDefaultWorkflowConfig(); + const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + + mockRunAgentSequence([ + makeResponse({ agent: 'plan', content: 'Plan done' }), + makeResponse({ agent: 'implement', content: 'Impl done' }), + makeResponse({ agent: 'ai_review', content: 'AI issues found' }), + // ai_fix (should see ai_review output) + makeResponse({ agent: 'ai_fix', content: 'AI issues fixed' }), + // reviewers (approved) + makeResponse({ agent: 'arch-review', content: 'Arch OK' }), + makeResponse({ agent: 'security-review', content: 'Sec OK' }), + // supervise (should see reviewers aggregate output) + makeResponse({ agent: 'supervise', content: 'All passed' }), + ]); + + mockDetectMatchedRuleSequence([ + { index: 0, method: 'phase1_tag' }, // plan → implement + { index: 0, method: 'phase1_tag' }, // implement → ai_review + { index: 1, method: 'phase1_tag' }, // ai_review → ai_fix + { index: 0, method: 'phase1_tag' }, // ai_fix → reviewers + { index: 0, method: 'phase1_tag' }, // arch-review → approved + { index: 0, method: 'phase1_tag' }, // security-review → approved + { index: 0, method: 'aggregate' }, // reviewers → supervise + { index: 0, method: 'phase1_tag' }, // supervise → COMPLETE + ]); + + const aiFixInstructions: string[] = []; + const superviseInstructions: string[] = []; + engine.on('movement:start', (step, _iteration, instruction) => { + if (step.name === 'ai_fix') { + aiFixInstructions.push(instruction); + } else if (step.name === 'supervise') { + superviseInstructions.push(instruction); + } + }); + + await engine.run(); + + expect(aiFixInstructions).toHaveLength(1); + const aiFix = aiFixInstructions[0]!; + expect(aiFix).toContain('## Previous Response'); + expect(aiFix).toContain('AI issues found'); + expect(aiFix).not.toContain('AI issues fixed'); + + expect(superviseInstructions).toHaveLength(1); + const supervise = superviseInstructions[0]!; + expect(supervise).toContain('## Previous Response'); + expect(supervise).toContain('Arch OK'); + expect(supervise).toContain('Sec OK'); + }); }); // ===================================================== diff --git a/src/__tests__/it-rule-evaluation.test.ts b/src/__tests__/it-rule-evaluation.test.ts index c6a42ab..d7f11f3 100644 --- a/src/__tests__/it-rule-evaluation.test.ts +++ b/src/__tests__/it-rule-evaluation.test.ts @@ -66,6 +66,7 @@ function makeState(movementOutputs?: Map): WorkflowState iteration: 1, status: 'running', movementOutputs: movementOutputs ?? new Map(), + lastOutput: undefined, movementIterations: new Map(), agentSessions: new Map(), userInputs: [], diff --git a/src/core/models/workflow-types.ts b/src/core/models/workflow-types.ts index 548e0fa..b033044 100644 --- a/src/core/models/workflow-types.ts +++ b/src/core/models/workflow-types.ts @@ -114,6 +114,8 @@ export interface WorkflowState { currentMovement: string; iteration: number; movementOutputs: Map; + /** Most recent movement output (used for Previous Response injection) */ + lastOutput?: AgentResponse; userInputs: string[]; agentSessions: Map; /** Per-movement iteration counters (how many times each movement has been executed) */ diff --git a/src/core/workflow/engine/MovementExecutor.ts b/src/core/workflow/engine/MovementExecutor.ts index 83f92fe..71310dd 100644 --- a/src/core/workflow/engine/MovementExecutor.ts +++ b/src/core/workflow/engine/MovementExecutor.ts @@ -134,6 +134,7 @@ export class MovementExecutor { } state.movementOutputs.set(step.name, response); + state.lastOutput = response; this.emitMovementReports(step); return { response, instruction }; } @@ -174,4 +175,3 @@ export class MovementExecutor { } } - diff --git a/src/core/workflow/engine/ParallelRunner.ts b/src/core/workflow/engine/ParallelRunner.ts index 46e6512..6c3b894 100644 --- a/src/core/workflow/engine/ParallelRunner.ts +++ b/src/core/workflow/engine/ParallelRunner.ts @@ -160,6 +160,7 @@ export class ParallelRunner { }; state.movementOutputs.set(step.name, aggregatedResponse); + state.lastOutput = aggregatedResponse; this.deps.movementExecutor.emitMovementReports(step); return { response: aggregatedResponse, instruction: aggregatedInstruction }; } diff --git a/src/core/workflow/engine/state-manager.ts b/src/core/workflow/engine/state-manager.ts index ead6d6e..9145fdb 100644 --- a/src/core/workflow/engine/state-manager.ts +++ b/src/core/workflow/engine/state-manager.ts @@ -39,6 +39,7 @@ export class StateManager { currentMovement: config.initialMovement, iteration: 0, movementOutputs: new Map(), + lastOutput: undefined, userInputs, agentSessions, movementIterations: new Map(), @@ -111,6 +112,7 @@ export function addUserInput(state: WorkflowState, input: string): void { * Get the most recent movement output. */ export function getPreviousOutput(state: WorkflowState): AgentResponse | undefined { + if (state.lastOutput) return state.lastOutput; const outputs = Array.from(state.movementOutputs.values()); return outputs[outputs.length - 1]; } diff --git a/src/features/tasks/execute/workflowExecution.ts b/src/features/tasks/execute/workflowExecution.ts index 225eb09..6a4666c 100644 --- a/src/features/tasks/execute/workflowExecution.ts +++ b/src/features/tasks/execute/workflowExecution.ts @@ -241,7 +241,7 @@ export async function executeWorkflow( phase, phaseName, status: phaseStatus, - ...(content ? { content } : {}), + content: content ?? '', timestamp: new Date().toISOString(), ...(phaseError ? { error: phaseError } : {}), }; diff --git a/src/shared/prompts/en/score_summary_system_prompt.md b/src/shared/prompts/en/score_summary_system_prompt.md index 0455c0c..060fae3 100644 --- a/src/shared/prompts/en/score_summary_system_prompt.md +++ b/src/shared/prompts/en/score_summary_system_prompt.md @@ -9,7 +9,9 @@ You are a task summarizer. Convert the conversation into a concrete task instruc Requirements: - Output only the final task instruction (no preamble). - Be specific about scope and targets (files/modules) if mentioned. -- Preserve constraints and "do not" instructions. +- Preserve constraints and "do not" instructions **only if they were explicitly stated by the user**. +- If the source of a constraint is unclear, do not include it; add it to Open Questions if needed. +- Do not include constraints proposed or inferred by the assistant. - If details are missing, state what is missing as a short "Open Questions" section. {{#if workflowInfo}} diff --git a/src/shared/prompts/ja/score_summary_system_prompt.md b/src/shared/prompts/ja/score_summary_system_prompt.md index 8145403..b5394e2 100644 --- a/src/shared/prompts/ja/score_summary_system_prompt.md +++ b/src/shared/prompts/ja/score_summary_system_prompt.md @@ -16,7 +16,9 @@ - 対象ファイル/モジュールごとに作業内容を明記する - 優先度(高/中/低)を付けて整理する - 再現手順や確認方法があれば含める -- 制約や「やらないこと」を保持する +- 制約や「やらないこと」は**ユーザーが明示したもののみ**保持する +- 制約の出所が不明な場合は保持せず、必要なら Open Questions に回す +- アシスタントが提案・推測した制約は指示書に含めない - 情報不足があれば「Open Questions」セクションを短く付ける {{#if workflowInfo}}