From 000adf891ce2b6f74b33be1847c84f80cb7a463f Mon Sep 17 00:00:00 2001 From: Sergio Esteban Date: Wed, 13 May 2026 22:33:58 +0200 Subject: [PATCH] feat(launch): add manual worktree support for Codex and Gemini Agents without native --worktree CLI support (Codex, Gemini) can now use worktrees via manual creation. When "Create worktree" is enabled for these agents, Kanban Code creates the git worktree before launching and cd's the agent into the worktree directory. - Remove supportsWorktree gate from launch dialog checkbox - Create worktree via GitWorktreeAdapter before non-native agent launch - Add isManual flag to WorktreeLink for cleanup tracking - Support .worktrees/ path pattern alongside .claude/worktrees/ across cleanup, reconciler, board state, fork, and git root resolution --- Sources/KanbanCode/ContentView+Launch.swift | 22 ++++- Sources/KanbanCode/ContentView+Worktree.swift | 2 + Sources/KanbanCode/ContentView.swift | 2 +- .../KanbanCode/LaunchConfirmationDialog.swift | 4 +- .../Adapters/ClaudeCode/JsonlParser.swift | 4 +- .../Adapters/Git/GitWorktreeAdapter.swift | 2 + .../KanbanCodeCore/Domain/Entities/Link.swift | 5 +- .../KanbanCodeCore/UseCases/BoardState.swift | 2 +- .../UseCases/CardReconciler.swift | 5 +- .../manual-worktree-for-codex.feature | 92 +++++++++++++++++++ 10 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 specs/sessions/manual-worktree-for-codex.feature 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