takt/src/workflow/instruction-builder.ts
nrslib 0f2aa896ae Execution Rules に cd 禁止ルールを追加
エージェントがBashコマンドで明示的にcdしてmainディレクトリで
作業してしまう問題を解決するため、「cdを使用しないでください」
というルールをmetadataに追加。
2026-01-29 01:42:04 +09:00

183 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Instruction template builder for workflow steps
*
* Builds the instruction string for agent execution by replacing
* template placeholders with actual values.
*/
import { join } from 'node:path';
import type { WorkflowStep, AgentResponse, Language } from '../models/types.js';
import { getGitDiff } from '../agents/runner.js';
/**
* Context for building instruction from template.
*/
export interface InstructionContext {
/** The main task/prompt */
task: string;
/** Current iteration number (workflow-wide turn count) */
iteration: number;
/** Maximum iterations allowed */
maxIterations: number;
/** Current step's iteration number (how many times this step has been executed) */
stepIteration: number;
/** Working directory (agent work dir, may be a worktree) */
cwd: string;
/** Project root directory (where .takt/ lives). Defaults to cwd. */
projectCwd?: string;
/** User inputs accumulated during workflow */
userInputs: string[];
/** Previous step output if available */
previousOutput?: AgentResponse;
/** Report directory path */
reportDir?: string;
/** Language for metadata rendering. Defaults to 'en'. */
language?: Language;
}
/** Execution environment metadata prepended to agent instructions */
export interface ExecutionMetadata {
/** The agent's working directory (may be a worktree) */
readonly workingDirectory: string;
/** Language for metadata rendering */
readonly language: Language;
}
/**
* Build execution metadata from instruction context.
*
* Pure function: InstructionContext → ExecutionMetadata.
*/
export function buildExecutionMetadata(context: InstructionContext): ExecutionMetadata {
return {
workingDirectory: context.cwd,
language: context.language ?? 'en',
};
}
/** Localized strings for execution metadata rendering */
const METADATA_STRINGS = {
en: {
heading: '## Execution Context',
workingDirectory: 'Working Directory',
rulesHeading: '## Execution Rules',
noCommit: '**Do NOT run git commit.** Commits are handled automatically by the system after workflow completion.',
noCd: '**Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories.',
note: 'Note: This section is metadata. Follow the language used in the rest of the prompt.',
},
ja: {
heading: '## 実行コンテキスト',
workingDirectory: '作業ディレクトリ',
rulesHeading: '## 実行ルール',
noCommit: '**git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。',
noCd: '**Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。',
note: '',
},
} as const;
/**
* Render execution metadata as a markdown string.
*
* Pure function: ExecutionMetadata → string.
* Always includes heading + Working Directory + Execution Rules.
* 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 = [
strings.heading,
`- ${strings.workingDirectory}: ${metadata.workingDirectory}`,
'',
strings.rulesHeading,
`- ${strings.noCommit}`,
`- ${strings.noCd}`,
];
if (strings.note) {
lines.push('');
lines.push(strings.note);
}
lines.push('');
return lines.join('\n');
}
/**
* Escape special characters in dynamic content to prevent template injection.
*/
function escapeTemplateChars(str: string): string {
return str.replace(/\{/g, '').replace(/\}/g, '');
}
/**
* Build instruction from template with context values.
*
* Supported placeholders:
* - {task} - The main task/prompt
* - {iteration} - Current iteration number (workflow-wide turn count)
* - {max_iterations} - Maximum iterations allowed
* - {step_iteration} - Current step's iteration number (how many times this step has been executed)
* - {previous_response} - Output from previous step (if passPreviousResponse is true)
* - {git_diff} - Current git diff output
* - {user_inputs} - Accumulated user inputs
* - {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
*/
export function buildInstruction(
step: WorkflowStep,
context: InstructionContext
): string {
let instruction = step.instructionTemplate;
// Replace {task}
instruction = instruction.replace(/\{task\}/g, escapeTemplateChars(context.task));
// Replace {iteration}, {max_iterations}, and {step_iteration}
instruction = instruction.replace(/\{iteration\}/g, String(context.iteration));
instruction = instruction.replace(/\{max_iterations\}/g, String(context.maxIterations));
instruction = instruction.replace(/\{step_iteration\}/g, String(context.stepIteration));
// Replace {previous_response}
if (step.passPreviousResponse) {
if (context.previousOutput) {
instruction = instruction.replace(
/\{previous_response\}/g,
escapeTemplateChars(context.previousOutput.content)
);
} else {
instruction = instruction.replace(/\{previous_response\}/g, '');
}
}
// Replace {git_diff}
const gitDiff = getGitDiff(context.cwd);
instruction = instruction.replace(/\{git_diff\}/g, gitDiff);
// Replace {user_inputs}
const userInputsStr = context.userInputs.join('\n');
instruction = instruction.replace(
/\{user_inputs\}/g,
escapeTemplateChars(userInputsStr)
);
// Replace .takt/reports/{report_dir} with absolute path first,
// then replace standalone {report_dir} with the directory name.
// This ensures agents always use the correct project root for reports,
// even when their cwd is a worktree.
if (context.reportDir) {
const projectRoot = context.projectCwd ?? context.cwd;
const reportDirFullPath = join(projectRoot, '.takt', 'reports', context.reportDir);
instruction = instruction.replace(/\.takt\/reports\/\{report_dir\}/g, reportDirFullPath);
instruction = instruction.replace(/\{report_dir\}/g, context.reportDir);
}
// Append status_rules_prompt if present
if (step.statusRulesPrompt) {
instruction = `${instruction}\n\n${step.statusRulesPrompt}`;
}
// 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 = `${renderExecutionMetadata(metadata)}\n${instruction}`;
return instruction;
}