feat(refinery,boot): add --agent flag for model selection (#469)
* feat(refinery,boot): add --agent flag for model selection (hq-7d5m) Add --agent flag to gt refinery start/attach/restart and gt boot spawn commands for consistent model selection across all agent launch points. Implementation follows the existing pattern from gt deacon start: - Add StringVar flag for agent alias - Pass override to Manager/Boot via SetAgentOverride() - Use BuildAgentStartupCommandWithAgentOverride when override is set Files affected: - cmd/gt/refinery.go: add flags to start/attach/restart commands - internal/refinery/manager.go: add SetAgentOverride and use in Start() - cmd/gt/boot.go: add flag to spawn command - internal/boot/boot.go: add SetAgentOverride and use in spawnTmux() Closes #438 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(refinery,boot): use parameter-passing pattern for --agent flag Address PR review feedback: 1. ADD TESTS: Add tests for --agent flag existence following witness_test.go pattern - internal/cmd/refinery_test.go: tests for start/attach/restart - internal/cmd/boot_test.go: test for spawn 2. ALIGN PATTERN: Change from setter pattern to parameter-passing pattern - Manager.Start(foreground, agentOverride) instead of SetAgentOverride + Start - Boot.Spawn(agentOverride) instead of SetAgentOverride + Spawn - Matches witness.go style: Start(foreground bool, agentOverride string, ...) Updated all callers to pass empty string for default agent: - internal/daemon/daemon.go - internal/cmd/rig.go - internal/cmd/start.go - internal/cmd/up.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: furiosa <will@saults.io> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+20
-10
@@ -41,11 +41,11 @@ type Status struct {
|
|||||||
|
|
||||||
// Boot manages the Boot watchdog lifecycle.
|
// Boot manages the Boot watchdog lifecycle.
|
||||||
type Boot struct {
|
type Boot struct {
|
||||||
townRoot string
|
townRoot string
|
||||||
bootDir string // ~/gt/deacon/dogs/boot/
|
bootDir string // ~/gt/deacon/dogs/boot/
|
||||||
deaconDir string // ~/gt/deacon/
|
deaconDir string // ~/gt/deacon/
|
||||||
tmux *tmux.Tmux
|
tmux *tmux.Tmux
|
||||||
degraded bool
|
degraded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Boot manager.
|
// New creates a new Boot manager.
|
||||||
@@ -145,7 +145,8 @@ func (b *Boot) LoadStatus() (*Status, error) {
|
|||||||
// Spawn starts Boot in a fresh tmux session.
|
// Spawn starts Boot in a fresh tmux session.
|
||||||
// Boot runs the mol-boot-triage molecule and exits when done.
|
// Boot runs the mol-boot-triage molecule and exits when done.
|
||||||
// In degraded mode (no tmux), it runs in a subprocess.
|
// In degraded mode (no tmux), it runs in a subprocess.
|
||||||
func (b *Boot) Spawn() error {
|
// The agentOverride parameter allows specifying an agent alias to use instead of the town default.
|
||||||
|
func (b *Boot) Spawn(agentOverride string) error {
|
||||||
if b.IsRunning() {
|
if b.IsRunning() {
|
||||||
return fmt.Errorf("boot is already running")
|
return fmt.Errorf("boot is already running")
|
||||||
}
|
}
|
||||||
@@ -155,11 +156,11 @@ func (b *Boot) Spawn() error {
|
|||||||
return b.spawnDegraded()
|
return b.spawnDegraded()
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.spawnTmux()
|
return b.spawnTmux(agentOverride)
|
||||||
}
|
}
|
||||||
|
|
||||||
// spawnTmux spawns Boot in a tmux session.
|
// spawnTmux spawns Boot in a tmux session.
|
||||||
func (b *Boot) spawnTmux() error {
|
func (b *Boot) spawnTmux(agentOverride string) error {
|
||||||
// Kill any stale session first
|
// Kill any stale session first
|
||||||
if b.IsSessionAlive() {
|
if b.IsSessionAlive() {
|
||||||
_ = b.tmux.KillSession(SessionName)
|
_ = b.tmux.KillSession(SessionName)
|
||||||
@@ -170,9 +171,18 @@ func (b *Boot) spawnTmux() error {
|
|||||||
return fmt.Errorf("ensuring boot dir: %w", err)
|
return fmt.Errorf("ensuring boot dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build startup command first
|
// Build startup command with optional agent override
|
||||||
// The "gt boot triage" prompt tells Boot to immediately start triage (GUPP principle)
|
// The "gt boot triage" prompt tells Boot to immediately start triage (GUPP principle)
|
||||||
startCmd := config.BuildAgentStartupCommand("boot", "deacon-boot", "", "gt boot triage")
|
var startCmd string
|
||||||
|
if agentOverride != "" {
|
||||||
|
var err error
|
||||||
|
startCmd, err = config.BuildAgentStartupCommandWithAgentOverride("boot", "deacon-boot", "", "gt boot triage", agentOverride)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building startup command with agent override: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startCmd = config.BuildAgentStartupCommand("boot", "deacon-boot", "", "gt boot triage")
|
||||||
|
}
|
||||||
|
|
||||||
// Create session with command directly to avoid send-keys race condition.
|
// Create session with command directly to avoid send-keys race condition.
|
||||||
// See: https://github.com/anthropics/gastown/issues/280
|
// See: https://github.com/anthropics/gastown/issues/280
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bootStatusJSON bool
|
bootStatusJSON bool
|
||||||
bootDegraded bool
|
bootDegraded bool
|
||||||
|
bootAgentOverride string
|
||||||
)
|
)
|
||||||
|
|
||||||
var bootCmd = &cobra.Command{
|
var bootCmd = &cobra.Command{
|
||||||
@@ -84,6 +85,7 @@ Use --degraded flag when running in degraded mode.`,
|
|||||||
func init() {
|
func init() {
|
||||||
bootStatusCmd.Flags().BoolVar(&bootStatusJSON, "json", false, "Output as JSON")
|
bootStatusCmd.Flags().BoolVar(&bootStatusJSON, "json", false, "Output as JSON")
|
||||||
bootTriageCmd.Flags().BoolVar(&bootDegraded, "degraded", false, "Run in degraded mode (no tmux)")
|
bootTriageCmd.Flags().BoolVar(&bootDegraded, "degraded", false, "Run in degraded mode (no tmux)")
|
||||||
|
bootSpawnCmd.Flags().StringVar(&bootAgentOverride, "agent", "", "Agent alias to run Boot with (overrides town default)")
|
||||||
|
|
||||||
bootCmd.AddCommand(bootStatusCmd)
|
bootCmd.AddCommand(bootStatusCmd)
|
||||||
bootCmd.AddCommand(bootSpawnCmd)
|
bootCmd.AddCommand(bootSpawnCmd)
|
||||||
@@ -206,7 +208,7 @@ func runBootSpawn(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn Boot
|
// Spawn Boot
|
||||||
if err := b.Spawn(); err != nil {
|
if err := b.Spawn(bootAgentOverride); err != nil {
|
||||||
status.Error = err.Error()
|
status.Error = err.Error()
|
||||||
status.CompletedAt = time.Now()
|
status.CompletedAt = time.Now()
|
||||||
status.Running = false
|
status.Running = false
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBootSpawnAgentFlag(t *testing.T) {
|
||||||
|
flag := bootSpawnCmd.Flags().Lookup("agent")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("expected boot spawn to define --agent flag")
|
||||||
|
}
|
||||||
|
if flag.DefValue != "" {
|
||||||
|
t.Errorf("expected default agent override to be empty, got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
if !strings.Contains(flag.Usage, "overrides town default") {
|
||||||
|
t.Errorf("expected --agent usage to mention overrides town default, got %q", flag.Usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,10 @@ import (
|
|||||||
|
|
||||||
// Refinery command flags
|
// Refinery command flags
|
||||||
var (
|
var (
|
||||||
refineryForeground bool
|
refineryForeground bool
|
||||||
refineryStatusJSON bool
|
refineryStatusJSON bool
|
||||||
refineryQueueJSON bool
|
refineryQueueJSON bool
|
||||||
|
refineryAgentOverride string
|
||||||
)
|
)
|
||||||
|
|
||||||
var refineryCmd = &cobra.Command{
|
var refineryCmd = &cobra.Command{
|
||||||
@@ -208,6 +209,13 @@ var refineryBlockedJSON bool
|
|||||||
func init() {
|
func init() {
|
||||||
// Start flags
|
// Start flags
|
||||||
refineryStartCmd.Flags().BoolVar(&refineryForeground, "foreground", false, "Run in foreground (default: background)")
|
refineryStartCmd.Flags().BoolVar(&refineryForeground, "foreground", false, "Run in foreground (default: background)")
|
||||||
|
refineryStartCmd.Flags().StringVar(&refineryAgentOverride, "agent", "", "Agent alias to run the Refinery with (overrides town default)")
|
||||||
|
|
||||||
|
// Attach flags
|
||||||
|
refineryAttachCmd.Flags().StringVar(&refineryAgentOverride, "agent", "", "Agent alias to run the Refinery with (overrides town default)")
|
||||||
|
|
||||||
|
// Restart flags
|
||||||
|
refineryRestartCmd.Flags().StringVar(&refineryAgentOverride, "agent", "", "Agent alias to run the Refinery with (overrides town default)")
|
||||||
|
|
||||||
// Status flags
|
// Status flags
|
||||||
refineryStatusCmd.Flags().BoolVar(&refineryStatusJSON, "json", false, "Output as JSON")
|
refineryStatusCmd.Flags().BoolVar(&refineryStatusJSON, "json", false, "Output as JSON")
|
||||||
@@ -277,7 +285,7 @@ func runRefineryStart(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
fmt.Printf("Starting refinery for %s...\n", rigName)
|
fmt.Printf("Starting refinery for %s...\n", rigName)
|
||||||
|
|
||||||
if err := mgr.Start(refineryForeground); err != nil {
|
if err := mgr.Start(refineryForeground, refineryAgentOverride); err != nil {
|
||||||
if err == refinery.ErrAlreadyRunning {
|
if err == refinery.ErrAlreadyRunning {
|
||||||
fmt.Printf("%s Refinery is already running\n", style.Dim.Render("⚠"))
|
fmt.Printf("%s Refinery is already running\n", style.Dim.Render("⚠"))
|
||||||
return nil
|
return nil
|
||||||
@@ -490,7 +498,7 @@ func runRefineryAttach(cmd *cobra.Command, args []string) error {
|
|||||||
if !running {
|
if !running {
|
||||||
// Auto-start if not running
|
// Auto-start if not running
|
||||||
fmt.Printf("Refinery not running for %s, starting...\n", rigName)
|
fmt.Printf("Refinery not running for %s, starting...\n", rigName)
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, refineryAgentOverride); err != nil {
|
||||||
return fmt.Errorf("starting refinery: %w", err)
|
return fmt.Errorf("starting refinery: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s Refinery started\n", style.Bold.Render("✓"))
|
fmt.Printf("%s Refinery started\n", style.Bold.Render("✓"))
|
||||||
@@ -519,7 +527,7 @@ func runRefineryRestart(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start fresh
|
// Start fresh
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, refineryAgentOverride); err != nil {
|
||||||
return fmt.Errorf("starting refinery: %w", err)
|
return fmt.Errorf("starting refinery: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRefineryStartAgentFlag(t *testing.T) {
|
||||||
|
flag := refineryStartCmd.Flags().Lookup("agent")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("expected refinery start to define --agent flag")
|
||||||
|
}
|
||||||
|
if flag.DefValue != "" {
|
||||||
|
t.Errorf("expected default agent override to be empty, got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
if !strings.Contains(flag.Usage, "overrides town default") {
|
||||||
|
t.Errorf("expected --agent usage to mention overrides town default, got %q", flag.Usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefineryAttachAgentFlag(t *testing.T) {
|
||||||
|
flag := refineryAttachCmd.Flags().Lookup("agent")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("expected refinery attach to define --agent flag")
|
||||||
|
}
|
||||||
|
if flag.DefValue != "" {
|
||||||
|
t.Errorf("expected default agent override to be empty, got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
if !strings.Contains(flag.Usage, "overrides town default") {
|
||||||
|
t.Errorf("expected --agent usage to mention overrides town default, got %q", flag.Usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefineryRestartAgentFlag(t *testing.T) {
|
||||||
|
flag := refineryRestartCmd.Flags().Lookup("agent")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("expected refinery restart to define --agent flag")
|
||||||
|
}
|
||||||
|
if flag.DefValue != "" {
|
||||||
|
t.Errorf("expected default agent override to be empty, got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
if !strings.Contains(flag.Usage, "overrides town default") {
|
||||||
|
t.Errorf("expected --agent usage to mention overrides town default, got %q", flag.Usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -779,7 +779,7 @@ func runRigBoot(cmd *cobra.Command, args []string) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting refinery...\n")
|
fmt.Printf(" Starting refinery...\n")
|
||||||
refMgr := refinery.NewManager(r)
|
refMgr := refinery.NewManager(r)
|
||||||
if err := refMgr.Start(false); err != nil { // false = background mode
|
if err := refMgr.Start(false, ""); err != nil { // false = background mode
|
||||||
return fmt.Errorf("starting refinery: %w", err)
|
return fmt.Errorf("starting refinery: %w", err)
|
||||||
}
|
}
|
||||||
started = append(started, "refinery")
|
started = append(started, "refinery")
|
||||||
@@ -859,7 +859,7 @@ func runRigStart(cmd *cobra.Command, args []string) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting refinery...\n")
|
fmt.Printf(" Starting refinery...\n")
|
||||||
refMgr := refinery.NewManager(r)
|
refMgr := refinery.NewManager(r)
|
||||||
if err := refMgr.Start(false); err != nil {
|
if err := refMgr.Start(false, ""); err != nil {
|
||||||
fmt.Printf(" %s Failed to start refinery: %v\n", style.Warning.Render("⚠"), err)
|
fmt.Printf(" %s Failed to start refinery: %v\n", style.Warning.Render("⚠"), err)
|
||||||
hasError = true
|
hasError = true
|
||||||
} else {
|
} else {
|
||||||
@@ -1437,7 +1437,7 @@ func runRigRestart(cmd *cobra.Command, args []string) error {
|
|||||||
skipped = append(skipped, "refinery")
|
skipped = append(skipped, "refinery")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting refinery...\n")
|
fmt.Printf(" Starting refinery...\n")
|
||||||
if err := refMgr.Start(false); err != nil {
|
if err := refMgr.Start(false, ""); err != nil {
|
||||||
fmt.Printf(" %s Failed to start refinery: %v\n", style.Warning.Render("⚠"), err)
|
fmt.Printf(" %s Failed to start refinery: %v\n", style.Warning.Render("⚠"), err)
|
||||||
startErrors = append(startErrors, fmt.Sprintf("refinery: %v", err))
|
startErrors = append(startErrors, fmt.Sprintf("refinery: %v", err))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ func startWitnessForRig(r *rig.Rig) string {
|
|||||||
// startRefineryForRig starts the refinery for a single rig and returns a status message.
|
// startRefineryForRig starts the refinery for a single rig and returns a status message.
|
||||||
func startRefineryForRig(r *rig.Rig) string {
|
func startRefineryForRig(r *rig.Rig) string {
|
||||||
refineryMgr := refinery.NewManager(r)
|
refineryMgr := refinery.NewManager(r)
|
||||||
if err := refineryMgr.Start(false); err != nil {
|
if err := refineryMgr.Start(false, ""); err != nil {
|
||||||
if errors.Is(err, refinery.ErrAlreadyRunning) {
|
if errors.Is(err, refinery.ErrAlreadyRunning) {
|
||||||
return fmt.Sprintf(" %s %s refinery already running\n", style.Dim.Render("○"), r.Name)
|
return fmt.Sprintf(" %s %s refinery already running\n", style.Dim.Render("○"), r.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ func runUp(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mgr := refinery.NewManager(r)
|
mgr := refinery.NewManager(r)
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, ""); err != nil {
|
||||||
if err == refinery.ErrAlreadyRunning {
|
if err == refinery.ErrAlreadyRunning {
|
||||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, mgr.SessionName())
|
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, mgr.SessionName())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ func (d *Daemon) ensureBootRunning() {
|
|||||||
|
|
||||||
// Spawn Boot in a fresh tmux session
|
// Spawn Boot in a fresh tmux session
|
||||||
d.logger.Println("Spawning Boot for triage...")
|
d.logger.Println("Spawning Boot for triage...")
|
||||||
if err := b.Spawn(); err != nil {
|
if err := b.Spawn(""); err != nil {
|
||||||
d.logger.Printf("Error spawning Boot: %v, falling back to direct Deacon check", err)
|
d.logger.Printf("Error spawning Boot: %v, falling back to direct Deacon check", err)
|
||||||
// Fallback: ensure Deacon is running directly
|
// Fallback: ensure Deacon is running directly
|
||||||
d.ensureDeaconRunning()
|
d.ensureDeaconRunning()
|
||||||
@@ -452,7 +452,7 @@ func (d *Daemon) ensureRefineryRunning(rigName string) {
|
|||||||
}
|
}
|
||||||
mgr := refinery.NewManager(r)
|
mgr := refinery.NewManager(r)
|
||||||
|
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, ""); err != nil {
|
||||||
if err == refinery.ErrAlreadyRunning {
|
if err == refinery.ErrAlreadyRunning {
|
||||||
// Already running - nothing to do
|
// Already running - nothing to do
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ func (m *Manager) Status() (*Refinery, error) {
|
|||||||
// Start starts the refinery.
|
// Start starts the refinery.
|
||||||
// If foreground is true, runs in the current process (blocking) using the Go-based polling loop.
|
// If foreground is true, runs in the current process (blocking) using the Go-based polling loop.
|
||||||
// Otherwise, spawns a Claude agent in a tmux session to process the merge queue.
|
// Otherwise, spawns a Claude agent in a tmux session to process the merge queue.
|
||||||
func (m *Manager) Start(foreground bool) error {
|
// The agentOverride parameter allows specifying an agent alias to use instead of the town default.
|
||||||
|
func (m *Manager) Start(foreground bool, agentOverride string) error {
|
||||||
ref, err := m.loadState()
|
ref, err := m.loadState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -174,7 +175,16 @@ func (m *Manager) Start(foreground bool) error {
|
|||||||
|
|
||||||
// Build startup command first
|
// Build startup command first
|
||||||
bdActor := fmt.Sprintf("%s/refinery", m.rig.Name)
|
bdActor := fmt.Sprintf("%s/refinery", m.rig.Name)
|
||||||
command := config.BuildAgentStartupCommand("refinery", bdActor, m.rig.Path, "")
|
var command string
|
||||||
|
if agentOverride != "" {
|
||||||
|
var err error
|
||||||
|
command, err = config.BuildAgentStartupCommandWithAgentOverride("refinery", bdActor, m.rig.Path, "", agentOverride)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building startup command with agent override: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
command = config.BuildAgentStartupCommand("refinery", bdActor, m.rig.Path, "")
|
||||||
|
}
|
||||||
|
|
||||||
// Create session with command directly to avoid send-keys race condition.
|
// Create session with command directly to avoid send-keys race condition.
|
||||||
// See: https://github.com/anthropics/gastown/issues/280
|
// See: https://github.com/anthropics/gastown/issues/280
|
||||||
|
|||||||
Reference in New Issue
Block a user