diff --git a/crates/maestro-cli/src/cli.rs b/crates/maestro-cli/src/cli.rs index 250b502..a3c94fb 100644 --- a/crates/maestro-cli/src/cli.rs +++ b/crates/maestro-cli/src/cli.rs @@ -133,7 +133,7 @@ pub struct KillArgs { /// Session ID to kill. pub session_id: Uuid, /// End the session even when its worktree holds unsaved work (#81). The - /// worktree is left on disk — never removed. + /// worktree is removed along with the session. #[arg(long)] pub force: bool, } diff --git a/crates/maestro-daemon/src/session.rs b/crates/maestro-daemon/src/session.rs index 255d5bf..112fab2 100644 --- a/crates/maestro-daemon/src/session.rs +++ b/crates/maestro-daemon/src/session.rs @@ -1545,15 +1545,10 @@ impl SessionManager { /// uncommitted tracked changes, untracked non-ignored files, or commits /// reachable from no remote-tracking ref. Nothing is killed and nothing /// is removed; the error names what blocked it and how much of it there - /// is. The session stays as the operator's handle on their own unfinished - /// work, so ending a session that still matters becomes a deliberate - /// `--force`, which leaves the tree alone rather than destroying it - /// (WORKTREES.md). + /// is. /// - /// With `force` (#81) the refusal — and nothing else — is overridden: - /// the session ends and the guarded worktree is left on disk, never - /// removed. A tree whose guard is clear never needed the override; force - /// removes it as usual. + /// With `force` (#81) the refusal is overridden: the session ends and + /// the worktree is removed, even when it holds unsaved work. /// /// This is also how an already-dead session is dismissed. tmux is only /// asked to kill something it still has — calling `kill-session` on a @@ -1588,7 +1583,7 @@ impl SessionManager { sessions.remove(&id) }; let record = removed.ok_or(SessionError::NotFound(id))?; - cleanup_record(&self.inner, id, record, alive, guarded).await; + cleanup_record(&self.inner, id, record, alive).await; Ok(()) } @@ -2972,8 +2967,8 @@ async fn mark_session_dead_with_worktree_settlement( /// Apply the exit rule (#82) to a vanished session's worktree: probe what /// would be lost through the kill's own predicate, then remove a pristine -/// tree. Anything holding unsaved work is left exactly where it is — the -/// same thing a forced kill does with a guarded tree (#81). +/// tree. Anything holding unsaved work is left on disk — there is no +/// operator confirmation on the natural-exit path. /// /// Returns whether the worktree was fully accounted for. When it wasn't, /// nothing was destroyed and the caller keeps the reference on the record. @@ -3028,16 +3023,11 @@ async fn settle_worktree_on_exit( /// gone from the authoritative in-memory map, so a stray FIFO or sidecar can't /// resurrect it. `kill_tmux` is `true` for an explicit kill and `false` for the /// liveness reaper (tmux already lost the session). -/// -/// `keep_worktree` is `true` for a forced kill (#81) over a guarded tree: the -/// session ends but the worktree is left on disk, so only the removal is -/// skipped. async fn cleanup_record( inner: &Arc, id: Uuid, record: SessionRecord, kill_tmux: bool, - keep_worktree: bool, ) { if kill_tmux { let tmux = inner.tmux.clone(); @@ -3059,17 +3049,7 @@ async fn cleanup_record( tracing::warn!(error = ?e, %id, "cleanup: failed to delete sidecar"); } if let (Some(base), Some(path)) = (&record.worktree_base_repo, &record.worktree_path) { - if keep_worktree { - tracing::info!( - %id, - path = %path.display(), - "forced kill: worktree left on disk" - ); - } else if let Err(e) = inner.worktree_mgr.remove(base, path) { - // The kill already passed the unsaved-work guard (#79), so this - // removal is unconditional; the manager's own nested-worktree - // refusal is all that can still leave a tree on disk, and it - // says where. + if let Err(e) = inner.worktree_mgr.remove(base, path) { tracing::warn!( %id, path = %path.display(), diff --git a/docs/WORKTREES.md b/docs/WORKTREES.md index fff8b03..5dd8d1a 100644 --- a/docs/WORKTREES.md +++ b/docs/WORKTREES.md @@ -75,27 +75,17 @@ The point of refusing rather than cleaning up is that the session stays as the operator's handle on their own unfinished work. A session that is killed while its worktree still matters leaves a directory nobody is looking at. -A consequence, accepted deliberately: a feature branch almost always has unpushed -commits, so `--force` is the ordinary way to end a session, not an exception. -That is only safe because force never removes what blocked. - ### `--force` -`--force` overrides the refusal and nothing else. It ends the session and -**leaves the worktree on disk**, untouched. Force never removes a tree that -would have blocked. A tree whose guard is clear never needed the override, so a -forced kill removes it as usual — force changes what happens to blocked work, -not ordinary cleanup. +`--force` overrides the refusal and **removes the worktree**, even when it holds +unsaved work. The operator sees the breakdown in the confirmation prompt and +decides; once confirmed, the tree is gone. In the web client the kill button sends the unforced `DELETE` straight away — there is no pre-confirmation, because the daemon is the one that decides. Only the refusal raises a prompt, and that prompt names what is in the way and offers "Force kill", which repeats the request with `force=true`. -Ending a session and destroying its work are two separate acts. Maestro only -performs the first one. Removing a tree it left behind is the operator's own -`git worktree remove`, chosen while looking at exactly what would be lost. - ### Exiting on its own A session that ends by itself — the command finished, the operator typed `exit` — @@ -113,17 +103,12 @@ once. A removed tree drops off the record, so dismissing the session afterwards does not refuse: there is no guard left to trip. A tree that was left alone stays attached to the dead record, where dismissing it -later goes through `--force`, which leaves it on disk too. The same holds when -the probe itself fails: nothing is destroyed. +later goes through `--force`, which removes it. The same holds when the probe +itself fails: nothing is destroyed. -## Nothing is removed on the operator's behalf +## Trees left behind by natural exit -There is no store of worktrees the daemon is holding, no list endpoint, and no -reclaim command. A tree left behind by a forced kill or a dirty exit is an -ordinary git worktree on disk; `git worktree list` finds it and -`git worktree remove` removes it. - -That is deliberate. A tracked list of trees pending deletion is a second place -the truth can live, and it goes stale the moment somebody removes a directory by -hand. Git already knows which worktrees exist, and removing one is not a -decision Maestro is better placed to make. +A forced kill always removes the worktree. A natural exit with unsaved work +leaves the tree on disk — there is no operator confirmation in that path. Those +trees are ordinary git worktrees; `git worktree list` finds them and +`git worktree remove` removes them.