feat(agentic): add brain seed-memory command

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 11:04:36 +00:00
parent 3d528e6963
commit 155230fb5b
4 changed files with 438 additions and 0 deletions

View file

@ -0,0 +1,309 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"sort"
core "dappco.re/go/core"
)
const brainSeedMemoryDefaultAgent = "virgil"
const brainSeedMemoryDefaultPath = "~/.claude/projects/*/memory/"
type BrainSeedMemoryInput struct {
WorkspaceID int
AgentID string
Path string
DryRun bool
}
type BrainSeedMemoryOutput struct {
Success bool `json:"success"`
WorkspaceID int `json:"workspace_id,omitempty"`
AgentID string `json:"agent_id,omitempty"`
Path string `json:"path,omitempty"`
Files int `json:"files,omitempty"`
Imported int `json:"imported,omitempty"`
Skipped int `json:"skipped,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
type brainSeedMemorySection struct {
Heading string
Content string
}
// result := c.Command("brain/seed-memory").Run(ctx, core.NewOptions(
//
// core.Option{Key: "workspace", Value: "1"},
// core.Option{Key: "path", Value: "/Users/snider/.claude/projects/*/memory/"},
//
// ))
func (s *PrepSubsystem) cmdBrainSeedMemory(options core.Options) core.Result {
input := BrainSeedMemoryInput{
WorkspaceID: parseIntString(optionStringValue(options, "workspace", "workspace_id", "workspace-id", "_arg")),
AgentID: optionStringValue(options, "agent", "agent_id", "agent-id"),
Path: optionStringValue(options, "path"),
DryRun: optionBoolValue(options, "dry-run"),
}
if input.WorkspaceID == 0 {
core.Print(nil, "usage: core-agent brain seed-memory --workspace=1 [--agent=virgil] [--path=~/.claude/projects/*/memory/] [--dry-run]")
return core.Result{Value: core.E("agentic.cmdBrainSeedMemory", "workspace is required", nil), OK: false}
}
if input.AgentID == "" {
input.AgentID = brainSeedMemoryDefaultAgent
}
if input.Path == "" {
input.Path = brainSeedMemoryDefaultPath
}
result := s.brainSeedMemory(s.commandContext(), input)
if !result.OK {
err := commandResultError("agentic.cmdBrainSeedMemory", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(BrainSeedMemoryOutput)
if !ok {
err := core.E("agentic.cmdBrainSeedMemory", "invalid brain seed memory output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
if output.Files == 0 {
core.Print(nil, "No markdown files found in: %s", output.Path)
return core.Result{Value: output, OK: true}
}
prefix := ""
if output.DryRun {
prefix = "[DRY RUN] "
}
core.Print(nil, "%sImported %d memories, skipped %d.", prefix, output.Imported, output.Skipped)
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) brainSeedMemory(ctx context.Context, input BrainSeedMemoryInput) core.Result {
if s.brainKey == "" {
return core.Result{Value: core.E("agentic.brainSeedMemory", "no brain API key configured", nil), OK: false}
}
scanPath := brainSeedMemoryScanPath(input.Path)
files := brainSeedMemoryFiles(scanPath)
output := BrainSeedMemoryOutput{
Success: true,
WorkspaceID: input.WorkspaceID,
AgentID: input.AgentID,
Path: scanPath,
Files: len(files),
DryRun: input.DryRun,
}
if len(files) == 0 {
return core.Result{Value: output, OK: true}
}
for _, path := range files {
readResult := fs.Read(path)
if !readResult.OK {
output.Skipped++
continue
}
sections := brainSeedMemorySections(readResult.Value.(string))
if len(sections) == 0 {
output.Skipped++
core.Print(nil, " Skipped %s (no sections found)", core.PathBase(path))
continue
}
project := brainSeedMemoryProject(path)
filename := core.TrimSuffix(core.PathBase(path), ".md")
for _, section := range sections {
memoryType := brainSeedMemoryType(section.Heading, section.Content)
if input.DryRun {
core.Print(nil, " [DRY RUN] %s :: %s (%s) — %d chars", core.PathBase(path), section.Heading, memoryType, core.RuneCount(section.Content))
output.Imported++
continue
}
body := map[string]any{
"workspace_id": input.WorkspaceID,
"agent_id": input.AgentID,
"type": memoryType,
"content": core.Concat(section.Heading, "\n\n", section.Content),
"tags": brainSeedMemoryTags(filename),
"project": project,
"confidence": 0.7,
}
if result := HTTPPost(ctx, core.Concat(s.brainURL, "/v1/brain/remember"), core.JSONMarshalString(body), s.brainKey, "Bearer"); !result.OK {
output.Skipped++
core.Print(nil, " Failed to import %s :: %s", core.PathBase(path), section.Heading)
continue
}
output.Imported++
}
}
return core.Result{Value: output, OK: true}
}
func brainSeedMemoryScanPath(path string) string {
trimmed := brainSeedMemoryExpandHome(core.Trim(path))
if trimmed == "" {
return brainSeedMemoryExpandHome(brainSeedMemoryDefaultPath)
}
if fs.IsFile(trimmed) || core.HasSuffix(trimmed, ".md") {
return trimmed
}
return core.JoinPath(trimmed, "*.md")
}
func brainSeedMemoryExpandHome(path string) string {
if core.HasPrefix(path, "~/") {
return core.Concat(HomeDir(), core.TrimPrefix(path, "~"))
}
return path
}
func brainSeedMemoryFiles(scanPath string) []string {
if scanPath == "" {
return nil
}
if fs.IsFile(scanPath) {
return []string{scanPath}
}
files := core.PathGlob(scanPath)
sort.Strings(files)
return files
}
func brainSeedMemorySections(content string) []brainSeedMemorySection {
lines := core.Split(content, "\n")
var sections []brainSeedMemorySection
currentHeading := ""
var currentContent []string
flush := func() {
if currentHeading == "" || len(currentContent) == 0 {
return
}
sectionContent := core.Trim(core.Join("\n", currentContent...))
if sectionContent == "" {
return
}
sections = append(sections, brainSeedMemorySection{
Heading: currentHeading,
Content: sectionContent,
})
}
for _, line := range lines {
if heading, ok := brainSeedMemoryHeading(line); ok {
flush()
currentHeading = heading
currentContent = currentContent[:0]
continue
}
if currentHeading == "" {
continue
}
currentContent = append(currentContent, line)
}
flush()
return sections
}
func brainSeedMemoryHeading(line string) (string, bool) {
trimmed := core.Trim(line)
if trimmed == "" || !core.HasPrefix(trimmed, "#") {
return "", false
}
hashes := 0
for _, r := range trimmed {
if r != '#' {
break
}
hashes++
}
if hashes < 1 || hashes > 3 || len(trimmed) <= hashes || trimmed[hashes] != ' ' {
return "", false
}
heading := core.Trim(trimmed[hashes:])
if heading == "" {
return "", false
}
return heading, true
}
func brainSeedMemoryType(heading, content string) string {
lower := core.Lower(core.Concat(heading, " ", content))
for _, candidate := range []struct {
memoryType string
keywords []string
}{
{memoryType: "architecture", keywords: []string{"architecture", "stack", "infrastructure", "layer", "service mesh"}},
{memoryType: "convention", keywords: []string{"convention", "standard", "naming", "pattern", "rule", "coding"}},
{memoryType: "decision", keywords: []string{"decision", "chose", "strategy", "approach", "domain"}},
{memoryType: "bug", keywords: []string{"bug", "fix", "broken", "error", "issue", "lesson"}},
{memoryType: "plan", keywords: []string{"plan", "todo", "roadmap", "milestone", "phase"}},
{memoryType: "research", keywords: []string{"research", "finding", "discovery", "analysis", "rfc"}},
} {
for _, keyword := range candidate.keywords {
if core.Contains(lower, keyword) {
return candidate.memoryType
}
}
}
return "observation"
}
func brainSeedMemoryTags(filename string) []string {
if filename == "" {
return []string{"memory-import"}
}
tags := []string{}
if core.Lower(filename) != "memory" {
tag := core.Replace(core.Replace(filename, "-", " "), "_", " ")
if tag != "" {
tags = append(tags, tag)
}
}
tags = append(tags, "memory-import")
return tags
}
func brainSeedMemoryProject(path string) string {
normalised := core.Replace(path, "\\", "/")
segments := core.Split(normalised, "/")
for i := 1; i < len(segments); i++ {
if segments[i] != "memory" {
continue
}
projectSegment := segments[i-1]
if projectSegment == "" {
continue
}
chunks := core.Split(projectSegment, "-")
for j := len(chunks) - 1; j >= 0; j-- {
if chunks[j] != "" {
return chunks[j]
}
}
}
return ""
}

View file

@ -0,0 +1,127 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"net/http"
"net/http/httptest"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBrainSeedMemory_CmdBrainSeedMemory_Good(t *testing.T) {
home := t.TempDir()
t.Setenv("CORE_HOME", home)
memoryDir := core.JoinPath(home, ".claude", "projects", "-Users-snider-Code-eaas", "memory")
require.True(t, fs.EnsureDir(memoryDir).OK)
require.True(t, fs.Write(core.JoinPath(memoryDir, "MEMORY.md"), "# Memory\n\n## Architecture\nUse Core.Process().\n\n## Decision\nPrefer named actions.").OK)
require.True(t, fs.Write(core.JoinPath(memoryDir, "notes.md"), "## Convention\nUse UK English.\n").OK)
var bodies []map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/brain/remember", r.URL.Path)
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
require.True(t, core.JSONUnmarshalString(bodyResult.Value.(string), &payload).OK)
bodies = append(bodies, payload)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"memory":{"id":"mem-1"}}`))
}))
defer srv.Close()
subsystem := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "brain-key",
}
result := subsystem.cmdBrainSeedMemory(core.NewOptions(
core.Option{Key: "workspace", Value: "42"},
core.Option{Key: "path", Value: memoryDir},
core.Option{Key: "agent", Value: "virgil"},
))
require.True(t, result.OK)
output, ok := result.Value.(BrainSeedMemoryOutput)
require.True(t, ok)
assert.Equal(t, 2, output.Files)
assert.Equal(t, 3, output.Imported)
assert.Equal(t, 0, output.Skipped)
assert.Equal(t, false, output.DryRun)
assert.Equal(t, core.JoinPath(memoryDir, "*.md"), output.Path)
require.Len(t, bodies, 3)
assert.Equal(t, float64(42), bodies[0]["workspace_id"])
assert.Equal(t, "virgil", bodies[0]["agent_id"])
assert.Equal(t, "architecture", bodies[0]["type"])
assert.Equal(t, "eaas", bodies[0]["project"])
assert.Contains(t, bodies[0]["content"].(string), "Architecture")
assert.Equal(t, []any{"memory-import"}, bodies[0]["tags"])
assert.Equal(t, "decision", bodies[1]["type"])
assert.Equal(t, []any{"memory-import"}, bodies[1]["tags"])
assert.Equal(t, "convention", bodies[2]["type"])
assert.Equal(t, []any{"notes", "memory-import"}, bodies[2]["tags"])
}
func TestBrainSeedMemory_CmdBrainSeedMemory_Bad_MissingWorkspace(t *testing.T) {
subsystem := &PrepSubsystem{brainURL: "https://example.com", brainKey: "brain-key"}
result := subsystem.cmdBrainSeedMemory(core.NewOptions(
core.Option{Key: "path", Value: "/tmp/memory"},
))
require.False(t, result.OK)
err, ok := result.Value.(error)
require.True(t, ok)
assert.Contains(t, err.Error(), "workspace is required")
}
func TestBrainSeedMemory_CmdBrainSeedMemory_Ugly_PartialImportFailure(t *testing.T) {
home := t.TempDir()
t.Setenv("CORE_HOME", home)
memoryDir := core.JoinPath(home, ".claude", "projects", "-Users-snider-Code-eaas", "memory")
require.True(t, fs.EnsureDir(memoryDir).OK)
require.True(t, fs.Write(core.JoinPath(memoryDir, "MEMORY.md"), "## Architecture\nUse Core.Process().\n\n## Decision\nPrefer named actions.").OK)
var calls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
require.True(t, core.JSONUnmarshalString(bodyResult.Value.(string), &payload).OK)
if calls == 1 {
http.Error(w, "boom", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"memory":{"id":"mem-2"}}`))
}))
defer srv.Close()
subsystem := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "brain-key",
}
result := subsystem.brainSeedMemory(context.Background(), BrainSeedMemoryInput{
WorkspaceID: 42,
AgentID: "virgil",
Path: memoryDir,
})
require.True(t, result.OK)
output, ok := result.Value.(BrainSeedMemoryOutput)
require.True(t, ok)
assert.Equal(t, 1, output.Imported)
assert.Equal(t, 1, output.Skipped)
assert.Equal(t, 2, calls)
}

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("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})
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})

View file

@ -580,6 +580,7 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) {
require.True(t, s.OnStartup(context.Background()).OK)
assert.Contains(t, c.Commands(), "generate")
assert.Contains(t, c.Commands(), "brain/seed-memory")
assert.Contains(t, c.Commands(), "plan-cleanup")
}