Fix: /handoff regression - use respawn-pane with direct claude command

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 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 00:23:18 -08:00
parent 441bafe7a8
commit a5a37f5d63
3 changed files with 26 additions and 17 deletions

View File

@@ -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.

View File

@@ -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-<rig>-crew-<name>
// 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-<rig>-witness
return "gt wit at", nil
return claudeCmd, nil
case strings.HasSuffix(sessionName, "-refinery"):
// gt-<rig>-refinery
return "gt ref at", nil
return claudeCmd, nil
default:
return "", fmt.Errorf("unknown session type: %s (try specifying role explicitly)", sessionName)

View File

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