Set GIT_AUTHOR_NAME per agent session (gt-6r18e.1)

Export GIT_AUTHOR_NAME alongside BD_ACTOR in all agent session startup
locations. This enables git log --author queries for agent work while
keeping GIT_AUTHOR_EMAIL as the workspace owner.

Files updated:
- internal/session/manager.go (polecat sessions)
- internal/daemon/daemon.go (deacon, witness, polecat via daemon)
- internal/daemon/lifecycle.go (polecat lifecycle)
- internal/cmd/*.go (crew, mayor, deacon, witness, refinery, up, handoff)
- internal/session/manager_test.go (updated test expectations)
- docs/federation.md (marked feature as implemented)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 16:09:00 -08:00
parent 78db2c768f
commit 3099d99424
13 changed files with 26 additions and 24 deletions

View File

@@ -155,7 +155,7 @@ bd audit --actor=gastown/crew/joe
## Implementation Status ## Implementation Status
- [ ] Agent identity in git commits - [x] Agent identity in git commits
- [ ] BD_ACTOR default in beads create - [ ] BD_ACTOR default in beads create
- [ ] Workspace metadata file - [ ] Workspace metadata file
- [ ] Cross-workspace URI scheme - [ ] Cross-workspace URI scheme

View File

@@ -151,7 +151,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
// Pass "gt prime" as initial prompt so Claude loads context immediately // Pass "gt prime" as initial prompt so Claude loads context immediately
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name)
claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor) claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor, bdActor)
if err := t.RespawnPane(paneID, claudeCmd); err != nil { if err := t.RespawnPane(paneID, claudeCmd); err != nil {
return fmt.Errorf("starting claude: %w", err) return fmt.Errorf("starting claude: %w", err)
} }
@@ -176,7 +176,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
// Pass "gt prime" as initial prompt so Claude loads context immediately // Pass "gt prime" as initial prompt so Claude loads context immediately
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name)
claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor) claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor, bdActor)
if err := t.RespawnPane(paneID, claudeCmd); err != nil { if err := t.RespawnPane(paneID, claudeCmd); err != nil {
return fmt.Errorf("restarting claude: %w", err) return fmt.Errorf("restarting claude: %w", err)
} }

View File

@@ -316,7 +316,7 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
// Start claude with skip permissions (crew workers are trusted) // Start claude with skip permissions (crew workers are trusted)
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name)
claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions", r.Name, name, bdActor) claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", r.Name, name, bdActor, bdActor)
if err := t.SendKeys(sessionID, claudeCmd); err != nil { if err := t.SendKeys(sessionID, claudeCmd); err != nil {
fmt.Printf("Error starting claude for %s: %v\n", arg, err) fmt.Printf("Error starting claude for %s: %v\n", arg, err)
lastErr = err lastErr = err
@@ -484,7 +484,7 @@ func restartCrewSession(rigName, crewName, clonePath string) error {
// Start claude with skip permissions // Start claude with skip permissions
bdActor := fmt.Sprintf("%s/crew/%s", rigName, crewName) bdActor := fmt.Sprintf("%s/crew/%s", rigName, crewName)
claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions", rigName, crewName, bdActor) claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", rigName, crewName, bdActor, bdActor)
if err := t.SendKeys(sessionID, claudeCmd); err != nil { if err := t.SendKeys(sessionID, claudeCmd); err != nil {
return fmt.Errorf("starting claude: %w", err) return fmt.Errorf("starting claude: %w", err)
} }

View File

@@ -193,7 +193,7 @@ func startDeaconSession(t *tmux.Tmux) error {
// 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 and BD_ACTOR 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 BD_ACTOR=deacon && claude --dangerously-skip-permissions"); err != nil { if err := t.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && claude --dangerously-skip-permissions"); err != nil {
return fmt.Errorf("sending command: %w", err) return fmt.Errorf("sending command: %w", err)
} }

View File

@@ -326,7 +326,7 @@ func buildRestartCommand(sessionName string) (string, error) {
// 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 BD_ACTOR=%s && exec claude --dangerously-skip-permissions", workDir, gtRole, gtRole), nil return fmt.Sprintf("cd %s && export GT_ROLE=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && exec claude --dangerously-skip-permissions", workDir, gtRole, 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
} }

View File

@@ -130,7 +130,7 @@ 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 and BD_ACTOR 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 BD_ACTOR=mayor && claude --dangerously-skip-permissions` claudeCmd := `export GT_ROLE=mayor BD_ACTOR=mayor GIT_AUTHOR_NAME=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)
} }

View File

@@ -340,7 +340,7 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) {
// Launch Claude in a respawn loop // Launch Claude in a respawn loop
// Export GT_ROLE and BD_ACTOR 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 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` loopCmd := `export GT_ROLE=refinery BD_ACTOR=` + bdActor + ` GIT_AUTHOR_NAME=` + 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)
} }

View File

@@ -262,9 +262,9 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
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 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` claudeCmd = `export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=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 BD_ACTOR=%s && claude --dangerously-skip-permissions`, role, role) claudeCmd = fmt.Sprintf(`export GT_ROLE=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions`, role, role, role)
} }
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil { if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
@@ -301,7 +301,7 @@ func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error {
// Launch Claude // Launch Claude
// Export GT_ROLE and BD_ACTOR 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 := fmt.Sprintf(`export GT_ROLE=witness BD_ACTOR=%s && claude --dangerously-skip-permissions`, bdActor) claudeCmd := fmt.Sprintf(`export GT_ROLE=witness BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions`, bdActor, bdActor)
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil { if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
return err return err
} }
@@ -512,7 +512,7 @@ func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName st
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Crew", crewName) _ = t.ConfigureGasTownSession(sessionName, theme, "", "Crew", crewName)
// Launch Claude // Launch Claude
claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions`, rigName, crewName, bdActor) claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions`, rigName, crewName, bdActor, bdActor)
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil { if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
return err return err
} }
@@ -600,7 +600,7 @@ func ensurePolecatSession(t *tmux.Tmux, sessionName, polecatPath, rigName, polec
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Polecat", polecatName) _ = t.ConfigureGasTownSession(sessionName, theme, "", "Polecat", polecatName)
// Launch Claude // Launch Claude
claudeCmd := fmt.Sprintf(`export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s && claude --dangerously-skip-permissions`, rigName, polecatName, bdActor) claudeCmd := fmt.Sprintf(`export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions`, rigName, polecatName, bdActor, bdActor)
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil { if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
return err return err
} }

View File

@@ -329,7 +329,7 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
// 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 and BD_ACTOR 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, fmt.Sprintf("export GT_ROLE=witness BD_ACTOR=%s && claude --dangerously-skip-permissions", bdActor)); err != nil { if err := t.SendKeys(sessionName, fmt.Sprintf("export GT_ROLE=witness BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", bdActor, bdActor)); err != nil {
return false, fmt.Errorf("sending command: %w", err) return false, fmt.Errorf("sending command: %w", err)
} }

View File

@@ -264,7 +264,7 @@ func (d *Daemon) ensureDeaconRunning() {
// 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 and BD_ACTOR so Claude inherits them (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 BD_ACTOR=deacon && claude --dangerously-skip-permissions"); err != nil { if err := d.tmux.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=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
} }
@@ -312,7 +312,8 @@ func (d *Daemon) ensureWitnessRunning(rigName string) {
_ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", rigName+"-witness") _ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", rigName+"-witness")
// Launch Claude // Launch Claude
envExport := fmt.Sprintf("export GT_ROLE=witness GT_RIG=%s BD_ACTOR=%s-witness && claude --dangerously-skip-permissions", rigName, rigName) bdActor := fmt.Sprintf("%s/witness", rigName)
envExport := fmt.Sprintf("export GT_ROLE=witness GT_RIG=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", rigName, bdActor, bdActor)
if err := d.tmux.SendKeys(sessionName, envExport); err != nil { if err := d.tmux.SendKeys(sessionName, envExport); err != nil {
d.logger.Printf("Error launching Claude in witness session for %s: %v", rigName, err) d.logger.Printf("Error launching Claude in witness session for %s: %v", rigName, err)
return return
@@ -610,8 +611,8 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string)
_ = d.tmux.SetPaneDiedHook(sessionName, agentID) _ = d.tmux.SetPaneDiedHook(sessionName, agentID)
// Launch Claude with environment exported inline // Launch Claude with environment exported inline
startCmd := fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s && claude --dangerously-skip-permissions", startCmd := fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions",
rigName, polecatName, bdActor) rigName, polecatName, bdActor, bdActor)
if err := d.tmux.SendKeys(sessionName, startCmd); err != nil { if err := d.tmux.SendKeys(sessionName, startCmd); err != nil {
return fmt.Errorf("sending startup command: %w", err) return fmt.Errorf("sending startup command: %w", err)
} }

View File

@@ -428,8 +428,8 @@ func (d *Daemon) getStartCommand(config *beads.RoleConfig, parsed *ParsedIdentit
// Polecats need environment variables set in the command // Polecats need environment variables set in the command
if parsed.RoleType == "polecat" { if parsed.RoleType == "polecat" {
bdActor := fmt.Sprintf("%s/polecats/%s", parsed.RigName, parsed.AgentName) bdActor := fmt.Sprintf("%s/polecats/%s", parsed.RigName, parsed.AgentName)
return fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s && %s", return fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && %s",
parsed.RigName, parsed.AgentName, bdActor, defaultCmd) parsed.RigName, parsed.AgentName, bdActor, bdActor, defaultCmd)
} }
return defaultCmd return defaultCmd

View File

@@ -178,8 +178,8 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
// Polecats run with full permissions - Gas Town is for grownups // Polecats run with full permissions - Gas Town is for grownups
// Export env vars inline so Claude's role detection works // Export env vars inline so Claude's role detection works
bdActor := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat) bdActor := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat)
command = fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s && claude --dangerously-skip-permissions", command = fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions",
m.rig.Name, polecat, bdActor) m.rig.Name, polecat, bdActor, bdActor)
} }
if err := m.tmux.SendKeys(sessionID, command); err != nil { if err := m.tmux.SendKeys(sessionID, command); err != nil {
return fmt.Errorf("sending command: %w", err) return fmt.Errorf("sending command: %w", err)

View File

@@ -163,7 +163,7 @@ func TestPolecatCommandFormat(t *testing.T) {
expectedBdActor := "gastown/polecats/Toast" expectedBdActor := "gastown/polecats/Toast"
// Build the expected command format (mirrors Start() logic) // Build the expected command format (mirrors Start() logic)
expectedPrefix := "export GT_ROLE=polecat GT_RIG=" + rigName + " GT_POLECAT=" + polecatName + " BD_ACTOR=" + expectedBdActor expectedPrefix := "export GT_ROLE=polecat GT_RIG=" + rigName + " GT_POLECAT=" + polecatName + " BD_ACTOR=" + expectedBdActor + " GIT_AUTHOR_NAME=" + expectedBdActor
expectedSuffix := "&& claude --dangerously-skip-permissions" expectedSuffix := "&& claude --dangerously-skip-permissions"
// The command must contain all required env exports // The command must contain all required env exports
@@ -173,6 +173,7 @@ func TestPolecatCommandFormat(t *testing.T) {
"GT_RIG=" + rigName, "GT_RIG=" + rigName,
"GT_POLECAT=" + polecatName, "GT_POLECAT=" + polecatName,
"BD_ACTOR=" + expectedBdActor, "BD_ACTOR=" + expectedBdActor,
"GIT_AUTHOR_NAME=" + expectedBdActor,
"claude --dangerously-skip-permissions", "claude --dangerously-skip-permissions",
} }