From 0ecbf6e56b81333f10d4662a3209f11e939116bd Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:41:24 +0900 Subject: [PATCH] =?UTF-8?q?Worktree=20=E3=82=92=E3=83=97=E3=83=AD=E3=82=B8?= =?UTF-8?q?=E3=82=A7=E3=82=AF=E3=83=88=E5=A4=96=E3=81=AB=E4=BD=9C=E6=88=90?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.yaml に worktree_dir 設定を追加 - デフォルトは ../{tree-name}(プロジェクトの兄弟ディレクトリ) - Claude Code のプロジェクト検出問題を回避 --- src/config/globalConfig.ts | 4 ++++ src/models/schemas.ts | 2 ++ src/models/types.ts | 2 ++ src/task/worktree.ts | 44 ++++++++++++++++++++++++++------------ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/config/globalConfig.ts b/src/config/globalConfig.ts index 4e4f5ca..141f5d6 100644 --- a/src/config/globalConfig.ts +++ b/src/config/globalConfig.ts @@ -35,6 +35,7 @@ export function loadGlobalConfig(): GlobalConfig { enabled: parsed.debug.enabled, logFile: parsed.debug.log_file, } : undefined, + worktreeDir: parsed.worktree_dir, }; } @@ -57,6 +58,9 @@ export function saveGlobalConfig(config: GlobalConfig): void { log_file: config.debug.logFile, }; } + if (config.worktreeDir) { + raw.worktree_dir = config.worktreeDir; + } writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); } diff --git a/src/models/schemas.ts b/src/models/schemas.ts index 0f3ae37..8cb0db0 100644 --- a/src/models/schemas.ts +++ b/src/models/schemas.ts @@ -127,6 +127,8 @@ export const GlobalConfigSchema = z.object({ provider: z.enum(['claude', 'codex', 'mock']).optional().default('claude'), model: z.string().optional(), debug: DebugConfigSchema.optional(), + /** Directory for worktrees. If empty, uses ../{tree-name} relative to project */ + worktree_dir: z.string().optional(), }); /** Project config schema */ diff --git a/src/models/types.ts b/src/models/types.ts index ec22dda..39cf422 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -160,6 +160,8 @@ export interface GlobalConfig { provider?: 'claude' | 'codex' | 'mock'; model?: string; debug?: DebugConfig; + /** Directory for worktrees. If empty, uses ../{tree-name} relative to project */ + worktreeDir?: string; } /** Project-level configuration */ diff --git a/src/task/worktree.ts b/src/task/worktree.ts index 4caedbc..d460a2e 100644 --- a/src/task/worktree.ts +++ b/src/task/worktree.ts @@ -9,7 +9,7 @@ import * as path from 'node:path'; import { execFileSync } from 'node:child_process'; import { createLogger } from '../utils/debug.js'; import { slugify } from '../utils/slug.js'; -import { isPathSafe } from '../config/paths.js'; +import { loadGlobalConfig } from '../config/globalConfig.js'; const log = createLogger('worktree'); @@ -37,29 +37,45 @@ function generateTimestamp(): string { } /** - * Resolve the worktree path based on options. - * Validates that custom paths stay within the project directory. + * Resolve the worktree path based on options and global config. * - * @throws Error if a custom path escapes projectDir (path traversal) + * Priority: + * 1. Custom path in options.worktree (string) + * 2. worktree_dir from config.yaml (if set) + * 3. Default: ../{tree-name} (outside project to avoid Claude Code project detection issues) */ function resolveWorktreePath(projectDir: string, options: WorktreeOptions): string { + const timestamp = generateTimestamp(); + const slug = slugify(options.taskSlug); + const dirName = slug ? `${timestamp}-${slug}` : timestamp; + + // Custom path specified in task options if (typeof options.worktree === 'string') { const resolved = path.isAbsolute(options.worktree) ? options.worktree : path.resolve(projectDir, options.worktree); - - if (!isPathSafe(projectDir, resolved)) { - throw new Error(`Worktree path escapes project directory: ${options.worktree}`); - } - return resolved; } - // worktree: true → .takt/worktrees/{timestamp}-{task-slug}/ - const timestamp = generateTimestamp(); - const slug = slugify(options.taskSlug); - const dirName = slug ? `${timestamp}-${slug}` : timestamp; - return path.join(projectDir, '.takt', 'worktrees', dirName); + // Load config to check for worktree_dir setting + let worktreeBaseDir: string | undefined; + try { + const globalConfig = loadGlobalConfig(); + worktreeBaseDir = globalConfig.worktreeDir; + } catch { + // Config not found, use default + } + + if (worktreeBaseDir) { + // Use configured worktree directory + const resolved = path.isAbsolute(worktreeBaseDir) + ? worktreeBaseDir + : path.resolve(projectDir, worktreeBaseDir); + return path.join(resolved, dirName); + } + + // Default: ../{tree-name} (sibling to project directory) + return path.join(projectDir, '..', dirName); } /**