Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd2184b128 | ||
|
|
1b2e5f5f05 |
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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<Inner>,
|
||||
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(),
|
||||
|
||||
+10
-25
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user