diff --git a/docs/federation.md b/docs/federation.md index 5d77a8ce..9ccbb662 100644 --- a/docs/federation.md +++ b/docs/federation.md @@ -155,7 +155,7 @@ bd audit --actor=gastown/crew/joe ## Implementation Status -- [ ] Agent identity in git commits +- [x] Agent identity in git commits - [ ] BD_ACTOR default in beads create - [ ] Workspace metadata file - [ ] Cross-workspace URI scheme diff --git a/internal/cmd/crew_at.go b/internal/cmd/crew_at.go index c1494db9..8310e822 100644 --- a/internal/cmd/crew_at.go +++ b/internal/cmd/crew_at.go @@ -151,7 +151,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error { // Pass "gt prime" as initial prompt so Claude loads context immediately // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes 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 { 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 // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes 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 { return fmt.Errorf("restarting claude: %w", err) } diff --git a/internal/cmd/crew_lifecycle.go b/internal/cmd/crew_lifecycle.go index a5dfe262..82841131 100644 --- a/internal/cmd/crew_lifecycle.go +++ b/internal/cmd/crew_lifecycle.go @@ -316,7 +316,7 @@ func runCrewRestart(cmd *cobra.Command, args []string) error { // Start claude with skip permissions (crew workers are trusted) // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes 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 { fmt.Printf("Error starting claude for %s: %v\n", arg, err) lastErr = err @@ -484,7 +484,7 @@ func restartCrewSession(rigName, crewName, clonePath string) error { // Start claude with skip permissions 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 { return fmt.Errorf("starting claude: %w", err) } diff --git a/internal/cmd/deacon.go b/internal/cmd/deacon.go index 26a2ed97..9edc78df 100644 --- a/internal/cmd/deacon.go +++ b/internal/cmd/deacon.go @@ -193,7 +193,7 @@ func startDeaconSession(t *tmux.Tmux) error { // Restarts are handled by daemon via ensureDeaconRunning on each heartbeat // The startup hook handles context loading automatically // 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) } diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index e3bcff4c..d760aa63 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -326,7 +326,7 @@ func buildRestartCommand(sessionName string) (string, error) { // The SessionStart hook will run gt prime. // Use exec to ensure clean process replacement. 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 } diff --git a/internal/cmd/mayor.go b/internal/cmd/mayor.go index 2ce3196b..dd2b6942 100644 --- a/internal/cmd/mayor.go +++ b/internal/cmd/mayor.go @@ -130,7 +130,7 @@ func startMayorSession(t *tmux.Tmux) error { // Launch Claude - the startup hook handles 'gt prime' automatically // Use SendKeysDelayed to allow shell initialization after NewSession // 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 { return fmt.Errorf("sending command: %w", err) } diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 2c312971..20dd38e7 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -340,7 +340,7 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) { // Launch Claude in a respawn loop // 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 { return false, fmt.Errorf("sending command: %w", err) } diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 4553f7ef..162b2cad 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -262,9 +262,9 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error { var claudeCmd string if role == "deacon" { // 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 { - 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 { @@ -301,7 +301,7 @@ func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error { // Launch Claude // 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 { return err } @@ -512,7 +512,7 @@ func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName st _ = t.ConfigureGasTownSession(sessionName, theme, "", "Crew", crewName) // 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 { return err } @@ -600,7 +600,7 @@ func ensurePolecatSession(t *tmux.Tmux, sessionName, polecatPath, rigName, polec _ = t.ConfigureGasTownSession(sessionName, theme, "", "Polecat", polecatName) // 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 { return err } diff --git a/internal/cmd/witness.go b/internal/cmd/witness.go index 1ea7c23c..0fd8083c 100644 --- a/internal/cmd/witness.go +++ b/internal/cmd/witness.go @@ -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 // 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 - 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) } diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 72ee3884..f321dbb2 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -264,7 +264,7 @@ func (d *Daemon) ensureDeaconRunning() { // Launch Claude directly (no shell respawn loop) // 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) - 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) return } @@ -312,7 +312,8 @@ func (d *Daemon) ensureWitnessRunning(rigName string) { _ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", rigName+"-witness") // 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 { d.logger.Printf("Error launching Claude in witness session for %s: %v", rigName, err) return @@ -610,8 +611,8 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string) _ = d.tmux.SetPaneDiedHook(sessionName, agentID) // 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", - rigName, polecatName, bdActor) + 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, bdActor) if err := d.tmux.SendKeys(sessionName, startCmd); err != nil { return fmt.Errorf("sending startup command: %w", err) } diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index a799f6a5..5a93a282 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -428,8 +428,8 @@ func (d *Daemon) getStartCommand(config *beads.RoleConfig, parsed *ParsedIdentit // Polecats need environment variables set in the command if parsed.RoleType == "polecat" { 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", - parsed.RigName, parsed.AgentName, bdActor, defaultCmd) + 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, bdActor, defaultCmd) } return defaultCmd diff --git a/internal/session/manager.go b/internal/session/manager.go index 39f02307..6ac11cf1 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -178,8 +178,8 @@ func (m *Manager) Start(polecat string, opts StartOptions) error { // Polecats run with full permissions - Gas Town is for grownups // Export env vars inline so Claude's role detection works 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", - m.rig.Name, polecat, bdActor) + 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, bdActor) } if err := m.tmux.SendKeys(sessionID, command); err != nil { return fmt.Errorf("sending command: %w", err) diff --git a/internal/session/manager_test.go b/internal/session/manager_test.go index af28b9ed..c0d4a7e5 100644 --- a/internal/session/manager_test.go +++ b/internal/session/manager_test.go @@ -163,7 +163,7 @@ func TestPolecatCommandFormat(t *testing.T) { expectedBdActor := "gastown/polecats/Toast" // 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" // The command must contain all required env exports @@ -173,6 +173,7 @@ func TestPolecatCommandFormat(t *testing.T) { "GT_RIG=" + rigName, "GT_POLECAT=" + polecatName, "BD_ACTOR=" + expectedBdActor, + "GIT_AUTHOR_NAME=" + expectedBdActor, "claude --dangerously-skip-permissions", }