diff --git a/.claude/settings.json b/.claude/settings.json index 9834b22c..21668ce7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -35,6 +35,17 @@ } ] } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt costs record" + } + ] + } ] } } diff --git a/internal/claude/config/settings-autonomous.json b/internal/claude/config/settings-autonomous.json index 14d04bd7..09e33d71 100644 --- a/internal/claude/config/settings-autonomous.json +++ b/internal/claude/config/settings-autonomous.json @@ -35,6 +35,17 @@ } ] } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt costs record" + } + ] + } ] } } diff --git a/internal/claude/config/settings-interactive.json b/internal/claude/config/settings-interactive.json index 2d9e0601..ecb6dd5f 100644 --- a/internal/claude/config/settings-interactive.json +++ b/internal/claude/config/settings-interactive.json @@ -35,6 +35,17 @@ } ] } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt costs record" + } + ] + } ] } } diff --git a/internal/cmd/costs.go b/internal/cmd/costs.go index f88a9cab..5f1e801c 100644 --- a/internal/cmd/costs.go +++ b/internal/cmd/costs.go @@ -519,11 +519,14 @@ func runCostsRecord(cmd *cobra.Command, args []string) error { // Get session from flag or try to detect from environment session := recordSession if session == "" { - // Try to get from TMUX_PANE or tmux environment session = os.Getenv("GT_SESSION") } if session == "" { - return fmt.Errorf("--session flag required (or set GT_SESSION env var)") + // Derive session name from GT_* environment variables + session = deriveSessionName() + } + if session == "" { + return fmt.Errorf("--session flag required (or set GT_SESSION env var, or GT_RIG/GT_ROLE)") } t := tmux.NewTmux() @@ -610,6 +613,41 @@ func runCostsRecord(cmd *cobra.Command, args []string) error { return nil } +// deriveSessionName derives the tmux session name from GT_* environment variables. +// Session naming patterns: +// - Polecats: gt-{rig}-{polecat} (e.g., gt-gastown-toast) +// - Crew: gt-{rig}-crew-{crew} (e.g., gt-gastown-crew-max) +// - Witness/Refinery: gt-{rig}-{role} (e.g., gt-gastown-witness) +// - Mayor/Deacon: gt-{role} (e.g., gt-mayor) +func deriveSessionName() string { + role := os.Getenv("GT_ROLE") + rig := os.Getenv("GT_RIG") + polecat := os.Getenv("GT_POLECAT") + crew := os.Getenv("GT_CREW") + + // Polecat: gt-{rig}-{polecat} + if polecat != "" && rig != "" { + return fmt.Sprintf("gt-%s-%s", rig, polecat) + } + + // Crew: gt-{rig}-crew-{crew} + if crew != "" && rig != "" { + return fmt.Sprintf("gt-%s-crew-%s", rig, crew) + } + + // Global roles without rig: gt-{role} + if role != "" && rig == "" { + return fmt.Sprintf("gt-%s", role) + } + + // Rig-based roles (witness, refinery): gt-{rig}-{role} + if role != "" && rig != "" { + return fmt.Sprintf("gt-%s-%s", rig, role) + } + + return "" +} + // buildAgentPath builds the agent path from role, rig, and worker. // Examples: "mayor", "gastown/witness", "gastown/polecats/toast" func buildAgentPath(role, rig, worker string) string { diff --git a/internal/cmd/costs_test.go b/internal/cmd/costs_test.go new file mode 100644 index 00000000..fb6da368 --- /dev/null +++ b/internal/cmd/costs_test.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "os" + "testing" +) + +func TestDeriveSessionName(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + expected string + }{ + { + name: "polecat session", + envVars: map[string]string{ + "GT_ROLE": "polecat", + "GT_RIG": "gastown", + "GT_POLECAT": "toast", + }, + expected: "gt-gastown-toast", + }, + { + name: "crew session", + envVars: map[string]string{ + "GT_ROLE": "crew", + "GT_RIG": "gastown", + "GT_CREW": "max", + }, + expected: "gt-gastown-crew-max", + }, + { + name: "witness session", + envVars: map[string]string{ + "GT_ROLE": "witness", + "GT_RIG": "gastown", + }, + expected: "gt-gastown-witness", + }, + { + name: "refinery session", + envVars: map[string]string{ + "GT_ROLE": "refinery", + "GT_RIG": "gastown", + }, + expected: "gt-gastown-refinery", + }, + { + name: "mayor session", + envVars: map[string]string{ + "GT_ROLE": "mayor", + }, + expected: "gt-mayor", + }, + { + name: "deacon session", + envVars: map[string]string{ + "GT_ROLE": "deacon", + }, + expected: "gt-deacon", + }, + { + name: "no env vars", + envVars: map[string]string{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save and clear relevant env vars + saved := make(map[string]string) + envKeys := []string{"GT_ROLE", "GT_RIG", "GT_POLECAT", "GT_CREW"} + for _, key := range envKeys { + saved[key] = os.Getenv(key) + os.Unsetenv(key) + } + defer func() { + // Restore env vars + for key, val := range saved { + if val != "" { + os.Setenv(key, val) + } + } + }() + + // Set test env vars + for key, val := range tt.envVars { + os.Setenv(key, val) + } + + result := deriveSessionName() + if result != tt.expected { + t.Errorf("deriveSessionName() = %q, want %q", result, tt.expected) + } + }) + } +}