diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 3a57ade..b5c906d 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -77,7 +77,7 @@ describe('instruction-builder', () => { }); describe('report_dir replacement', () => { - it('should replace .takt/reports/{report_dir} with full absolute path', () => { + it('should replace {report_dir} in paths keeping them relative', () => { const step = createMinimalStep( '- Report Directory: .takt/reports/{report_dir}/' ); @@ -89,31 +89,31 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); expect(result).toContain( - '- Report Directory: /project/.takt/reports/20260128-test-report/' + '- Report Directory: .takt/reports/20260128-test-report/' ); }); - it('should use projectCwd for report path when cwd is a worktree', () => { + it('should not leak projectCwd absolute path into instruction', () => { const step = createMinimalStep( '- Report: .takt/reports/{report_dir}/00-plan.md' ); const context = createMinimalContext({ - cwd: '/project/.takt/worktrees/my-task', + cwd: '/clone/my-task', projectCwd: '/project', reportDir: '20260128-worktree-report', }); const result = buildInstruction(step, context); + // Path should be relative, not absolute with projectCwd expect(result).toContain( - '- Report: /project/.takt/reports/20260128-worktree-report/00-plan.md' + '- Report: .takt/reports/20260128-worktree-report/00-plan.md' ); - expect(result).toContain('Working Directory: /project/.takt/worktrees/my-task'); - // Project Root should NOT be included in metadata (to avoid agent confusion) - expect(result).not.toContain('Project Root'); + expect(result).not.toContain('/project/.takt/reports/'); + expect(result).toContain('Working Directory: /clone/my-task'); }); - it('should replace multiple .takt/reports/{report_dir} occurrences', () => { + it('should replace multiple {report_dir} occurrences', () => { const step = createMinimalStep( '- Scope: .takt/reports/{report_dir}/01-scope.md\n- Decisions: .takt/reports/{report_dir}/02-decisions.md' ); @@ -125,8 +125,9 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('/project/.takt/reports/20260128-multi/01-scope.md'); - expect(result).toContain('/project/.takt/reports/20260128-multi/02-decisions.md'); + expect(result).toContain('.takt/reports/20260128-multi/01-scope.md'); + expect(result).toContain('.takt/reports/20260128-multi/02-decisions.md'); + expect(result).not.toContain('/project/.takt/reports/'); }); it('should replace standalone {report_dir} with directory name only', () => { @@ -141,22 +142,6 @@ describe('instruction-builder', () => { expect(result).toContain('Report dir name: 20260128-standalone'); }); - - it('should fall back to cwd when projectCwd is not provided', () => { - const step = createMinimalStep( - '- Dir: .takt/reports/{report_dir}/' - ); - const context = createMinimalContext({ - cwd: '/fallback-project', - reportDir: '20260128-fallback', - }); - - const result = buildInstruction(step, context); - - expect(result).toContain( - '- Dir: /fallback-project/.takt/reports/20260128-fallback/' - ); - }); }); describe('buildExecutionMetadata', () => { diff --git a/src/workflow/engine.ts b/src/workflow/engine.ts index db76a1a..ae61595 100644 --- a/src/workflow/engine.ts +++ b/src/workflow/engine.ts @@ -3,7 +3,7 @@ */ import { EventEmitter } from 'node:events'; -import { mkdirSync, existsSync } from 'node:fs'; +import { mkdirSync, existsSync, symlinkSync } from 'node:fs'; import { join } from 'node:path'; import type { WorkflowConfig, @@ -79,6 +79,18 @@ export class WorkflowEngine extends EventEmitter { if (!existsSync(reportDirPath)) { mkdirSync(reportDirPath, { recursive: true }); } + + // Worktree mode: create symlink so agents can access reports via relative path + if (this.cwd !== this.projectCwd) { + const cwdReportsDir = join(this.cwd, '.takt', 'reports'); + if (!existsSync(cwdReportsDir)) { + mkdirSync(join(this.cwd, '.takt'), { recursive: true }); + symlinkSync( + join(this.projectCwd, '.takt', 'reports'), + cwdReportsDir, + ); + } + } } /** Validate workflow configuration at construction time */ diff --git a/src/workflow/instruction-builder.ts b/src/workflow/instruction-builder.ts index 5b6a7dd..72bee38 100644 --- a/src/workflow/instruction-builder.ts +++ b/src/workflow/instruction-builder.ts @@ -5,7 +5,6 @@ * 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'; @@ -180,14 +179,11 @@ export function buildInstruction( 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 clone. + // Replace {report_dir} with the directory name, keeping paths relative. + // In worktree mode, a symlink from cwd/.takt/reports → projectCwd/.takt/reports + // ensures the relative path resolves correctly without embedding absolute paths + // that could cause agents to operate on the wrong repository. 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); }