From e3500543f37ae9cf724bfbb5c70e40fb6fad38b8 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 12:59:41 -0800 Subject: [PATCH] Fix handoff to set correct working directory for each role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildRestartCommand now detects the town root and includes a cd to the appropriate directory before launching claude. This ensures each role starts in its correct working directory: - gt-mayor → town root (~/gt) - gt-deacon → ~/gt/deacon - crew sessions → ~/gt//crew/ - witness sessions → ~/gt//witness - refinery sessions → ~/gt//refinery 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/handoff.go | 78 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index bfcdaa71..26b4149b 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" "github.com/spf13/cobra" @@ -168,32 +169,93 @@ func resolveRoleToSession(role string) (string, error) { // 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. 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" + // Detect town root from current directory + townRoot := detectTownRootFromCwd() + if townRoot == "" { + return "", fmt.Errorf("cannot detect town root - run from within a Gas Town workspace") + } + // Determine the working directory for this session type + workDir, err := sessionWorkDir(sessionName, townRoot) + if err != nil { + return "", err + } + + // For respawn-pane, we cd to the right directory then run claude. + // The SessionStart hook will run gt prime. + // Use exec to ensure clean process replacement. + return fmt.Sprintf("cd %s && exec claude --dangerously-skip-permissions", workDir), nil +} + +// sessionWorkDir returns the correct working directory for a session. +func sessionWorkDir(sessionName, townRoot string) (string, error) { switch { case sessionName == "gt-mayor": - return claudeCmd, nil + return townRoot, nil case sessionName == "gt-deacon": - return claudeCmd, nil + return townRoot + "/deacon", nil case strings.Contains(sessionName, "-crew-"): - return claudeCmd, nil + // gt--crew- -> //crew/ + parts := strings.Split(sessionName, "-") + if len(parts) < 4 { + return "", fmt.Errorf("invalid crew session name: %s", sessionName) + } + // Find the index of "crew" to split rig name (may contain dashes) + for i, p := range parts { + if p == "crew" && i > 1 && i < len(parts)-1 { + rig := strings.Join(parts[1:i], "-") + name := strings.Join(parts[i+1:], "-") + return fmt.Sprintf("%s/%s/crew/%s", townRoot, rig, name), nil + } + } + return "", fmt.Errorf("cannot parse crew session name: %s", sessionName) case strings.HasSuffix(sessionName, "-witness"): - return claudeCmd, nil + // gt--witness -> //witness + rig := strings.TrimPrefix(sessionName, "gt-") + rig = strings.TrimSuffix(rig, "-witness") + return fmt.Sprintf("%s/%s/witness", townRoot, rig), nil case strings.HasSuffix(sessionName, "-refinery"): - return claudeCmd, nil + // gt--refinery -> //refinery + rig := strings.TrimPrefix(sessionName, "gt-") + rig = strings.TrimSuffix(rig, "-refinery") + return fmt.Sprintf("%s/%s/refinery", townRoot, rig), nil default: return "", fmt.Errorf("unknown session type: %s (try specifying role explicitly)", sessionName) } } +// detectTownRootFromCwd walks up from the current directory to find the town root. +func detectTownRootFromCwd() string { + cwd, err := os.Getwd() + if err != nil { + return "" + } + + dir := cwd + for { + // Check for primary marker (mayor/town.json) + markerPath := filepath.Join(dir, "mayor", "town.json") + if _, err := os.Stat(markerPath); err == nil { + return dir + } + + // Move up + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "" +} + // handoffRemoteSession respawns a different session and optionally switches to it. func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error { // Check if target session exists