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() }