diff --git a/Sources/KanbanCode/ContentView+Launch.swift b/Sources/KanbanCode/ContentView+Launch.swift index 29323cd9..647f55f7 100644 --- a/Sources/KanbanCode/ContentView+Launch.swift +++ b/Sources/KanbanCode/ContentView+Launch.swift @@ -153,9 +153,20 @@ extension ContentView { ) } + // For agents without native --worktree support, create the worktree manually + var launchPath = projectPath + var manualWorktreeLink: WorktreeLink? + if let wtName = worktreeName, !wtName.isEmpty, !assistant.supportsWorktree { + let adapter = GitWorktreeAdapter() + let wt = try await adapter.createWorktree(repoRoot: projectPath, name: wtName) + launchPath = wt.path + manualWorktreeLink = WorktreeLink(path: wt.path, branch: wt.branch ?? wtName, isManual: true) + KanbanCodeLog.info("launch", "Created manual worktree for \(assistant.displayName): \(wt.path)") + } + let tmuxName = try await launcher.launch( sessionName: predictedTmuxName, - projectPath: projectPath, + projectPath: launchPath, prompt: prompt, worktreeName: assistant.supportsWorktree ? worktreeName : nil, shellOverride: shellOverride, @@ -263,9 +274,12 @@ extension ContentView { if sessionLink != nil { break } } - // If worktree launch, try to extract branch from the session file immediately + // Resolve worktreeLink: manual worktrees are known immediately, + // native worktrees need extraction from the session file metadata var worktreeLink: WorktreeLink? - if worktreeName != nil, let sl = sessionLink, let sp = sl.sessionPath { + if let mwl = manualWorktreeLink { + worktreeLink = mwl + } else if worktreeName != nil, let sl = sessionLink, let sp = sl.sessionPath { worktreeLink = Self.extractWorktreeLink(sessionPath: sp, projectPath: projectPath) } @@ -463,7 +477,7 @@ extension ContentView { if !keepWorktree { // Extract parent project if projectPath is a worktree path if let pp = forkProjectPath, - let range = pp.range(of: "/.claude/worktrees/") { + let range = pp.range(of: "/.claude/worktrees/") ?? pp.range(of: "/.worktrees/") { forkProjectPath = String(pp[..` suffix since worktrees are inside the repo. private static func resolveGitRoot(_ path: String) -> String { - // Pattern: /repo/.claude/worktrees/ → /repo - if let range = path.range(of: "/.claude/worktrees/") { + // Pattern: /repo/.claude/worktrees/ or /repo/.worktrees/ → /repo + if let range = path.range(of: "/.claude/worktrees/") ?? path.range(of: "/.worktrees/") { return String(path[path.startIndex.. Bool { guard let projectRoot else { return false } return sessionPath.hasPrefix(projectRoot + "/.claude/worktrees/") + || sessionPath.hasPrefix(projectRoot + "/.worktrees/") } } diff --git a/specs/sessions/manual-worktree-for-codex.feature b/specs/sessions/manual-worktree-for-codex.feature new file mode 100644 index 00000000..665da6b2 --- /dev/null +++ b/specs/sessions/manual-worktree-for-codex.feature @@ -0,0 +1,92 @@ +Feature: Manual worktree creation for agents without native worktree support + + Agents like Codex CLI and Gemini CLI don't have a `--worktree` flag. + Kanban Code should create git worktrees manually before launching these agents, + launch them cd'd into the worktree directory, track the worktree as "manual", + and clean it up when the card is archived/deleted. + + Background: + Given a project at "/tmp/my-app" that is a git repository + And the user has Codex enabled as a coding assistant + And the project has a clean working tree on branch "main" + + @unit + Scenario: Launch dialog shows "Create worktree" for Codex + Given a card with assistant "codex" and no existing worktree + When the launch confirmation dialog is shown + Then the "Create worktree" checkbox is visible + And the "Branch name" text field appears when the checkbox is enabled + + @unit + Scenario: Launch dialog shows "Create worktree" for Gemini + Given a card with assistant "gemini" and no existing worktree + When the launch confirmation dialog is shown + Then the "Create worktree" checkbox is visible + + @unit + Scenario: WorktreeLink tracks manual worktrees + Given a WorktreeLink with path "/tmp/my-app/.worktrees/feature-auth" and branch "feature-auth" + When isManual is set to true + Then encoding and decoding preserves the isManual flag + + @integration + Scenario: Codex card launches in a manually-created worktree + Given a card with assistant "codex" and worktreeName "feature-auth" + And the user has "Create worktree" enabled + When the card is launched + Then a git worktree is created at "/.worktrees/feature-auth" on branch "feature-auth" + And the tmux session is launched with cd to the worktree directory + And the card's worktreeLink has path "/.worktrees/feature-auth" + And the card's worktreeLink has branch "feature-auth" + And the card's worktreeLink has isManual = true + + @integration + Scenario: Codex card launches without worktree when checkbox is disabled + Given a card with assistant "codex" and worktreeName nil + And the user has "Create worktree" disabled + When the card is launched + Then no git worktree is created + And the tmux session is launched with cd to the project root + And the card's worktreeLink is nil + + @integration + Scenario: Manual worktree is cleaned up on card archive + Given a card with assistant "codex" and a manual worktreeLink at "/tmp/my-app/.worktrees/feature-auth" + When the card is archived + Then the git worktree at "/tmp/my-app/.worktrees/feature-auth" is removed + And the worktreeLink is cleared from the card + + @integration + Scenario: Manual worktree is cleaned up on card delete + Given a card with assistant "codex" and a manual worktreeLink at "/tmp/my-app/.worktrees/feature-auth" + When the card is deleted + Then the git worktree at "/tmp/my-app/.worktrees/feature-auth" is removed + + @integration + Scenario: Codex resumes inside the worktree directory + Given a card with assistant "codex" and worktreeLink at "/tmp/my-app/.worktrees/feature-auth" + When the card is resumed + Then the resume command cd's into "/tmp/my-app/.worktrees/feature-auth" + + @unit + Scenario: GitWorktreeAdapter creates worktree with custom base directory + Given a repo root at "/tmp/my-app" + When createWorktree is called with name "feature-auth" + Then the worktree is created at "/tmp/my-app/.worktrees/feature-auth" + And the branch is "feature-auth" + + @unit + Scenario: Worktree cleanup handles both .worktrees/ and .claude/worktrees/ paths + Given a worktree path "/tmp/my-app/.worktrees/feature-auth" + When removeWorktree is called + Then the repo root is derived as "/tmp/my-app" + And git worktree remove is executed successfully + + @unit + Scenario: effectiveCreateWorktree is true for Codex when conditions are met + Given assistant is "codex" + And isGitRepo is true + And createWorktree checkbox is true + And isResume is false + And hasExistingWorktree is false + Then effectiveCreateWorktree returns true