From a5a37f5d63da3ee318dcca10f83dffed06d2f793 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 00:23:18 -0800 Subject: [PATCH] Fix: /handoff regression - use respawn-pane with direct claude command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related fixes: 1. lifecycle.go: Use gt mail delete instead of gt mail read to prevent lifecycle requests from accumulating. 2. handoff.go: Return exec claude command for respawn-pane instead of gt crew at which tries to attach to existing session. 3. handoff.md skill: Work without --cycle and -m flags, send mail separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/commands/handoff.md | 14 ++++++++++---- internal/cmd/handoff.go | 21 +++++++++++---------- internal/daemon/lifecycle.go | 8 +++++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.claude/commands/handoff.md b/.claude/commands/handoff.md index 22a4091a..6f9e880c 100644 --- a/.claude/commands/handoff.md +++ b/.claude/commands/handoff.md @@ -1,6 +1,6 @@ --- description: Hand off to fresh session, work continues from hook -allowed-tools: Bash(gt handoff:*) +allowed-tools: Bash(gt mail send:*),Bash(gt handoff:*) argument-hint: [message] --- @@ -8,8 +8,14 @@ Hand off to a fresh session. User's handoff message (if any): $ARGUMENTS -Execute the appropriate command: -- If user provided a message: `gt handoff --cycle -m "USER_MESSAGE_HERE"` -- If no message provided: `gt handoff --cycle` +Execute these steps in order: +1. If user provided a message, send handoff mail to yourself first. + Construct your mail address from your identity (e.g., gastown/crew/max for crew, mayor/ for mayor). + Example: `gt mail send gastown/crew/max -s "🤝 HANDOFF: Session cycling" -m "USER_MESSAGE_HERE"` + +2. Run the handoff command (this will respawn your session with a fresh Claude): + `gt handoff` + +Note: The new session will auto-prime via the SessionStart hook and find your handoff mail. End watch. A new session takes over, picking up any molecule on the hook. diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 0a101a9f..e4cecc39 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -152,27 +152,28 @@ func resolveRoleToSession(role string) (string, error) { } } -// buildRestartCommand creates the gt command to restart a session. +// buildRestartCommand creates the command to run when respawning a session's pane. +// This needs to be the actual command to execute (e.g., claude), not a session attach command. func buildRestartCommand(sessionName string) (string, error) { + // For respawn-pane, we run claude directly. The SessionStart hook will run gt prime. + // Use exec to ensure clean process replacement. + claudeCmd := "exec claude --dangerously-skip-permissions" + switch { case sessionName == "gt-mayor": - return "gt may at", nil + return claudeCmd, nil case sessionName == "gt-deacon": - return "gt dea at", nil + return claudeCmd, nil case strings.Contains(sessionName, "-crew-"): - // gt--crew- - // The attach command can auto-detect from cwd, so just use `gt crew at` - return "gt crew at", nil + return claudeCmd, nil case strings.HasSuffix(sessionName, "-witness"): - // gt--witness - return "gt wit at", nil + return claudeCmd, nil case strings.HasSuffix(sessionName, "-refinery"): - // gt--refinery - return "gt ref at", nil + return claudeCmd, nil default: return "", fmt.Errorf("unknown session type: %s (try specifying role explicitly)", sessionName) diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 9335255f..f7d43f2e 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -300,10 +300,12 @@ func (d *Daemon) syncWorkspace(workDir string) { } } -// closeMessage marks a mail message as read by closing the beads issue. +// closeMessage removes a lifecycle mail message after processing. +// We use delete instead of read because gt mail read intentionally +// doesn't mark messages as read (to preserve handoff messages). func (d *Daemon) closeMessage(id string) error { - // Use gt mail commands for town-level beads - cmd := exec.Command("gt", "mail", "read", id) + // Use gt mail delete to actually remove the message + cmd := exec.Command("gt", "mail", "delete", id) cmd.Dir = d.config.TownRoot return cmd.Run() }