言語設定を追加

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('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', () => {

View File

@ -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;
}

View File

@ -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;

View File

@ -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,
});
}

View File

@ -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('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('');
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;
}

View File

@ -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 */