feat(agentic): add brain list CLI command

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 11:54:44 +00:00
parent a50248f5ae
commit 524810cbda
3 changed files with 201 additions and 0 deletions

View file

@ -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 ""
}

View file

@ -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) {

View file

@ -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")