feat(agentic): add scan CLI command

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 11:44:26 +00:00
parent b20978f8d3
commit 0d05ccac55
2 changed files with 97 additions and 0 deletions

View file

@ -21,6 +21,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) {
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("generate", core.Command{Description: "Generate content from a prompt using the platform content pipeline", Action: s.cmdGenerate})
c.Command("scan", core.Command{Description: "Scan Forge repos for actionable issues", Action: s.cmdScan})
c.Command("brain/ingest", core.Command{Description: "Bulk ingest memories into OpenBrain", Action: s.cmdBrainIngest})
c.Command("brain/seed-memory", core.Command{Description: "Import markdown memories into OpenBrain from a project memory directory", Action: s.cmdBrainSeedMemory})
c.Command("plan-cleanup", core.Command{Description: "Permanently delete archived plans past the retention period", Action: s.cmdPlanCleanup})
@ -205,6 +206,36 @@ func (s *PrepSubsystem) cmdGenerate(options core.Options) core.Result {
return core.Result{OK: true}
}
func (s *PrepSubsystem) cmdScan(options core.Options) core.Result {
result := s.handleScan(s.commandContext(), core.NewOptions(
core.Option{Key: "org", Value: optionStringValue(options, "org")},
core.Option{Key: "labels", Value: optionStringSliceValue(options, "labels")},
core.Option{Key: "limit", Value: optionIntValue(options, "limit")},
))
if !result.OK {
err := commandResultError("agentic.cmdScan", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(ScanOutput)
if !ok {
err := core.E("agentic.cmdScan", "invalid scan output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "count: %d", output.Count)
for _, issue := range output.Issues {
if len(issue.Labels) > 0 {
core.Print(nil, " %s#%d %s [%s]", issue.Repo, issue.Number, issue.Title, core.Join(",", issue.Labels...))
continue
}
core.Print(nil, " %s#%d %s", issue.Repo, issue.Number, issue.Title)
}
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdStatus(_ core.Options) core.Result {
workspaceRoot := WorkspaceRoot()
filesystem := s.Core().Fs()

View file

@ -754,6 +754,71 @@ func TestCommands_CmdGenerate_Good_BriefTemplate(t *testing.T) {
assert.Contains(t, output, "content: Template draft")
}
func TestCommands_CmdScan_Good(t *testing.T) {
server := mockScanServer(t)
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forge: forge.NewForge(server.URL, "secret-token"),
forgeURL: server.URL,
forgeToken: "secret-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
output := captureStdout(t, func() {
r := s.cmdScan(core.NewOptions(
core.Option{Key: "org", Value: "core"},
core.Option{Key: "labels", Value: "agentic,bug"},
core.Option{Key: "limit", Value: 5},
))
assert.True(t, r.OK)
})
assert.Contains(t, output, "count:")
assert.Contains(t, output, "go-io#10")
assert.Contains(t, output, "Add missing tests")
}
func TestCommands_CmdScan_Bad_NoForgeToken(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
s.forgeToken = ""
r := s.cmdScan(core.NewOptions())
assert.False(t, r.OK)
}
func TestCommands_CmdScan_Ugly_EmptyResults(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/api/v1/orgs/core/repos":
_, _ = w.Write([]byte(core.JSONMarshalString([]map[string]any{
{"name": "go-io"},
})))
default:
_, _ = w.Write([]byte(core.JSONMarshalString([]map[string]any{})))
}
}))
defer server.Close()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forge: forge.NewForge(server.URL, "secret-token"),
forgeURL: server.URL,
forgeToken: "secret-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
output := captureStdout(t, func() {
r := s.cmdScan(core.NewOptions(
core.Option{Key: "org", Value: "core"},
core.Option{Key: "limit", Value: 1},
))
assert.True(t, r.OK)
})
assert.Contains(t, output, "count: 0")
}
func TestCommands_CmdPlanCreate_Good(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
@ -867,6 +932,7 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) {
assert.Contains(t, cmds, "run/task")
assert.Contains(t, cmds, "run/orchestrator")
assert.Contains(t, cmds, "prep")
assert.Contains(t, cmds, "scan")
assert.Contains(t, cmds, "status")
assert.Contains(t, cmds, "prompt")
assert.Contains(t, cmds, "extract")