diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 86ba038..eaf4cbe 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -503,6 +503,121 @@ describe('instruction-builder', () => { expect(result).toContain('- Step Iteration: 3(このステップの実行回数)'); }); + + it('should include workflow structure when workflowSteps is provided', () => { + const step = createMinimalStep('Do work'); + step.name = 'implement'; + const context = createMinimalContext({ + language: 'en', + workflowSteps: [ + { name: 'plan' }, + { name: 'implement' }, + { name: 'review' }, + ], + currentStepIndex: 1, + }); + + const result = buildInstruction(step, context); + + expect(result).toContain('This workflow consists of 3 steps:'); + expect(result).toContain('- Step 1: plan'); + expect(result).toContain('- Step 2: implement'); + expect(result).toContain('← current'); + expect(result).toContain('- Step 3: review'); + }); + + it('should mark current step with marker', () => { + const step = createMinimalStep('Do work'); + step.name = 'plan'; + const context = createMinimalContext({ + language: 'en', + workflowSteps: [ + { name: 'plan' }, + { name: 'implement' }, + ], + currentStepIndex: 0, + }); + + const result = buildInstruction(step, context); + + expect(result).toContain('- Step 1: plan ← current'); + expect(result).not.toContain('- Step 2: implement ← current'); + }); + + it('should include description in parentheses when provided', () => { + const step = createMinimalStep('Do work'); + step.name = 'plan'; + const context = createMinimalContext({ + language: 'ja', + workflowSteps: [ + { name: 'plan', description: 'タスクを分析し実装計画を作成する' }, + { name: 'implement' }, + ], + currentStepIndex: 0, + }); + + const result = buildInstruction(step, context); + + expect(result).toContain('- Step 1: plan(タスクを分析し実装計画を作成する) ← 現在'); + }); + + it('should skip workflow structure when workflowSteps is not provided', () => { + const step = createMinimalStep('Do work'); + const context = createMinimalContext({ language: 'en' }); + + const result = buildInstruction(step, context); + + expect(result).not.toContain('This workflow consists of'); + }); + + it('should skip workflow structure when workflowSteps is empty', () => { + const step = createMinimalStep('Do work'); + const context = createMinimalContext({ + language: 'en', + workflowSteps: [], + currentStepIndex: -1, + }); + + const result = buildInstruction(step, context); + + expect(result).not.toContain('This workflow consists of'); + }); + + it('should render workflow structure in Japanese', () => { + const step = createMinimalStep('Do work'); + step.name = 'plan'; + const context = createMinimalContext({ + language: 'ja', + workflowSteps: [ + { name: 'plan' }, + { name: 'implement' }, + ], + currentStepIndex: 0, + }); + + const result = buildInstruction(step, context); + + expect(result).toContain('このワークフローは2ステップで構成されています:'); + expect(result).toContain('← 現在'); + }); + + it('should not show current marker when currentStepIndex is -1', () => { + const step = createMinimalStep('Do work'); + step.name = 'sub-step'; + const context = createMinimalContext({ + language: 'en', + workflowSteps: [ + { name: 'plan' }, + { name: 'implement' }, + ], + currentStepIndex: -1, + }); + + const result = buildInstruction(step, context); + + expect(result).toContain('This workflow consists of 2 steps:'); + expect(result).not.toContain('← current'); + }); }); describe('buildInstruction report-free (phase separation)', () => { diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index d3247aa..1ec14b5 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -133,6 +133,7 @@ export const ParallelSubStepRawSchema = z.object({ /** Workflow step schema - raw YAML format */ export const WorkflowStepRawSchema = z.object({ name: z.string().min(1), + description: z.string().optional(), /** Agent is required for normal steps, optional for parallel container steps */ agent: z.string().optional(), /** Session handling for this step */ diff --git a/src/core/models/workflow-types.ts b/src/core/models/workflow-types.ts index 56f0fd5..3d3d7f6 100644 --- a/src/core/models/workflow-types.ts +++ b/src/core/models/workflow-types.ts @@ -53,6 +53,8 @@ export interface ReportObjectConfig { /** Single step in a workflow */ export interface WorkflowStep { name: string; + /** Brief description of this step's role in the workflow */ + description?: string; /** Agent name, path, or inline prompt as specified in workflow YAML. Undefined when step runs without an agent. */ agent?: string; /** Session handling for this step */ diff --git a/src/core/workflow/engine/StepExecutor.ts b/src/core/workflow/engine/StepExecutor.ts index 7aff644..2073032 100644 --- a/src/core/workflow/engine/StepExecutor.ts +++ b/src/core/workflow/engine/StepExecutor.ts @@ -31,6 +31,7 @@ export interface StepExecutorDeps { readonly getReportDir: () => string; readonly getLanguage: () => Language | undefined; readonly getInteractive: () => boolean; + readonly getWorkflowSteps: () => ReadonlyArray<{ name: string; description?: string }>; readonly detectRuleIndex: (content: string, stepName: string) => number; readonly callAiJudge: ( agentOutput: string, @@ -52,6 +53,7 @@ export class StepExecutor { task: string, maxIterations: number, ): string { + const workflowSteps = this.deps.getWorkflowSteps(); return new InstructionBuilder(step, { task, iteration: state.iteration, @@ -64,6 +66,8 @@ export class StepExecutor { reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()), language: this.deps.getLanguage(), interactive: this.deps.getInteractive(), + workflowSteps, + currentStepIndex: workflowSteps.findIndex(s => s.name === step.name), }).build(); } diff --git a/src/core/workflow/engine/WorkflowEngine.ts b/src/core/workflow/engine/WorkflowEngine.ts index 0b6ed95..7e235c2 100644 --- a/src/core/workflow/engine/WorkflowEngine.ts +++ b/src/core/workflow/engine/WorkflowEngine.ts @@ -101,6 +101,7 @@ export class WorkflowEngine extends EventEmitter { getReportDir: () => this.reportDir, getLanguage: () => this.options.language, getInteractive: () => this.options.interactive === true, + getWorkflowSteps: () => this.config.steps.map(s => ({ name: s.name, description: s.description })), detectRuleIndex: this.detectRuleIndex, callAiJudge: this.callAiJudge, }); diff --git a/src/core/workflow/instruction/InstructionBuilder.ts b/src/core/workflow/instruction/InstructionBuilder.ts index 3746377..82abe47 100644 --- a/src/core/workflow/instruction/InstructionBuilder.ts +++ b/src/core/workflow/instruction/InstructionBuilder.ts @@ -26,6 +26,8 @@ export function isReportObjectConfig(report: string | ReportConfig[] | ReportObj /** Shape of localized section strings */ interface SectionStrings { workflowContext: string; + workflowStructure: string; + currentStepMarker: string; iteration: string; iterationWorkflowWide: string; stepIteration: string; @@ -127,12 +129,23 @@ export class InstructionBuilder { private renderWorkflowContext(language: Language): string { const s = getPromptObject('instruction.sections', language); - const lines: string[] = [ - s.workflowContext, - `- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`, - `- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`, - `- ${s.step}: ${this.step.name}`, - ]; + const lines: string[] = [s.workflowContext]; + + // Workflow structure (if workflow steps info is available) + if (this.context.workflowSteps && this.context.workflowSteps.length > 0) { + lines.push(s.workflowStructure.replace('{count}', String(this.context.workflowSteps.length))); + this.context.workflowSteps.forEach((ws, index) => { + const isCurrent = index === this.context.currentStepIndex; + const marker = isCurrent ? ` ← ${s.currentStepMarker}` : ''; + const desc = ws.description ? `(${ws.description})` : ''; + lines.push(`- Step ${index + 1}: ${ws.name}${desc}${marker}`); + }); + lines.push(''); + } + + lines.push(`- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`); + lines.push(`- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`); + lines.push(`- ${s.step}: ${this.step.name}`); // If step has report config, include Report Directory path and phase note if (this.step.report && this.context.reportDir) { diff --git a/src/core/workflow/instruction/instruction-context.ts b/src/core/workflow/instruction/instruction-context.ts index 06d6913..3e51f00 100644 --- a/src/core/workflow/instruction/instruction-context.ts +++ b/src/core/workflow/instruction/instruction-context.ts @@ -34,6 +34,10 @@ export interface InstructionContext { language?: Language; /** Whether interactive-only rules are enabled */ interactive?: boolean; + /** Top-level workflow steps for workflow structure display */ + workflowSteps?: ReadonlyArray<{ name: string; description?: string }>; + /** Index of the current step in workflowSteps (0-based) */ + currentStepIndex?: number; } /** Execution environment metadata prepended to agent instructions */ diff --git a/src/infra/config/loaders/workflowParser.ts b/src/infra/config/loaders/workflowParser.ts index 4b79819..a2d8ef5 100644 --- a/src/infra/config/loaders/workflowParser.ts +++ b/src/infra/config/loaders/workflowParser.ts @@ -185,6 +185,7 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep const result: WorkflowStep = { name: step.name, + description: step.description, agent: agentSpec, session: step.session, agentDisplayName: step.agent_name || (agentSpec ? extractAgentDisplayName(agentSpec) : step.name), diff --git a/src/shared/prompts/prompts_en.yaml b/src/shared/prompts/prompts_en.yaml index e1a22b2..6740d5e 100644 --- a/src/shared/prompts/prompts_en.yaml +++ b/src/shared/prompts/prompts_en.yaml @@ -133,6 +133,8 @@ instruction: sections: workflowContext: "## Workflow Context" + workflowStructure: "This workflow consists of {count} steps:" + currentStepMarker: "current" iteration: "Iteration" iterationWorkflowWide: "(workflow-wide)" stepIteration: "Step Iteration" diff --git a/src/shared/prompts/prompts_ja.yaml b/src/shared/prompts/prompts_ja.yaml index db0053c..a238796 100644 --- a/src/shared/prompts/prompts_ja.yaml +++ b/src/shared/prompts/prompts_ja.yaml @@ -146,6 +146,8 @@ instruction: sections: workflowContext: "## Workflow Context" + workflowStructure: "このワークフローは{count}ステップで構成されています:" + currentStepMarker: "現在" iteration: "Iteration" iterationWorkflowWide: "(ワークフロー全体)" stepIteration: "Step Iteration"