fix(prime): use startup beacon instead of bare "gt prime" prompt

Agents were confused when receiving "gt prime" as their first prompt,
interpreting it as a command to investigate rather than understanding
they were starting a Gas Town session.

Changed crew_at.go, start.go, and handoff.go to use FormatStartupNudge()
which produces a proper beacon like:
  [GAS TOWN] george/crew/george <- human • 2026-01-09T10:30 • start

The SessionStart hook (gt prime --hook) still injects context - the
prompt just needs to be something agents recognize as a greeting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/george
2026-01-09 13:08:48 -08:00
committed by Steve Yegge
parent ff3d1b2e23
commit 86739556c2
3 changed files with 56 additions and 14 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/runtime"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
@@ -162,11 +163,20 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
return fmt.Errorf("getting pane ID: %w", err)
}
// Build startup beacon for predecessor discovery via /resume
// Use FormatStartupNudge instead of bare "gt prime" which confuses agents
// The SessionStart hook handles context injection (gt prime --hook)
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
Recipient: address,
Sender: "human",
Topic: "start",
})
// Use respawn-pane to replace shell with runtime directly
// This gives cleaner lifecycle: runtime exits → session ends (no intermediate shell)
// Pass "gt prime" as initial prompt if supported
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, "gt prime", crewAgentOverride)
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, beacon, crewAgentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
@@ -198,10 +208,18 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
return fmt.Errorf("getting pane ID: %w", err)
}
// Build startup beacon for predecessor discovery via /resume
// Use FormatStartupNudge instead of bare "gt prime" which confuses agents
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
Recipient: address,
Sender: "human",
Topic: "restart",
})
// Use respawn-pane to replace shell with runtime directly
// Pass "gt prime" as initial prompt if supported
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, "gt prime", crewAgentOverride)
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, beacon, crewAgentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
@@ -218,13 +236,19 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
// Check if we're already in the target session
if isInTmuxSession(sessionID) {
// We're in the session at a shell prompt - just start the agent directly
// Pass "gt prime" as initial prompt so it loads context immediately
// Build startup beacon for predecessor discovery via /resume
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
Recipient: address,
Sender: "human",
Topic: "start",
})
agentCfg, _, err := config.ResolveAgentConfigWithOverride(townRoot, r.Path, crewAgentOverride)
if err != nil {
return fmt.Errorf("resolving agent: %w", err)
}
fmt.Printf("Starting %s in current session...\n", agentCfg.Command)
return execAgent(agentCfg, "gt prime")
return execAgent(agentCfg, beacon)
}
// If inside tmux (but different session), don't switch - just inform user

View File

@@ -340,19 +340,29 @@ func buildRestartCommand(sessionName string) (string, error) {
return "", err
}
// Determine GT_ROLE and BD_ACTOR values for this session
gtRole := sessionToGTRole(sessionName)
// Parse the session name to get the identity (used for GT_ROLE and beacon)
identity, err := session.ParseSessionName(sessionName)
if err != nil {
return "", fmt.Errorf("cannot parse session name %q: %w", sessionName, err)
}
gtRole := identity.GTRole()
// Build startup beacon for predecessor discovery via /resume
// Use FormatStartupNudge instead of bare "gt prime" which confuses agents
// The SessionStart hook handles context injection (gt prime --hook)
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
Recipient: identity.Address(),
Sender: "self",
Topic: "handoff",
})
// For respawn-pane, we:
// 1. cd to the right directory (role's canonical home)
// 2. export GT_ROLE and BD_ACTOR so role detection works correctly
// 3. export Claude-related env vars (not inherited by fresh shell)
// 4. run claude with "gt prime" as initial prompt (triggers GUPP)
// 4. run claude with the startup beacon (triggers immediate context loading)
// Use exec to ensure clean process replacement.
// IMPORTANT: Passing "gt prime" as argument injects it as the first prompt,
// which triggers the agent to execute immediately. Without this, agents
// wait for user input despite all GUPP prompting in hooks.
runtimeCmd := config.GetRuntimeCommandWithPrompt("", "gt prime")
runtimeCmd := config.GetRuntimeCommandWithPrompt("", beacon)
// Build environment exports - role vars first, then Claude vars
var exports []string

View File

@@ -19,6 +19,7 @@ import (
"github.com/steveyegge/gastown/internal/polecat"
"github.com/steveyegge/gastown/internal/refinery"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/witness"
@@ -277,7 +278,14 @@ func startConfiguredCrew(t *tmux.Tmux, townRoot string) {
if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) {
// Claude has exited, restart it
fmt.Printf(" %s %s/%s session exists, restarting Claude...\n", style.Dim.Render("○"), r.Name, crewName)
claudeCmd := config.BuildCrewStartupCommand(r.Name, crewName, r.Path, "gt prime")
// Build startup beacon for predecessor discovery via /resume
address := fmt.Sprintf("%s/crew/%s", r.Name, crewName)
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
Recipient: address,
Sender: "human",
Topic: "restart",
})
claudeCmd := config.BuildCrewStartupCommand(r.Name, crewName, r.Path, beacon)
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
fmt.Printf(" %s %s/%s restart failed: %v\n", style.Dim.Render("○"), r.Name, crewName, err)
} else {