From 5971f4c4702619a735ead7522b5d61f4a9ecdd12 Mon Sep 17 00:00:00 2001 From: riker Date: Thu, 22 Jan 2026 11:26:47 -0800 Subject: [PATCH] fix(dog): spawn session and set BD_ACTOR for dog dispatch Recovered from reflog - these commits were lost during a rebase/force-push. Dogs are directories with state files but no sessions. When `gt dog dispatch` assigned work and sent mail, nothing executed because no session existed. Changes: 1. Spawn tmux session after dispatch (gt--deacon-) 2. Set BD_ACTOR=deacon/dogs/ so dogs can find their mail 3. Add dog case to AgentEnv for proper identity Session spawn is non-blocking - if it fails, mail was sent and human can manually start the session. Co-Authored-By: Claude Opus 4.5 --- internal/cmd/dog.go | 38 +++++++++++++++++++++++++++++++++++++ internal/config/env.go | 7 ++++++- internal/config/env_test.go | 17 +++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/internal/cmd/dog.go b/internal/cmd/dog.go index 33435e30..dc0ad0f2 100644 --- a/internal/cmd/dog.go +++ b/internal/cmd/dog.go @@ -836,6 +836,44 @@ func runDogDispatch(cmd *cobra.Command, args []string) error { return fmt.Errorf("sending plugin mail to dog: %w", err) } + // Spawn a session for the dog to execute the work. + // Without a session, the dog's mail inbox is never checked. + // See: https://github.com/steveyegge/gastown/issues/XXX (dog dispatch doesn't execute) + t := tmux.NewTmux() + townName, err := workspace.GetTownName(townRoot) + if err != nil { + townName = "gt" // fallback + } + dogSessionName := fmt.Sprintf("gt-%s-deacon-%s", townName, targetDog.Name) + + // Kill any stale session first + if has, _ := t.HasSession(dogSessionName); has { + _ = t.KillSessionWithProcesses(dogSessionName) + } + + // Build startup command with initial prompt to check mail and execute plugin + initialPrompt := fmt.Sprintf("I am dog %s. Check my mail inbox with 'gt mail inbox' and execute the plugin instructions I received.", targetDog.Name) + startCmd := config.BuildAgentStartupCommand("dog", "", townRoot, targetDog.Path, initialPrompt) + + // Create session from dog's directory + if err := t.NewSessionWithCommand(dogSessionName, targetDog.Path, startCmd); err != nil { + if !dogDispatchJSON { + fmt.Printf(" Warning: could not spawn dog session: %v\n", err) + } + // Non-fatal: mail was sent, dog is marked as working, but no session to execute + // The deacon or human can manually start the session later + } else { + // Set environment for the dog session + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "dog", + AgentName: targetDog.Name, + TownRoot: townRoot, + }) + for k, v := range envVars { + _ = t.SetEnvironment(dogSessionName, k, v) + } + } + // Success - output result if dogDispatchJSON { return json.NewEncoder(os.Stdout).Encode(result) diff --git a/internal/config/env.go b/internal/config/env.go index a7dcad8c..d9e7bc62 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -61,6 +61,11 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string { env["GIT_AUTHOR_NAME"] = "boot" env["GIT_AUTHOR_EMAIL"] = "boot@gastown.local" + case "dog": + env["BD_ACTOR"] = fmt.Sprintf("deacon/dogs/%s", cfg.AgentName) + env["GIT_AUTHOR_NAME"] = fmt.Sprintf("dog-%s", cfg.AgentName) + env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("dog-%s@gastown.local", cfg.AgentName) + case "witness": env["GT_RIG"] = cfg.Rig env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig) @@ -128,7 +133,7 @@ func AgentEnvSimple(role, rig, agentName string) map[string]string { // ShellQuote returns a shell-safe quoted string. // Values containing special characters are wrapped in single quotes. -// Single quotes within the value are escaped using the '\'' idiom. +// Single quotes within the value are escaped using the '\” idiom. func ShellQuote(s string) string { // Check if quoting is needed (contains shell special chars) needsQuoting := false diff --git a/internal/config/env_test.go b/internal/config/env_test.go index 55b54db7..77ecf263 100644 --- a/internal/config/env_test.go +++ b/internal/config/env_test.go @@ -125,6 +125,23 @@ func TestAgentEnv_Boot(t *testing.T) { assertNotSet(t, env, "BEADS_NO_DAEMON") } +func TestAgentEnv_Dog(t *testing.T) { + t.Parallel() + env := AgentEnv(AgentEnvConfig{ + Role: "dog", + AgentName: "alpha", + TownRoot: "/town", + }) + + assertEnv(t, env, "GT_ROLE", "dog") + assertEnv(t, env, "BD_ACTOR", "deacon/dogs/alpha") + assertEnv(t, env, "GIT_AUTHOR_NAME", "dog-alpha") + assertEnv(t, env, "GIT_AUTHOR_EMAIL", "dog-alpha@gastown.local") + assertEnv(t, env, "GT_ROOT", "/town") + assertNotSet(t, env, "GT_RIG") + assertNotSet(t, env, "BEADS_NO_DAEMON") +} + func TestAgentEnv_WithRuntimeConfigDir(t *testing.T) { t.Parallel() env := AgentEnv(AgentEnvConfig{