diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 73ef660..312ca7f 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -20,6 +20,9 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { c.Command("run/task", core.Command{Description: "Run a single task end-to-end", Action: s.cmdRunTask}) c.Command("run/orchestrator", core.Command{Description: "Run the queue orchestrator (standalone, no MCP)", Action: s.cmdOrchestrator}) c.Command("dispatch", core.Command{Description: "Dispatch queued agents", Action: s.cmdDispatch}) + c.Command("dispatch/start", core.Command{Description: "Start the dispatch queue runner", Action: s.cmdDispatchStart}) + c.Command("dispatch/shutdown", core.Command{Description: "Freeze the dispatch queue gracefully", Action: s.cmdDispatchShutdown}) + c.Command("dispatch/shutdown-now", core.Command{Description: "Hard stop the dispatch queue and kill running agents", Action: s.cmdDispatchShutdownNow}) c.Command("prep", core.Command{Description: "Prepare a workspace: clone repo, build prompt", Action: s.cmdPrep}) c.Command("prep-workspace", core.Command{Description: "Prepare a workspace: clone repo, build prompt", Action: s.cmdPrep}) c.Command("generate", core.Command{Description: "Generate content from a prompt using the platform content pipeline", Action: s.cmdGenerate}) @@ -110,6 +113,49 @@ func (s *PrepSubsystem) cmdDispatch(_ core.Options) core.Result { return s.runDispatchLoop("dispatch") } +func (s *PrepSubsystem) cmdDispatchStart(_ core.Options) core.Result { + _, output, err := s.dispatchStart(s.commandContext(), nil, ShutdownInput{}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + if output.Message != "" { + core.Print(nil, "%s", output.Message) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdDispatchShutdown(_ core.Options) core.Result { + _, output, err := s.shutdownGraceful(s.commandContext(), nil, ShutdownInput{}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + if output.Message != "" { + core.Print(nil, "%s", output.Message) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdDispatchShutdownNow(_ core.Options) core.Result { + _, output, err := s.shutdownNow(s.commandContext(), nil, ShutdownInput{}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + if output.Message != "" { + core.Print(nil, "%s", output.Message) + } + if output.Running > 0 || output.Queued > 0 { + core.Print(nil, "running: %d", output.Running) + core.Print(nil, "queued: %d", output.Queued) + } + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) runDispatchLoop(label string) core.Result { ctx := s.commandContext() core.Print(nil, "core-agent %s running (pid %s)", label, core.Env("PID")) diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 96df5d9..a521958 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -1146,6 +1146,57 @@ func TestCommands_CmdDispatch_Good_CancelledCtx(t *testing.T) { assert.True(t, r.OK) } +func TestCommands_CmdDispatchStart_Good(t *testing.T) { + s, c := testPrepWithCore(t, nil) + called := false + c.Action("runner.start", func(_ context.Context, _ core.Options) core.Result { + called = true + return core.Result{OK: true} + }) + + output := captureStdout(t, func() { + r := s.cmdDispatchStart(core.NewOptions()) + assert.True(t, r.OK) + }) + + assert.True(t, called) + assert.Contains(t, output, "dispatch started") +} + +func TestCommands_CmdDispatchShutdown_Good(t *testing.T) { + s, c := testPrepWithCore(t, nil) + called := false + c.Action("runner.stop", func(_ context.Context, _ core.Options) core.Result { + called = true + return core.Result{OK: true} + }) + + output := captureStdout(t, func() { + r := s.cmdDispatchShutdown(core.NewOptions()) + assert.True(t, r.OK) + }) + + assert.True(t, called) + assert.Contains(t, output, "queue frozen") +} + +func TestCommands_CmdDispatchShutdownNow_Good(t *testing.T) { + s, c := testPrepWithCore(t, nil) + called := false + c.Action("runner.kill", func(_ context.Context, _ core.Options) core.Result { + called = true + return core.Result{OK: true} + }) + + output := captureStdout(t, func() { + r := s.cmdDispatchShutdownNow(core.NewOptions()) + assert.True(t, r.OK) + }) + + assert.True(t, called) + assert.Contains(t, output, "killed all agents") +} + func TestCommands_ParseIntStr_Good(t *testing.T) { assert.Equal(t, 42, parseIntString("42")) assert.Equal(t, 123, parseIntString("issue-123")) @@ -1166,6 +1217,9 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "run/task") assert.Contains(t, cmds, "run/orchestrator") assert.Contains(t, cmds, "dispatch") + assert.Contains(t, cmds, "dispatch/start") + assert.Contains(t, cmds, "dispatch/shutdown") + assert.Contains(t, cmds, "dispatch/shutdown-now") assert.Contains(t, cmds, "prep") assert.Contains(t, cmds, "complete") assert.Contains(t, cmds, "scan")