From 54581dcbd7f504d32dfa45fa1e7ddd0a5d84666f Mon Sep 17 00:00:00 2001 From: Virgil Date: Sun, 29 Mar 2026 23:37:28 +0000 Subject: [PATCH] fix(ax): register named agentic commands Co-Authored-By: Virgil --- pkg/agentic/commands.go | 31 ++++++++------ pkg/agentic/commands_test.go | 78 +++++++++++------------------------- pkg/agentic/prep.go | 1 + 3 files changed, 44 insertions(+), 66 deletions(-) diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 683878d..ebadfb0 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -13,21 +13,32 @@ import ( // registerCommands adds agentic CLI commands to Core's command tree. func (s *PrepSubsystem) registerCommands(ctx context.Context) { + s.commandCtx = ctx c := s.Core() - c.Command("run/task", core.Command{Description: "Run a single task end-to-end", Action: s.cmdRunTaskFactory(ctx)}) - c.Command("run/orchestrator", core.Command{Description: "Run the queue orchestrator (standalone, no MCP)", Action: s.cmdOrchestratorFactory(ctx)}) + 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("prep", core.Command{Description: "Prepare a workspace: clone repo, build prompt", Action: s.cmdPrep}) c.Command("status", core.Command{Description: "List agent workspace statuses", Action: s.cmdStatus}) c.Command("prompt", core.Command{Description: "Build and display an agent prompt for a repo", Action: s.cmdPrompt}) c.Command("extract", core.Command{Description: "Extract a workspace template to a directory", Action: s.cmdExtract}) } -// cmdRunTaskFactory returns the run/task action closure (needs ctx for DispatchSync). -func (s *PrepSubsystem) cmdRunTaskFactory(ctx context.Context) func(core.Options) core.Result { - return func(opts core.Options) core.Result { return s.cmdRunTask(ctx, opts) } +// commandContext returns the startup context captured during command registration. +// +// ctx := s.commandContext() +// _ = ctx.Err() +func (s *PrepSubsystem) commandContext() context.Context { + if s.commandCtx != nil { + return s.commandCtx + } + return context.Background() } -func (s *PrepSubsystem) cmdRunTask(ctx context.Context, opts core.Options) core.Result { +func (s *PrepSubsystem) cmdRunTask(opts core.Options) core.Result { + return s.runTask(s.commandContext(), opts) +} + +func (s *PrepSubsystem) runTask(ctx context.Context, opts core.Options) core.Result { repo := opts.String("repo") agent := opts.String("agent") task := opts.String("task") @@ -72,12 +83,8 @@ func (s *PrepSubsystem) cmdRunTask(ctx context.Context, opts core.Options) core. return core.Result{OK: true} } -// cmdOrchestratorFactory returns the orchestrator action closure (needs ctx for blocking). -func (s *PrepSubsystem) cmdOrchestratorFactory(ctx context.Context) func(core.Options) core.Result { - return func(opts core.Options) core.Result { return s.cmdOrchestrator(ctx, opts) } -} - -func (s *PrepSubsystem) cmdOrchestrator(ctx context.Context, _ core.Options) core.Result { +func (s *PrepSubsystem) cmdOrchestrator(_ core.Options) core.Result { + ctx := s.commandContext() core.Print(nil, "core-agent orchestrator running (pid %s)", core.Env("PID")) core.Print(nil, " workspace: %s", WorkspaceRoot()) core.Print(nil, " watching queue, draining on 30s tick + completion poke") diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index a6ac4f8..b33f4b5 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -640,7 +640,8 @@ func TestCommands_CmdRunTask_Bad_MissingArgs(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - r := s.cmdRunTask(ctx, core.NewOptions()) + s.commandCtx = ctx + r := s.cmdRunTask(core.NewOptions()) assert.False(t, r.OK) } @@ -648,7 +649,8 @@ func TestCommands_CmdRunTask_Bad_MissingTask(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - r := s.cmdRunTask(ctx, core.NewOptions(core.Option{Key: "repo", Value: "go-io"})) + s.commandCtx = ctx + r := s.cmdRunTask(core.NewOptions(core.Option{Key: "repo", Value: "go-io"})) assert.False(t, r.OK) } @@ -656,7 +658,8 @@ func TestCommands_CmdOrchestrator_Good_CancelledCtx(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel immediately - r := s.cmdOrchestrator(ctx, core.NewOptions()) + s.commandCtx = ctx + r := s.cmdOrchestrator(core.NewOptions()) assert.True(t, r.OK) } @@ -719,7 +722,8 @@ func TestCommands_CmdOrchestrator_Bad_DoneContext(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second)) defer cancel() - r := s.cmdOrchestrator(ctx, core.NewOptions()) + s.commandCtx = ctx + r := s.cmdOrchestrator(core.NewOptions()) assert.True(t, r.OK) // returns OK after ctx.Done() } @@ -727,7 +731,8 @@ func TestCommands_CmdOrchestrator_Ugly_CancelledImmediately(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithCancel(context.Background()) cancel() - r := s.cmdOrchestrator(ctx, core.NewOptions()) + s.commandCtx = ctx + r := s.cmdOrchestrator(core.NewOptions()) assert.True(t, r.OK) // exits immediately when context is already cancelled } @@ -770,8 +775,9 @@ func TestCommands_CmdRunTask_Good_DefaultsApplied(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + s.commandCtx = ctx // Provide repo + task but omit agent + org — tests that defaults (codex, core) are applied - r := s.cmdRunTask(ctx, core.NewOptions( + r := s.cmdRunTask(core.NewOptions( core.Option{Key: "repo", Value: "go-io"}, core.Option{Key: "task", Value: "run all tests"}, )) @@ -783,7 +789,8 @@ func TestCommands_CmdRunTask_Ugly_MixedIssueString(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - r := s.cmdRunTask(ctx, core.NewOptions( + s.commandCtx = ctx + r := s.cmdRunTask(core.NewOptions( core.Option{Key: "repo", Value: "go-io"}, core.Option{Key: "task", Value: "fix it"}, core.Option{Key: "issue", Value: "issue-42abc"}, @@ -792,65 +799,28 @@ func TestCommands_CmdRunTask_Ugly_MixedIssueString(t *testing.T) { assert.False(t, r.OK) } -// --- CmdRunTaskFactory Good/Bad/Ugly --- +// --- CommandContext Good/Bad/Ugly --- -func TestCommands_CmdRunTaskFactory_Good(t *testing.T) { +func TestCommands_CommandContext_Good_StoredStartupContext(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - - fn := s.cmdRunTaskFactory(ctx) - assert.NotNil(t, fn, "factory should return a non-nil func") + s.registerCommands(ctx) + assert.Same(t, ctx, s.commandContext()) } -func TestCommands_CmdRunTaskFactory_Bad(t *testing.T) { +func TestCommands_CommandContext_Bad_FallsBackToBackground(t *testing.T) { s, _ := testPrepWithCore(t, nil) - ctx, cancel := context.WithCancel(context.Background()) - cancel() // cancelled ctx - - fn := s.cmdRunTaskFactory(ctx) - assert.NotNil(t, fn, "factory should return a func even with cancelled ctx") + assert.NotNil(t, s.commandContext()) + assert.NoError(t, s.commandContext().Err()) } -func TestCommands_CmdRunTaskFactory_Ugly(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - fn := s.cmdRunTaskFactory(ctx) - // Call with empty options — should fail gracefully (missing repo+task) - r := fn(core.NewOptions()) - assert.False(t, r.OK) -} - -// --- CmdOrchestratorFactory Good/Bad/Ugly --- - -func TestCommands_CmdOrchestratorFactory_Good(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - fn := s.cmdOrchestratorFactory(ctx) - assert.NotNil(t, fn, "factory should return a non-nil func") -} - -func TestCommands_CmdOrchestratorFactory_Bad(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - ctx, cancel := context.WithCancel(context.Background()) - cancel() // cancelled ctx - - fn := s.cmdOrchestratorFactory(ctx) - assert.NotNil(t, fn, "factory should return a func even with cancelled ctx") -} - -func TestCommands_CmdOrchestratorFactory_Ugly(t *testing.T) { +func TestCommands_CommandContext_Ugly_CancelledStartupContext(t *testing.T) { s, _ := testPrepWithCore(t, nil) ctx, cancel := context.WithCancel(context.Background()) cancel() // pre-cancelled - - fn := s.cmdOrchestratorFactory(ctx) - // Calling the factory result with a cancelled ctx should return OK (exits immediately) - r := fn(core.NewOptions()) + s.commandCtx = ctx + r := s.cmdOrchestrator(core.NewOptions()) assert.True(t, r.OK) } diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 509a65e..4c6ae0c 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -33,6 +33,7 @@ type PrepSubsystem struct { brainURL string brainKey string codePath string + commandCtx context.Context dispatchMu sync.Mutex // serialises concurrency check + spawn drainMu sync.Mutex pokeCh chan struct{}