feat(witness): add --env flag for environment variable overrides

Extends the --agent flag with a more general --env flag that allows
setting arbitrary environment variables when starting a witness.

Precedence (highest to lowest):
1. CLI --env overrides
2. Role bead env_vars
3. config.AgentEnv() defaults

Examples:
  gt witness start greenplace --env ANTHROPIC_MODEL=claude-3-haiku
  gt witness restart greenplace --env DEBUG=1 --env VERBOSE=true

Co-authored-by: joshuavial <git@codewithjv.com>
This commit is contained in:
gastown/crew/gus
2026-01-09 22:00:43 -08:00
committed by Steve Yegge
parent f9473c7b9e
commit 86751e1ea5
6 changed files with 36 additions and 24 deletions

View File

@@ -759,7 +759,7 @@ func runRigBoot(cmd *cobra.Command, args []string) error {
} else { } else {
fmt.Printf(" Starting witness...\n") fmt.Printf(" Starting witness...\n")
witMgr := witness.NewManager(r) witMgr := witness.NewManager(r)
if err := witMgr.Start(false, ""); err != nil { if err := witMgr.Start(false, "", nil); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
skipped = append(skipped, "witness (already running)") skipped = append(skipped, "witness (already running)")
} else { } else {
@@ -839,7 +839,7 @@ func runRigStart(cmd *cobra.Command, args []string) error {
} else { } else {
fmt.Printf(" Starting witness...\n") fmt.Printf(" Starting witness...\n")
witMgr := witness.NewManager(r) witMgr := witness.NewManager(r)
if err := witMgr.Start(false, ""); err != nil { if err := witMgr.Start(false, "", nil); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
skipped = append(skipped, "witness") skipped = append(skipped, "witness")
} else { } else {
@@ -1418,7 +1418,7 @@ func runRigRestart(cmd *cobra.Command, args []string) error {
skipped = append(skipped, "witness") skipped = append(skipped, "witness")
} else { } else {
fmt.Printf(" Starting witness...\n") fmt.Printf(" Starting witness...\n")
if err := witMgr.Start(false, ""); err != nil { if err := witMgr.Start(false, "", nil); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
skipped = append(skipped, "witness") skipped = append(skipped, "witness")
} else { } else {

View File

@@ -235,7 +235,7 @@ func startRigAgents(t *tmux.Tmux, townRoot string) {
fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name) fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name)
} else { } else {
witMgr := witness.NewManager(r) witMgr := witness.NewManager(r)
if err := witMgr.Start(false, ""); err != nil { if err := witMgr.Start(false, "", nil); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name) fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name)
} else { } else {

View File

@@ -118,7 +118,7 @@ func runUp(cmd *cobra.Command, args []string) error {
} }
mgr := witness.NewManager(r) mgr := witness.NewManager(r)
if err := mgr.Start(false, ""); err != nil { if err := mgr.Start(false, "", nil); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName()) printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
} else { } else {

View File

@@ -18,6 +18,7 @@ var (
witnessForeground bool witnessForeground bool
witnessStatusJSON bool witnessStatusJSON bool
witnessAgentOverride string witnessAgentOverride string
witnessEnvOverrides []string
) )
var witnessCmd = &cobra.Command{ var witnessCmd = &cobra.Command{
@@ -43,6 +44,7 @@ states and takes action to keep work flowing.
Examples: Examples:
gt witness start greenplace gt witness start greenplace
gt witness start greenplace --agent codex gt witness start greenplace --agent codex
gt witness start greenplace --env ANTHROPIC_MODEL=claude-3-haiku
gt witness start greenplace --foreground`, gt witness start greenplace --foreground`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runWitnessStart, RunE: runWitnessStart,
@@ -96,7 +98,8 @@ Stops the current session (if running) and starts a fresh one.
Examples: Examples:
gt witness restart greenplace gt witness restart greenplace
gt witness restart greenplace --agent codex`, gt witness restart greenplace --agent codex
gt witness restart greenplace --env ANTHROPIC_MODEL=claude-3-haiku`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runWitnessRestart, RunE: runWitnessRestart,
} }
@@ -105,12 +108,14 @@ func init() {
// Start flags // Start flags
witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false, "Run in foreground (default: background)") witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false, "Run in foreground (default: background)")
witnessStartCmd.Flags().StringVar(&witnessAgentOverride, "agent", "", "Agent alias to run the Witness with (overrides town default)") witnessStartCmd.Flags().StringVar(&witnessAgentOverride, "agent", "", "Agent alias to run the Witness with (overrides town default)")
witnessStartCmd.Flags().StringArrayVar(&witnessEnvOverrides, "env", nil, "Environment variable override (KEY=VALUE, can be repeated)")
// Status flags // Status flags
witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON") witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON")
// Restart flags // Restart flags
witnessRestartCmd.Flags().StringVar(&witnessAgentOverride, "agent", "", "Agent alias to run the Witness with (overrides town default)") witnessRestartCmd.Flags().StringVar(&witnessAgentOverride, "agent", "", "Agent alias to run the Witness with (overrides town default)")
witnessRestartCmd.Flags().StringArrayVar(&witnessEnvOverrides, "env", nil, "Environment variable override (KEY=VALUE, can be repeated)")
// Add subcommands // Add subcommands
witnessCmd.AddCommand(witnessStartCmd) witnessCmd.AddCommand(witnessStartCmd)
@@ -143,7 +148,7 @@ func runWitnessStart(cmd *cobra.Command, args []string) error {
fmt.Printf("Starting witness for %s...\n", rigName) fmt.Printf("Starting witness for %s...\n", rigName)
if err := mgr.Start(witnessForeground, witnessAgentOverride); err != nil { if err := mgr.Start(witnessForeground, witnessAgentOverride, witnessEnvOverrides); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠")) fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠"))
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect")) fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
@@ -296,7 +301,7 @@ func runWitnessAttach(cmd *cobra.Command, args []string) error {
sessionName := witnessSessionName(rigName) sessionName := witnessSessionName(rigName)
// Ensure session exists (creates if needed) // Ensure session exists (creates if needed)
if err := mgr.Start(false, ""); err != nil && err != witness.ErrAlreadyRunning { if err := mgr.Start(false, "", nil); err != nil && err != witness.ErrAlreadyRunning {
return err return err
} else if err == nil { } else if err == nil {
fmt.Printf("Started witness session for %s\n", rigName) fmt.Printf("Started witness session for %s\n", rigName)
@@ -329,7 +334,7 @@ func runWitnessRestart(cmd *cobra.Command, args []string) error {
_ = mgr.Stop() _ = mgr.Stop()
// Start fresh // Start fresh
if err := mgr.Start(false, witnessAgentOverride); err != nil { if err := mgr.Start(false, witnessAgentOverride, witnessEnvOverrides); err != nil {
return fmt.Errorf("starting witness: %w", err) return fmt.Errorf("starting witness: %w", err)
} }

View File

@@ -404,7 +404,7 @@ func (d *Daemon) ensureWitnessRunning(rigName string) {
} }
mgr := witness.NewManager(r) mgr := witness.NewManager(r)
if err := mgr.Start(false, ""); err != nil { if err := mgr.Start(false, "", nil); err != nil {
if err == witness.ErrAlreadyRunning { if err == witness.ErrAlreadyRunning {
// Already running - nothing to do // Already running - nothing to do
return return

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/steveyegge/gastown/internal/agent" "github.com/steveyegge/gastown/internal/agent"
@@ -101,7 +102,8 @@ func (m *Manager) witnessDir() string {
// If foreground is true, only updates state (no tmux session - deprecated). // If foreground is true, only updates state (no tmux session - deprecated).
// Otherwise, spawns a Claude agent in a tmux session. // Otherwise, spawns a Claude agent in a tmux session.
// agentOverride optionally specifies a different agent alias to use. // agentOverride optionally specifies a different agent alias to use.
func (m *Manager) Start(foreground bool, agentOverride string) error { // envOverrides are KEY=VALUE pairs that override all other env var sources.
func (m *Manager) Start(foreground bool, agentOverride string, envOverrides []string) error {
w, err := m.loadState() w, err := m.loadState()
if err != nil { if err != nil {
return err return err
@@ -159,19 +161,6 @@ func (m *Manager) Start(foreground bool, agentOverride string) error {
return fmt.Errorf("creating tmux session: %w", err) return fmt.Errorf("creating tmux session: %w", err)
} }
// Set environment variables (non-fatal: session works without these)
// Use centralized AgentEnv for consistency across all role startup paths
townRoot := filepath.Dir(m.rig.Path)
envVars := config.AgentEnv(config.AgentEnvConfig{
Role: "witness",
Rig: m.rig.Name,
TownRoot: townRoot,
BeadsDir: beads.ResolveBeadsDir(m.rig.Path),
})
for k, v := range envVars {
_ = t.SetEnvironment(sessionID, k, v)
}
// Apply Gas Town theming (non-fatal: theming failure doesn't affect operation) // Apply Gas Town theming (non-fatal: theming failure doesn't affect operation)
theme := tmux.AssignTheme(m.rig.Name) theme := tmux.AssignTheme(m.rig.Name)
_ = t.ConfigureGasTownSession(sessionID, theme, m.rig.Name, "witness", "witness") _ = t.ConfigureGasTownSession(sessionID, theme, m.rig.Name, "witness", "witness")
@@ -183,10 +172,28 @@ func (m *Manager) Start(foreground bool, agentOverride string) error {
} }
townRoot := m.townRoot() townRoot := m.townRoot()
// Set environment variables (non-fatal: session works without these)
// Use centralized AgentEnv for consistency across all role startup paths
envVars := config.AgentEnv(config.AgentEnvConfig{
Role: "witness",
Rig: m.rig.Name,
TownRoot: townRoot,
BeadsDir: beads.ResolveBeadsDir(m.rig.Path),
})
for k, v := range envVars {
_ = t.SetEnvironment(sessionID, k, v)
}
// Apply role config env vars if present (non-fatal). // Apply role config env vars if present (non-fatal).
for key, value := range roleConfigEnvVars(roleConfig, townRoot, m.rig.Name) { for key, value := range roleConfigEnvVars(roleConfig, townRoot, m.rig.Name) {
_ = t.SetEnvironment(sessionID, key, value) _ = t.SetEnvironment(sessionID, key, value)
} }
// Apply CLI env overrides (highest priority, non-fatal).
for _, override := range envOverrides {
if key, value, ok := strings.Cut(override, "="); ok {
_ = t.SetEnvironment(sessionID, key, value)
}
}
// Update state to running // Update state to running
now := time.Now() now := time.Now()