From 9c85b834cd9e33444f13dcae0315b8b19eaed27c Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 13:25:38 -0800 Subject: [PATCH] Support full session paths in gt handoff (gt-tocb) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveRoleToSession now accepts paths like /crew/, /witness, and /refinery in addition to role shortcuts. For example: 'gt handoff gastown/crew/max' now works. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/handoff.go | 49 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 558b352c..10eed250 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -136,9 +136,19 @@ func getCurrentTmuxSession() (string, error) { return strings.TrimSpace(string(out)), nil } -// resolveRoleToSession converts a role name to a tmux session name. -// For roles that need context (crew, witness, refinery), it auto-detects from environment. +// resolveRoleToSession converts a role name or path to a tmux session name. +// Accepts: +// - Role shortcuts: "crew", "witness", "refinery", "mayor", "deacon" +// - Full paths: "/crew/", "/witness", "/refinery" +// - Direct session names (passed through) +// +// For role shortcuts that need context (crew, witness, refinery), it auto-detects from environment. func resolveRoleToSession(role string) (string, error) { + // First, check if it's a path format (contains /) + if strings.Contains(role, "/") { + return resolvePathToSession(role) + } + switch strings.ToLower(role) { case "mayor", "may": return "gt-mayor", nil @@ -178,11 +188,44 @@ func resolveRoleToSession(role string) (string, error) { return fmt.Sprintf("gt-%s-refinery", rig), nil default: - // Assume it's a direct session name + // Assume it's a direct session name (e.g., gt-gastown-crew-max) return role, nil } } +// resolvePathToSession converts a path like "/crew/" to a session name. +// Supported formats: +// - /crew/ -> gt--crew- +// - /witness -> gt--witness +// - /refinery -> gt--refinery +func resolvePathToSession(path string) (string, error) { + parts := strings.Split(path, "/") + + // Handle /crew/ format + if len(parts) == 3 && parts[1] == "crew" { + rig := parts[0] + name := parts[2] + return fmt.Sprintf("gt-%s-crew-%s", rig, name), nil + } + + // Handle / format (witness, refinery) + if len(parts) == 2 { + rig := parts[0] + role := strings.ToLower(parts[1]) + switch role { + case "witness": + return fmt.Sprintf("gt-%s-witness", rig), nil + case "refinery": + return fmt.Sprintf("gt-%s-refinery", rig), nil + case "crew": + // Just "/crew" without a name - need more info + return "", fmt.Errorf("crew path requires name: %s/crew/", rig) + } + } + + return "", fmt.Errorf("cannot parse path '%s' - expected /crew/, /witness, or /refinery", path) +} + // 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. // The command includes a cd to the correct working directory for the role.