@@ -71,8 +71,9 @@ Commands:
7171
7272Examples:
7373 sah auth login
74- sah run --agent codex
75- sah daemon install --agent claude --interval 30m
74+ sah run --rotate-installed
75+ sah run --agents codex,gemini,claude --models codex=gpt-5.4-mini,gemini=gemini3-flash,claude=sonnet
76+ sah daemon install --agents codex,claude --interval 30m
7677 sah me
7778` )
7879}
@@ -169,7 +170,10 @@ func runCmd(args []string) error {
169170 fs := flag .NewFlagSet ("run" , flag .ContinueOnError )
170171 fs .SetOutput (os .Stderr )
171172 agent := fs .String ("agent" , "" , "Agent CLI to use: codex, gemini, claude" )
173+ agents := fs .String ("agents" , "" , "Comma-separated round-robin agent order, e.g. codex,gemini,claude" )
174+ rotateInstalled := fs .Bool ("rotate-installed" , false , "Rotate through every supported agent CLI installed on this Mac" )
172175 model := fs .String ("model" , "" , "Optional model override passed to the agent CLI" )
176+ models := fs .String ("models" , "" , "Per-agent model overrides, e.g. codex=gpt-5.4-mini,gemini=gemini3-flash,claude=sonnet" )
173177 interval := fs .String ("interval" , "" , "Polling interval" )
174178 timeout := fs .String ("timeout" , "" , "Per-assignment agent timeout" )
175179 taskType := fs .String ("task-type" , "" , "Optional task type filter" )
@@ -179,6 +183,12 @@ func runCmd(args []string) error {
179183 if err := fs .Parse (args ); err != nil {
180184 return err
181185 }
186+ if err := validateAgentFlags (* agent , * agents , * rotateInstalled ); err != nil {
187+ return err
188+ }
189+ if ! * daemonMode {
190+ sah .PrintRunBanner (os .Stdout )
191+ }
182192
183193 ctx , cancel := signalContext ()
184194 defer cancel ()
@@ -205,6 +215,16 @@ func runCmd(args []string) error {
205215 return err
206216 }
207217 }
218+ agentPool := sah .ParseAgentList (* agents )
219+ if len (agentPool ) > 0 {
220+ if _ , err := sah .ResolveAgentPool (config , sah.WorkerOptions {Agents : agentPool }); err != nil {
221+ return err
222+ }
223+ }
224+ agentModels , err := sah .ParseAgentModels (* models )
225+ if err != nil {
226+ return err
227+ }
208228
209229 pollInterval , err := sah .ParsePollInterval (pickString (* interval , config .PollInterval ))
210230 if err != nil {
@@ -215,16 +235,29 @@ func runCmd(args []string) error {
215235 return err
216236 }
217237
218- return sah .RunWorker (ctx , config , sah.WorkerOptions {
219- Agent : pickString (* agent , config .DefaultAgent ),
220- Model : pickString (* model , config .AgentModel ),
221- Interval : pollInterval ,
222- Timeout : agentTimeout ,
223- TaskType : strings .TrimSpace (* taskType ),
224- Once : * once ,
225- Output : os .Stdout ,
226- ErrorOutput : os .Stderr ,
227- })
238+ options := sah.WorkerOptions {
239+ Agent : pickString (* agent , config .DefaultAgent ),
240+ Agents : agentPool ,
241+ RotateInstalled : * rotateInstalled ,
242+ Model : pickString (* model , config .AgentModel ),
243+ Models : sah .MergeAgentModels (config .AgentModels , agentModels ),
244+ Interval : pollInterval ,
245+ Timeout : agentTimeout ,
246+ TaskType : strings .TrimSpace (* taskType ),
247+ Once : * once ,
248+ Output : os .Stdout ,
249+ ErrorOutput : os .Stderr ,
250+ }
251+
252+ picker , err := sah .NewAgentPicker (config , options )
253+ if err != nil {
254+ return err
255+ }
256+ if ! * daemonMode {
257+ sah .PrintRunPlan (os .Stdout , config , options , picker .Pool ())
258+ }
259+
260+ return sah .RunWorker (ctx , config , options )
228261}
229262
230263func daemonCmd (args []string ) error {
@@ -237,13 +270,19 @@ func daemonCmd(args []string) error {
237270 fs := flag .NewFlagSet ("daemon install" , flag .ContinueOnError )
238271 fs .SetOutput (os .Stderr )
239272 agent := fs .String ("agent" , "" , "Default agent CLI for the daemon" )
273+ agents := fs .String ("agents" , "" , "Comma-separated round-robin agent order for the daemon" )
274+ rotateInstalled := fs .Bool ("rotate-installed" , false , "Rotate through every installed supported agent CLI" )
240275 model := fs .String ("model" , "" , "Default model override" )
276+ models := fs .String ("models" , "" , "Per-agent model overrides, e.g. codex=gpt-5.4-mini,gemini=gemini3-flash,claude=sonnet" )
241277 interval := fs .String ("interval" , "" , "Default polling interval" )
242278 timeout := fs .String ("timeout" , "" , "Default per-assignment timeout" )
243279 baseURL := fs .String ("base-url" , "" , "SCIENCE@home base URL" )
244280 if err := fs .Parse (args [1 :]); err != nil {
245281 return err
246282 }
283+ if err := validateAgentFlags (* agent , * agents , * rotateInstalled ); err != nil {
284+ return err
285+ }
247286
248287 ctx , cancel := signalContext ()
249288 defer cancel ()
@@ -260,10 +299,31 @@ func daemonCmd(args []string) error {
260299 return err
261300 }
262301 config .DefaultAgent = * agent
302+ config .AgentPool = nil
303+ config .RotateInstalled = false
304+ }
305+ if pool := sah .ParseAgentList (* agents ); len (pool ) > 0 {
306+ if _ , err := sah .ResolveAgentPool (config , sah.WorkerOptions {Agents : pool }); err != nil {
307+ return err
308+ }
309+ config .AgentPool = pool
310+ config .RotateInstalled = false
311+ }
312+ if * rotateInstalled {
313+ if _ , err := sah .ResolveAgentPool (config , sah.WorkerOptions {RotateInstalled : true }); err != nil {
314+ return err
315+ }
316+ config .AgentPool = nil
317+ config .RotateInstalled = true
263318 }
264319 if strings .TrimSpace (* model ) != "" {
265320 config .AgentModel = * model
266321 }
322+ if parsedModels , err := sah .ParseAgentModels (* models ); err != nil {
323+ return err
324+ } else if parsedModels != nil {
325+ config .AgentModels = parsedModels
326+ }
267327 if strings .TrimSpace (* interval ) != "" {
268328 if _ , err := sah .ParsePollInterval (* interval ); err != nil {
269329 return err
@@ -342,7 +402,13 @@ func daemonCmd(args []string) error {
342402 return err
343403 }
344404 fmt .Printf ("Base URL: %s\n " , config .BaseURL )
345- fmt .Printf ("Default agent: %s\n " , config .DefaultAgent )
405+ fmt .Printf ("Agents: %s\n " , sah .DescribeAgentMode (config , sah.WorkerOptions {}))
406+ if model := strings .TrimSpace (config .AgentModel ); model != "" {
407+ fmt .Printf ("Model: %s\n " , model )
408+ }
409+ if models := sah .FormatAgentModels (config .AgentModels ); models != "" {
410+ fmt .Printf ("Per-agent models: %s\n " , models )
411+ }
346412 fmt .Printf ("Interval: %s\n " , config .PollInterval )
347413 if loaded {
348414 fmt .Println ("Launchd: loaded" )
@@ -492,13 +558,13 @@ func agentsCmd(args []string) error {
492558 }
493559
494560 writer := tabwriter .NewWriter (os .Stdout , 0 , 0 , 2 , ' ' , 0 )
495- fmt .Fprintln (writer , "NAME\t INSTALLED\t PATH\t DESCRIPTION" )
561+ fmt .Fprintln (writer , "NAME\t INSTALLED\t PATH\t DESCRIPTION\t MODEL FLAG " )
496562 for _ , status := range sah .InstalledAgents () {
497563 installed := "no"
498564 if status .Installed {
499565 installed = "yes"
500566 }
501- fmt .Fprintf (writer , "%s\t %s\t %s\t %s\n " , status .Name , installed , status .Path , status .Description )
567+ fmt .Fprintf (writer , "%s\t %s\t %s\t %s\t --model / --models %s=<name> \ n " , status .Name , installed , status .Path , status .Description , status . Name )
502568 }
503569 return writer .Flush ()
504570}
@@ -587,6 +653,16 @@ func pickString(primary string, fallback string) string {
587653 return fallback
588654}
589655
656+ func validateAgentFlags (agent string , agents string , rotateInstalled bool ) error {
657+ if rotateInstalled && (strings .TrimSpace (agent ) != "" || strings .TrimSpace (agents ) != "" ) {
658+ return fmt .Errorf ("--rotate-installed cannot be combined with --agent or --agents" )
659+ }
660+ if strings .TrimSpace (agent ) != "" && strings .TrimSpace (agents ) != "" {
661+ return fmt .Errorf ("--agent cannot be combined with --agents" )
662+ }
663+ return nil
664+ }
665+
590666func signalContext () (context.Context , context.CancelFunc ) {
591667 return signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGTERM )
592668}
0 commit comments