From 524810cbda23365063a8dbe20cb1379c29a0c005 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 11:54:44 +0000 Subject: [PATCH] feat(agentic): add brain list CLI command Co-Authored-By: Virgil --- pkg/agentic/commands.go | 138 +++++++++++++++++++++++++++++++++++ pkg/agentic/commands_test.go | 62 ++++++++++++++++ pkg/agentic/prep_test.go | 1 + 3 files changed, 201 insertions(+) diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 4d263a6..612cf12 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -24,6 +24,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { 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("brain/list", core.Command{Description: "List memories in OpenBrain", Action: s.cmdBrainList}) c.Command("plan-cleanup", core.Command{Description: "Permanently delete archived plans past the retention period", Action: s.cmdPlanCleanup}) c.Command("pr-manage", core.Command{Description: "Manage open PRs (merge, close, review)", Action: s.cmdPRManage}) c.Command("status", core.Command{Description: "List agent workspace statuses", Action: s.cmdStatus}) @@ -236,6 +237,65 @@ func (s *PrepSubsystem) cmdScan(options core.Options) core.Result { return core.Result{Value: output, OK: true} } +func (s *PrepSubsystem) cmdBrainList(options core.Options) core.Result { + result := s.Core().Action("brain.list").Run(s.commandContext(), core.NewOptions( + core.Option{Key: "project", Value: optionStringValue(options, "project")}, + core.Option{Key: "type", Value: optionStringValue(options, "type")}, + core.Option{Key: "agent_id", Value: optionStringValue(options, "agent_id", "agent")}, + core.Option{Key: "limit", Value: optionIntValue(options, "limit")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdBrainList", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + payload, ok := result.Value.(map[string]any) + if !ok { + jsonResult := core.JSONMarshalString(result.Value) + if jsonResult == "" { + err := core.E("agentic.cmdBrainList", "invalid brain list output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + var decoded any + if parseResult := core.JSONUnmarshalString(jsonResult, &decoded); !parseResult.OK { + err, _ := parseResult.Value.(error) + if err == nil { + err = core.E("agentic.cmdBrainList", "invalid brain list output", nil) + } + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + payload, ok = decoded.(map[string]any) + if !ok { + err := core.E("agentic.cmdBrainList", "invalid brain list output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + } + + output := brainListOutputFromPayload(payload) + core.Print(nil, "count: %d", output.Count) + if len(output.Memories) == 0 { + core.Print(nil, "no memories") + return core.Result{Value: output, OK: true} + } + + for _, memory := range output.Memories { + if memory.Project != "" || memory.AgentID != "" || memory.Confidence != 0 { + core.Print(nil, " %s %-12s %s %s %.2f", memory.ID, memory.Type, memory.Project, memory.AgentID, memory.Confidence) + } else { + core.Print(nil, " %s %-12s", memory.ID, memory.Type) + } + if memory.Content != "" { + core.Print(nil, " %s", memory.Content) + } + } + + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) cmdStatus(_ core.Options) core.Result { workspaceRoot := WorkspaceRoot() filesystem := s.Core().Fs() @@ -341,3 +401,81 @@ func parseIntString(s string) int { } return n } + +type brainListOutput struct { + Count int `json:"count"` + Memories []brainListOutputEntry `json:"memories"` +} + +type brainListOutputEntry struct { + ID string `json:"id"` + Type string `json:"type"` + Content string `json:"content"` + Project string `json:"project"` + AgentID string `json:"agent_id"` + Confidence float64 `json:"confidence"` + Tags []string `json:"tags"` +} + +func brainListOutputFromPayload(payload map[string]any) brainListOutput { + output := brainListOutput{} + switch count := payload["count"].(type) { + case float64: + output.Count = int(count) + case int: + output.Count = count + } + if memories, ok := payload["memories"].([]any); ok { + for _, item := range memories { + entryMap, ok := item.(map[string]any) + if !ok { + continue + } + entry := brainListOutputEntry{ + ID: brainListStringValue(entryMap["id"]), + Type: brainListStringValue(entryMap["type"]), + Content: brainListStringValue(entryMap["content"]), + Project: brainListStringValue(entryMap["project"]), + AgentID: brainListStringValue(entryMap["agent_id"]), + } + switch confidence := entryMap["confidence"].(type) { + case float64: + entry.Confidence = confidence + case int: + entry.Confidence = float64(confidence) + } + if entry.Confidence == 0 { + switch confidence := entryMap["score"].(type) { + case float64: + entry.Confidence = confidence + case int: + entry.Confidence = float64(confidence) + } + } + if tags, ok := entryMap["tags"].([]any); ok { + for _, tag := range tags { + entry.Tags = append(entry.Tags, brainListStringValue(tag)) + } + } + output.Memories = append(output.Memories, entry) + } + } + if output.Count == 0 { + output.Count = len(output.Memories) + } + return output +} + +func brainListStringValue(value any) string { + switch typed := value.(type) { + case string: + return typed + case int: + return core.Sprint(typed) + case int64: + return core.Sprint(typed) + case float64: + return core.Sprint(typed) + } + return "" +} diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index f8ff353..d27f77b 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -225,6 +225,68 @@ func TestCommandsforge_CmdIssueCreate_Good_WithLabelsAndMilestone(t *testing.T) assert.True(t, r.OK) } +func TestCommands_CmdBrainList_Good(t *testing.T) { + s, c := testPrepWithCore(t, nil) + c.Action("brain.list", func(_ context.Context, options core.Options) core.Result { + assert.Equal(t, "agent", options.String("project")) + assert.Equal(t, "architecture", options.String("type")) + assert.Equal(t, "virgil", options.String("agent_id")) + return core.Result{Value: map[string]any{ + "success": true, + "count": 1, + "memories": []any{ + map[string]any{ + "id": "mem-1", + "type": "architecture", + "content": "Use named actions.", + "project": "agent", + "agent_id": "virgil", + "confidence": 0.9, + "tags": []any{"architecture", "convention"}, + }, + }, + }, OK: true} + }) + + output := captureStdout(t, func() { + result := s.cmdBrainList(core.NewOptions( + core.Option{Key: "project", Value: "agent"}, + core.Option{Key: "type", Value: "architecture"}, + core.Option{Key: "agent", Value: "virgil"}, + )) + require.True(t, result.OK) + }) + + assert.Contains(t, output, "count: 1") + assert.Contains(t, output, "mem-1 architecture") + assert.Contains(t, output, "Use named actions.") +} + +func TestCommands_CmdBrainList_Bad_MissingAction(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + result := s.cmdBrainList(core.NewOptions()) + + require.False(t, result.OK) + err, ok := result.Value.(error) + require.True(t, ok) + assert.Contains(t, err.Error(), "action not registered") +} + +func TestCommands_CmdBrainList_Ugly_InvalidOutput(t *testing.T) { + s, c := testPrepWithCore(t, nil) + c.Action("brain.list", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: 123, OK: true} + }) + + result := s.cmdBrainList(core.NewOptions()) + + require.False(t, result.OK) + err, ok := result.Value.(error) + require.True(t, ok) + assert.Contains(t, err.Error(), "invalid brain list output") +} + func TestCommandsforge_CmdIssueCreate_Bad_APIError(t *testing.T) { callCount := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 06145e7..7abecb2 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -583,6 +583,7 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) { assert.Contains(t, c.Commands(), "generate") assert.Contains(t, c.Commands(), "brain/ingest") assert.Contains(t, c.Commands(), "brain/seed-memory") + assert.Contains(t, c.Commands(), "brain/list") assert.Contains(t, c.Commands(), "plan-cleanup") assert.Contains(t, c.Commands(), "task") assert.Contains(t, c.Commands(), "task/create")