nebula spawn --worktree <branch> starts the new session in that branch's worktree, cut first when the branch has none - #125
Open
michael-dg wants to merge 1 commit into
Conversation
…h's worktree, cut first when the branch has none - `nebula spawn "<task>" --worktree <branch>` starts the sibling session in the project's worktree on <branch> instead of the caller's: the checkout already on that branch (the root one included), or a new one cut the way `nebula worktree` cuts it. The branch words are slugified as `nebula worktree` slugifies them, the session keeps the caller's harness, model and effort unless `--kind` says otherwise, and its default `agent-N` name is the first one free in the worktree it lands in. The caller stays where it is. - Whatever the create would refuse is refused before git cuts anything, so a refused spawn never leaves a worktree behind: a blank branch, an unknown or archived caller, and — through `Daemon::check_cold_launch`, the same refusals `create_agent` gives a cold launch on a starting prompt — the prompt, a harness that does not resolve and a CLI that is not installed. - The find-or-cut half of `nebula worktree` moves into `Daemon::worktree_on_branch`, which both commands share. It holds `worktree_ops` across the lookup and the cut, as `pr_worktree` does, so two requests for one new branch share one checkout instead of the second failing on "worktree path already exists". `create_worktree`'s body becomes `cut_worktree`, for a lock holder. - `--base <ref>` picks a new branch's start point and is refused when the branch already exists instead of being dropped, so neither command reports a start point the checkout lacks: `worktree_on_branch` refuses it for a branch with a worktree (naming its path), and `git::add_worktree_off_ref` — the path only a user-named base takes — for a branch kept without one. Both sit on the shared path, so `nebula worktree --base` gets the same refusals; bases nobody named still check an existing branch out. `--base` is refused without `--worktree`, or blank. - Claude's spawn guidance names `--worktree` for "in a new worktree" / "on branch X" requests and lists `grok` among the `--kind` values. - Protocol 45: `SpawnSiblingAgent` carries `worktree` and `base`. The codec writes structs as arrays, so a v44 daemon cannot read the longer request even when both are None; refusing at the handshake gives the usual `nebula kill` message instead of a dropped connection. - `--help` for both commands, docs/commands.md and docs/how-it-works.md describe the flag and when --base is refused. Tests: a_named_worktree_names_the_spawn_against_its_own_rows, a_branch_with_a_worktree_is_reused_not_cut (and `--base` for it refused), a_worktree_spawn_is_refused_before_any_worktree_is_cut (a blank branch, a blank task, an unknown caller and a Custom caller with no registry id, none registering a worktree), a_named_base_is_refused_for_a_branch_that_already_exists, enter_worktree_takes_an_existing_branch_and_moves_the_row_now extended (`--base` on an existing checkout refused), and nebula_spawn_cli_starts_a_sibling_session_in_the_same_worktree extended end to end (a real worktree cut for `feat login`, the session alive in it as agent-1, the same branch reused on a second spawn, `--base` refused alone, blank, on an existing worktree and on a branch kept without one). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
nebula spawncould only start a session in the caller's own worktree, so two agents working in parallel shared one checkout, index and branch.--worktree <branch>starts the new session in that branch's worktree instead, cutting it first when the branch has none, so an agent (or a.mdworkflow it follows) can hand each piece of work to its own session on its own branch with one command.Contents
nebula worktree✨ What you get
🌿 Parallel work on separate branches
nebula spawn --worktree <branch> "<task>"nebula worktreeusesnebula spawn--kind, AUTO-TITLE--kindnames anotheragent-Nname is the first one free in the worktree it lands in, so it titles itselfnebula worktreeslugifies them:feat loginisfeat-login--base <ref>nebula worktree --baseresolves it--worktree, or blank--worktreefor "in a new worktree" / "on branch X" requests, and listsgrokamong the--kindvalues🛡️ Nothing left behind
--worktreeon the same branch🔁 Also changes
nebula worktreeBoth commands now share one find-or-cut path, so two of its fixes reach
nebula worktreeas well. Flagged here so neither lands unnoticed:nebula worktreeracing a spawn (or anothernebula worktree) for one new branch shares the checkout instead of failing on "worktree path already exists"--baseon a branch that already exists is refused — the one behaviour change outsidespawn--basenebula worktreeas it was;spawnwould then refuse the case on its ownnebula worktreewithout--base, and every worktree cut from the TUI, behave exactly as before📸 Screenshots
No PNG: the change is a CLI flag with no screen of its own. The card appears on the grid exactly as a plain
nebula spawn's does, under the worktree's band. This is what the model reads back:🧭 How it flows
sequenceDiagram participant A as Agent CLI (caller) participant C as nebula spawn --worktree participant D as DAEMON participant G as git A->>C: runs it (NEBULA_AGENT_ID) C->>D: SpawnSiblingAgent { worktree, base } D->>D: refuse blank branch, unknown or archived caller D->>D: check_cold_launch (prompt, harness, CLI installed) Note over D: worktree_ops lock held alt project has a worktree on the branch D->>D: worktree_on_branch reuses it, refused if --base was given else no worktree yet D->>G: cut_worktree (fetch, worktree add, WORKTREE HOOK) Note over D,G: an existing branch with --base is refused G-->>D: new checkout end D->>D: create_agent in that worktree, task as STARTING PROMPT D-->>C: Ack { created: Agent } C-->>A: started a new session in the worktree on branch …🎯 Attack surface
ClientRequest.SpawnSiblingAgentnow carriesworktreeandbase. Reached by: any process on the DAEMON's socket (mode0700, same user) that names a live agent id — the same reachnebula spawnandnebula worktreealready had. Held by: the values go to the samecreate_worktreethatEnterWorktreealready feeds the same two strings to (git argv, never a shell;/in the branch folded to-for the directory). Not held: nothing new — a caller who could already move its own session into any branch can now start a sibling there.Verdict: 🟢 Low risk — an existing request grows two optional fields that reuse an existing, already-exposed path; the one cost is the protocol bump.
EnterWorktree's; the PROTOCOL VERSION bump means an upgraded client refuses an old DAEMON at the handshake with the usualnebula killmessage;nebula worktree --baseon an existing branch now errors where it used to succeed on the wrong start point--worktree, waits on the fetch and the WORKTREE HOOK exactly asnebula worktreealready doesenter_worktreeis extracted, not duplicated, and now locks likepr_worktree; the pre-create check reusescreate_agent's own refusals; tests follow the module's in-file ande2e_ptypatternsRollback:
git revert <merge>removes the flag; it does not undo Protocol 45, so clients and DAEMONs built across the revert still neednebula killonce.🔧 Technical overview
--worktree, trims--base, and sends both onSpawnSiblingAgent.Daemon::spawn_sibling_agentreads the caller once, refuses a blank branch, then runsDaemon::check_cold_launch— the prompt, harness and missing-CLI refusalscreate_agentwould give — before resolving the target throughDaemon::worktree_on_branch: the find-or-cut thatenter_worktreeused to do inline, now holdingworktree_opsacross the lookup and the cut (cut_worktreeiscreate_worktree's body for a lock holder) and refusing abasefor a branch that already has a checkout;git::add_worktree_off_refrefuses one for a branch kept without a checkout.sibling_specthen picks the default name against the target worktree's rows.rmp_serde::to_vec), so even with#[serde(default)]a v44 DAEMON fails to decode the longer request (LengthMismatch, checked with a throwaway program) and drops the connection. The bump turns that into a handshake refusal, as 5bebba4 did for Protocol 43.crates/nebula-daemon/src/sibling.rs— the worktree branch of the spawn and its guidance text;crates/nebula-daemon/src/registry.rs—worktree_on_branch(shared withenter_worktree),cut_worktree,check_cold_launch;crates/nebula-daemon/src/git.rs—add_worktree_off_refrefuses an existing branch;crates/nebula-core/src/protocol.rs— the two fields and the bump;crates/nebula/src/cli.rsandcrates/nebula-tui/src/ipc.rs— the flags and the request;docs/commands.md,docs/how-it-works.md.--worktreeis empty — it is refused, since a spawn names where its work goes. No rollback of a cut worktree when the create fails anyway (a spawn error after the checks): removing a checkout is destructive, so the checks move ahead of the cut instead.make ci: fmt clean, clippy adds no warning (the remaining ones are in untouched files), 1701 tests pass. Two fail on this machine and fail the same way onmainat 0f98b95:e2e_tui'snebula_open_from_inside_a_session_raises_the_file_tabsandtui_drag_past_the_pane_top_autoscrolls_and_copies_the_run(time out waiting for the TUI);ipc::tests::kill_stops_a_skewed_daemon_whose_pidfile_is_goneis flaky on both (it failed 1 run in 6 onmain, alone). New:a_named_worktree_names_the_spawn_against_its_own_rows,a_branch_with_a_worktree_is_reused_not_cut,a_worktree_spawn_is_refused_before_any_worktree_is_cut,a_named_base_is_refused_for_a_branch_that_already_exists,enter_worktree_takes_an_existing_branch_and_moves_the_row_nowextended (nebula worktree --baseon an existing checkout refused), andnebula_spawn_cli_starts_a_sibling_session_in_the_same_worktreeextended end to end (a real worktree cut and reused,--baserefused alone, blank, on an existing worktree and on a branch kept without one). The parallel-spawn race has no test: it would need two requests held at the lock at once.📝 Notes
mainat 0f98b95 (v0.42.0); no conflicts.nebula killonce after upgrading so the DAEMON restarts on the new build.🤖 Generated with Claude Code