言語設定を追加

This commit is contained in:
nrslib 2026-01-28 19:50:52 +09:00
parent 19ced26d00
commit 722c827cc4
6 changed files with 108 additions and 17 deletions

View File

@ -87,6 +87,17 @@ describe('instruction-builder', () => {
expect(result).not.toContain('Project Root'); expect(result).not.toContain('Project Root');
expect(result).not.toContain('Mode:'); 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', () => { describe('report_dir replacement', () => {
@ -210,11 +221,25 @@ describe('instruction-builder', () => {
expect(metadata.workingDirectory).toBe('/same-path'); expect(metadata.workingDirectory).toBe('/same-path');
expect(metadata.projectRoot).toBeUndefined(); 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', () => { describe('renderExecutionMetadata', () => {
it('should render normal mode without Project Root or Mode', () => { 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('## Execution Context');
expect(rendered).toContain('- Working Directory: /project'); expect(rendered).toContain('- Working Directory: /project');
@ -226,6 +251,7 @@ describe('instruction-builder', () => {
const rendered = renderExecutionMetadata({ const rendered = renderExecutionMetadata({
workingDirectory: '/worktree', workingDirectory: '/worktree',
projectRoot: '/project', projectRoot: '/project',
language: 'en',
}); });
expect(rendered).toContain('## Execution Context'); expect(rendered).toContain('## Execution Context');
@ -235,10 +261,38 @@ describe('instruction-builder', () => {
}); });
it('should end with a trailing empty line', () => { it('should end with a trailing empty line', () => {
const rendered = renderExecutionMetadata({ workingDirectory: '/project' }); const rendered = renderExecutionMetadata({ workingDirectory: '/project', language: 'en' });
expect(rendered).toMatch(/\n$/); 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', () => { describe('basic placeholder replacement', () => {

View File

@ -2,7 +2,7 @@
* Task execution logic * 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 { TaskRunner, type TaskInfo } from '../task/index.js';
import { createWorktree } from '../task/worktree.js'; import { createWorktree } from '../task/worktree.js';
import { autoCommitWorktree } from '../task/autoCommit.js'; import { autoCommitWorktree } from '../task/autoCommit.js';
@ -47,8 +47,10 @@ export async function executeTask(
steps: workflowConfig.steps.map(s => s.name), steps: workflowConfig.steps.map(s => s.name),
}); });
const globalConfig = loadGlobalConfig();
const result = await executeWorkflow(workflowConfig, task, cwd, { const result = await executeWorkflow(workflowConfig, task, cwd, {
projectCwd, projectCwd,
language: globalConfig.language,
}); });
return result.success; return result.success;
} }

View File

@ -3,7 +3,7 @@
*/ */
import { WorkflowEngine } from '../workflow/engine.js'; 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 type { IterationLimitRequest } from '../workflow/types.js';
import { loadAgentSessions, updateAgentSession } from '../config/paths.js'; import { loadAgentSessions, updateAgentSession } from '../config/paths.js';
import { import {
@ -59,6 +59,8 @@ export interface WorkflowExecutionOptions {
headerPrefix?: string; headerPrefix?: string;
/** Project root directory (where .takt/ lives). Defaults to cwd. */ /** Project root directory (where .takt/ lives). Defaults to cwd. */
projectCwd?: string; projectCwd?: string;
/** Language for instruction metadata */
language?: Language;
} }
/** /**
@ -165,6 +167,7 @@ export async function executeWorkflow(
onSessionUpdate: sessionUpdateHandler, onSessionUpdate: sessionUpdateHandler,
onIterationLimit: iterationLimitHandler, onIterationLimit: iterationLimitHandler,
projectCwd, projectCwd,
language: options.language,
}); });
let abortReason: string | undefined; let abortReason: string | undefined;

View File

@ -49,6 +49,7 @@ export class WorkflowEngine extends EventEmitter {
private task: string; private task: string;
private options: WorkflowEngineOptions; private options: WorkflowEngineOptions;
private loopDetector: LoopDetector; private loopDetector: LoopDetector;
private language: WorkflowEngineOptions['language'];
private reportDir: string; private reportDir: string;
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) { constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) {
@ -58,6 +59,7 @@ export class WorkflowEngine extends EventEmitter {
this.cwd = cwd; this.cwd = cwd;
this.task = task; this.task = task;
this.options = options; this.options = options;
this.language = options.language;
this.loopDetector = new LoopDetector(config.loopDetection); this.loopDetector = new LoopDetector(config.loopDetection);
this.reportDir = generateReportDir(task); this.reportDir = generateReportDir(task);
this.ensureReportDirExists(); this.ensureReportDirExists();
@ -138,6 +140,7 @@ export class WorkflowEngine extends EventEmitter {
userInputs: this.state.userInputs, userInputs: this.state.userInputs,
previousOutput: getPreviousOutput(this.state), previousOutput: getPreviousOutput(this.state),
reportDir: this.reportDir, reportDir: this.reportDir,
language: this.language,
}); });
} }

View File

@ -6,7 +6,7 @@
*/ */
import { join } from 'node:path'; 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'; import { getGitDiff } from '../agents/runner.js';
/** /**
@ -31,6 +31,8 @@ export interface InstructionContext {
previousOutput?: AgentResponse; previousOutput?: AgentResponse;
/** Report directory path */ /** Report directory path */
reportDir?: string; reportDir?: string;
/** Language for metadata rendering. Defaults to 'en'. */
language?: Language;
} }
/** Execution environment metadata prepended to agent instructions */ /** Execution environment metadata prepended to agent instructions */
@ -39,6 +41,8 @@ export interface ExecutionMetadata {
readonly workingDirectory: string; readonly workingDirectory: string;
/** Project root where .takt/ lives. Present only in worktree mode. */ /** Project root where .takt/ lives. Present only in worktree mode. */
readonly projectRoot?: string; readonly projectRoot?: string;
/** Language for metadata rendering */
readonly language: Language;
} }
/** /**
@ -54,27 +58,50 @@ export function buildExecutionMetadata(context: InstructionContext): ExecutionMe
return { return {
workingDirectory: context.cwd, workingDirectory: context.cwd,
...(isWorktree ? { projectRoot } : {}), ...(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. * Render execution metadata as a markdown string.
* *
* Pure function: ExecutionMetadata string. * Pure function: ExecutionMetadata string.
* Always includes `## Execution Context` + `Working Directory`. * Always includes heading + Working Directory.
* Adds `Project Root` and `Mode` only in worktree mode (when projectRoot is present). * 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 { export function renderExecutionMetadata(metadata: ExecutionMetadata): string {
const strings = METADATA_STRINGS[metadata.language];
const lines = [ const lines = [
'## Execution Context', strings.heading,
`- Working Directory: ${metadata.workingDirectory}`, `- ${strings.workingDirectory}: ${metadata.workingDirectory}`,
]; ];
if (metadata.projectRoot !== undefined) { if (metadata.projectRoot !== undefined) {
lines.push(`- Project Root: ${metadata.projectRoot}`); lines.push(`- ${strings.projectRoot}: ${metadata.projectRoot}`);
lines.push('- Mode: worktree (source edits in Working Directory, reports in Project Root)'); lines.push(`- ${strings.mode}`);
} }
if (strings.note) {
lines.push(''); 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(strings.note);
}
lines.push(''); lines.push('');
return lines.join('\n'); return lines.join('\n');
} }
@ -152,10 +179,10 @@ export function buildInstruction(
instruction = `${instruction}\n\n${step.statusRulesPrompt}`; instruction = `${instruction}\n\n${step.statusRulesPrompt}`;
} }
// Append execution context metadata at the end so the agent's language // Prepend execution context metadata so agents see it first.
// is not influenced by this English-only section. // Now language-aware, so no need to hide it at the end.
const metadata = buildExecutionMetadata(context); const metadata = buildExecutionMetadata(context);
instruction = `${instruction}\n\n${renderExecutionMetadata(metadata)}`; instruction = `${renderExecutionMetadata(metadata)}\n${instruction}`;
return instruction; return instruction;
} }

View File

@ -5,7 +5,7 @@
* used by the workflow execution engine. * 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 { StreamCallback } from '../agents/runner.js';
import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js'; import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js';
@ -72,6 +72,8 @@ export interface WorkflowEngineOptions {
bypassPermissions?: boolean; bypassPermissions?: boolean;
/** Project root directory (where .takt/ lives). Defaults to cwd if not specified. */ /** Project root directory (where .takt/ lives). Defaults to cwd if not specified. */
projectCwd?: string; projectCwd?: string;
/** Language for instruction metadata. Defaults to 'en'. */
language?: Language;
} }
/** Loop detection result */ /** Loop detection result */