feat: Set BD_ACTOR env var when spawning agents (gt-rhfji)
When gt spawns agents (polecats, crew, patrol roles), it now sets the BD_ACTOR env var so that bd commands (like `bd hook`) know the agent identity without coupling to gt. Updated spawn points: - gt up (mayor, deacon, witness via ensureSession/ensureWitness) - gt deacon start - gt witness start - gt start refinery - gt mayor start - Daemon deacon restart - Daemon lifecycle restart - Handoff respawn - Refinery manager start BD_ACTOR uses slash format (e.g., gastown/witness, gastown/crew/max) while GT_ROLE may use dash format internally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,7 @@ func startDeaconSession(t *tmux.Tmux) error {
|
|||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
_ = t.SetEnvironment(DeaconSessionName, "GT_ROLE", "deacon")
|
_ = t.SetEnvironment(DeaconSessionName, "GT_ROLE", "deacon")
|
||||||
|
_ = t.SetEnvironment(DeaconSessionName, "BD_ACTOR", "deacon")
|
||||||
|
|
||||||
// Apply Deacon theme
|
// Apply Deacon theme
|
||||||
theme := tmux.DeaconTheme()
|
theme := tmux.DeaconTheme()
|
||||||
@@ -210,8 +211,8 @@ func startDeaconSession(t *tmux.Tmux) error {
|
|||||||
// Launch Claude directly (no shell respawn loop)
|
// Launch Claude directly (no shell respawn loop)
|
||||||
// Restarts are handled by daemon via ensureDeaconRunning on each heartbeat
|
// Restarts are handled by daemon via ensureDeaconRunning on each heartbeat
|
||||||
// The startup hook handles context loading automatically
|
// The startup hook handles context loading automatically
|
||||||
// Export GT_ROLE in the command since tmux SetEnvironment only affects new panes
|
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||||
if err := t.SendKeys(DeaconSessionName, "export GT_ROLE=deacon && claude --dangerously-skip-permissions"); err != nil {
|
if err := t.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon && claude --dangerously-skip-permissions"); err != nil {
|
||||||
return fmt.Errorf("sending command: %w", err)
|
return fmt.Errorf("sending command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,17 +268,17 @@ func buildRestartCommand(sessionName string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine GT_ROLE value for this session
|
// Determine GT_ROLE and BD_ACTOR values for this session
|
||||||
gtRole := sessionToGTRole(sessionName)
|
gtRole := sessionToGTRole(sessionName)
|
||||||
|
|
||||||
// For respawn-pane, we:
|
// For respawn-pane, we:
|
||||||
// 1. cd to the right directory (role's canonical home)
|
// 1. cd to the right directory (role's canonical home)
|
||||||
// 2. export GT_ROLE so role detection works correctly
|
// 2. export GT_ROLE and BD_ACTOR so role detection works correctly
|
||||||
// 3. run claude
|
// 3. run claude
|
||||||
// The SessionStart hook will run gt prime.
|
// The SessionStart hook will run gt prime.
|
||||||
// Use exec to ensure clean process replacement.
|
// Use exec to ensure clean process replacement.
|
||||||
if gtRole != "" {
|
if gtRole != "" {
|
||||||
return fmt.Sprintf("cd %s && export GT_ROLE=%s && exec claude --dangerously-skip-permissions", workDir, gtRole), nil
|
return fmt.Sprintf("cd %s && export GT_ROLE=%s BD_ACTOR=%s && exec claude --dangerously-skip-permissions", workDir, gtRole, gtRole), nil
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("cd %s && exec claude --dangerously-skip-permissions", workDir), nil
|
return fmt.Sprintf("cd %s && exec claude --dangerously-skip-permissions", workDir), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ func startMayorSession(t *tmux.Tmux) error {
|
|||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
_ = t.SetEnvironment(MayorSessionName, "GT_ROLE", "mayor")
|
_ = t.SetEnvironment(MayorSessionName, "GT_ROLE", "mayor")
|
||||||
|
_ = t.SetEnvironment(MayorSessionName, "BD_ACTOR", "mayor")
|
||||||
|
|
||||||
// Apply Mayor theme
|
// Apply Mayor theme
|
||||||
theme := tmux.MayorTheme()
|
theme := tmux.MayorTheme()
|
||||||
@@ -127,8 +128,8 @@ func startMayorSession(t *tmux.Tmux) error {
|
|||||||
|
|
||||||
// Launch Claude - the startup hook handles 'gt prime' automatically
|
// Launch Claude - the startup hook handles 'gt prime' automatically
|
||||||
// Use SendKeysDelayed to allow shell initialization after NewSession
|
// Use SendKeysDelayed to allow shell initialization after NewSession
|
||||||
// Export GT_ROLE in the command since tmux SetEnvironment only affects new panes
|
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||||
claudeCmd := `export GT_ROLE=mayor && claude --dangerously-skip-permissions`
|
claudeCmd := `export GT_ROLE=mayor BD_ACTOR=mayor && claude --dangerously-skip-permissions`
|
||||||
if err := t.SendKeysDelayed(MayorSessionName, claudeCmd, 200); err != nil {
|
if err := t.SendKeysDelayed(MayorSessionName, claudeCmd, 200); err != nil {
|
||||||
return fmt.Errorf("sending command: %w", err)
|
return fmt.Errorf("sending command: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,8 +271,10 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
|
bdActor := fmt.Sprintf("%s/refinery", rigName)
|
||||||
t.SetEnvironment(sessionName, "GT_ROLE", "refinery")
|
t.SetEnvironment(sessionName, "GT_ROLE", "refinery")
|
||||||
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||||
|
t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||||
|
|
||||||
// Set beads environment
|
// Set beads environment
|
||||||
beadsDir := filepath.Join(r.Path, "mayor", "rig", ".beads")
|
beadsDir := filepath.Join(r.Path, "mayor", "rig", ".beads")
|
||||||
@@ -285,8 +287,8 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) {
|
|||||||
_ = t.ConfigureGasTownSession(sessionName, theme, rigName, "refinery", "refinery")
|
_ = t.ConfigureGasTownSession(sessionName, theme, rigName, "refinery", "refinery")
|
||||||
|
|
||||||
// Launch Claude in a respawn loop
|
// Launch Claude in a respawn loop
|
||||||
// Export GT_ROLE in the command since tmux SetEnvironment only affects new panes
|
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||||
loopCmd := `export GT_ROLE=refinery && while true; do echo "🛢️ Starting Refinery for ` + rigName + `..."; claude --dangerously-skip-permissions; echo ""; echo "Refinery exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`
|
loopCmd := `export GT_ROLE=refinery BD_ACTOR=` + bdActor + ` && while true; do echo "🛢️ Starting Refinery for ` + rigName + `..."; claude --dangerously-skip-permissions; echo ""; echo "Refinery exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`
|
||||||
if err := t.SendKeysDelayed(sessionName, loopCmd, 200); err != nil {
|
if err := t.SendKeysDelayed(sessionName, loopCmd, 200); err != nil {
|
||||||
return false, fmt.Errorf("sending command: %w", err)
|
return false, fmt.Errorf("sending command: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
|
|||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", role)
|
_ = t.SetEnvironment(sessionName, "GT_ROLE", role)
|
||||||
|
_ = t.SetEnvironment(sessionName, "BD_ACTOR", role)
|
||||||
|
|
||||||
// Apply theme based on role
|
// Apply theme based on role
|
||||||
switch role {
|
switch role {
|
||||||
@@ -214,13 +215,13 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Launch Claude
|
// Launch Claude
|
||||||
// Export GT_ROLE in the command since tmux SetEnvironment only affects new panes
|
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||||
var claudeCmd string
|
var claudeCmd string
|
||||||
if role == "deacon" {
|
if role == "deacon" {
|
||||||
// Deacon uses respawn loop
|
// Deacon uses respawn loop
|
||||||
claudeCmd = `export GT_ROLE=deacon && while true; do echo "⛪ Starting Deacon session..."; claude --dangerously-skip-permissions; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`
|
claudeCmd = `export GT_ROLE=deacon BD_ACTOR=deacon && while true; do echo "⛪ Starting Deacon session..."; claude --dangerously-skip-permissions; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`
|
||||||
} else {
|
} else {
|
||||||
claudeCmd = fmt.Sprintf(`export GT_ROLE=%s && claude --dangerously-skip-permissions`, role)
|
claudeCmd = fmt.Sprintf(`export GT_ROLE=%s BD_ACTOR=%s && claude --dangerously-skip-permissions`, role, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
||||||
@@ -246,16 +247,18 @@ func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
|
bdActor := fmt.Sprintf("%s/witness", rigName)
|
||||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
_ = t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
||||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||||
|
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||||
|
|
||||||
// Apply theme (use rig-based theme)
|
// Apply theme (use rig-based theme)
|
||||||
theme := tmux.AssignTheme(rigName)
|
theme := tmux.AssignTheme(rigName)
|
||||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Witness", rigName)
|
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Witness", rigName)
|
||||||
|
|
||||||
// Launch Claude
|
// Launch Claude
|
||||||
// Export GT_ROLE in the command since tmux SetEnvironment only affects new panes
|
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||||
claudeCmd := `export GT_ROLE=witness && claude --dangerously-skip-permissions`
|
claudeCmd := fmt.Sprintf(`export GT_ROLE=witness BD_ACTOR=%s && claude --dangerously-skip-permissions`, bdActor)
|
||||||
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,8 +321,10 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
|
bdActor := fmt.Sprintf("%s/witness", rigName)
|
||||||
t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
||||||
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||||
|
t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||||
|
|
||||||
// Apply Gas Town theming
|
// Apply Gas Town theming
|
||||||
theme := tmux.AssignTheme(rigName)
|
theme := tmux.AssignTheme(rigName)
|
||||||
@@ -331,8 +333,8 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
|
|||||||
// Launch Claude directly (no shell respawn loop)
|
// Launch Claude directly (no shell respawn loop)
|
||||||
// Restarts are handled by daemon via LIFECYCLE mail or deacon health-scan
|
// Restarts are handled by daemon via LIFECYCLE mail or deacon health-scan
|
||||||
// NOTE: No gt prime injection needed - SessionStart hook handles it automatically
|
// NOTE: No gt prime injection needed - SessionStart hook handles it automatically
|
||||||
// Export GT_ROLE in the command since tmux SetEnvironment only affects new panes
|
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||||
if err := t.SendKeys(sessionName, "export GT_ROLE=witness && claude --dangerously-skip-permissions"); err != nil {
|
if err := t.SendKeys(sessionName, fmt.Sprintf("export GT_ROLE=witness BD_ACTOR=%s && claude --dangerously-skip-permissions", bdActor)); err != nil {
|
||||||
return false, fmt.Errorf("sending command: %w", err)
|
return false, fmt.Errorf("sending command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ func (d *Daemon) ensureDeaconRunning() {
|
|||||||
|
|
||||||
// Claude has exited (shell is showing) - restart it
|
// Claude has exited (shell is showing) - restart it
|
||||||
d.logger.Printf("Deacon session exists but Claude exited (cmd=%s), restarting...", cmd)
|
d.logger.Printf("Deacon session exists but Claude exited (cmd=%s), restarting...", cmd)
|
||||||
if err := d.tmux.SendKeys(DeaconSessionName, "export GT_ROLE=deacon && claude --dangerously-skip-permissions"); err != nil {
|
if err := d.tmux.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon && claude --dangerously-skip-permissions"); err != nil {
|
||||||
d.logger.Printf("Error restarting Claude in Deacon session: %v", err)
|
d.logger.Printf("Error restarting Claude in Deacon session: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -215,11 +215,12 @@ func (d *Daemon) ensureDeaconRunning() {
|
|||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
_ = d.tmux.SetEnvironment(DeaconSessionName, "GT_ROLE", "deacon")
|
_ = d.tmux.SetEnvironment(DeaconSessionName, "GT_ROLE", "deacon")
|
||||||
|
_ = d.tmux.SetEnvironment(DeaconSessionName, "BD_ACTOR", "deacon")
|
||||||
|
|
||||||
// Launch Claude directly (no shell respawn loop)
|
// Launch Claude directly (no shell respawn loop)
|
||||||
// The daemon will detect if Claude exits and restart it on next heartbeat
|
// The daemon will detect if Claude exits and restart it on next heartbeat
|
||||||
// Export GT_ROLE so Claude inherits it (tmux SetEnvironment doesn't export to processes)
|
// Export GT_ROLE and BD_ACTOR so Claude inherits them (tmux SetEnvironment doesn't export to processes)
|
||||||
if err := d.tmux.SendKeys(DeaconSessionName, "export GT_ROLE=deacon && claude --dangerously-skip-permissions"); err != nil {
|
if err := d.tmux.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon && claude --dangerously-skip-permissions"); err != nil {
|
||||||
d.logger.Printf("Error launching Claude in Deacon session: %v", err)
|
d.logger.Printf("Error launching Claude in Deacon session: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ func (d *Daemon) restartSession(sessionName, identity string) error {
|
|||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "GT_ROLE", identity)
|
_ = d.tmux.SetEnvironment(sessionName, "GT_ROLE", identity)
|
||||||
|
// BD_ACTOR uses slashes instead of dashes for path-like identity
|
||||||
|
bdActor := identityToBDActor(identity)
|
||||||
|
_ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||||
|
|
||||||
// Apply theme
|
// Apply theme
|
||||||
if identity == "mayor" {
|
if identity == "mayor" {
|
||||||
@@ -427,3 +430,36 @@ func (d *Daemon) identityToStateFile(identity string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// identityToBDActor converts a daemon identity (with dashes) to BD_ACTOR format (with slashes).
|
||||||
|
// Examples:
|
||||||
|
// - "mayor" → "mayor"
|
||||||
|
// - "gastown-witness" → "gastown/witness"
|
||||||
|
// - "gastown-refinery" → "gastown/refinery"
|
||||||
|
// - "gastown-crew-max" → "gastown/crew/max"
|
||||||
|
func identityToBDActor(identity string) string {
|
||||||
|
switch identity {
|
||||||
|
case "mayor", "deacon":
|
||||||
|
return identity
|
||||||
|
default:
|
||||||
|
// Pattern: <rig>-witness → <rig>/witness
|
||||||
|
if strings.HasSuffix(identity, "-witness") {
|
||||||
|
rigName := strings.TrimSuffix(identity, "-witness")
|
||||||
|
return rigName + "/witness"
|
||||||
|
}
|
||||||
|
// Pattern: <rig>-refinery → <rig>/refinery
|
||||||
|
if strings.HasSuffix(identity, "-refinery") {
|
||||||
|
rigName := strings.TrimSuffix(identity, "-refinery")
|
||||||
|
return rigName + "/refinery"
|
||||||
|
}
|
||||||
|
// Pattern: <rig>-crew-<name> → <rig>/crew/<name>
|
||||||
|
if strings.Contains(identity, "-crew-") {
|
||||||
|
parts := strings.SplitN(identity, "-crew-", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return parts[0] + "/crew/" + parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unknown format - return as-is
|
||||||
|
return identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,9 +200,11 @@ func (m *Manager) Start(foreground bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set environment variables
|
// Set environment variables
|
||||||
|
bdActor := fmt.Sprintf("%s/refinery", m.rig.Name)
|
||||||
_ = t.SetEnvironment(sessionID, "GT_RIG", m.rig.Name)
|
_ = t.SetEnvironment(sessionID, "GT_RIG", m.rig.Name)
|
||||||
_ = t.SetEnvironment(sessionID, "GT_REFINERY", "1")
|
_ = t.SetEnvironment(sessionID, "GT_REFINERY", "1")
|
||||||
_ = t.SetEnvironment(sessionID, "GT_ROLE", "refinery")
|
_ = t.SetEnvironment(sessionID, "GT_ROLE", "refinery")
|
||||||
|
_ = t.SetEnvironment(sessionID, "BD_ACTOR", bdActor)
|
||||||
|
|
||||||
// Set beads environment - refinery uses rig-level beads
|
// Set beads environment - refinery uses rig-level beads
|
||||||
beadsDir := filepath.Join(m.rig.Path, "mayor", "rig", ".beads")
|
beadsDir := filepath.Join(m.rig.Path, "mayor", "rig", ".beads")
|
||||||
|
|||||||
Reference in New Issue
Block a user