Skip to content

feat: add entire stop command#739

Draft
peyton-alt wants to merge 3 commits intomainfrom
feat/entire-stop
Draft

feat: add entire stop command#739
peyton-alt wants to merge 3 commits intomainfrom
feat/entire-stop

Conversation

@peyton-alt
Copy link
Contributor

@peyton-alt peyton-alt commented Mar 19, 2026

Adds entire stop to manually mark one or more active sessions as ended,
preventing future checkpoint leakage without waiting for the agent to stop
naturally.

Pure state mutation — no writes to the checkpoints branch, no condensation.
The existing PostCommit hook handles any remaining condensation on the next
commit.

Flags: --session , --all (worktree-scoped), -f/--force

Output

Session with a committed checkpoint:
✓ Session 2026-03-19-demo-checkpoint stopped.
Checkpoint: a3b2c4d5e6f7

Session with uncommitted work:
✓ Session 2026-03-19-demo-work stopped.
Work will be captured in your next checkpoint.

Session with no work:
✓ Session 46f9e86a-02c2-46c8-a733-e4323cb5c2cb stopped.
No work recorded.

Already stopped:
Session 2026-03-19-demo-work is already stopped.

Not found:
Session not found.

No active sessions:
No active sessions.


Note

Medium Risk
Adds a new CLI command that mutates on-disk session state (marking sessions as ENDED) and includes interactive selection/confirmation paths, so mistakes could prematurely end sessions or affect worktree scoping.

Overview
Introduces a new entire stop command that manually marks session state as ENDED (without writing checkpoints), supporting --session <id>, worktree-scoped --all, and -f/--force to skip confirmation.

Implements interactive selection/confirmation flows (single-session prompt or multi-select when multiple sessions are active), and adds unit tests covering main stop paths and error cases. Documentation is updated in README.md and the changelog to include the new command.

Written by Cursor Bugbot for commit f14859a. Configure here.

Adds `entire stop` to manually mark one or more active sessions as ended,
preventing future checkpoint leakage without waiting for the agent to stop
naturally or for a SessionStop lifecycle hook.

Pure state mutation — no writes to the checkpoints branch, no condensation.
The existing PostCommit hook handles any remaining condensation on the next commit.

Flags: --session <id>, --all (worktree-scoped), --force/-f
TUI multi-select for 2+ sessions; confirmation prompt for single session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 19, 2026 22:54
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

stopErr = err
}
}
return stopErr
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch stop silently discards all errors except last

Low Severity

In both runStopAll and runStopMultiSelect, the loop overwrites stopErr on each failure, discarding all errors except the last one. If multiple sessions fail, only the final error is returned and earlier failure details are silently lost. The codebase already uses errors.Join in setup.go for aggregating errors from similar batch operations.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new entire stop command to manually mark one or more sessions as ended (state-only mutation) to prevent further checkpoint leakage without waiting for the agent lifecycle to naturally close the session.

Changes:

  • Introduces entire stop with --session, --all (worktree-scoped), and --force flows, including interactive selection when multiple sessions exist.
  • Adds unit tests covering the non-interactive stop flows and common error cases.
  • Documents the new command in README.md and CHANGELOG.md, and wires it into the root command.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
README.md Adds entire stop to the command list.
cmd/entire/cli/stop.go Implements the stop command logic (single/all/multi-select) and output messaging.
cmd/entire/cli/stop_test.go Adds unit tests for stop behavior (force, flags, not-found, non-git repo, etc.).
cmd/entire/cli/root.go Registers the new stop subcommand.
CHANGELOG.md Notes the addition of entire stop.

Comment on lines +92 to +100
// filterActiveSessions returns sessions where Phase != PhaseEnded.
func filterActiveSessions(states []*strategy.SessionState) []*strategy.SessionState {
var active []*strategy.SessionState
for _, s := range states {
if s.Phase != session.PhaseEnded {
active = append(active, s)
}
}
return active
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterActiveSessions treats “active” as Phase != PhaseEnded, but the rest of the CLI (e.g., status.go filters by EndedAt == nil) uses EndedAt as the primary “session is active” marker. If a legacy session has EndedAt set but Phase missing/idle (Phase normalizes to idle), it will incorrectly show up as active here. Consider filtering out sessions with EndedAt != nil (or treating a session as stopped when either Phase == ended OR EndedAt != nil).

Copilot uses AI. Check for mistakes.
return NewSilentError(fmt.Errorf("session not found: %s", sessionID))
}

if state.Phase == session.PhaseEnded {
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “already stopped” check only looks at state.Phase. If a session has EndedAt != nil but Phase is empty/idle (possible for older state files since NormalizeAfterLoad defaults empty phase to idle), this will re-stop the session and overwrite EndedAt. Consider treating EndedAt != nil as already stopped (or checking both fields).

Suggested change
if state.Phase == session.PhaseEnded {
if state.Phase == session.PhaseEnded || state.EndedAt != nil {

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +155
// Scope to current worktree: include sessions where WorktreePath matches
// or WorktreePath is empty (safer than silently excluding).
worktreePath, err := paths.WorktreeRoot(ctx)
if err != nil {
return fmt.Errorf("failed to resolve worktree root: %w", err)
}

var toStop []*strategy.SessionState
for _, s := range activeSessions {
if s.WorktreePath == worktreePath || s.WorktreePath == "" {
toStop = append(toStop, s)
}
}
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--all is described as “worktree-scoped”, but this filter also includes sessions where WorktreePath == "", which can stop sessions from other worktrees (or unknown origins) when run from the current worktree. This differs from existing worktree scoping logic (e.g., strategy.FindMostRecentSession only matches WorktreePath == current). Consider limiting --all strictly to WorktreePath == worktreePath and handling unknown/empty worktree sessions explicitly (e.g., separate flag or a warning + confirmation list).

Copilot uses AI. Check for mistakes.
var stopErr error
for _, s := range toStop {
if err := stopSessionAndPrint(ctx, cmd, s); err != nil {
stopErr = err
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When stopping multiple sessions, the loop overwrites stopErr and returns only the last failure, discarding earlier errors. This makes troubleshooting harder if more than one stop fails. Consider collecting errors and returning errors.Join(errs...) (pattern used elsewhere, e.g., setup.go).

Suggested change
stopErr = err
stopErr = errors.Join(stopErr, err)

Copilot uses AI. Check for mistakes.
continue
}
if err := stopSessionAndPrint(ctx, cmd, s); err != nil {
stopErr = err
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as runStopAll: the multi-select stop loop returns only the last error (stopErr = err). Consider collecting and returning a joined error so multiple failures aren’t lost.

Suggested change
stopErr = err
stopErr = errors.Join(stopErr, err)

Copilot uses AI. Check for mistakes.
peyton-alt and others added 2 commits March 19, 2026 16:22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ire stop

- stopSelectedSessions: print ✗ per failed session to stderr so batch
  failures are visible immediately, not just as a joined error at the end
- stopSessionAndPrint: wrap markSessionEnded error with session ID for
  context in both single and batch stop paths
- runStopMultiSelect: warn and skip sessions concurrently removed between
  TUI render and confirmation; guard empty toStop after filtering
- filterActiveSessions: add nil guard; document intentional dual-check
  vs status.go's EndedAt-only filter
- handleFormCancellation: treat huh.ErrTimeout same as ErrUserAborted;
  wrap unexpected errors with action name for context
- --session flag description: note it is not scoped to current worktree
- tests: add TestStopCmd_AllFlag_ExcludesOtherWorktrees,
  TestStopCmd_AllFlag_NoActiveSessions, TestStopSelectedSessions_StopsAll
  (replaces non-executing cop-out multi-select test); add PhaseActive
  coverage to filter unit test; rename SingleSession_Force to clarify
  empty-WorktreePath intent; add ErrTimeout case to utils_test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants