diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index b47f4f3..399a91f 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -15,252 +15,238 @@ import ( // registerCommands adds agentic CLI commands to Core's command tree. func (s *PrepSubsystem) registerCommands(ctx context.Context) { c := s.core - - c.Command("run/task", core.Command{ - Description: "Run a single task end-to-end", - Action: func(opts core.Options) core.Result { - repo := opts.String("repo") - agent := opts.String("agent") - task := opts.String("task") - issueStr := opts.String("issue") - org := opts.String("org") - - if repo == "" || task == "" { - core.Print(nil, "usage: core-agent run task --repo= --task=\"...\" --agent=codex [--issue=N] [--org=core]") - return core.Result{OK: false} - } - if agent == "" { - agent = "codex" - } - if org == "" { - org = "core" - } - - issue := 0 - if issueStr != "" { - for _, ch := range issueStr { - if ch >= '0' && ch <= '9' { - issue = issue*10 + int(ch-'0') - } - } - } - - core.Print(os.Stderr, "core-agent run task") - core.Print(os.Stderr, " repo: %s/%s", org, repo) - core.Print(os.Stderr, " agent: %s", agent) - if issue > 0 { - core.Print(os.Stderr, " issue: #%d", issue) - } - core.Print(os.Stderr, " task: %s", task) - core.Print(os.Stderr, "") - - result := s.DispatchSync(ctx, DispatchSyncInput{ - Org: org, - Repo: repo, - Agent: agent, - Task: task, - Issue: issue, - }) - - if !result.OK { - core.Print(os.Stderr, "FAILED: %v", result.Error) - return core.Result{Value: result.Error, OK: false} - } - - core.Print(os.Stderr, "DONE: %s", result.Status) - if result.PRURL != "" { - core.Print(os.Stderr, " PR: %s", result.PRURL) - } - return core.Result{OK: true} - }, - }) - - c.Command("run/orchestrator", core.Command{ - Description: "Run the queue orchestrator (standalone, no MCP)", - Action: func(opts core.Options) core.Result { - core.Print(os.Stderr, "core-agent orchestrator running (pid %s)", core.Env("PID")) - core.Print(os.Stderr, " workspace: %s", WorkspaceRoot()) - core.Print(os.Stderr, " watching queue, draining on 30s tick + completion poke") - - <-ctx.Done() - core.Print(os.Stderr, "orchestrator shutting down") - return core.Result{OK: true} - }, - }) - - c.Command("prep", core.Command{ - Description: "Prepare a workspace: clone repo, build prompt", - Action: func(opts core.Options) core.Result { - repo := opts.String("_arg") - if repo == "" { - core.Print(nil, "usage: core-agent prep --issue=N|--pr=N|--branch=X --task=\"...\"") - return core.Result{OK: false} - } - - input := PrepInput{ - Repo: repo, - Org: opts.String("org"), - Task: opts.String("task"), - Template: opts.String("template"), - Persona: opts.String("persona"), - DryRun: opts.Bool("dry-run"), - } - - if v := opts.String("issue"); v != "" { - n := 0 - for _, ch := range v { - if ch >= '0' && ch <= '9' { - n = n*10 + int(ch-'0') - } - } - input.Issue = n - } - if v := opts.String("pr"); v != "" { - n := 0 - for _, ch := range v { - if ch >= '0' && ch <= '9' { - n = n*10 + int(ch-'0') - } - } - input.PR = n - } - if v := opts.String("branch"); v != "" { - input.Branch = v - } - if v := opts.String("tag"); v != "" { - input.Tag = v - } - - if input.Issue == 0 && input.PR == 0 && input.Branch == "" && input.Tag == "" { - input.Branch = "dev" - } - - _, out, err := s.TestPrepWorkspace(context.Background(), input) - if err != nil { - core.Print(nil, "error: %v", err) - return core.Result{Value: err, OK: false} - } - - core.Print(nil, "workspace: %s", out.WorkspaceDir) - core.Print(nil, "repo: %s", out.RepoDir) - core.Print(nil, "branch: %s", out.Branch) - core.Print(nil, "resumed: %v", out.Resumed) - core.Print(nil, "memories: %d", out.Memories) - core.Print(nil, "consumers: %d", out.Consumers) - if out.Prompt != "" { - core.Print(nil, "") - core.Print(nil, "--- prompt (%d chars) ---", len(out.Prompt)) - core.Print(nil, "%s", out.Prompt) - } - return core.Result{OK: true} - }, - }) - - c.Command("status", core.Command{ - Description: "List agent workspace statuses", - Action: func(opts core.Options) core.Result { - wsRoot := WorkspaceRoot() - fsys := c.Fs() - r := fsys.List(wsRoot) - if !r.OK { - core.Print(nil, "no workspaces found at %s", wsRoot) - return core.Result{OK: true} - } - - entries := r.Value.([]os.DirEntry) - if len(entries) == 0 { - core.Print(nil, "no workspaces") - return core.Result{OK: true} - } - - for _, e := range entries { - if !e.IsDir() { - continue - } - statusFile := core.JoinPath(wsRoot, e.Name(), "status.json") - if sr := fsys.Read(statusFile); sr.OK { - core.Print(nil, " %s", e.Name()) - } - } - return core.Result{OK: true} - }, - }) - - c.Command("prompt", core.Command{ - Description: "Build and display an agent prompt for a repo", - Action: func(opts core.Options) core.Result { - repo := opts.String("_arg") - if repo == "" { - core.Print(nil, "usage: core-agent prompt --task=\"...\"") - return core.Result{OK: false} - } - - org := opts.String("org") - if org == "" { - org = "core" - } - task := opts.String("task") - if task == "" { - task = "Review and report findings" - } - - repoPath := core.JoinPath(core.Env("DIR_HOME"), "Code", org, repo) - - input := PrepInput{ - Repo: repo, - Org: org, - Task: task, - Template: opts.String("template"), - Persona: opts.String("persona"), - } - - prompt, memories, consumers := s.TestBuildPrompt(context.Background(), input, "dev", repoPath) - core.Print(nil, "memories: %d", memories) - core.Print(nil, "consumers: %d", consumers) - core.Print(nil, "") - core.Print(nil, "%s", prompt) - return core.Result{OK: true} - }, - }) - - c.Command("extract", core.Command{ - Description: "Extract a workspace template to a directory", - Action: func(opts core.Options) core.Result { - tmpl := opts.String("_arg") - if tmpl == "" { - tmpl = "default" - } - target := opts.String("target") - if target == "" { - target = core.Path("Code", ".core", "workspace", "test-extract") - } - - data := &lib.WorkspaceData{ - Repo: "test-repo", - Branch: "dev", - Task: "test extraction", - Agent: "codex", - } - - core.Print(nil, "extracting template %q to %s", tmpl, target) - if err := lib.ExtractWorkspace(tmpl, target, data); err != nil { - return core.Result{Value: err, OK: false} - } - - fsys := c.Fs() - r := fsys.List(target) - if r.OK { - for _, e := range r.Value.([]os.DirEntry) { - marker := " " - if e.IsDir() { - marker = "/" - } - core.Print(nil, " %s%s", e.Name(), marker) - } - } - - core.Print(nil, "done") - return core.Result{OK: true} - }, - }) + 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("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) } +} + +func (s *PrepSubsystem) cmdRunTask(ctx context.Context, opts core.Options) core.Result { + repo := opts.String("repo") + agent := opts.String("agent") + task := opts.String("task") + issueStr := opts.String("issue") + org := opts.String("org") + + if repo == "" || task == "" { + core.Print(nil, "usage: core-agent run task --repo= --task=\"...\" --agent=codex [--issue=N] [--org=core]") + return core.Result{OK: false} + } + if agent == "" { + agent = "codex" + } + if org == "" { + org = "core" + } + + issue := parseIntStr(issueStr) + + core.Print(os.Stderr, "core-agent run task") + core.Print(os.Stderr, " repo: %s/%s", org, repo) + core.Print(os.Stderr, " agent: %s", agent) + if issue > 0 { + core.Print(os.Stderr, " issue: #%d", issue) + } + core.Print(os.Stderr, " task: %s", task) + core.Print(os.Stderr, "") + + result := s.DispatchSync(ctx, DispatchSyncInput{ + Org: org, Repo: repo, Agent: agent, Task: task, Issue: issue, + }) + + if !result.OK { + core.Print(os.Stderr, "FAILED: %v", result.Error) + return core.Result{Value: result.Error, OK: false} + } + + core.Print(os.Stderr, "DONE: %s", result.Status) + if result.PRURL != "" { + core.Print(os.Stderr, " PR: %s", result.PRURL) + } + 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 { + core.Print(os.Stderr, "core-agent orchestrator running (pid %s)", core.Env("PID")) + core.Print(os.Stderr, " workspace: %s", WorkspaceRoot()) + core.Print(os.Stderr, " watching queue, draining on 30s tick + completion poke") + + <-ctx.Done() + core.Print(os.Stderr, "orchestrator shutting down") + return core.Result{OK: true} +} + +func (s *PrepSubsystem) cmdPrep(opts core.Options) core.Result { + repo := opts.String("_arg") + if repo == "" { + core.Print(nil, "usage: core-agent prep --issue=N|--pr=N|--branch=X --task=\"...\"") + return core.Result{OK: false} + } + + input := PrepInput{ + Repo: repo, + Org: opts.String("org"), + Task: opts.String("task"), + Template: opts.String("template"), + Persona: opts.String("persona"), + DryRun: opts.Bool("dry-run"), + } + + if v := opts.String("issue"); v != "" { + input.Issue = parseIntStr(v) + } + if v := opts.String("pr"); v != "" { + input.PR = parseIntStr(v) + } + if v := opts.String("branch"); v != "" { + input.Branch = v + } + if v := opts.String("tag"); v != "" { + input.Tag = v + } + + if input.Issue == 0 && input.PR == 0 && input.Branch == "" && input.Tag == "" { + input.Branch = "dev" + } + + _, out, err := s.TestPrepWorkspace(context.Background(), input) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "workspace: %s", out.WorkspaceDir) + core.Print(nil, "repo: %s", out.RepoDir) + core.Print(nil, "branch: %s", out.Branch) + core.Print(nil, "resumed: %v", out.Resumed) + core.Print(nil, "memories: %d", out.Memories) + core.Print(nil, "consumers: %d", out.Consumers) + if out.Prompt != "" { + core.Print(nil, "") + core.Print(nil, "--- prompt (%d chars) ---", len(out.Prompt)) + core.Print(nil, "%s", out.Prompt) + } + return core.Result{OK: true} +} + +func (s *PrepSubsystem) cmdStatus(opts core.Options) core.Result { + wsRoot := WorkspaceRoot() + fsys := s.core.Fs() + r := fsys.List(wsRoot) + if !r.OK { + core.Print(nil, "no workspaces found at %s", wsRoot) + return core.Result{OK: true} + } + + entries := r.Value.([]os.DirEntry) + if len(entries) == 0 { + core.Print(nil, "no workspaces") + return core.Result{OK: true} + } + + for _, e := range entries { + if !e.IsDir() { + continue + } + statusFile := core.JoinPath(wsRoot, e.Name(), "status.json") + if sr := fsys.Read(statusFile); sr.OK { + core.Print(nil, " %s", e.Name()) + } + } + return core.Result{OK: true} +} + +func (s *PrepSubsystem) cmdPrompt(opts core.Options) core.Result { + repo := opts.String("_arg") + if repo == "" { + core.Print(nil, "usage: core-agent prompt --task=\"...\"") + return core.Result{OK: false} + } + + org := opts.String("org") + if org == "" { + org = "core" + } + task := opts.String("task") + if task == "" { + task = "Review and report findings" + } + + repoPath := core.JoinPath(core.Env("DIR_HOME"), "Code", org, repo) + + input := PrepInput{ + Repo: repo, + Org: org, + Task: task, + Template: opts.String("template"), + Persona: opts.String("persona"), + } + + prompt, memories, consumers := s.TestBuildPrompt(context.Background(), input, "dev", repoPath) + core.Print(nil, "memories: %d", memories) + core.Print(nil, "consumers: %d", consumers) + core.Print(nil, "") + core.Print(nil, "%s", prompt) + return core.Result{OK: true} +} + +func (s *PrepSubsystem) cmdExtract(opts core.Options) core.Result { + tmpl := opts.String("_arg") + if tmpl == "" { + tmpl = "default" + } + target := opts.String("target") + if target == "" { + target = core.Path("Code", ".core", "workspace", "test-extract") + } + + data := &lib.WorkspaceData{ + Repo: "test-repo", + Branch: "dev", + Task: "test extraction", + Agent: "codex", + } + + core.Print(nil, "extracting template %q to %s", tmpl, target) + if err := lib.ExtractWorkspace(tmpl, target, data); err != nil { + return core.Result{Value: err, OK: false} + } + + fsys := s.core.Fs() + r := fsys.List(target) + if r.OK { + for _, e := range r.Value.([]os.DirEntry) { + marker := " " + if e.IsDir() { + marker = "/" + } + core.Print(nil, " %s%s", e.Name(), marker) + } + } + + core.Print(nil, "done") + return core.Result{OK: true} +} + +// parseIntStr extracts digits from a string and returns the integer value. +func parseIntStr(s string) int { + n := 0 + for _, ch := range s { + if ch >= '0' && ch <= '9' { + n = n*10 + int(ch-'0') + } + } + return n } diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 44e31b4..c97c1b8 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -355,6 +355,94 @@ func TestCmdWorkspaceDispatch_Good_Stub(t *testing.T) { assert.True(t, r.OK) } +// --- commands.go extracted methods --- + +func TestCmdPrep_Bad_MissingRepo(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdPrep(core.NewOptions()) + assert.False(t, r.OK) +} + +func TestCmdPrep_Good_DefaultsToDev(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + // Will fail (no local clone) but exercises the default branch logic + r := s.cmdPrep(core.NewOptions(core.Option{Key: "_arg", Value: "nonexistent-repo"})) + assert.False(t, r.OK) // expected — no local repo +} + +func TestCmdStatus_Good_Empty(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdStatus(core.NewOptions()) + assert.True(t, r.OK) +} + +func TestCmdStatus_Good_WithWorkspaces(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + wsRoot := WorkspaceRoot() + ws := filepath.Join(wsRoot, "ws-1") + os.MkdirAll(ws, 0o755) + data, _ := json.Marshal(WorkspaceStatus{Status: "completed", Repo: "test", Agent: "codex"}) + os.WriteFile(filepath.Join(ws, "status.json"), data, 0o644) + + r := s.cmdStatus(core.NewOptions()) + assert.True(t, r.OK) +} + +func TestCmdPrompt_Bad_MissingRepo(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdPrompt(core.NewOptions()) + assert.False(t, r.OK) +} + +func TestCmdPrompt_Good_DefaultTask(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdPrompt(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"})) + assert.True(t, r.OK) +} + +func TestCmdExtract_Good(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + target := filepath.Join(t.TempDir(), "extract-test") + r := s.cmdExtract(core.NewOptions( + core.Option{Key: "_arg", Value: "default"}, + core.Option{Key: "target", Value: target}, + )) + assert.True(t, r.OK) +} + +func TestCmdRunTask_Bad_MissingArgs(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + r := s.cmdRunTask(ctx, core.NewOptions()) + assert.False(t, r.OK) +} + +func TestCmdRunTask_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"})) + assert.False(t, r.OK) +} + +func TestCmdOrchestrator_Good_CancelledCtx(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + r := s.cmdOrchestrator(ctx, core.NewOptions()) + assert.True(t, r.OK) +} + +func TestParseIntStr_Good(t *testing.T) { + assert.Equal(t, 42, parseIntStr("42")) + assert.Equal(t, 123, parseIntStr("issue-123")) + assert.Equal(t, 0, parseIntStr("")) + assert.Equal(t, 0, parseIntStr("abc")) + assert.Equal(t, 7, parseIntStr("#7")) +} + // --- Registration verification --- func TestRegisterCommands_Good_AllRegistered(t *testing.T) {