worktreeのバグフィックス

This commit is contained in:
nrslib 2026-01-29 20:26:27 +09:00
parent 73c2d6b381
commit ee0d846c5b
3 changed files with 29 additions and 36 deletions

View File

@ -77,7 +77,7 @@ describe('instruction-builder', () => {
}); });
describe('report_dir replacement', () => { 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( const step = createMinimalStep(
'- Report Directory: .takt/reports/{report_dir}/' '- Report Directory: .takt/reports/{report_dir}/'
); );
@ -89,31 +89,31 @@ describe('instruction-builder', () => {
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
expect(result).toContain( 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( const step = createMinimalStep(
'- Report: .takt/reports/{report_dir}/00-plan.md' '- Report: .takt/reports/{report_dir}/00-plan.md'
); );
const context = createMinimalContext({ const context = createMinimalContext({
cwd: '/project/.takt/worktrees/my-task', cwd: '/clone/my-task',
projectCwd: '/project', projectCwd: '/project',
reportDir: '20260128-worktree-report', reportDir: '20260128-worktree-report',
}); });
const result = buildInstruction(step, context); const result = buildInstruction(step, context);
// Path should be relative, not absolute with projectCwd
expect(result).toContain( 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'); expect(result).not.toContain('/project/.takt/reports/');
// Project Root should NOT be included in metadata (to avoid agent confusion) expect(result).toContain('Working Directory: /clone/my-task');
expect(result).not.toContain('Project Root');
}); });
it('should replace multiple .takt/reports/{report_dir} occurrences', () => { it('should replace multiple {report_dir} occurrences', () => {
const step = createMinimalStep( const step = createMinimalStep(
'- Scope: .takt/reports/{report_dir}/01-scope.md\n- Decisions: .takt/reports/{report_dir}/02-decisions.md' '- 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); const result = buildInstruction(step, context);
expect(result).toContain('/project/.takt/reports/20260128-multi/01-scope.md'); expect(result).toContain('.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/02-decisions.md');
expect(result).not.toContain('/project/.takt/reports/');
}); });
it('should replace standalone {report_dir} with directory name only', () => { 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'); 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', () => { describe('buildExecutionMetadata', () => {

View File

@ -3,7 +3,7 @@
*/ */
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { mkdirSync, existsSync } from 'node:fs'; import { mkdirSync, existsSync, symlinkSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import type { import type {
WorkflowConfig, WorkflowConfig,
@ -79,6 +79,18 @@ export class WorkflowEngine extends EventEmitter {
if (!existsSync(reportDirPath)) { if (!existsSync(reportDirPath)) {
mkdirSync(reportDirPath, { recursive: true }); 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 */ /** Validate workflow configuration at construction time */

View File

@ -5,7 +5,6 @@
* template placeholders with actual values. * template placeholders with actual values.
*/ */
import { join } from 'node:path';
import type { WorkflowStep, AgentResponse, Language } 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';
@ -180,14 +179,11 @@ export function buildInstruction(
escapeTemplateChars(userInputsStr) escapeTemplateChars(userInputsStr)
); );
// Replace .takt/reports/{report_dir} with absolute path first, // Replace {report_dir} with the directory name, keeping paths relative.
// then replace standalone {report_dir} with the directory name. // In worktree mode, a symlink from cwd/.takt/reports → projectCwd/.takt/reports
// This ensures agents always use the correct project root for reports, // ensures the relative path resolves correctly without embedding absolute paths
// even when their cwd is a clone. // that could cause agents to operate on the wrong repository.
if (context.reportDir) { 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); instruction = instruction.replace(/\{report_dir\}/g, context.reportDir);
} }