From ede5d6f561f0a88894fb092c32846cc6c781179c Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 09:08:35 +0000 Subject: [PATCH 1/5] feat(lib): migrate to Core Embed system with Result returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All public functions return core.Result instead of (string, error) - Mount scopes basedir — no path prefix needed in ReadString calls - Add Bundle struct replacing (string, map, error) anti-pattern - listDir takes *core.Embed not embed.FS - ListTasks/ListPersonas use FS() + BaseDirectory() for WalkDir - Remove bytes, os, text/template imports - 22 tests: Prompt, Task, TaskBundle, Flow, Persona, Template, List functions, ExtractWorkspace (Good/Bad patterns) Co-Authored-By: Virgil --- pkg/lib/lib.go | 218 +++++++++++++-------------- pkg/lib/lib_test.go | 197 +++++++++++++++++++++++- pkg/lib/workspace/default/.gitignore | 4 - 3 files changed, 300 insertions(+), 119 deletions(-) delete mode 100644 pkg/lib/workspace/default/.gitignore diff --git a/pkg/lib/lib.go b/pkg/lib/lib.go index b976a81..41dc379 100644 --- a/pkg/lib/lib.go +++ b/pkg/lib/lib.go @@ -14,104 +14,134 @@ // // Usage: // -// prompt, _ := lib.Prompt("coding") -// task, _ := lib.Task("code/review") -// persona, _ := lib.Persona("secops/developer") -// flow, _ := lib.Flow("go") +// r := lib.Prompt("coding") // r.Value.(string) +// r := lib.Task("code/review") // r.Value.(string) +// r := lib.Persona("secops/dev") // r.Value.(string) +// r := lib.Flow("go") // r.Value.(string) // lib.ExtractWorkspace("default", "/tmp/ws", data) package lib import ( - "bytes" "embed" "io/fs" - "os" "path/filepath" - "text/template" core "dappco.re/go/core" ) -//go:embed prompt/*.md -var promptFS embed.FS +//go:embed all:prompt +var promptFiles embed.FS //go:embed all:task -var taskFS embed.FS +var taskFiles embed.FS -//go:embed flow/*.md -var flowFS embed.FS +//go:embed all:flow +var flowFiles embed.FS -//go:embed persona -var personaFS embed.FS +//go:embed all:persona +var personaFiles embed.FS //go:embed all:workspace -var workspaceFS embed.FS +var workspaceFiles embed.FS + +var ( + promptFS = mustMount(promptFiles, "prompt") + taskFS = mustMount(taskFiles, "task") + flowFS = mustMount(flowFiles, "flow") + personaFS = mustMount(personaFiles, "persona") + workspaceFS = mustMount(workspaceFiles, "workspace") +) + +func mustMount(fsys embed.FS, basedir string) *core.Embed { + r := core.Mount(fsys, basedir) + if !r.OK { + panic(r.Value) + } + return r.Value.(*core.Embed) +} // --- Prompts --- // Template tries Prompt then Task (backwards compat). -func Template(slug string) (string, error) { - if content, err := Prompt(slug); err == nil { - return content, nil +// +// r := lib.Template("coding") +// if r.OK { content := r.Value.(string) } +func Template(slug string) core.Result { + if r := Prompt(slug); r.OK { + return r } return Task(slug) } -func Prompt(slug string) (string, error) { - data, err := promptFS.ReadFile("prompt/" + slug + ".md") - if err != nil { - return "", err - } - return string(data), nil +// Prompt reads a system prompt by slug. +// +// r := lib.Prompt("coding") +// if r.OK { content := r.Value.(string) } +func Prompt(slug string) core.Result { + return promptFS.ReadString(slug + ".md") } -func Task(slug string) (string, error) { +// Task reads a structured task plan by slug. Tries .md, .yaml, .yml. +// +// r := lib.Task("code/review") +// if r.OK { content := r.Value.(string) } +func Task(slug string) core.Result { for _, ext := range []string{".md", ".yaml", ".yml"} { - data, err := taskFS.ReadFile("task/" + slug + ext) - if err == nil { - return string(data), nil + if r := taskFS.ReadString(slug + ext); r.OK { + return r } } - return "", fs.ErrNotExist + return core.Result{Value: fs.ErrNotExist} } -func TaskBundle(slug string) (string, map[string]string, error) { - main, err := Task(slug) - if err != nil { - return "", nil, err +// Bundle holds a task's main content plus companion files. +// +// r := lib.TaskBundle("code/review") +// if r.OK { b := r.Value.(lib.Bundle) } +type Bundle struct { + Main string + Files map[string]string +} + +// TaskBundle reads a task and its companion files. +// +// r := lib.TaskBundle("code/review") +// if r.OK { b := r.Value.(lib.Bundle) } +func TaskBundle(slug string) core.Result { + main := Task(slug) + if !main.OK { + return main } - bundleDir := "task/" + slug - entries, err := fs.ReadDir(taskFS, bundleDir) - if err != nil { - return main, nil, nil + b := Bundle{Main: main.Value.(string), Files: make(map[string]string)} + r := taskFS.ReadDir(slug) + if !r.OK { + return core.Result{Value: b, OK: true} } - bundle := make(map[string]string) - for _, e := range entries { + for _, e := range r.Value.([]fs.DirEntry) { if e.IsDir() { continue } - data, err := taskFS.ReadFile(bundleDir + "/" + e.Name()) - if err == nil { - bundle[e.Name()] = string(data) + if fr := taskFS.ReadString(slug + "/" + e.Name()); fr.OK { + b.Files[e.Name()] = fr.Value.(string) } } - return main, bundle, nil + return core.Result{Value: b, OK: true} } -func Flow(slug string) (string, error) { - data, err := flowFS.ReadFile("flow/" + slug + ".md") - if err != nil { - return "", err - } - return string(data), nil +// Flow reads a build/release workflow by slug. +// +// r := lib.Flow("go") +// if r.OK { content := r.Value.(string) } +func Flow(slug string) core.Result { + return flowFS.ReadString(slug + ".md") } -func Persona(path string) (string, error) { - data, err := personaFS.ReadFile("persona/" + path + ".md") - if err != nil { - return "", err - } - return string(data), nil +// Persona reads a domain/role persona by path. +// +// r := lib.Persona("secops/developer") +// if r.OK { content := r.Value.(string) } +func Persona(path string) core.Result { + return personaFS.ReadString(path + ".md") } // --- Workspace Templates --- @@ -138,67 +168,36 @@ type WorkspaceData struct { // ExtractWorkspace creates an agent workspace from a template. // Template names: "default", "security", "review". func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) error { - wsDir := "workspace/" + tmplName - - if err := os.MkdirAll(targetDir, 0755); err != nil { - return err + r := workspaceFS.Sub(tmplName) + if !r.OK { + if err, ok := r.Value.(error); ok { + return err + } + return core.E("ExtractWorkspace", "template not found: "+tmplName, nil) } - - return fs.WalkDir(workspaceFS, wsDir, func(path string, d fs.DirEntry, err error) error { - if err != nil { + result := core.Extract(r.Value.(*core.Embed).FS(), targetDir, data) + if !result.OK { + if err, ok := result.Value.(error); ok { return err } - - // Get relative path from template root - rel, err := filepath.Rel(wsDir, path) - if err != nil || rel == "." { - return nil - } - - targetPath := filepath.Join(targetDir, rel) - - if d.IsDir() { - return os.MkdirAll(targetPath, 0755) - } - - content, err := fs.ReadFile(workspaceFS, path) - if err != nil { - return err - } - - // Process .tmpl files through text/template - outputName := filepath.Base(targetPath) - if core.HasSuffix(outputName, ".tmpl") { - outputName = core.TrimSuffix(outputName, ".tmpl") - targetPath = filepath.Join(filepath.Dir(targetPath), outputName) - tmpl, err := template.New(outputName).Parse(string(content)) - if err != nil { - return err - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return err - } - content = buf.Bytes() - } - - return os.WriteFile(targetPath, content, 0644) - }) + } + return nil } // --- List Functions --- -func ListPrompts() []string { return listDir(promptFS, "prompt") } -func ListFlows() []string { return listDir(flowFS, "flow") } -func ListWorkspaces() []string { return listDir(workspaceFS, "workspace") } +func ListPrompts() []string { return listDir(promptFS) } +func ListFlows() []string { return listDir(flowFS) } +func ListWorkspaces() []string { return listDir(workspaceFS) } func ListTasks() []string { var slugs []string - fs.WalkDir(taskFS, "task", func(path string, d fs.DirEntry, err error) error { + base := taskFS.BaseDirectory() + fs.WalkDir(taskFS.FS(), base, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return nil } - rel := core.TrimPrefix(path, "task/") + rel := core.TrimPrefix(path, base+"/") ext := filepath.Ext(rel) slugs = append(slugs, core.TrimSuffix(rel, ext)) return nil @@ -208,12 +207,13 @@ func ListTasks() []string { func ListPersonas() []string { var paths []string - fs.WalkDir(personaFS, "persona", func(path string, d fs.DirEntry, err error) error { + base := personaFS.BaseDirectory() + fs.WalkDir(personaFS.FS(), base, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return nil } if core.HasSuffix(path, ".md") { - rel := core.TrimPrefix(path, "persona/") + rel := core.TrimPrefix(path, base+"/") rel = core.TrimSuffix(rel, ".md") paths = append(paths, rel) } @@ -222,13 +222,13 @@ func ListPersonas() []string { return paths } -func listDir(fsys embed.FS, dir string) []string { - entries, err := fsys.ReadDir(dir) - if err != nil { +func listDir(emb *core.Embed) []string { + r := emb.ReadDir(".") + if !r.OK { return nil } var slugs []string - for _, e := range entries { + for _, e := range r.Value.([]fs.DirEntry) { name := e.Name() if e.IsDir() { slugs = append(slugs, name) diff --git a/pkg/lib/lib_test.go b/pkg/lib/lib_test.go index 9b6b10e..32fc1c9 100644 --- a/pkg/lib/lib_test.go +++ b/pkg/lib/lib_test.go @@ -1,11 +1,202 @@ package lib import ( + "io/fs" "os" "path/filepath" "testing" ) +// --- Prompt --- + +func TestPrompt_Good(t *testing.T) { + r := Prompt("coding") + if !r.OK { + t.Fatal("Prompt('coding') returned !OK") + } + if r.Value.(string) == "" { + t.Error("Prompt('coding') returned empty string") + } +} + +func TestPrompt_Bad(t *testing.T) { + r := Prompt("nonexistent-slug") + if r.OK { + t.Error("Prompt('nonexistent-slug') should return !OK") + } +} + +// --- Task --- + +func TestTask_Good_Yaml(t *testing.T) { + r := Task("bug-fix") + if !r.OK { + t.Fatal("Task('bug-fix') returned !OK") + } + if r.Value.(string) == "" { + t.Error("Task('bug-fix') returned empty string") + } +} + +func TestTask_Good_Md(t *testing.T) { + r := Task("code/review") + if !r.OK { + t.Fatal("Task('code/review') returned !OK") + } + if r.Value.(string) == "" { + t.Error("Task('code/review') returned empty string") + } +} + +func TestTask_Bad(t *testing.T) { + r := Task("nonexistent-slug") + if r.OK { + t.Error("Task('nonexistent-slug') should return !OK") + } + if r.Value != fs.ErrNotExist { + t.Error("Task('nonexistent-slug') should return fs.ErrNotExist") + } +} + +// --- TaskBundle --- + +func TestTaskBundle_Good(t *testing.T) { + r := TaskBundle("code/review") + if !r.OK { + t.Fatal("TaskBundle('code/review') returned !OK") + } + b := r.Value.(Bundle) + if b.Main == "" { + t.Error("Bundle.Main is empty") + } + if len(b.Files) == 0 { + t.Error("Bundle.Files is empty — expected companion files") + } +} + +func TestTaskBundle_Bad(t *testing.T) { + r := TaskBundle("nonexistent") + if r.OK { + t.Error("TaskBundle('nonexistent') should return !OK") + } +} + +// --- Flow --- + +func TestFlow_Good(t *testing.T) { + r := Flow("go") + if !r.OK { + t.Fatal("Flow('go') returned !OK") + } + if r.Value.(string) == "" { + t.Error("Flow('go') returned empty string") + } +} + +// --- Persona --- + +func TestPersona_Good(t *testing.T) { + // Use first persona from list to avoid hardcoding + personas := ListPersonas() + if len(personas) == 0 { + t.Skip("no personas found") + } + r := Persona(personas[0]) + if !r.OK { + t.Fatalf("Persona(%q) returned !OK", personas[0]) + } + if r.Value.(string) == "" { + t.Errorf("Persona(%q) returned empty string", personas[0]) + } +} + +// --- Template --- + +func TestTemplate_Good_Prompt(t *testing.T) { + r := Template("coding") + if !r.OK { + t.Fatal("Template('coding') returned !OK") + } + if r.Value.(string) == "" { + t.Error("Template('coding') returned empty string") + } +} + +func TestTemplate_Good_TaskFallback(t *testing.T) { + r := Template("bug-fix") + if !r.OK { + t.Fatal("Template('bug-fix') returned !OK — should fall through to Task") + } +} + +func TestTemplate_Bad(t *testing.T) { + r := Template("nonexistent-slug") + if r.OK { + t.Error("Template('nonexistent-slug') should return !OK") + } +} + +// --- List Functions --- + +func TestListPrompts(t *testing.T) { + prompts := ListPrompts() + if len(prompts) == 0 { + t.Error("ListPrompts() returned empty") + } +} + +func TestListTasks(t *testing.T) { + tasks := ListTasks() + if len(tasks) == 0 { + t.Fatal("ListTasks() returned empty") + } + // Verify nested paths are included (e.g., "code/review") + found := false + for _, s := range tasks { + if s == "code/review" { + found = true + break + } + } + if !found { + t.Error("ListTasks() missing nested path 'code/review'") + } +} + +func TestListPersonas(t *testing.T) { + personas := ListPersonas() + if len(personas) == 0 { + t.Error("ListPersonas() returned empty") + } + // Should have nested paths like "code/go" + hasNested := false + for _, p := range personas { + if len(p) > 0 && filepath.Dir(p) != "." { + hasNested = true + break + } + } + if !hasNested { + t.Error("ListPersonas() has no nested paths") + } +} + +func TestListFlows(t *testing.T) { + flows := ListFlows() + if len(flows) == 0 { + t.Error("ListFlows() returned empty") + } +} + +func TestListWorkspaces(t *testing.T) { + workspaces := ListWorkspaces() + if len(workspaces) == 0 { + t.Error("ListWorkspaces() returned empty") + } +} + +// --- ExtractWorkspace --- + func TestExtractWorkspace_CreatesFiles(t *testing.T) { dir := t.TempDir() data := &WorkspaceData{Repo: "test-repo", Task: "test task"} @@ -15,7 +206,6 @@ func TestExtractWorkspace_CreatesFiles(t *testing.T) { t.Fatalf("ExtractWorkspace failed: %v", err) } - // Check top-level template files exist for _, name := range []string{"CODEX.md", "CLAUDE.md", "PROMPT.md", "TODO.md", "CONTEXT.md"} { path := filepath.Join(dir, name) if _, err := os.Stat(path); os.IsNotExist(err) { @@ -33,19 +223,16 @@ func TestExtractWorkspace_CreatesSubdirectories(t *testing.T) { t.Fatalf("ExtractWorkspace failed: %v", err) } - // Check .core/reference/ directory exists with files refDir := filepath.Join(dir, ".core", "reference") if _, err := os.Stat(refDir); os.IsNotExist(err) { t.Fatalf(".core/reference/ directory not created") } - // Check AX spec exists axSpec := filepath.Join(refDir, "RFC-025-AGENT-EXPERIENCE.md") if _, err := os.Stat(axSpec); os.IsNotExist(err) { t.Errorf("AX spec not extracted: %s", axSpec) } - // Check Core source files exist entries, err := os.ReadDir(refDir) if err != nil { t.Fatalf("failed to read reference dir: %v", err) @@ -61,7 +248,6 @@ func TestExtractWorkspace_CreatesSubdirectories(t *testing.T) { t.Error("no .go files in .core/reference/") } - // Check docs subdirectory docsDir := filepath.Join(refDir, "docs") if _, err := os.Stat(docsDir); os.IsNotExist(err) { t.Errorf(".core/reference/docs/ not created") @@ -77,7 +263,6 @@ func TestExtractWorkspace_TemplateSubstitution(t *testing.T) { t.Fatalf("ExtractWorkspace failed: %v", err) } - // TODO.md should contain the task content, err := os.ReadFile(filepath.Join(dir, "TODO.md")) if err != nil { t.Fatalf("failed to read TODO.md: %v", err) diff --git a/pkg/lib/workspace/default/.gitignore b/pkg/lib/workspace/default/.gitignore deleted file mode 100644 index cdc6f76..0000000 --- a/pkg/lib/workspace/default/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea/ -.vscode/ -*.log -.core/ From 3022f05fb87f73f262686276a459f2dbd98258ab Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 09:08:45 +0000 Subject: [PATCH 2/5] refactor(agentic): route file I/O through core.Fs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw os.* file operations with Core Fs equivalents: - os.Stat → fs.Exists/fs.IsFile/fs.IsDir (resume, pr, plan, mirror, prep) - os.ReadDir → fs.List (queue, status, plan, mirror, review_queue) - os.Remove → fs.Delete (dispatch) - os.OpenFile(append) → fs.Append (events, review_queue) - strings.Replace → core.Replace (scan) Eliminates os import from resume.go, pr.go. Eliminates strings import from scan.go. Trades os for io in events.go. Co-Authored-By: Virgil --- pkg/agentic/dispatch.go | 2 +- pkg/agentic/events.go | 11 ++++++----- pkg/agentic/mirror.go | 7 ++++--- pkg/agentic/plan.go | 9 +++++---- pkg/agentic/pr.go | 3 +-- pkg/agentic/prep.go | 36 ++++++++++++++++++++++-------------- pkg/agentic/queue.go | 10 ++++++---- pkg/agentic/resume.go | 3 +-- pkg/agentic/review_queue.go | 16 ++++++++++------ pkg/agentic/scan.go | 3 +-- pkg/agentic/status.go | 7 ++++--- 11 files changed, 61 insertions(+), 46 deletions(-) diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index ef98bc5..0fd2e47 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -125,7 +125,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st // Clean up stale BLOCKED.md from previous runs so it doesn't // prevent this run from completing - os.Remove(core.JoinPath(srcDir, "BLOCKED.md")) + fs.Delete(core.JoinPath(srcDir, "BLOCKED.md")) proc, err := process.StartWithOptions(context.Background(), process.RunOptions{ Command: command, diff --git a/pkg/agentic/events.go b/pkg/agentic/events.go index 4b0a66f..be02918 100644 --- a/pkg/agentic/events.go +++ b/pkg/agentic/events.go @@ -4,7 +4,7 @@ package agentic import ( "encoding/json" - "os" + "io" "time" core "dappco.re/go/core" @@ -42,10 +42,11 @@ func emitCompletionEvent(agent, workspace, status string) { } // Append to events log - f, err := os.OpenFile(eventsFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { + r := fs.Append(eventsFile) + if !r.OK { return } - defer f.Close() - f.Write(append(data, '\n')) + wc := r.Value.(io.WriteCloser) + defer wc.Close() + wc.Write(append(data, '\n')) } diff --git a/pkg/agentic/mirror.go b/pkg/agentic/mirror.go index 934da95..bb57d74 100644 --- a/pkg/agentic/mirror.go +++ b/pkg/agentic/mirror.go @@ -246,17 +246,18 @@ func filesChanged(repoDir, base, head string) int { // listLocalRepos returns repo names that exist as directories in basePath. func (s *PrepSubsystem) listLocalRepos(basePath string) []string { - entries, err := os.ReadDir(basePath) - if err != nil { + r := fs.List(basePath) + if !r.OK { return nil } + entries := r.Value.([]os.DirEntry) var repos []string for _, e := range entries { if !e.IsDir() { continue } // Must have a .git directory - if _, err := os.Stat(core.JoinPath(basePath, e.Name(), ".git")); err == nil { + if fs.IsDir(core.JoinPath(basePath, e.Name(), ".git")) { repos = append(repos, e.Name()) } } diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index 2e447fb..d2fdde8 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -279,7 +279,7 @@ func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, in } path := planPath(PlansRoot(), input.ID) - if _, err := os.Stat(path); err != nil { + if !fs.Exists(path) { return nil, PlanDeleteOutput{}, core.E("planDelete", "plan not found: "+input.ID, nil) } @@ -301,10 +301,11 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu return nil, PlanListOutput{}, core.E("planList", "failed to access plans directory", err) } - entries, err := os.ReadDir(dir) - if err != nil { - return nil, PlanListOutput{}, core.E("planList", "failed to read plans directory", err) + r := fs.List(dir) + if !r.OK { + return nil, PlanListOutput{}, nil } + entries := r.Value.([]os.DirEntry) var plans []Plan for _, entry := range entries { diff --git a/pkg/agentic/pr.go b/pkg/agentic/pr.go index a4f8d5e..89f2310 100644 --- a/pkg/agentic/pr.go +++ b/pkg/agentic/pr.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "net/http" - "os" "os/exec" core "dappco.re/go/core" @@ -58,7 +57,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in wsDir := core.JoinPath(WorkspaceRoot(), input.Workspace) srcDir := core.JoinPath(wsDir, "src") - if _, err := os.Stat(srcDir); err != nil { + if !fs.IsDir(srcDir) { return nil, CreatePROutput{}, core.E("createPR", "workspace not found: "+input.Workspace, nil) } diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 7596c26..c8dc8f8 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -250,12 +250,20 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques wsTmpl = "review" } - promptContent, _ := lib.Prompt(input.Template) + promptContent := "" + if r := lib.Prompt(input.Template); r.OK { + promptContent = r.Value.(string) + } personaContent := "" if input.Persona != "" { - personaContent, _ = lib.Persona(input.Persona) + if r := lib.Persona(input.Persona); r.OK { + personaContent = r.Value.(string) + } + } + flowContent := "" + if r := lib.Flow(detectLanguage(repoPath)); r.OK { + flowContent = r.Value.(string) } - flowContent, _ := lib.Flow(detectLanguage(repoPath)) wsData := &lib.WorkspaceData{ Repo: input.Repo, @@ -319,13 +327,13 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // --- Prompt templates --- func (s *PrepSubsystem) writePromptTemplate(template, wsDir string) { - prompt, err := lib.Template(template) - if err != nil { - // Fallback to default template - prompt, _ = lib.Template("default") - if prompt == "" { - prompt = "Read TODO.md and complete the task. Work in src/.\n" - } + r := lib.Template(template) + if !r.OK { + r = lib.Template("default") + } + prompt := "Read TODO.md and complete the task. Work in src/.\n" + if r.OK { + prompt = r.Value.(string) } fs.Write(core.JoinPath(wsDir, "src", "PROMPT.md"), prompt) @@ -337,12 +345,12 @@ func (s *PrepSubsystem) writePromptTemplate(template, wsDir string) { // and writes PLAN.md into the workspace src/ directory. func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map[string]string, task string, wsDir string) { // Load template from embedded prompts package - data, err := lib.Template(templateSlug) - if err != nil { + r := lib.Template(templateSlug) + if !r.OK { return // Template not found, skip silently } - content := data + content := r.Value.(string) // Substitute variables ({{variable_name}} → value) for key, value := range variables { @@ -649,7 +657,7 @@ func detectLanguage(repoPath string) string { {"Dockerfile", "docker"}, } for _, c := range checks { - if _, err := os.Stat(core.JoinPath(repoPath, c.file)); err == nil { + if fs.IsFile(core.JoinPath(repoPath, c.file)) { return c.lang } } diff --git a/pkg/agentic/queue.go b/pkg/agentic/queue.go index e5034ff..78a6be5 100644 --- a/pkg/agentic/queue.go +++ b/pkg/agentic/queue.go @@ -119,10 +119,11 @@ func (s *PrepSubsystem) delayForAgent(agent string) time.Duration { func (s *PrepSubsystem) countRunningByAgent(agent string) int { wsRoot := WorkspaceRoot() - entries, err := os.ReadDir(wsRoot) - if err != nil { + r := fs.List(wsRoot) + if !r.OK { return 0 } + entries := r.Value.([]os.DirEntry) count := 0 for _, entry := range entries { @@ -171,10 +172,11 @@ func (s *PrepSubsystem) drainQueue() { wsRoot := WorkspaceRoot() - entries, err := os.ReadDir(wsRoot) - if err != nil { + r := fs.List(wsRoot) + if !r.OK { return } + entries := r.Value.([]os.DirEntry) for _, entry := range entries { if !entry.IsDir() { diff --git a/pkg/agentic/resume.go b/pkg/agentic/resume.go index 0f45f1a..34195f1 100644 --- a/pkg/agentic/resume.go +++ b/pkg/agentic/resume.go @@ -4,7 +4,6 @@ package agentic import ( "context" - "os" core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -48,7 +47,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu srcDir := core.JoinPath(wsDir, "src") // Verify workspace exists - if _, err := os.Stat(srcDir); err != nil { + if !fs.IsDir(srcDir) { return nil, ResumeOutput{}, core.E("resume", "workspace not found: "+input.Workspace, nil) } diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index 7e47232..c834092 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -5,6 +5,7 @@ package agentic import ( "context" "encoding/json" + "io" "os" "os/exec" "regexp" @@ -134,10 +135,11 @@ func (s *PrepSubsystem) reviewQueue(ctx context.Context, _ *mcp.CallToolRequest, // findReviewCandidates returns repos that are ahead of GitHub main. func (s *PrepSubsystem) findReviewCandidates(basePath string) []string { - entries, err := os.ReadDir(basePath) - if err != nil { + r := fs.List(basePath) + if !r.OK { return nil } + entries := r.Value.([]os.DirEntry) var candidates []string for _, e := range entries { @@ -361,11 +363,13 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string jsonLine, _ := json.Marshal(entry) jsonlPath := core.JoinPath(dataDir, "reviews.jsonl") - f, err := os.OpenFile(jsonlPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err == nil { - defer f.Close() - f.Write(append(jsonLine, '\n')) + r := fs.Append(jsonlPath) + if !r.OK { + return } + wc := r.Value.(io.WriteCloser) + defer wc.Close() + wc.Write(append(jsonLine, '\n')) } // saveRateLimitState persists rate limit info for cross-run awareness. diff --git a/pkg/agentic/scan.go b/pkg/agentic/scan.go index 107e0b6..91c3ee8 100644 --- a/pkg/agentic/scan.go +++ b/pkg/agentic/scan.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "net/http" - "strings" core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -197,7 +196,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str Title: issue.Title, Labels: labels, Assignee: assignee, - URL: strings.Replace(issue.HTMLURL, "https://forge.lthn.ai", s.forgeURL, 1), + URL: core.Replace(issue.HTMLURL, "https://forge.lthn.ai", s.forgeURL), }) } diff --git a/pkg/agentic/status.go b/pkg/agentic/status.go index 7ea8fbf..061cbee 100644 --- a/pkg/agentic/status.go +++ b/pkg/agentic/status.go @@ -113,10 +113,11 @@ func (s *PrepSubsystem) registerStatusTool(server *mcp.Server) { func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, input StatusInput) (*mcp.CallToolResult, StatusOutput, error) { wsRoot := WorkspaceRoot() - entries, err := os.ReadDir(wsRoot) - if err != nil { - return nil, StatusOutput{}, core.E("status", "no workspaces found", err) + r := fs.List(wsRoot) + if !r.OK { + return nil, StatusOutput{}, core.E("status", "no workspaces found", nil) } + entries := r.Value.([]os.DirEntry) var workspaces []WorkspaceInfo From 6393bfe4da4cf9748b9ff88e9a12e38756a7c00c Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 10:15:15 +0000 Subject: [PATCH 3/5] refactor(agentic): adopt core.Env() + core.Path() across package Replace all os.UserHomeDir/os.Getenv/os.Hostname with core.Env(). Replace all filepath.Base/Dir/Glob/IsAbs with core.PathBase/PathDir/ PathGlob/PathIsAbs. 10 files migrated: paths, prep, review_queue, remote, dispatch, ingest, mirror, plan, verify, watch. Imports eliminated: 5x os, 7x filepath. All file I/O and path construction now routes through Core primitives. Bumps dappco.re/go/core to v0.6.0. Co-Authored-By: Virgil --- go.mod | 4 ++-- go.sum | 11 +++++++++++ pkg/agentic/dispatch.go | 7 ++----- pkg/agentic/ingest.go | 7 ++----- pkg/agentic/mirror.go | 3 +-- pkg/agentic/paths.go | 13 +++++-------- pkg/agentic/plan.go | 3 +-- pkg/agentic/prep.go | 16 +++++++--------- pkg/agentic/remote.go | 7 +++---- pkg/agentic/review_queue.go | 9 +++------ pkg/agentic/verify.go | 3 +-- pkg/agentic/watch.go | 12 ++++-------- 12 files changed, 42 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 07694de..ed716de 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module dappco.re/go/agent go 1.26.0 require ( - dappco.re/go/core v0.5.0 + dappco.re/go/core v0.6.0 dappco.re/go/core/api v0.2.0 dappco.re/go/core/process v0.3.0 dappco.re/go/core/ws v0.3.0 forge.lthn.ai/core/api v0.1.6 forge.lthn.ai/core/cli v0.3.7 - forge.lthn.ai/core/mcp v0.4.4 + forge.lthn.ai/core/mcp v0.4.8 github.com/gin-gonic/gin v1.12.0 github.com/gorilla/websocket v1.5.3 github.com/modelcontextprotocol/go-sdk v1.4.1 diff --git a/go.sum b/go.sum index d7c347c..337ec30 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,18 @@ dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U= dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core v0.6.0 h1:0wmuO/UmCWXxJkxQ6XvVLnqkAuWitbd49PhxjCsplyk= +dappco.re/go/core v0.6.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core/api v0.2.0 h1:5OcN9nawpp18Jp6dB1OwI2CBfs0Tacb0y0zqxFB6TJ0= dappco.re/go/core/api v0.2.0/go.mod h1:AtgNAx8lDY+qhVObFdNQOjSUQrHX1BeiDdMuA6RIfzo= +dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok= dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM= dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as= +dappco.re/go/core/scm v0.4.0/go.mod h1:ufb7si6HBkaT6zC8L67kLm8zzBaD1aQoTn4OsVAM1aI= +dappco.re/go/core/store v0.2.0/go.mod h1:QQGJiruayjna3nywbf0N2gcO502q/oEkPoSpBpSKbLM= dappco.re/go/core/ws v0.3.0 h1:ZxR8y5pfrWvnCHVN7qExXz7fdP5a063uNqyqE0Ab8pQ= dappco.re/go/core/ws v0.3.0/go.mod h1:aLyXrJnbCOGL0SW9rC1EHAAIS83w3djO374gHIz4Nic= forge.lthn.ai/core/api v0.1.5 h1:NwZrcOyBjaiz5/cn0n0tnlMUodi8Or6FHMx59C7Kv2o= @@ -42,6 +47,12 @@ forge.lthn.ai/core/mcp v0.4.0 h1:t4HMTI6CpoGB/VmE1aTklSEM8EI4Z/uKWyjGHxa1f4M= forge.lthn.ai/core/mcp v0.4.0/go.mod h1:eU35WT/8Mc0oJDVWdKaXEtNp27+Hc8KvnTKPf4DAqXE= forge.lthn.ai/core/mcp v0.4.4 h1:VTCOA1Dj/L7S8JCRg9BfYw7KfowW/Vvrp39bxc0dYyw= forge.lthn.ai/core/mcp v0.4.4/go.mod h1:eU35WT/8Mc0oJDVWdKaXEtNp27+Hc8KvnTKPf4DAqXE= +forge.lthn.ai/core/mcp v0.4.6 h1:jZY72sfPiCppKU4YyX7Gwy7ynbgVzUto+3S6oAj5Qs4= +forge.lthn.ai/core/mcp v0.4.6/go.mod h1:eU35WT/8Mc0oJDVWdKaXEtNp27+Hc8KvnTKPf4DAqXE= +forge.lthn.ai/core/mcp v0.4.7 h1:Iy/83laUpkaH8W2EoDlVMJbyv60xJ4aMgQe6sOcwL7k= +forge.lthn.ai/core/mcp v0.4.7/go.mod h1:eU35WT/8Mc0oJDVWdKaXEtNp27+Hc8KvnTKPf4DAqXE= +forge.lthn.ai/core/mcp v0.4.8 h1:nd1x3AL8AkUfl0kziltoJUX96Nx1BeFWEbgHmfrkKz8= +forge.lthn.ai/core/mcp v0.4.8/go.mod h1:eU35WT/8Mc0oJDVWdKaXEtNp27+Hc8KvnTKPf4DAqXE= github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc= github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 0fd2e47..5ff978d 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -4,8 +4,6 @@ package agentic import ( "context" - "os" - "path/filepath" "syscall" "time" @@ -101,8 +99,7 @@ func agentCommand(agent, prompt string) (string, []string, error) { } return "coderabbit", args, nil case "local": - home, _ := os.UserHomeDir() - script := core.JoinPath(home, "Code", "core", "agent", "scripts", "local-agent.sh") + script := core.JoinPath(core.Env("DIR_HOME"), "Code", "core", "agent", "scripts", "local-agent.sh") return "bash", []string{script, prompt}, nil default: return "", nil, core.E("agentCommand", "unknown agent: "+agent, nil) @@ -191,7 +188,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st } // Emit completion event with actual status - emitCompletionEvent(agent, filepath.Base(wsDir), finalStatus) + emitCompletionEvent(agent, core.PathBase(wsDir), finalStatus) // Notify monitor immediately (push to connected clients) if s.onComplete != nil { diff --git a/pkg/agentic/ingest.go b/pkg/agentic/ingest.go index 27b8eb7..d033258 100644 --- a/pkg/agentic/ingest.go +++ b/pkg/agentic/ingest.go @@ -6,8 +6,6 @@ import ( "bytes" "encoding/json" "net/http" - "os" - "path/filepath" core "dappco.re/go/core" ) @@ -21,7 +19,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { } // Read the log file - logFiles, _ := filepath.Glob(core.JoinPath(wsDir, "agent-*.log")) + logFiles := core.PathGlob(core.JoinPath(wsDir, "agent-*.log")) if len(logFiles) == 0 { return } @@ -92,8 +90,7 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p } // Read the agent API key from file - home, _ := os.UserHomeDir() - r := fs.Read(core.JoinPath(home, ".claude", "agent-api.key")) + r := fs.Read(core.JoinPath(core.Env("DIR_HOME"), ".claude", "agent-api.key")) if !r.OK { return } diff --git a/pkg/agentic/mirror.go b/pkg/agentic/mirror.go index bb57d74..5723389 100644 --- a/pkg/agentic/mirror.go +++ b/pkg/agentic/mirror.go @@ -60,8 +60,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu basePath := s.codePath if basePath == "" { - home, _ := os.UserHomeDir() - basePath = core.JoinPath(home, "Code", "core") + basePath = core.JoinPath(core.Env("DIR_HOME"), "Code", "core") } else { basePath = core.JoinPath(basePath, "core") } diff --git a/pkg/agentic/paths.go b/pkg/agentic/paths.go index 89c5657..4277a79 100644 --- a/pkg/agentic/paths.go +++ b/pkg/agentic/paths.go @@ -3,7 +3,6 @@ package agentic import ( - "os" "os/exec" "strconv" "unsafe" @@ -45,11 +44,10 @@ func WorkspaceRoot() string { // // root := agentic.CoreRoot() func CoreRoot() string { - if root := os.Getenv("CORE_WORKSPACE"); root != "" { + if root := core.Env("CORE_WORKSPACE"); root != "" { return root } - home, _ := os.UserHomeDir() - return core.JoinPath(home, "Code", ".core") + return core.JoinPath(core.Env("DIR_HOME"), "Code", ".core") } // PlansRoot returns the root directory for agent plans. @@ -64,11 +62,10 @@ func PlansRoot() string { // // name := agentic.AgentName() // "cladius" on Snider's Mac, "charon" elsewhere func AgentName() string { - if name := os.Getenv("AGENT_NAME"); name != "" { + if name := core.Env("AGENT_NAME"); name != "" { return name } - hostname, _ := os.Hostname() - h := core.Lower(hostname) + h := core.Lower(core.Env("HOSTNAME")) if core.Contains(h, "snider") || core.Contains(h, "studio") || core.Contains(h, "mac") { return "cladius" } @@ -102,7 +99,7 @@ func DefaultBranch(repoDir string) string { // // org := agentic.GitHubOrg() // "dAppCore" func GitHubOrg() string { - if org := os.Getenv("GITHUB_ORG"); org != "" { + if org := core.Env("GITHUB_ORG"); org != "" { return org } return "dAppCore" diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index d2fdde8..31bf276 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -8,7 +8,6 @@ import ( "encoding/hex" "encoding/json" "os" - "path/filepath" "strings" "time" @@ -341,7 +340,7 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu func planPath(dir, id string) string { // Sanitise ID to prevent path traversal - safe := filepath.Base(id) + safe := core.PathBase(id) if safe == "." || safe == ".." || safe == "" { safe = "invalid" } diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index c8dc8f8..9de94ad 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -10,9 +10,7 @@ import ( "encoding/json" goio "io" "net/http" - "os" "os/exec" - "path/filepath" "strings" "sync" "time" @@ -56,14 +54,14 @@ var _ coremcp.Subsystem = (*PrepSubsystem)(nil) // sub.SetCompletionNotifier(monitor) // sub.RegisterTools(server) func NewPrep() *PrepSubsystem { - home, _ := os.UserHomeDir() + home := core.Env("DIR_HOME") - forgeToken := os.Getenv("FORGE_TOKEN") + forgeToken := core.Env("FORGE_TOKEN") if forgeToken == "" { - forgeToken = os.Getenv("GITEA_TOKEN") + forgeToken = core.Env("GITEA_TOKEN") } - brainKey := os.Getenv("CORE_BRAIN_KEY") + brainKey := core.Env("CORE_BRAIN_KEY") if brainKey == "" { if r := fs.Read(core.JoinPath(home, ".claude", "brain.key")); r.OK { brainKey = core.Trim(r.Value.(string)) @@ -89,7 +87,7 @@ func (s *PrepSubsystem) SetCompletionNotifier(n CompletionNotifier) { } func envOr(key, fallback string) string { - if v := os.Getenv(key); v != "" { + if v := core.Env(key); v != "" { return v } return fallback @@ -192,7 +190,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques out := PrepOutput{WorkspaceDir: wsDir} // Source repo path — sanitise to prevent path traversal - repoName := filepath.Base(input.Repo) // strips ../ and absolute paths + repoName := core.PathBase(input.Repo) // strips ../ and absolute paths if repoName == "." || repoName == ".." || repoName == "" { return nil, PrepOutput{}, core.E("prep", "invalid repo name: "+input.Repo, nil) } @@ -573,7 +571,7 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { } modData := mr.Value.(string) if core.Contains(modData, modulePath) && !core.HasPrefix(modData, "module "+modulePath) { - consumers = append(consumers, filepath.Base(dir)) + consumers = append(consumers, core.PathBase(dir)) } } diff --git a/pkg/agentic/remote.go b/pkg/agentic/remote.go index b3ec52a..3dcb55d 100644 --- a/pkg/agentic/remote.go +++ b/pkg/agentic/remote.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "net/http" - "os" "time" core "dappco.re/go/core" @@ -180,17 +179,17 @@ func resolveHost(host string) string { func remoteToken(host string) string { // Check environment first envKey := core.Sprintf("AGENT_TOKEN_%s", core.Upper(host)) - if token := os.Getenv(envKey); token != "" { + if token := core.Env(envKey); token != "" { return token } // Fallback to shared agent token - if token := os.Getenv("MCP_AUTH_TOKEN"); token != "" { + if token := core.Env("MCP_AUTH_TOKEN"); token != "" { return token } // Try reading from file - home, _ := os.UserHomeDir() + home := core.Env("DIR_HOME") tokenFiles := []string{ core.Sprintf("%s/.core/tokens/%s.token", home, core.Lower(host)), core.Sprintf("%s/.core/agent-token", home), diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index c834092..2f8d4c8 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -339,8 +339,7 @@ func (s *PrepSubsystem) buildReviewCommand(ctx context.Context, repoDir, reviewe // storeReviewOutput saves raw review output for training data collection. func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string) { - home, _ := os.UserHomeDir() - dataDir := core.JoinPath(home, ".core", "training", "reviews") + dataDir := core.JoinPath(core.Env("DIR_HOME"), ".core", "training", "reviews") fs.EnsureDir(dataDir) timestamp := time.Now().Format("2006-01-02T15-04-05") @@ -374,16 +373,14 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string // saveRateLimitState persists rate limit info for cross-run awareness. func (s *PrepSubsystem) saveRateLimitState(info *RateLimitInfo) { - home, _ := os.UserHomeDir() - path := core.JoinPath(home, ".core", "coderabbit-ratelimit.json") + path := core.JoinPath(core.Env("DIR_HOME"), ".core", "coderabbit-ratelimit.json") data, _ := json.Marshal(info) fs.Write(path, string(data)) } // loadRateLimitState reads persisted rate limit info. func (s *PrepSubsystem) loadRateLimitState() *RateLimitInfo { - home, _ := os.UserHomeDir() - path := core.JoinPath(home, ".core", "coderabbit-ratelimit.json") + path := core.JoinPath(core.Env("DIR_HOME"), ".core", "coderabbit-ratelimit.json") r := fs.Read(path) if !r.OK { return nil diff --git a/pkg/agentic/verify.go b/pkg/agentic/verify.go index 05890c2..c2ca5d5 100644 --- a/pkg/agentic/verify.go +++ b/pkg/agentic/verify.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "os/exec" - "path/filepath" "time" core "dappco.re/go/core" @@ -130,7 +129,7 @@ func (s *PrepSubsystem) rebaseBranch(srcDir, branch string) bool { } // Force-push the rebased branch to Forge (origin is local clone) - st, _ := readStatus(filepath.Dir(srcDir)) + st, _ := readStatus(core.PathDir(srcDir)) org := "core" repo := "" if st != nil { diff --git a/pkg/agentic/watch.go b/pkg/agentic/watch.go index f1c9a01..ac10bff 100644 --- a/pkg/agentic/watch.go +++ b/pkg/agentic/watch.go @@ -4,7 +4,6 @@ package agentic import ( "context" - "path/filepath" "time" core "dappco.re/go/core" @@ -192,20 +191,17 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp // findActiveWorkspaces returns workspace names that are running or queued. func (s *PrepSubsystem) findActiveWorkspaces() []string { wsRoot := WorkspaceRoot() - entries, err := filepath.Glob(core.JoinPath(wsRoot, "*/status.json")) - if err != nil { - return nil - } + entries := core.PathGlob(core.JoinPath(wsRoot, "*/status.json")) var active []string for _, entry := range entries { - wsDir := filepath.Dir(entry) + wsDir := core.PathDir(entry) st, err := readStatus(wsDir) if err != nil { continue } if st.Status == "running" || st.Status == "queued" { - active = append(active, filepath.Base(wsDir)) + active = append(active, core.PathBase(wsDir)) } } return active @@ -213,7 +209,7 @@ func (s *PrepSubsystem) findActiveWorkspaces() []string { // resolveWorkspaceDir converts a workspace name to full path. func (s *PrepSubsystem) resolveWorkspaceDir(name string) string { - if filepath.IsAbs(name) { + if core.PathIsAbs(name) { return name } return core.JoinPath(WorkspaceRoot(), name) From a422eb1b6be865aa4a50a7bdc6c79f80e0aca36c Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 10:18:57 +0000 Subject: [PATCH 4/5] feat(lib): add go.work.tmpl to workspace template Codex agents are sandboxed to src/ and don't get ~/Code/go.work. This template creates a go.work with `use .` so the Go toolchain works in workspace mode inside the sandbox. Co-Authored-By: Virgil --- pkg/lib/workspace/default/go.work.tmpl | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pkg/lib/workspace/default/go.work.tmpl diff --git a/pkg/lib/workspace/default/go.work.tmpl b/pkg/lib/workspace/default/go.work.tmpl new file mode 100644 index 0000000..957f5b9 --- /dev/null +++ b/pkg/lib/workspace/default/go.work.tmpl @@ -0,0 +1,3 @@ +go 1.26.0 + +use . From ed842122a20b8c176b1e17d705946a3dc167fac9 Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 13:02:37 +0000 Subject: [PATCH 5/5] fix(brain): resolve direct AX findings Co-Authored-By: Virgil --- pkg/brain/direct.go | 15 ++++++--- pkg/brain/direct_test.go | 6 ++-- pkg/brain/provider.go | 18 +++++++++- pkg/brain/tools.go | 71 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/pkg/brain/direct.go b/pkg/brain/direct.go index 9b2dc9d..fa13651 100644 --- a/pkg/brain/direct.go +++ b/pkg/brain/direct.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "net/http" - "os" "time" "dappco.re/go/agent/pkg/agentic" @@ -33,16 +32,15 @@ var _ coremcp.Subsystem = (*DirectSubsystem)(nil) // sub := brain.NewDirect() // sub.RegisterTools(server) func NewDirect() *DirectSubsystem { - apiURL := os.Getenv("CORE_BRAIN_URL") + apiURL := core.Env("CORE_BRAIN_URL") if apiURL == "" { apiURL = "https://api.lthn.sh" } - apiKey := os.Getenv("CORE_BRAIN_KEY") + apiKey := core.Env("CORE_BRAIN_KEY") keyPath := "" if apiKey == "" { - home, _ := os.UserHomeDir() - keyPath = brainKeyPath(home) + keyPath = brainKeyPath(brainHomeDir()) if keyPath != "" { if r := fs.Read(keyPath); r.OK { apiKey = core.Trim(r.Value.(string)) @@ -104,6 +102,13 @@ func brainKeyPath(home string) string { return core.JoinPath(core.TrimSuffix(home, "/"), ".claude", "brain.key") } +func brainHomeDir() string { + if home := core.Env("CORE_HOME"); home != "" { + return home + } + return core.Env("DIR_HOME") +} + func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body any) (map[string]any, error) { if s.apiKey == "" { return nil, core.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil) diff --git a/pkg/brain/direct_test.go b/pkg/brain/direct_test.go index b87c08d..ea0f686 100644 --- a/pkg/brain/direct_test.go +++ b/pkg/brain/direct_test.go @@ -5,12 +5,12 @@ package brain import ( "context" "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "path/filepath" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // newTestDirect returns a DirectSubsystem wired to the given test server. @@ -59,7 +59,7 @@ func TestNewDirect_Good_KeyFromFile(t *testing.T) { t.Setenv("CORE_BRAIN_KEY", "") tmpHome := t.TempDir() - t.Setenv("HOME", tmpHome) + t.Setenv("CORE_HOME", tmpHome) keyDir := filepath.Join(tmpHome, ".claude") require.True(t, fs.EnsureDir(keyDir).OK) require.True(t, fs.Write(filepath.Join(keyDir, "brain.key"), " file-key-456 \n").OK) diff --git a/pkg/brain/provider.go b/pkg/brain/provider.go index 2b64a61..dd8bcd2 100644 --- a/pkg/brain/provider.go +++ b/pkg/brain/provider.go @@ -4,6 +4,7 @@ package brain import ( "net/http" + "strconv" "dappco.re/go/core/api" "dappco.re/go/core/api/pkg/provider" @@ -14,6 +15,11 @@ import ( // BrainProvider wraps the brain Subsystem as a service provider with REST // endpoints. It delegates to the same IDE bridge that the MCP tools use. +// +// Usage example: +// +// provider := brain.NewProvider(bridge, hub) +// provider.RegisterRoutes(router.Group("/api/brain")) type BrainProvider struct { bridge *ide.Bridge hub *ws.Hub @@ -294,13 +300,23 @@ func (p *BrainProvider) list(c *gin.Context) { return } + limit := 0 + if rawLimit := c.Query("limit"); rawLimit != "" { + parsedLimit, err := strconv.Atoi(rawLimit) + if err != nil { + c.JSON(http.StatusBadRequest, api.Fail("invalid_limit", "limit must be an integer")) + return + } + limit = parsedLimit + } + err := p.bridge.Send(ide.BridgeMessage{ Type: "brain_list", Data: map[string]any{ "project": c.Query("project"), "type": c.Query("type"), "agent_id": c.Query("agent_id"), - "limit": c.Query("limit"), + "limit": limit, }, }) if err != nil { diff --git a/pkg/brain/tools.go b/pkg/brain/tools.go index ae7f74b..c693b4d 100644 --- a/pkg/brain/tools.go +++ b/pkg/brain/tools.go @@ -14,6 +14,13 @@ import ( // -- Input/Output types ------------------------------------------------------- // RememberInput is the input for brain_remember. +// +// Usage example: +// +// input := brain.RememberInput{ +// Content: "Use core.Env for system paths.", +// Type: "convention", +// } type RememberInput struct { Content string `json:"content"` Type string `json:"type"` @@ -25,6 +32,13 @@ type RememberInput struct { } // RememberOutput is the output for brain_remember. +// +// Usage example: +// +// output := brain.RememberOutput{ +// Success: true, +// MemoryID: "mem_123", +// } type RememberOutput struct { Success bool `json:"success"` MemoryID string `json:"memoryId,omitempty"` @@ -32,6 +46,13 @@ type RememberOutput struct { } // RecallInput is the input for brain_recall. +// +// Usage example: +// +// input := brain.RecallInput{ +// Query: "core.Env conventions", +// TopK: 5, +// } type RecallInput struct { Query string `json:"query"` TopK int `json:"top_k,omitempty"` @@ -39,6 +60,13 @@ type RecallInput struct { } // RecallFilter holds optional filter criteria for brain_recall. +// +// Usage example: +// +// filter := brain.RecallFilter{ +// Project: "agent", +// Type: "convention", +// } type RecallFilter struct { Project string `json:"project,omitempty"` Type any `json:"type,omitempty"` @@ -47,6 +75,13 @@ type RecallFilter struct { } // RecallOutput is the output for brain_recall. +// +// Usage example: +// +// output := brain.RecallOutput{ +// Success: true, +// Count: 1, +// } type RecallOutput struct { Success bool `json:"success"` Count int `json:"count"` @@ -54,6 +89,14 @@ type RecallOutput struct { } // Memory is a single memory entry returned by recall or list. +// +// Usage example: +// +// memory := brain.Memory{ +// ID: "mem_123", +// Type: "convention", +// Content: "Use core.Env for system paths.", +// } type Memory struct { ID string `json:"id"` AgentID string `json:"agent_id"` @@ -69,12 +112,26 @@ type Memory struct { } // ForgetInput is the input for brain_forget. +// +// Usage example: +// +// input := brain.ForgetInput{ +// ID: "mem_123", +// Reason: "superseded", +// } type ForgetInput struct { ID string `json:"id"` Reason string `json:"reason,omitempty"` } // ForgetOutput is the output for brain_forget. +// +// Usage example: +// +// output := brain.ForgetOutput{ +// Success: true, +// Forgotten: "mem_123", +// } type ForgetOutput struct { Success bool `json:"success"` Forgotten string `json:"forgotten"` @@ -82,6 +139,13 @@ type ForgetOutput struct { } // ListInput is the input for brain_list. +// +// Usage example: +// +// input := brain.ListInput{ +// Project: "agent", +// Limit: 20, +// } type ListInput struct { Project string `json:"project,omitempty"` Type string `json:"type,omitempty"` @@ -90,6 +154,13 @@ type ListInput struct { } // ListOutput is the output for brain_list. +// +// Usage example: +// +// output := brain.ListOutput{ +// Success: true, +// Count: 2, +// } type ListOutput struct { Success bool `json:"success"` Count int `json:"count"`