feat(cli): add fleet worktree and adopt externally-created sessions - #230
Conversation
Closes #227. Creating a worktree meant opening the TUI and pressing `w`; nothing in the CLI surface touched worktrees, so it couldn't be scripted or fired from wherever you already were. `fleet worktree <branch>` (alias `wt`) mirrors the `w` key: create the worktree, copy `.claude/settings.local.json` + `copy_files` entries, start a session. `--base` / `--path` / `--agent` steer it; `--no-session` prints only the path so `cd "$(fleet worktree foo --no-session)"` composes. Two details worth calling out. Flags parse on either side of the branch — a single `flag.Parse` stops at the first positional and would reject `fleet worktree foo --no-session`, the order most people type. And `--agent` is validated explicitly, because `agent.Parse` falls back to Claude for anything unrecognized, so a typo would silently launch the wrong agent. The command resolves to the main worktree before creating, so running it from inside `repo-foo` yields `repo-bar` rather than `repo-foo-bar` — same as pressing `w` on an origin header. It also calls `debuglog.Init()` first: the fallback logger writes to stderr and the provider logs at Info, so without it the command spews slog lines over the terminal (`fleet add` still does). The TUI read sessions from SQLite exactly once, at startup, which would have made a CLI-created session invisible until restart — the new command feeling broken for exactly the user most likely to run it. A self-throttled sweep in the worker's heavy pass now diffs the table and adopts unknown rows. Adoption only: rows deleted elsewhere are never dropped. Unlike a session the user just created, an adopted row arrives on a timer, so the cursor is captured by row identity and restored after the rebuild rather than left to drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1yXo3oP17DQwVbnagnWDY
|
Warning Review limit reached
Next review available in: 26 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Note
|
| Layer / File(s) | Summary |
|---|---|
CLI worktree creation cmd/fleet/main.go, cmd/fleet/worktree.go, cmd/fleet/worktree_test.go, changelog/unreleased/worktree-from-cli.md, CLAUDE.md |
Adds fleet worktree and fleet wt. The command validates arguments and agents, resolves the main worktree, creates and configures the new worktree, and optionally starts and persists a session. |
External session adoption internal/ui/app.go, internal/ui/adopt_test.go, changelog/unreleased/adopt-external-sessions.md, CLAUDE.md |
The TUI scans SQLite every five seconds for unknown sessions, adopts and pins them, avoids duplicates, preserves cursor selection, and ignores deleted rows. |
Estimated code review effort: 4 (Complex) | ~45 minutes
Sequence Diagram(s)
sequenceDiagram
participant User
participant runWorktree
participant GitMainWorktree
participant WorkspaceProvider
participant AgentSession
User->>runWorktree: worktree branch and flags
runWorktree->>GitMainWorktree: resolve repository and main worktree
runWorktree->>WorkspaceProvider: create and initialize worktree
runWorktree->>AgentSession: start and persist optional session
AgentSession-->>User: report session or created path
sequenceDiagram
participant StatusWorker
participant SQLite
participant unknownSessions
participant TUIUpdate
participant handleAdoptSessions
StatusWorker->>SQLite: reload sessions every five seconds
SQLite-->>unknownSessions: session rows
unknownSessions->>TUIUpdate: send unknown sessions
TUIUpdate->>handleAdoptSessions: adoptSessionsMsg
handleAdoptSessions-->>TUIUpdate: update sidebar and restore cursor
Possibly related PRs
- brizzai/fleet#169: Both changes resolve the repository’s main worktree during worktree creation.
- brizzai/fleet#213: This change uses agent support involved in session launch and agent validation.
Suggested reviewers: copilot
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes check | The TUI changes for adopting externally created sessions are unrelated to linked issue #227. |
Move external-session adoption into a separate pull request or link an issue that explicitly requires this functionality. | |
| Docstring Coverage | Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title clearly summarizes both the new CLI command and external-session adoption. |
| Linked Issues check | ✅ Passed | The PR implements issue #227 by adding the fleet worktree command, alias, options, validation, and worktree creation flow. |
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
worktree-from-cli
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
changelog/unreleased/worktree-from-cli.md (1)
1-5: 📐 Maintainability & Code Quality | 🔵 TrivialConfirm the
highlightdecision.This fragment has no
highlightkey, so it will not appear in the What's New reel. A CLI command that removes the need to open the TUI is a plausible candidate, but most fragments are not highlights. My recommendation isno.Do you want
highlight: trueadded?Based on learnings: never decide whether a changelog fragment is a highlight independently; ask the user and recommend
noby default.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@changelog/unreleased/worktree-from-cli.md` around lines 1 - 5, Add an explicit `highlight` decision to the changelog fragment front matter, using `highlight: false` unless the user confirms this CLI addition should appear in the What's New reel.Source: Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@changelog/unreleased/adopt-external-sessions.md`:
- Line 5: Shorten the bold changelog headline to two-to-four words while
preserving the session-refresh meaning. Leave the fragment’s missing highlight
key unchanged, so it remains excluded from the What's New reel.
In `@cmd/fleet/worktree.go`:
- Around line 96-107: Update runWorktree and the parseWorktreeArgs flag setup so
invalid flag parse errors are emitted only once: suppress flag package output
via its configured output, or avoid reprinting errors identified as parse
failures. Preserve the existing help handling and custom missing-branch usage
output.
In `@internal/ui/app.go`:
- Around line 3045-3061: Remove the `session.GetRepoRoot` call from the `Update`
adoption loop and resolve each adopted session’s repository root inside the
worker function `maybeAdoptExternalSessions`. Extend `adoptSessionsMsg` to carry
the resolved root, then use that value in the loop before `setExpanded` and
pinning, preserving the existing adoption behavior without performing blocking
git I/O in `Update`.
- Around line 5532-5536: Move the maybeAdoptExternalSessions call in
statusWorkerCycle before the early return that handles len(sessions) == 0 &&
len(shells) == 0, or otherwise ensure it executes for an empty fleet. Preserve
the existing sweep behavior for fleets containing sessions or shells.
---
Nitpick comments:
In `@changelog/unreleased/worktree-from-cli.md`:
- Around line 1-5: Add an explicit `highlight` decision to the changelog
fragment front matter, using `highlight: false` unless the user confirms this
CLI addition should appear in the What's New reel.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 914467e5-69cf-43f4-a00f-4bb7b21c1659
📒 Files selected for processing (8)
CLAUDE.mdchangelog/unreleased/adopt-external-sessions.mdchangelog/unreleased/worktree-from-cli.mdcmd/fleet/main.gocmd/fleet/worktree.gocmd/fleet/worktree_test.gointernal/ui/adopt_test.gointernal/ui/app.go
hayke102
left a comment
There was a problem hiding this comment.
Read the whole diff; go build ./... and go test ./cmd/fleet/... ./internal/ui/... both pass. Shape is good — the adopt sweep mirrors the suspend sweep's cadence/send-guard exactly, and unknownSessions being pure + the demo-prefix parity test is the right call. Five threads below: one real hole in the cursor invariant this PR sets out to hold, one error-path leak in the CLI, and three judgment calls.
hayke102
left a comment
There was a problem hiding this comment.
Second pass — three more, all reproduced locally. The first one is the headline: I missed it on the first read and it defeats the feature in its most likely scenario.
Ten findings, all verified against the code before fixing; none were false positives. The serious one: the adoption sweep never ran on an empty fleet. statusWorkerCycle returns early when there are no sessions and no shells, and the sweep sat below that guard — so a fresh install, or "I just deleted my last session", would never pick up a CLI-created session at all. That is precisely the case this feature exists for, and my manual verification missed it because the test TUI had one session, which put the cycle past the guard. The sweep now runs above the early return, with a regression test that drives a zero-session cycle (confirmed failing without the fix). Also on the adoption path: session.GetRepoRoot shelled out to git inside Update(), and for an adopted session the cache miss is guaranteed — the path was created by another process — so the roots are now resolved on the worker and carried in the message. Cursor preservation missed "Creating…" phantom rows, which is the row handleWorkspaceCreate auto-selects, so the cursor could drift during exactly the window this PR's command lands in; contextMenuTarget grows a pendingID case. Auto-expanding the group is gone: setExpanded persists, so a 5s timer would overwrite a collapse the user chose — sharpest against snooze, where the collapse is the visual half of the snooze. On the CLI side, flag parse errors printed twice (flag.ContinueOnError doesn't suppress output), a SaveSession failure left a live tmux session no DB row pointed at — invisible to the very adoption sweep this PR adds — and --base was silently dropped when the branch already existed, while the success line still implied a fresh branch. The command also missed migration.Run(), so a legacy brizz-code config dir could be stranded permanently by creating state.db before the migration ever looked; scoped to runWorktree rather than main() so hook-handler's hot path is untouched. Telemetry now fires at adoption with source: "cli", and the existing TUI call is tagged "tui" so the property isn't null on half the events. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q1yXo3oP17DQwVbnagnWDY
Closes #227.
Creating a worktree meant opening the TUI and pressing
w. Nothing in the CLI surface (add/list/remove/hooks/update) touched worktrees, so a terminal-first user couldn't script it or fire it from wherever they already were.fleet worktree <branch>Alias
wt. Mirrors thewkey: create the worktree, copy.claude/settings.local.json+copy_filesentries, start a session.Three decisions worth review:
flag.Parsestops at the first positional, sofleet worktree foo --no-session— the order most people type — would fail. Caught during manual testing, not planning.--agentis validated explicitly.agent.Parsefalls back to Claude for anything unrecognized, so--agent codekwould otherwise silently launch the wrong agent.git.GetMainWorktreePath). Running it insidemyrepo-scratch-agivesmyrepo-nested, notmyrepo-scratch-a-nested— matching whatwdoes on an origin header.It also calls
debuglog.Init()first. The fallbackdebuglog.Loggerwrites to stderr and both the provider andSession.Startlog at Info, so without it the command sprays slog lines over the terminal.fleet add/fleet liststill do this — left alone as adjacent code, but it's a one-line fix if wanted.Adoption of externally-created sessions
The TUI reads sessions from SQLite exactly once, in
Init. Without this, a session created by the CLI while fleet is open stays invisible until restart — the new command feeling broken for exactly the user most likely to run it.maybeAdoptExternalSessionsruns in the worker's heavy pass, self-throttled to 5s, diffs the table against the cycle's snapshot, and sendsadoptSessionsMsg. The handler mirrorshandleSessionCreateResultminusSaveSessionand analytics.fleet removefrom a second terminal still leaves a ghost until restart, unchanged from today.targetForCursor()and restored by identity after the rebuild — the hazardCLAUDE.mddocuments for the context menu.deferDeleteremoves the SQLite row immediately, ahead of the 5s undo window, so the sweep can't re-adopt a just-deleted session.Verification
make build,make lint(0 issues),make test(-race) all pass. 15 new cases acrosscmd/fleet/worktree_test.goandinternal/ui/adopt_test.go.Manual, against a scratch repo under a throwaway
$HOME:--no-sessionemits only the path,--base/--pathwork, all six error paths give clean messages, the full path starts tmux + writes the DB, and a worktree created from inside a worktree lands next to the main clone.For adoption I ran the TUI in a detached tmux pane and created a session from a second shell — it appeared without a restart, and the breadcrumb confirmed the cursor stayed on its row even though the new one was inserted above it.
The cursor test carries a vacuity guard: it fails if the adopted row doesn't actually shift the parked row's index, so it can't silently stop testing anything.
Two changelog fragments (
added+improved), split per the one-file-one-bullet rule. Neither is a highlight.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q1yXo3oP17DQwVbnagnWDY
Summary by CodeRabbit
New Features
fleet worktreecommand for creating worktrees from the CLI.Documentation