diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 98d8205..646ef94 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -87,6 +87,17 @@ describe('instruction-builder', () => { expect(result).not.toContain('Project Root'); expect(result).not.toContain('Mode:'); }); + + it('should prepend metadata before the instruction body', () => { + const step = createMinimalStep('Do some work'); + const context = createMinimalContext({ cwd: '/project' }); + + const result = buildInstruction(step, context); + const metadataIndex = result.indexOf('## Execution Context'); + const bodyIndex = result.indexOf('Do some work'); + + expect(metadataIndex).toBeLessThan(bodyIndex); + }); }); describe('report_dir replacement', () => { @@ -210,11 +221,25 @@ describe('instruction-builder', () => { expect(metadata.workingDirectory).toBe('/same-path'); expect(metadata.projectRoot).toBeUndefined(); }); + + 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'); + }); }); describe('renderExecutionMetadata', () => { it('should render normal mode without Project Root or Mode', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project' }); + const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en' }); expect(rendered).toContain('## Execution Context'); expect(rendered).toContain('- Working Directory: /project'); @@ -226,6 +251,7 @@ describe('instruction-builder', () => { const rendered = renderExecutionMetadata({ workingDirectory: '/worktree', projectRoot: '/project', + language: 'en', }); expect(rendered).toContain('## Execution Context'); @@ -235,10 +261,38 @@ describe('instruction-builder', () => { }); it('should end with a trailing empty line', () => { - const rendered = renderExecutionMetadata({ workingDirectory: '/project' }); + 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).not.toContain('Execution Context'); + expect(rendered).not.toContain('Working Directory'); + }); + + it('should render worktree mode in Japanese', () => { + const rendered = renderExecutionMetadata({ + workingDirectory: '/worktree', + projectRoot: '/project', + language: 'ja', + }); + + expect(rendered).toContain('- プロジェクトルート: /project'); + expect(rendered).toContain('モード: worktree'); + }); + + 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:'); + }); }); describe('basic placeholder replacement', () => { diff --git a/src/commands/taskExecution.ts b/src/commands/taskExecution.ts index f5c2be0..98e3bd7 100644 --- a/src/commands/taskExecution.ts +++ b/src/commands/taskExecution.ts @@ -2,7 +2,7 @@ * Task execution logic */ -import { loadWorkflow } from '../config/index.js'; +import { loadWorkflow, loadGlobalConfig } from '../config/index.js'; import { TaskRunner, type TaskInfo } from '../task/index.js'; import { createWorktree } from '../task/worktree.js'; import { autoCommitWorktree } from '../task/autoCommit.js'; @@ -47,8 +47,10 @@ export async function executeTask( steps: workflowConfig.steps.map(s => s.name), }); + const globalConfig = loadGlobalConfig(); const result = await executeWorkflow(workflowConfig, task, cwd, { projectCwd, + language: globalConfig.language, }); return result.success; } diff --git a/src/commands/workflowExecution.ts b/src/commands/workflowExecution.ts index 9542a16..2fa9ee2 100644 --- a/src/commands/workflowExecution.ts +++ b/src/commands/workflowExecution.ts @@ -3,7 +3,7 @@ */ import { WorkflowEngine } from '../workflow/engine.js'; -import type { WorkflowConfig } from '../models/types.js'; +import type { WorkflowConfig, Language } from '../models/types.js'; import type { IterationLimitRequest } from '../workflow/types.js'; import { loadAgentSessions, updateAgentSession } from '../config/paths.js'; import { @@ -59,6 +59,8 @@ export interface WorkflowExecutionOptions { headerPrefix?: string; /** Project root directory (where .takt/ lives). Defaults to cwd. */ projectCwd?: string; + /** Language for instruction metadata */ + language?: Language; } /** @@ -165,6 +167,7 @@ export async function executeWorkflow( onSessionUpdate: sessionUpdateHandler, onIterationLimit: iterationLimitHandler, projectCwd, + language: options.language, }); let abortReason: string | undefined; diff --git a/src/workflow/engine.ts b/src/workflow/engine.ts index 3e6bbb5..adb3b9b 100644 --- a/src/workflow/engine.ts +++ b/src/workflow/engine.ts @@ -49,6 +49,7 @@ export class WorkflowEngine extends EventEmitter { private task: string; private options: WorkflowEngineOptions; private loopDetector: LoopDetector; + private language: WorkflowEngineOptions['language']; private reportDir: string; constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) { @@ -58,6 +59,7 @@ export class WorkflowEngine extends EventEmitter { this.cwd = cwd; this.task = task; this.options = options; + this.language = options.language; this.loopDetector = new LoopDetector(config.loopDetection); this.reportDir = generateReportDir(task); this.ensureReportDirExists(); @@ -138,6 +140,7 @@ export class WorkflowEngine extends EventEmitter { userInputs: this.state.userInputs, previousOutput: getPreviousOutput(this.state), reportDir: this.reportDir, + language: this.language, }); } diff --git a/src/workflow/instruction-builder.ts b/src/workflow/instruction-builder.ts index 44562c4..ab3dc6d 100644 --- a/src/workflow/instruction-builder.ts +++ b/src/workflow/instruction-builder.ts @@ -6,7 +6,7 @@ */ import { join } from 'node:path'; -import type { WorkflowStep, AgentResponse } from '../models/types.js'; +import type { WorkflowStep, AgentResponse, Language } from '../models/types.js'; import { getGitDiff } from '../agents/runner.js'; /** @@ -31,6 +31,8 @@ export interface InstructionContext { previousOutput?: AgentResponse; /** Report directory path */ reportDir?: string; + /** Language for metadata rendering. Defaults to 'en'. */ + language?: Language; } /** Execution environment metadata prepended to agent instructions */ @@ -39,6 +41,8 @@ export interface ExecutionMetadata { readonly workingDirectory: string; /** Project root where .takt/ lives. Present only in worktree mode. */ readonly projectRoot?: string; + /** Language for metadata rendering */ + readonly language: Language; } /** @@ -54,27 +58,50 @@ export function buildExecutionMetadata(context: InstructionContext): ExecutionMe return { workingDirectory: context.cwd, ...(isWorktree ? { projectRoot } : {}), + language: context.language ?? 'en', }; } +/** Localized strings for execution metadata rendering */ +const METADATA_STRINGS = { + en: { + heading: '## Execution Context', + workingDirectory: 'Working Directory', + projectRoot: 'Project Root', + mode: 'Mode: worktree (source edits in Working Directory, reports in Project Root)', + note: 'Note: This section is metadata. Follow the language used in the rest of the prompt.', + }, + ja: { + heading: '## 実行コンテキスト', + workingDirectory: '作業ディレクトリ', + projectRoot: 'プロジェクトルート', + mode: 'モード: worktree(ソース編集は作業ディレクトリ、レポートはプロジェクトルート)', + note: '', + }, +} as const; + /** * Render execution metadata as a markdown string. * * Pure function: ExecutionMetadata → string. - * Always includes `## Execution Context` + `Working Directory`. - * Adds `Project Root` and `Mode` only in worktree mode (when projectRoot is present). + * Always includes heading + Working Directory. + * Adds Project Root and Mode only in worktree mode (when projectRoot is present). + * Language determines the output language; 'en' includes a note about language consistency. */ export function renderExecutionMetadata(metadata: ExecutionMetadata): string { + const strings = METADATA_STRINGS[metadata.language]; const lines = [ - '## Execution Context', - `- Working Directory: ${metadata.workingDirectory}`, + strings.heading, + `- ${strings.workingDirectory}: ${metadata.workingDirectory}`, ]; if (metadata.projectRoot !== undefined) { - lines.push(`- Project Root: ${metadata.projectRoot}`); - lines.push('- Mode: worktree (source edits in Working Directory, reports in Project Root)'); + lines.push(`- ${strings.projectRoot}: ${metadata.projectRoot}`); + lines.push(`- ${strings.mode}`); + } + if (strings.note) { + lines.push(''); + lines.push(strings.note); } - lines.push(''); - lines.push('Note: This metadata is written in English for consistency. Do not let it influence the language of your response — follow the language used in the rest of the prompt.'); lines.push(''); return lines.join('\n'); } @@ -152,10 +179,10 @@ export function buildInstruction( instruction = `${instruction}\n\n${step.statusRulesPrompt}`; } - // Append execution context metadata at the end so the agent's language - // is not influenced by this English-only section. + // Prepend execution context metadata so agents see it first. + // Now language-aware, so no need to hide it at the end. const metadata = buildExecutionMetadata(context); - instruction = `${instruction}\n\n${renderExecutionMetadata(metadata)}`; + instruction = `${renderExecutionMetadata(metadata)}\n${instruction}`; return instruction; } diff --git a/src/workflow/types.ts b/src/workflow/types.ts index 3c84642..bfabea8 100644 --- a/src/workflow/types.ts +++ b/src/workflow/types.ts @@ -5,7 +5,7 @@ * used by the workflow execution engine. */ -import type { WorkflowStep, AgentResponse, WorkflowState } from '../models/types.js'; +import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; import type { StreamCallback } from '../agents/runner.js'; import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js'; @@ -72,6 +72,8 @@ export interface WorkflowEngineOptions { bypassPermissions?: boolean; /** Project root directory (where .takt/ lives). Defaults to cwd if not specified. */ projectCwd?: string; + /** Language for instruction metadata. Defaults to 'en'. */ + language?: Language; } /** Loop detection result */