fix(startup): unify agent startup with beacon + instructions in CLI prompt (#977)

All agents now receive their startup beacon + role-specific instructions via
the CLI prompt, making sessions identifiable in /resume picker while removing
unreliable post-startup nudges.

Changes:
- Rename FormatStartupNudge → FormatStartupBeacon, StartupNudgeConfig → BeaconConfig
- Remove StartupNudge() function (no longer needed)
- Remove PropulsionNudge() and PropulsionNudgeForRole() functions
- Update deacon, witness, refinery, polecat managers to include beacon in CLI prompt
- Update boot to inline beacon (can't import session due to import cycle)
- Update daemon/lifecycle.go to include beacon via BuildCommandWithPrompt
- Update cmd/deacon.go to include beacon in startup command
- Remove redundant StartupNudge and PropulsionNudge calls from all startup paths

The beacon is now part of the CLI prompt which is queued before Claude starts,
making it more reliable than post-startup nudges which had timing issues.
SessionStart hook runs gt prime automatically, so PropulsionNudge was redundant.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
aleiby
2026-01-25 18:08:57 -08:00
committed by GitHub
parent 92ccacffd9
commit 31bd120077
20 changed files with 163 additions and 285 deletions

View File

@@ -12,16 +12,10 @@ import (
"time"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/tmux"
)
// SessionName is the tmux session name for Boot.
// Note: We use "gt-boot" instead of "hq-deacon-boot" to avoid tmux prefix
// matching collisions. Tmux matches session names by prefix, so "hq-deacon-boot"
// would match when checking for "hq-deacon", causing HasSession("hq-deacon")
// to return true when only Boot is running.
const SessionName = "gt-boot"
// MarkerFileName is the lock file for Boot startup coordination.
const MarkerFileName = ".boot-running"
@@ -81,7 +75,7 @@ func (b *Boot) IsRunning() bool {
// IsSessionAlive checks if the Boot tmux session exists.
func (b *Boot) IsSessionAlive() bool {
has, err := b.tmux.HasSession(SessionName)
has, err := b.tmux.HasSession(session.BootSessionName())
return err == nil && has
}
@@ -163,7 +157,7 @@ func (b *Boot) spawnTmux(agentOverride string) error {
// Kill any stale session first.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
if b.IsSessionAlive() {
_ = b.tmux.KillSessionWithProcesses(SessionName)
_ = b.tmux.KillSessionWithProcesses(session.BootSessionName())
}
// Ensure boot directory exists (it should have CLAUDE.md with Boot context)
@@ -171,22 +165,26 @@ func (b *Boot) spawnTmux(agentOverride string) error {
return fmt.Errorf("ensuring boot dir: %w", err)
}
// Build startup command with optional agent override
// The "gt boot triage" prompt tells Boot to immediately start triage (GUPP principle)
initialPrompt := session.BuildStartupPrompt(session.BeaconConfig{
Recipient: "boot",
Sender: "daemon",
Topic: "triage",
}, "Run `gt boot triage` now.")
var startCmd string
if agentOverride != "" {
var err error
startCmd, err = config.BuildAgentStartupCommandWithAgentOverride("boot", "", b.townRoot, "", "gt boot triage", agentOverride)
startCmd, err = config.BuildAgentStartupCommandWithAgentOverride("boot", "", b.townRoot, "", initialPrompt, agentOverride)
if err != nil {
return fmt.Errorf("building startup command with agent override: %w", err)
}
} else {
startCmd = config.BuildAgentStartupCommand("boot", "", b.townRoot, "", "gt boot triage")
startCmd = config.BuildAgentStartupCommand("boot", "", b.townRoot, "", initialPrompt)
}
// Create session with command directly to avoid send-keys race condition.
// See: https://github.com/anthropics/gastown/issues/280
if err := b.tmux.NewSessionWithCommand(SessionName, b.bootDir, startCmd); err != nil {
if err := b.tmux.NewSessionWithCommand(session.BootSessionName(), b.bootDir, startCmd); err != nil {
return fmt.Errorf("creating boot session: %w", err)
}
@@ -196,7 +194,7 @@ func (b *Boot) spawnTmux(agentOverride string) error {
TownRoot: b.townRoot,
})
for k, v := range envVars {
_ = b.tmux.SetEnvironment(SessionName, k, v)
_ = b.tmux.SetEnvironment(session.BootSessionName(), k, v)
}
return nil