refactor(pkg): migrate go-io/go-log to Core primitives

Replace separate go-io (coreio) and go-log (coreerr) packages with
Core's built-in Fs and error/logging functions. This is the reference
implementation for how all Core ecosystem packages should migrate.

Changes:
- coreio.Local.Read/Write/EnsureDir/Delete/IsFile → core.Fs methods
- coreerr.E() → core.E(), coreerr.Info/Warn/Error → core.Info/Warn/Error
- (value, error) return pattern → core.Result pattern (r.OK, r.Value)
- go-io and go-log moved from direct to indirect deps in go.mod
- Added AX usage-example comments on key public types
- Added newFs("/") helper for unrestricted filesystem access

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-22 03:41:07 +00:00
parent 52c3e67692
commit deaa06a54d
32 changed files with 311 additions and 260 deletions

9
.gitignore vendored
View file

@ -1,8 +1,5 @@
.idea/
.vscode/
*.log
.core/
docker/.env
ui/node_modules
# Compiled binaries
core-agent
mcp
*.exe
var/

4
go.mod
View file

@ -4,8 +4,6 @@ go 1.26.0
require (
dappco.re/go/core v0.5.0
dappco.re/go/core/io v0.2.0
dappco.re/go/core/log v0.1.0
dappco.re/go/core/process v0.3.0
dappco.re/go/core/ws v0.3.0
forge.lthn.ai/core/api v0.1.5
@ -19,6 +17,8 @@ require (
)
require (
dappco.re/go/core/io v0.2.0 // indirect
dappco.re/go/core/log v0.1.0 // indirect
forge.lthn.ai/core/go v0.3.3 // indirect
forge.lthn.ai/core/go-ai v0.1.12 // indirect
forge.lthn.ai/core/go-i18n v0.1.7 // indirect

View file

@ -11,8 +11,7 @@ import (
"syscall"
"time"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"dappco.re/go/core/process"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -104,7 +103,7 @@ func agentCommand(agent, prompt string) (string, []string, error) {
script := filepath.Join(home, "Code", "core", "agent", "scripts", "local-agent.sh")
return "bash", []string{script, prompt}, nil
default:
return "", nil, coreerr.E("agentCommand", "unknown agent: "+agent, nil)
return "", nil, core.E("agentCommand", "unknown agent: "+agent, nil)
}
}
@ -134,7 +133,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
Detach: true,
})
if err != nil {
return 0, "", coreerr.E("dispatch.spawnAgent", "failed to spawn "+agent, err)
return 0, "", core.E("dispatch.spawnAgent", "failed to spawn "+agent, err)
}
// Close stdin immediately — agents use -p mode, not interactive stdin.
@ -162,7 +161,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
// Write captured output to log file
if output := proc.Output(); output != "" {
coreio.Local.Write(outputFile, output)
fs.Write(outputFile, output)
}
// Determine final status: check exit code, BLOCKED.md, and output
@ -172,9 +171,9 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
question := ""
blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md")
if blockedContent, err := coreio.Local.Read(blockedPath); err == nil && strings.TrimSpace(blockedContent) != "" {
if r := fs.Read(blockedPath); r.OK && strings.TrimSpace(r.Value.(string)) != "" {
finalStatus = "blocked"
question = strings.TrimSpace(blockedContent)
question = strings.TrimSpace(r.Value.(string))
} else if exitCode != 0 || procStatus == "failed" || procStatus == "killed" {
finalStatus = "failed"
if exitCode != 0 {
@ -215,10 +214,10 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, input DispatchInput) (*mcp.CallToolResult, DispatchOutput, error) {
if input.Repo == "" {
return nil, DispatchOutput{}, coreerr.E("dispatch", "repo is required", nil)
return nil, DispatchOutput{}, core.E("dispatch", "repo is required", nil)
}
if input.Task == "" {
return nil, DispatchOutput{}, coreerr.E("dispatch", "task is required", nil)
return nil, DispatchOutput{}, core.E("dispatch", "task is required", nil)
}
if input.Org == "" {
input.Org = "core"
@ -243,7 +242,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
}
_, prepOut, err := s.prepWorkspace(ctx, req, prepInput)
if err != nil {
return nil, DispatchOutput{}, coreerr.E("dispatch", "prep workspace failed", err)
return nil, DispatchOutput{}, core.E("dispatch", "prep workspace failed", err)
}
wsDir := prepOut.WorkspaceDir
@ -254,7 +253,11 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
if input.DryRun {
// Read PROMPT.md for the dry run output
promptContent, _ := coreio.Local.Read(filepath.Join(srcDir, "PROMPT.md"))
r := fs.Read(filepath.Join(srcDir, "PROMPT.md"))
promptContent := ""
if r.OK {
promptContent = r.Value.(string)
}
return nil, DispatchOutput{
Success: true,
Agent: input.Agent,

View file

@ -10,7 +10,7 @@ import (
"net/http"
"strings"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -54,13 +54,13 @@ func (s *PrepSubsystem) registerEpicTool(server *mcp.Server) {
func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest, input EpicInput) (*mcp.CallToolResult, EpicOutput, error) {
if input.Title == "" {
return nil, EpicOutput{}, coreerr.E("createEpic", "title is required", nil)
return nil, EpicOutput{}, core.E("createEpic", "title is required", nil)
}
if len(input.Tasks) == 0 {
return nil, EpicOutput{}, coreerr.E("createEpic", "at least one task is required", nil)
return nil, EpicOutput{}, core.E("createEpic", "at least one task is required", nil)
}
if s.forgeToken == "" {
return nil, EpicOutput{}, coreerr.E("createEpic", "no Forge token configured", nil)
return nil, EpicOutput{}, core.E("createEpic", "no Forge token configured", nil)
}
if input.Org == "" {
input.Org = "core"
@ -113,7 +113,7 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest
epicLabels := append(labelIDs, s.resolveLabelIDs(ctx, input.Org, input.Repo, []string{"epic"})...)
epic, err := s.createIssue(ctx, input.Org, input.Repo, input.Title, body.String(), epicLabels)
if err != nil {
return nil, EpicOutput{}, coreerr.E("createEpic", "failed to create epic", err)
return nil, EpicOutput{}, core.E("createEpic", "failed to create epic", err)
}
out := EpicOutput{
@ -163,12 +163,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
resp, err := s.client.Do(req)
if err != nil {
return ChildRef{}, coreerr.E("createIssue", "create issue request failed", err)
return ChildRef{}, core.E("createIssue", "create issue request failed", err)
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("create issue returned %d", resp.StatusCode), nil)
return ChildRef{}, core.E("createIssue", fmt.Sprintf("create issue returned %d", resp.StatusCode), nil)
}
var result struct {

View file

@ -10,8 +10,6 @@ import (
"os"
"path/filepath"
"strings"
coreio "dappco.re/go/core/io"
)
// ingestFindings reads the agent output log and creates issues via the API
@ -28,12 +26,12 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
return
}
contentStr, err := coreio.Local.Read(logFiles[0])
if err != nil || len(contentStr) < 100 {
r := fs.Read(logFiles[0])
if !r.OK || len(r.Value.(string)) < 100 {
return
}
body := contentStr
body := r.Value.(string)
// Skip quota errors
if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") {
@ -95,11 +93,11 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p
// Read the agent API key from file
home, _ := os.UserHomeDir()
apiKeyStr, err := coreio.Local.Read(filepath.Join(home, ".claude", "agent-api.key"))
if err != nil {
r := fs.Read(filepath.Join(home, ".claude", "agent-api.key"))
if !r.OK {
return
}
apiKey := strings.TrimSpace(apiKeyStr)
apiKey := strings.TrimSpace(r.Value.(string))
payload, _ := json.Marshal(map[string]string{
"title": title,

View file

@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -88,7 +88,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
fetchCmd.Run()
// Check how far ahead local default branch is vs github
localBase := gitDefaultBranch(repoDir)
localBase := DefaultBranch(repoDir)
ahead := commitsAhead(repoDir, "github/main", localBase)
if ahead == 0 {
continue // Already in sync
@ -120,7 +120,7 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
ensureDevBranch(repoDir)
// Push local main to github dev (explicit main, not HEAD)
base := gitDefaultBranch(repoDir)
base := DefaultBranch(repoDir)
pushCmd := exec.CommandContext(ctx, "git", "push", "github", base+":refs/heads/dev", "--force")
pushCmd.Dir = repoDir
if err := pushCmd.Run(); err != nil {
@ -187,7 +187,7 @@ func (s *PrepSubsystem) createGitHubPR(ctx context.Context, repoDir, repo string
prCmd.Dir = repoDir
prOut, err := prCmd.CombinedOutput()
if err != nil {
return "", coreerr.E("createGitHubPR", string(prOut), err)
return "", core.E("createGitHubPR", string(prOut), err)
}
// gh pr create outputs the PR URL on the last line

View file

@ -7,10 +7,33 @@ import (
"os/exec"
"path/filepath"
"strings"
"unsafe"
core "dappco.re/go/core"
)
// fs provides unrestricted filesystem access (root "/" = no sandbox).
//
// r := fs.Read("/etc/hostname")
// if r.OK { fmt.Println(r.Value.(string)) }
var fs = newFs("/")
// newFs creates a core.Fs with the given root directory.
// Root "/" means unrestricted access (same as coreio.Local).
func newFs(root string) *core.Fs {
type fsRoot struct{ root string }
f := &core.Fs{}
(*fsRoot)(unsafe.Pointer(f)).root = root
return f
}
// LocalFs returns an unrestricted filesystem instance for use by other packages.
func LocalFs() *core.Fs { return fs }
// WorkspaceRoot returns the root directory for agent workspaces.
// Checks CORE_WORKSPACE env var first, falls back to ~/Code/.core/workspace.
//
// wsDir := filepath.Join(agentic.WorkspaceRoot(), "go-io-1774149757")
func WorkspaceRoot() string {
return filepath.Join(CoreRoot(), "workspace")
}
@ -32,6 +55,8 @@ func PlansRoot() string {
// AgentName returns the name of this agent based on hostname.
// Checks AGENT_NAME env var first.
//
// name := agentic.AgentName() // "cladius" on Snider's Mac, "charon" elsewhere
func AgentName() string {
if name := os.Getenv("AGENT_NAME"); name != "" {
return name
@ -44,8 +69,8 @@ func AgentName() string {
return "charon"
}
// gitDefaultBranch detects the default branch of a repo (main, master, etc.).
func gitDefaultBranch(repoDir string) string {
// DefaultBranch detects the default branch of a repo (main, master, etc.).
func DefaultBranch(repoDir string) string {
cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short")
cmd.Dir = repoDir
if out, err := cmd.Output(); err == nil {

View file

@ -12,12 +12,14 @@ import (
"strings"
"time"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// Plan represents an implementation plan for agent work.
//
// plan := &Plan{ID: "migrate-core-abc", Title: "Migrate Core", Status: "draft", Objective: "..."}
// writePlan(PlansRoot(), plan)
type Plan struct {
ID string `json:"id"`
Title string `json:"title"`
@ -146,10 +148,10 @@ func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) {
func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, PlanCreateOutput, error) {
if input.Title == "" {
return nil, PlanCreateOutput{}, coreerr.E("planCreate", "title is required", nil)
return nil, PlanCreateOutput{}, core.E("planCreate", "title is required", nil)
}
if input.Objective == "" {
return nil, PlanCreateOutput{}, coreerr.E("planCreate", "objective is required", nil)
return nil, PlanCreateOutput{}, core.E("planCreate", "objective is required", nil)
}
id := generatePlanID(input.Title)
@ -178,7 +180,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in
path, err := writePlan(PlansRoot(), &plan)
if err != nil {
return nil, PlanCreateOutput{}, coreerr.E("planCreate", "failed to write plan", err)
return nil, PlanCreateOutput{}, core.E("planCreate", "failed to write plan", err)
}
return nil, PlanCreateOutput{
@ -190,7 +192,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in
func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, input PlanReadInput) (*mcp.CallToolResult, PlanReadOutput, error) {
if input.ID == "" {
return nil, PlanReadOutput{}, coreerr.E("planRead", "id is required", nil)
return nil, PlanReadOutput{}, core.E("planRead", "id is required", nil)
}
plan, err := readPlan(PlansRoot(), input.ID)
@ -206,7 +208,7 @@ func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, inpu
func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, input PlanUpdateInput) (*mcp.CallToolResult, PlanUpdateOutput, error) {
if input.ID == "" {
return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "id is required", nil)
return nil, PlanUpdateOutput{}, core.E("planUpdate", "id is required", nil)
}
plan, err := readPlan(PlansRoot(), input.ID)
@ -217,7 +219,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in
// Apply partial updates
if input.Status != "" {
if !validPlanStatus(input.Status) {
return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "invalid status: "+input.Status+" (valid: draft, ready, in_progress, needs_verification, verified, approved)", nil)
return nil, PlanUpdateOutput{}, core.E("planUpdate", "invalid status: "+input.Status+" (valid: draft, ready, in_progress, needs_verification, verified, approved)", nil)
}
plan.Status = input.Status
}
@ -240,7 +242,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in
plan.UpdatedAt = time.Now()
if _, err := writePlan(PlansRoot(), plan); err != nil {
return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "failed to write plan", err)
return nil, PlanUpdateOutput{}, core.E("planUpdate", "failed to write plan", err)
}
return nil, PlanUpdateOutput{
@ -251,16 +253,16 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in
func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, input PlanDeleteInput) (*mcp.CallToolResult, PlanDeleteOutput, error) {
if input.ID == "" {
return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "id is required", nil)
return nil, PlanDeleteOutput{}, core.E("planDelete", "id is required", nil)
}
path := planPath(PlansRoot(), input.ID)
if _, err := os.Stat(path); err != nil {
return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "plan not found: "+input.ID, nil)
return nil, PlanDeleteOutput{}, core.E("planDelete", "plan not found: "+input.ID, nil)
}
if err := coreio.Local.Delete(path); err != nil {
return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "failed to delete plan", err)
if r := fs.Delete(path); !r.OK {
return nil, PlanDeleteOutput{}, core.E("planDelete", "failed to delete plan", nil)
}
return nil, PlanDeleteOutput{
@ -271,13 +273,13 @@ func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, in
func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, input PlanListInput) (*mcp.CallToolResult, PlanListOutput, error) {
dir := PlansRoot()
if err := coreio.Local.EnsureDir(dir); err != nil {
return nil, PlanListOutput{}, coreerr.E("planList", "failed to access plans directory", err)
if r := fs.EnsureDir(dir); !r.OK {
return nil, PlanListOutput{}, core.E("planList", "failed to access plans directory", nil)
}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, PlanListOutput{}, coreerr.E("planList", "failed to read plans directory", err)
return nil, PlanListOutput{}, core.E("planList", "failed to read plans directory", err)
}
var plans []Plan
@ -352,21 +354,21 @@ func generatePlanID(title string) string {
}
func readPlan(dir, id string) (*Plan, error) {
data, err := coreio.Local.Read(planPath(dir, id))
if err != nil {
return nil, coreerr.E("readPlan", "plan not found: "+id, nil)
r := fs.Read(planPath(dir, id))
if !r.OK {
return nil, core.E("readPlan", "plan not found: "+id, nil)
}
var plan Plan
if err := json.Unmarshal([]byte(data), &plan); err != nil {
return nil, coreerr.E("readPlan", "failed to parse plan "+id, err)
if err := json.Unmarshal([]byte(r.Value.(string)), &plan); err != nil {
return nil, core.E("readPlan", "failed to parse plan "+id, err)
}
return &plan, nil
}
func writePlan(dir string, plan *Plan) (string, error) {
if err := coreio.Local.EnsureDir(dir); err != nil {
return "", coreerr.E("writePlan", "failed to create plans directory", err)
if r := fs.EnsureDir(dir); !r.OK {
return "", core.E("writePlan", "failed to create plans directory", nil)
}
path := planPath(dir, plan.ID)
@ -375,7 +377,10 @@ func writePlan(dir string, plan *Plan) (string, error) {
return "", err
}
return path, coreio.Local.Write(path, string(data))
if r := fs.Write(path, string(data)); !r.OK {
return "", core.E("writePlan", "failed to write plan", nil)
}
return path, nil
}
func validPlanStatus(status string) bool {

View file

@ -7,7 +7,6 @@ import (
"strings"
"testing"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -31,7 +30,7 @@ func TestWritePlan_Good(t *testing.T) {
assert.Equal(t, filepath.Join(dir, "test-plan-abc123.json"), path)
// Verify file exists
assert.True(t, coreio.Local.IsFile(path))
assert.True(t, fs.IsFile(path))
}
func TestWritePlan_Good_CreatesDirectory(t *testing.T) {
@ -96,7 +95,7 @@ func TestReadPlan_Bad_NotFound(t *testing.T) {
func TestReadPlan_Bad_InvalidJSON(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "bad-json.json"), "{broken"))
require.True(t, fs.Write(filepath.Join(dir, "bad-json.json"), "{broken").OK)
_, err := readPlan(dir, "bad-json")
assert.Error(t, err)
@ -205,7 +204,7 @@ func TestWritePlan_Good_OverwriteExisting(t *testing.T) {
func TestReadPlan_Ugly_EmptyFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "empty.json"), ""))
require.True(t, fs.Write(filepath.Join(dir, "empty.json"), "").OK)
_, err := readPlan(dir, "empty")
assert.Error(t, err)

View file

@ -13,7 +13,7 @@ import (
"path/filepath"
"strings"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -48,23 +48,23 @@ func (s *PrepSubsystem) registerCreatePRTool(server *mcp.Server) {
func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, input CreatePRInput) (*mcp.CallToolResult, CreatePROutput, error) {
if input.Workspace == "" {
return nil, CreatePROutput{}, coreerr.E("createPR", "workspace is required", nil)
return nil, CreatePROutput{}, core.E("createPR", "workspace is required", nil)
}
if s.forgeToken == "" {
return nil, CreatePROutput{}, coreerr.E("createPR", "no Forge token configured", nil)
return nil, CreatePROutput{}, core.E("createPR", "no Forge token configured", nil)
}
wsDir := filepath.Join(WorkspaceRoot(), input.Workspace)
srcDir := filepath.Join(wsDir, "src")
if _, err := os.Stat(srcDir); err != nil {
return nil, CreatePROutput{}, coreerr.E("createPR", "workspace not found: "+input.Workspace, nil)
return nil, CreatePROutput{}, core.E("createPR", "workspace not found: "+input.Workspace, nil)
}
// Read workspace status for repo, branch, issue context
st, err := readStatus(wsDir)
if err != nil {
return nil, CreatePROutput{}, coreerr.E("createPR", "no status.json", err)
return nil, CreatePROutput{}, core.E("createPR", "no status.json", err)
}
if st.Branch == "" {
@ -73,7 +73,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
branchCmd.Dir = srcDir
out, err := branchCmd.Output()
if err != nil {
return nil, CreatePROutput{}, coreerr.E("createPR", "failed to detect branch", err)
return nil, CreatePROutput{}, core.E("createPR", "failed to detect branch", err)
}
st.Branch = strings.TrimSpace(string(out))
}
@ -117,13 +117,13 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
pushCmd.Dir = srcDir
pushOut, err := pushCmd.CombinedOutput()
if err != nil {
return nil, CreatePROutput{}, coreerr.E("createPR", "git push failed: "+string(pushOut), err)
return nil, CreatePROutput{}, core.E("createPR", "git push failed: "+string(pushOut), err)
}
// Create PR via Forge API
prURL, prNum, err := s.forgeCreatePR(ctx, org, st.Repo, st.Branch, base, title, body)
if err != nil {
return nil, CreatePROutput{}, coreerr.E("createPR", "failed to create PR", err)
return nil, CreatePROutput{}, core.E("createPR", "failed to create PR", err)
}
// Update status with PR URL
@ -178,7 +178,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base
resp, err := s.client.Do(req)
if err != nil {
return "", 0, coreerr.E("forgeCreatePR", "request failed", err)
return "", 0, core.E("forgeCreatePR", "request failed", err)
}
defer resp.Body.Close()
@ -186,7 +186,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base
var errBody map[string]any
json.NewDecoder(resp.Body).Decode(&errBody)
msg, _ := errBody["message"].(string)
return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil)
return "", 0, core.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil)
}
var pr struct {
@ -253,7 +253,7 @@ func (s *PrepSubsystem) registerListPRsTool(server *mcp.Server) {
func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, input ListPRsInput) (*mcp.CallToolResult, ListPRsOutput, error) {
if s.forgeToken == "" {
return nil, ListPRsOutput{}, coreerr.E("listPRs", "no Forge token configured", nil)
return nil, ListPRsOutput{}, core.E("listPRs", "no Forge token configured", nil)
}
if input.Org == "" {
@ -310,12 +310,12 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string
resp, err := s.client.Do(req)
if err != nil {
return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err)
return nil, core.E("listRepoPRs", "failed to list PRs for "+repo, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("listRepoPRs", fmt.Sprintf("HTTP %d listing PRs for %s", resp.StatusCode, repo), nil)
return nil, core.E("listRepoPRs", fmt.Sprintf("HTTP %d listing PRs for %s", resp.StatusCode, repo), nil)
}
var prs []struct {

View file

@ -18,9 +18,8 @@ import (
"sync"
"time"
core "dappco.re/go/core"
"dappco.re/go/agent/pkg/lib"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
"gopkg.in/yaml.v3"
)
@ -31,7 +30,10 @@ type CompletionNotifier interface {
Poke()
}
// PrepSubsystem provides agentic MCP tools.
// PrepSubsystem provides agentic MCP tools for workspace orchestration.
//
// sub := agentic.NewPrep()
// sub.RegisterTools(server)
type PrepSubsystem struct {
forgeURL string
forgeToken string
@ -45,6 +47,10 @@ type PrepSubsystem struct {
}
// NewPrep creates an agentic subsystem.
//
// sub := agentic.NewPrep()
// sub.SetCompletionNotifier(monitor)
// sub.RegisterTools(server)
func NewPrep() *PrepSubsystem {
home, _ := os.UserHomeDir()
@ -55,8 +61,8 @@ func NewPrep() *PrepSubsystem {
brainKey := os.Getenv("CORE_BRAIN_KEY")
if brainKey == "" {
if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil {
brainKey = strings.TrimSpace(data)
if r := fs.Read(filepath.Join(home, ".claude", "brain.key")); r.OK {
brainKey = strings.TrimSpace(r.Value.(string))
}
}
@ -145,7 +151,7 @@ type PrepOutput struct {
func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolRequest, input PrepInput) (*mcp.CallToolResult, PrepOutput, error) {
if input.Repo == "" {
return nil, PrepOutput{}, coreerr.E("prepWorkspace", "repo is required", nil)
return nil, PrepOutput{}, core.E("prepWorkspace", "repo is required", nil)
}
if input.Org == "" {
input.Org = "core"
@ -164,7 +170,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
// Ensure workspace directory exists
if err := os.MkdirAll(wsDir, 0755); err != nil {
return nil, PrepOutput{}, coreerr.E("prep", "failed to create workspace dir", err)
return nil, PrepOutput{}, core.E("prep", "failed to create workspace dir", err)
}
out := PrepOutput{WorkspaceDir: wsDir}
@ -172,7 +178,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
// Source repo path — sanitise to prevent path traversal
repoName := filepath.Base(input.Repo) // strips ../ and absolute paths
if repoName == "." || repoName == ".." || repoName == "" {
return nil, PrepOutput{}, coreerr.E("prep", "invalid repo name: "+input.Repo, nil)
return nil, PrepOutput{}, core.E("prep", "invalid repo name: "+input.Repo, nil)
}
repoPath := filepath.Join(s.codePath, "core", repoName)
@ -180,7 +186,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
srcDir := filepath.Join(wsDir, "src")
cloneCmd := exec.CommandContext(ctx, "git", "clone", repoPath, srcDir)
if err := cloneCmd.Run(); err != nil {
return nil, PrepOutput{}, coreerr.E("prep", "git clone failed for "+input.Repo, err)
return nil, PrepOutput{}, core.E("prep", "git clone failed for "+input.Repo, err)
}
// Create feature branch
@ -209,13 +215,13 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
branchCmd := exec.CommandContext(ctx, "git", "checkout", "-b", branchName)
branchCmd.Dir = srcDir
if err := branchCmd.Run(); err != nil {
return nil, PrepOutput{}, coreerr.E("prep.branch", fmt.Sprintf("failed to create branch %q", branchName), err)
return nil, PrepOutput{}, core.E("prep.branch", fmt.Sprintf("failed to create branch %q", branchName), err)
}
out.Branch = branchName
// Create context dirs inside src/
coreio.Local.EnsureDir(filepath.Join(srcDir, "kb"))
coreio.Local.EnsureDir(filepath.Join(srcDir, "specs"))
fs.EnsureDir(filepath.Join(srcDir, "kb"))
fs.EnsureDir(filepath.Join(srcDir, "specs"))
// Remote stays as local clone origin — agent cannot push to forge.
// Reviewer pulls changes from workspace and pushes after verification.
@ -253,13 +259,13 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
// Copy repo's own CLAUDE.md over template if it exists
claudeMdPath := filepath.Join(repoPath, "CLAUDE.md")
if data, err := coreio.Local.Read(claudeMdPath); err == nil {
coreio.Local.Write(filepath.Join(srcDir, "CLAUDE.md"), data)
if r := fs.Read(claudeMdPath); r.OK {
fs.Write(filepath.Join(srcDir, "CLAUDE.md"), r.Value.(string))
}
// Copy GEMINI.md from core/agent (ethics framework for all agents)
agentGeminiMd := filepath.Join(s.codePath, "core", "agent", "GEMINI.md")
if data, err := coreio.Local.Read(agentGeminiMd); err == nil {
coreio.Local.Write(filepath.Join(srcDir, "GEMINI.md"), data)
if r := fs.Read(agentGeminiMd); r.OK {
fs.Write(filepath.Join(srcDir, "GEMINI.md"), r.Value.(string))
}
// 3. Generate TODO.md from issue (overrides template)
@ -306,7 +312,7 @@ func (s *PrepSubsystem) writePromptTemplate(template, wsDir string) {
}
}
coreio.Local.Write(filepath.Join(wsDir, "src", "PROMPT.md"), prompt)
fs.Write(filepath.Join(wsDir, "src", "PROMPT.md"), prompt)
}
// --- Plan template rendering ---
@ -380,7 +386,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map
plan.WriteString("\n**Commit after completing this phase.**\n\n---\n\n")
}
coreio.Local.Write(filepath.Join(wsDir, "src", "PLAN.md"), plan.String())
fs.Write(filepath.Join(wsDir, "src", "PLAN.md"), plan.String())
}
// --- Helpers (unchanged) ---
@ -448,7 +454,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i
return '-'
}, page.Title) + ".md"
coreio.Local.Write(filepath.Join(wsDir, "src", "kb", filename), string(content))
fs.Write(filepath.Join(wsDir, "src", "kb", filename), string(content))
count++
}
@ -461,8 +467,8 @@ func (s *PrepSubsystem) copySpecs(wsDir string) int {
for _, file := range specFiles {
src := filepath.Join(s.specsPath, file)
if data, err := coreio.Local.Read(src); err == nil {
coreio.Local.Write(filepath.Join(wsDir, "src", "specs", file), data)
if r := fs.Read(src); r.OK {
fs.Write(filepath.Join(wsDir, "src", "specs", file), r.Value.(string))
count++
}
}
@ -515,7 +521,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string)
content.WriteString(fmt.Sprintf("### %d. %s [%s] (score: %.3f)\n\n%s\n\n", i+1, memProject, memType, score, memContent))
}
coreio.Local.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String())
fs.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String())
return len(result.Memories)
}
@ -523,10 +529,11 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int {
goWorkPath := filepath.Join(s.codePath, "go.work")
modulePath := "forge.lthn.ai/core/" + repo
workData, err := coreio.Local.Read(goWorkPath)
if err != nil {
r := fs.Read(goWorkPath)
if !r.OK {
return 0
}
workData := r.Value.(string)
var consumers []string
for _, line := range strings.Split(workData, "\n") {
@ -536,10 +543,11 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int {
}
dir := filepath.Join(s.codePath, strings.TrimPrefix(line, "./"))
goMod := filepath.Join(dir, "go.mod")
modData, err := coreio.Local.Read(goMod)
if err != nil {
mr := fs.Read(goMod)
if !mr.OK {
continue
}
modData := mr.Value.(string)
if strings.Contains(modData, modulePath) && !strings.HasPrefix(modData, "module "+modulePath) {
consumers = append(consumers, filepath.Base(dir))
}
@ -552,7 +560,7 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int {
content += "- " + c + "\n"
}
content += fmt.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers))
coreio.Local.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content)
fs.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content)
}
return len(consumers)
@ -569,7 +577,7 @@ func (s *PrepSubsystem) gitLog(repoPath, wsDir string) int {
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) > 0 && lines[0] != "" {
content := "# Recent Changes\n\n```\n" + string(output) + "```\n"
coreio.Local.Write(filepath.Join(wsDir, "src", "RECENT.md"), content)
fs.Write(filepath.Join(wsDir, "src", "RECENT.md"), content)
}
return len(lines)
@ -606,7 +614,7 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu
content += fmt.Sprintf("**Repo:** %s/%s\n\n---\n\n", org, repo)
content += "## Objective\n\n" + issueData.Body + "\n"
coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), content)
fs.Write(filepath.Join(wsDir, "src", "TODO.md"), content)
}
// detectLanguage guesses the primary language from repo contents.

View file

@ -5,8 +5,6 @@ package agentic
import (
"path/filepath"
"testing"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -28,43 +26,43 @@ func TestEnvOr_Good_UnsetUsesFallback(t *testing.T) {
func TestDetectLanguage_Good_Go(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "go.mod"), "module test"))
require.True(t, fs.Write(filepath.Join(dir, "go.mod"), "module test").OK)
assert.Equal(t, "go", detectLanguage(dir))
}
func TestDetectLanguage_Good_PHP(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "composer.json"), "{}"))
require.True(t, fs.Write(filepath.Join(dir, "composer.json"), "{}").OK)
assert.Equal(t, "php", detectLanguage(dir))
}
func TestDetectLanguage_Good_TypeScript(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "package.json"), "{}"))
require.True(t, fs.Write(filepath.Join(dir, "package.json"), "{}").OK)
assert.Equal(t, "ts", detectLanguage(dir))
}
func TestDetectLanguage_Good_Rust(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "Cargo.toml"), "[package]"))
require.True(t, fs.Write(filepath.Join(dir, "Cargo.toml"), "[package]").OK)
assert.Equal(t, "rust", detectLanguage(dir))
}
func TestDetectLanguage_Good_Python(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "requirements.txt"), "flask"))
require.True(t, fs.Write(filepath.Join(dir, "requirements.txt"), "flask").OK)
assert.Equal(t, "py", detectLanguage(dir))
}
func TestDetectLanguage_Good_Cpp(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "CMakeLists.txt"), "cmake_minimum_required"))
require.True(t, fs.Write(filepath.Join(dir, "CMakeLists.txt"), "cmake_minimum_required").OK)
assert.Equal(t, "cpp", detectLanguage(dir))
}
func TestDetectLanguage_Good_Docker(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "Dockerfile"), "FROM alpine"))
require.True(t, fs.Write(filepath.Join(dir, "Dockerfile"), "FROM alpine").OK)
assert.Equal(t, "docker", detectLanguage(dir))
}
@ -90,7 +88,7 @@ func TestDetectBuildCmd_Good(t *testing.T) {
for _, tt := range tests {
t.Run(tt.file, func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, tt.file), tt.content))
require.True(t, fs.Write(filepath.Join(dir, tt.file), tt.content).OK)
assert.Equal(t, tt.expected, detectBuildCmd(dir))
})
}
@ -118,7 +116,7 @@ func TestDetectTestCmd_Good(t *testing.T) {
for _, tt := range tests {
t.Run(tt.file, func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, tt.file), tt.content))
require.True(t, fs.Write(filepath.Join(dir, tt.file), tt.content).OK)
assert.Equal(t, tt.expected, detectTestCmd(dir))
})
}

View file

@ -10,7 +10,6 @@ import (
"syscall"
"time"
coreio "dappco.re/go/core/io"
"gopkg.in/yaml.v3"
)
@ -47,12 +46,12 @@ func (s *PrepSubsystem) loadAgentsConfig() *AgentsConfig {
}
for _, path := range paths {
data, err := coreio.Local.Read(path)
if err != nil {
r := fs.Read(path)
if !r.OK {
continue
}
var cfg AgentsConfig
if err := yaml.Unmarshal([]byte(data), &cfg); err != nil {
if err := yaml.Unmarshal([]byte(r.Value.(string)), &cfg); err != nil {
continue
}
return &cfg

View file

@ -5,8 +5,6 @@ package agentic
import (
"path/filepath"
"testing"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -36,7 +34,7 @@ func TestCanDispatchAgent_Good_NoConfig(t *testing.T) {
// With no running workspaces and default config, should be able to dispatch
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace")))
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
s := &PrepSubsystem{codePath: t.TempDir()}
assert.True(t, s.canDispatchAgent("gemini"))
@ -46,7 +44,7 @@ func TestCanDispatchAgent_Good_UnknownAgent(t *testing.T) {
// Unknown agent has no limit, so always allowed
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace")))
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
s := &PrepSubsystem{codePath: t.TempDir()}
assert.True(t, s.canDispatchAgent("unknown-agent"))
@ -55,7 +53,7 @@ func TestCanDispatchAgent_Good_UnknownAgent(t *testing.T) {
func TestCountRunningByAgent_Good_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace")))
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByAgent("gemini"))
@ -68,7 +66,7 @@ func TestCountRunningByAgent_Good_NoRunning(t *testing.T) {
// Create a workspace with completed status under workspace/
ws := filepath.Join(root, "workspace", "test-ws")
require.NoError(t, coreio.Local.EnsureDir(ws))
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Agent: "gemini",

View file

@ -11,8 +11,7 @@ import (
"strings"
"time"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -50,13 +49,13 @@ func (s *PrepSubsystem) registerRemoteDispatchTool(server *mcp.Server) {
func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolRequest, input RemoteDispatchInput) (*mcp.CallToolResult, RemoteDispatchOutput, error) {
if input.Host == "" {
return nil, RemoteDispatchOutput{}, coreerr.E("dispatchRemote", "host is required", nil)
return nil, RemoteDispatchOutput{}, core.E("dispatchRemote", "host is required", nil)
}
if input.Repo == "" {
return nil, RemoteDispatchOutput{}, coreerr.E("dispatchRemote", "repo is required", nil)
return nil, RemoteDispatchOutput{}, core.E("dispatchRemote", "repo is required", nil)
}
if input.Task == "" {
return nil, RemoteDispatchOutput{}, coreerr.E("dispatchRemote", "task is required", nil)
return nil, RemoteDispatchOutput{}, core.E("dispatchRemote", "task is required", nil)
}
// Resolve host aliases
@ -105,7 +104,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque
return nil, RemoteDispatchOutput{
Host: input.Host,
Error: fmt.Sprintf("init failed: %v", err),
}, coreerr.E("dispatchRemote", "MCP initialize failed", err)
}, core.E("dispatchRemote", "MCP initialize failed", err)
}
// Step 2: Call the tool
@ -115,7 +114,7 @@ func (s *PrepSubsystem) dispatchRemote(ctx context.Context, _ *mcp.CallToolReque
return nil, RemoteDispatchOutput{
Host: input.Host,
Error: fmt.Sprintf("call failed: %v", err),
}, coreerr.E("dispatchRemote", "tool call failed", err)
}, core.E("dispatchRemote", "tool call failed", err)
}
// Parse result
@ -195,8 +194,8 @@ func remoteToken(host string) string {
fmt.Sprintf("%s/.core/agent-token", home),
}
for _, f := range tokenFiles {
if data, err := coreio.Local.Read(f); err == nil {
return strings.TrimSpace(data)
if r := fs.Read(f); r.OK {
return strings.TrimSpace(r.Value.(string))
}
}

View file

@ -3,6 +3,7 @@
package agentic
import (
core "dappco.re/go/core"
"bufio"
"bytes"
"context"
@ -11,7 +12,6 @@ import (
"net/http"
"strings"
coreerr "dappco.re/go/core/log"
)
// mcpInitialize performs the MCP initialize handshake over Streamable HTTP.
@ -35,18 +35,18 @@ func mcpInitialize(ctx context.Context, client *http.Client, url, token string)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return "", coreerr.E("mcpInitialize", "create request", err)
return "", core.E("mcpInitialize", "create request", err)
}
setHeaders(req, token, "")
resp, err := client.Do(req)
if err != nil {
return "", coreerr.E("mcpInitialize", "request failed", err)
return "", core.E("mcpInitialize", "request failed", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", coreerr.E("mcpInitialize", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
return "", core.E("mcpInitialize", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
}
sessionID := resp.Header.Get("Mcp-Session-Id")
@ -77,18 +77,18 @@ func mcpInitialize(ctx context.Context, client *http.Client, url, token string)
func mcpCall(ctx context.Context, client *http.Client, url, token, sessionID string, body []byte) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return nil, coreerr.E("mcpCall", "create request", err)
return nil, core.E("mcpCall", "create request", err)
}
setHeaders(req, token, sessionID)
resp, err := client.Do(req)
if err != nil {
return nil, coreerr.E("mcpCall", "request failed", err)
return nil, core.E("mcpCall", "request failed", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("mcpCall", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
return nil, core.E("mcpCall", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
}
// Parse SSE response — extract data: lines
@ -104,7 +104,7 @@ func readSSEData(resp *http.Response) ([]byte, error) {
return []byte(strings.TrimPrefix(line, "data: ")), nil
}
}
return nil, coreerr.E("readSSEData", "no data in SSE response", nil)
return nil, core.E("readSSEData", "no data in SSE response", nil)
}
// setHeaders applies standard MCP HTTP headers.

View file

@ -8,7 +8,7 @@ import (
"net/http"
"time"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -37,7 +37,7 @@ func (s *PrepSubsystem) registerRemoteStatusTool(server *mcp.Server) {
func (s *PrepSubsystem) statusRemote(ctx context.Context, _ *mcp.CallToolRequest, input RemoteStatusInput) (*mcp.CallToolResult, RemoteStatusOutput, error) {
if input.Host == "" {
return nil, RemoteStatusOutput{}, coreerr.E("statusRemote", "host is required", nil)
return nil, RemoteStatusOutput{}, core.E("statusRemote", "host is required", nil)
}
addr := resolveHost(input.Host)

View file

@ -8,8 +8,7 @@ import (
"os"
"path/filepath"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -40,7 +39,7 @@ func (s *PrepSubsystem) registerResumeTool(server *mcp.Server) {
func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, input ResumeInput) (*mcp.CallToolResult, ResumeOutput, error) {
if input.Workspace == "" {
return nil, ResumeOutput{}, coreerr.E("resume", "workspace is required", nil)
return nil, ResumeOutput{}, core.E("resume", "workspace is required", nil)
}
wsDir := filepath.Join(WorkspaceRoot(), input.Workspace)
@ -48,17 +47,17 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
// Verify workspace exists
if _, err := os.Stat(srcDir); err != nil {
return nil, ResumeOutput{}, coreerr.E("resume", "workspace not found: "+input.Workspace, nil)
return nil, ResumeOutput{}, core.E("resume", "workspace not found: "+input.Workspace, nil)
}
// Read current status
st, err := readStatus(wsDir)
if err != nil {
return nil, ResumeOutput{}, coreerr.E("resume", "no status.json in workspace", err)
return nil, ResumeOutput{}, core.E("resume", "no status.json in workspace", err)
}
if st.Status != "blocked" && st.Status != "failed" && st.Status != "completed" {
return nil, ResumeOutput{}, coreerr.E("resume", "workspace is "+st.Status+", not resumable (must be blocked, failed, or completed)", nil)
return nil, ResumeOutput{}, core.E("resume", "workspace is "+st.Status+", not resumable (must be blocked, failed, or completed)", nil)
}
// Determine agent
@ -71,8 +70,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
if input.Answer != "" {
answerPath := filepath.Join(srcDir, "ANSWER.md")
content := fmt.Sprintf("# Answer\n\n%s\n", input.Answer)
if err := coreio.Local.Write(answerPath, content); err != nil {
return nil, ResumeOutput{}, coreerr.E("resume", "failed to write ANSWER.md", err)
if r := fs.Write(answerPath, content); !r.OK {
return nil, ResumeOutput{}, core.E("resume", "failed to write ANSWER.md", nil)
}
}

View file

@ -14,8 +14,7 @@ import (
"strings"
"time"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -223,7 +222,7 @@ func (s *PrepSubsystem) reviewRepo(ctx context.Context, repoDir, repo, reviewer
// Save findings for agent dispatch
findingsFile := filepath.Join(repoDir, ".core", "coderabbit-findings.txt")
coreio.Local.Write(findingsFile, output)
fs.Write(findingsFile, output)
// Dispatch fix agent with the findings
task := fmt.Sprintf("Fix CodeRabbit findings. The review output is in .core/coderabbit-findings.txt. "+
@ -248,7 +247,7 @@ func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string)
pushCmd := exec.CommandContext(ctx, "git", "push", "github", "HEAD:refs/heads/dev", "--force")
pushCmd.Dir = repoDir
if out, err := pushCmd.CombinedOutput(); err != nil {
return coreerr.E("pushAndMerge", "push failed: "+string(out), err)
return core.E("pushAndMerge", "push failed: "+string(out), err)
}
// Mark PR ready if draft
@ -260,7 +259,7 @@ func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string)
mergeCmd := exec.CommandContext(ctx, "gh", "pr", "merge", "--merge", "--delete-branch")
mergeCmd.Dir = repoDir
if out, err := mergeCmd.CombinedOutput(); err != nil {
return coreerr.E("pushAndMerge", "merge failed: "+string(out), err)
return core.E("pushAndMerge", "merge failed: "+string(out), err)
}
return nil
@ -279,7 +278,7 @@ func (s *PrepSubsystem) dispatchFixFromQueue(ctx context.Context, repo, task str
return err
}
if !out.Success {
return coreerr.E("dispatchFixFromQueue", "dispatch failed for "+repo, nil)
return core.E("dispatchFixFromQueue", "dispatch failed for "+repo, nil)
}
return nil
}
@ -336,13 +335,13 @@ func (s *PrepSubsystem) buildReviewCommand(ctx context.Context, repoDir, reviewe
func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string) {
home, _ := os.UserHomeDir()
dataDir := filepath.Join(home, ".core", "training", "reviews")
coreio.Local.EnsureDir(dataDir)
fs.EnsureDir(dataDir)
timestamp := time.Now().Format("2006-01-02T15-04-05")
filename := fmt.Sprintf("%s_%s_%s.txt", repo, reviewer, timestamp)
// Write raw output
coreio.Local.Write(filepath.Join(dataDir, filename), output)
fs.Write(filepath.Join(dataDir, filename), output)
// Append to JSONL for structured training
entry := map[string]string{
@ -370,19 +369,19 @@ func (s *PrepSubsystem) saveRateLimitState(info *RateLimitInfo) {
home, _ := os.UserHomeDir()
path := filepath.Join(home, ".core", "coderabbit-ratelimit.json")
data, _ := json.Marshal(info)
coreio.Local.Write(path, string(data))
fs.Write(path, string(data))
}
// loadRateLimitState reads persisted rate limit info.
func (s *PrepSubsystem) loadRateLimitState() *RateLimitInfo {
home, _ := os.UserHomeDir()
path := filepath.Join(home, ".core", "coderabbit-ratelimit.json")
data, err := coreio.Local.Read(path)
if err != nil {
r := fs.Read(path)
if !r.OK {
return nil
}
var info RateLimitInfo
if json.Unmarshal([]byte(data), &info) != nil {
if json.Unmarshal([]byte(r.Value.(string)), &info) != nil {
return nil
}
return &info

View file

@ -9,7 +9,7 @@ import (
"net/http"
"strings"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -39,7 +39,7 @@ type ScanIssue struct {
func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input ScanInput) (*mcp.CallToolResult, ScanOutput, error) {
if s.forgeToken == "" {
return nil, ScanOutput{}, coreerr.E("scan", "no Forge token configured", nil)
return nil, ScanOutput{}, core.E("scan", "no Forge token configured", nil)
}
if input.Org == "" {
@ -107,18 +107,18 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string,
u := fmt.Sprintf("%s/api/v1/orgs/%s/repos?limit=50&page=%d", s.forgeURL, org, page)
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return nil, coreerr.E("scan.listOrgRepos", "failed to create request", err)
return nil, core.E("scan.listOrgRepos", "failed to create request", err)
}
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
if err != nil {
return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err)
return nil, core.E("scan.listOrgRepos", "failed to list repos", err)
}
if resp.StatusCode != 200 {
resp.Body.Close()
return nil, coreerr.E("scan.listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil)
return nil, core.E("scan.listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil)
}
var repos []struct {
@ -148,18 +148,18 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str
}
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return nil, coreerr.E("scan.listRepoIssues", "failed to create request", err)
return nil, core.E("scan.listRepoIssues", "failed to create request", err)
}
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
if err != nil {
return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
return nil, core.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("scan.listRepoIssues", fmt.Sprintf("HTTP %d listing issues for %s", resp.StatusCode, repo), nil)
return nil, core.E("scan.listRepoIssues", fmt.Sprintf("HTTP %d listing issues for %s", resp.StatusCode, repo), nil)
}
var issues []struct {

View file

@ -12,8 +12,7 @@ import (
"syscall"
"time"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -31,6 +30,9 @@ import (
// running → failed (agent crashed / non-zero exit)
// WorkspaceStatus represents the current state of an agent workspace.
//
// st, err := readStatus(wsDir)
// if err == nil && st.Status == "completed" { autoCreatePR(wsDir) }
type WorkspaceStatus struct {
Status string `json:"status"` // running, completed, blocked, failed
Agent string `json:"agent"` // gemini, claude, codex
@ -53,16 +55,19 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error {
if err != nil {
return err
}
return coreio.Local.Write(filepath.Join(wsDir, "status.json"), string(data))
if r := fs.Write(filepath.Join(wsDir, "status.json"), string(data)); !r.OK {
return core.E("writeStatus", "failed to write status", nil)
}
return nil
}
func readStatus(wsDir string) (*WorkspaceStatus, error) {
data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json"))
if err != nil {
return nil, err
r := fs.Read(filepath.Join(wsDir, "status.json"))
if !r.OK {
return nil, core.E("readStatus", "status not found", nil)
}
var s WorkspaceStatus
if err := json.Unmarshal([]byte(data), &s); err != nil {
if err := json.Unmarshal([]byte(r.Value.(string)), &s); err != nil {
return nil, err
}
return &s, nil
@ -102,7 +107,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
entries, err := os.ReadDir(wsRoot)
if err != nil {
return nil, StatusOutput{}, coreerr.E("status", "no workspaces found", err)
return nil, StatusOutput{}, core.E("status", "no workspaces found", err)
}
var workspaces []WorkspaceInfo
@ -152,16 +157,16 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
if err := syscall.Kill(st.PID, 0); err != nil {
// Process died — check for BLOCKED.md
blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md")
if data, err := coreio.Local.Read(blockedPath); err == nil {
if r := fs.Read(blockedPath); r.OK {
info.Status = "blocked"
info.Question = strings.TrimSpace(data)
info.Question = strings.TrimSpace(r.Value.(string))
st.Status = "blocked"
st.Question = info.Question
} else {
// Dead PID without BLOCKED.md — check exit code from log
// If no evidence of success, mark as failed (not completed)
logFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", st.Agent))
if _, err := coreio.Local.Read(logFile); err != nil {
if r := fs.Read(logFile); !r.OK {
info.Status = "failed"
st.Status = "failed"
st.Question = "Agent process died (no output log)"

View file

@ -7,8 +7,6 @@ import (
"path/filepath"
"testing"
"time"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -28,12 +26,12 @@ func TestWriteStatus_Good(t *testing.T) {
err := writeStatus(dir, status)
require.NoError(t, err)
// Verify file was written via coreio
data, err := coreio.Local.Read(filepath.Join(dir, "status.json"))
require.NoError(t, err)
// Verify file was written via core.Fs
r := fs.Read(filepath.Join(dir, "status.json"))
require.True(t, r.OK)
var read WorkspaceStatus
err = json.Unmarshal([]byte(data), &read)
err = json.Unmarshal([]byte(r.Value.(string)), &read)
require.NoError(t, err)
assert.Equal(t, "running", read.Status)
@ -77,7 +75,7 @@ func TestReadStatus_Good(t *testing.T) {
data, err := json.MarshalIndent(status, "", " ")
require.NoError(t, err)
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), string(data)))
require.True(t, fs.Write(filepath.Join(dir, "status.json"), string(data)).OK)
read, err := readStatus(dir)
require.NoError(t, err)
@ -99,7 +97,7 @@ func TestReadStatus_Bad_NoFile(t *testing.T) {
func TestReadStatus_Bad_InvalidJSON(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), "not json{"))
require.True(t, fs.Write(filepath.Join(dir, "status.json"), "not json{").OK)
_, err := readStatus(dir)
assert.Error(t, err)
@ -117,7 +115,7 @@ func TestReadStatus_Good_BlockedWithQuestion(t *testing.T) {
data, err := json.MarshalIndent(status, "", " ")
require.NoError(t, err)
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), string(data)))
require.True(t, fs.Write(filepath.Join(dir, "status.json"), string(data)).OK)
read, err := readStatus(dir)
require.NoError(t, err)
@ -177,7 +175,7 @@ func TestWriteStatus_Good_OverwriteExisting(t *testing.T) {
func TestReadStatus_Ugly_EmptyFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), ""))
require.True(t, fs.Write(filepath.Join(dir, "status.json"), "").OK)
_, err := readStatus(dir)
assert.Error(t, err)

View file

@ -3,6 +3,7 @@
package agentic
import (
core "dappco.re/go/core"
"bytes"
"context"
"encoding/json"
@ -14,8 +15,6 @@ import (
"strings"
"time"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// autoVerifyAndMerge runs inline tests (fast gate) and merges if they pass.
@ -112,7 +111,7 @@ func (s *PrepSubsystem) attemptVerifyAndMerge(srcDir, org, repo, branch string,
// rebaseBranch rebases the current branch onto the default branch and force-pushes.
func (s *PrepSubsystem) rebaseBranch(srcDir, branch string) bool {
base := gitDefaultBranch(srcDir)
base := DefaultBranch(srcDir)
// Fetch latest default branch
fetch := exec.Command("git", "fetch", "origin", base)
@ -282,15 +281,15 @@ func (s *PrepSubsystem) runPHPTests(srcDir string) verifyResult {
}
func (s *PrepSubsystem) runNodeTests(srcDir string) verifyResult {
data, err := coreio.Local.Read(filepath.Join(srcDir, "package.json"))
if err != nil {
r := fs.Read(filepath.Join(srcDir, "package.json"))
if !r.OK {
return verifyResult{passed: true, testCmd: "none", output: "Could not read package.json"}
}
var pkg struct {
Scripts map[string]string `json:"scripts"`
}
if json.Unmarshal([]byte(data), &pkg) != nil || pkg.Scripts["test"] == "" {
if json.Unmarshal([]byte(r.Value.(string)), &pkg) != nil || pkg.Scripts["test"] == "" {
return verifyResult{passed: true, testCmd: "none", output: "No test script in package.json"}
}
@ -325,7 +324,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu
resp, err := s.client.Do(req)
if err != nil {
return coreerr.E("forgeMergePR", "request failed", err)
return core.E("forgeMergePR", "request failed", err)
}
defer resp.Body.Close()
@ -333,7 +332,7 @@ func (s *PrepSubsystem) forgeMergePR(ctx context.Context, org, repo string, prNu
var errBody map[string]any
json.NewDecoder(resp.Body).Decode(&errBody)
msg, _ := errBody["message"].(string)
return coreerr.E("forgeMergePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil)
return core.E("forgeMergePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil)
}
return nil
@ -352,5 +351,5 @@ func extractPRNumber(prURL string) int {
// fileExists checks if a file exists.
func fileExists(path string) bool {
return coreio.Local.IsFile(path)
return fs.IsFile(path)
}

View file

@ -8,7 +8,7 @@ import (
"path/filepath"
"time"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -99,7 +99,7 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp
select {
case <-ctx.Done():
return nil, WatchOutput{}, coreerr.E("watch", "cancelled", ctx.Err())
return nil, WatchOutput{}, core.E("watch", "cancelled", ctx.Err())
case <-time.After(pollInterval):
}

View file

@ -6,15 +6,30 @@ package brain
import (
"context"
"unsafe"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"forge.lthn.ai/core/mcp/pkg/mcp/ide"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// fs provides unrestricted filesystem access (root "/" = no sandbox).
//
// r := fs.Read(filepath.Join(home, ".claude", "brain.key"))
// if r.OK { apiKey = strings.TrimSpace(r.Value.(string)) }
var fs = newFs("/")
// newFs creates a core.Fs with the given root directory.
func newFs(root string) *core.Fs {
type fsRoot struct{ root string }
f := &core.Fs{}
(*fsRoot)(unsafe.Pointer(f)).root = root
return f
}
// errBridgeNotAvailable is returned when a tool requires the Laravel bridge
// but it has not been initialised (headless mode).
var errBridgeNotAvailable = coreerr.E("brain", "bridge not available", nil)
var errBridgeNotAvailable = core.E("brain", "bridge not available", nil)
// Subsystem implements mcp.Subsystem for OpenBrain knowledge store operations.
// It proxies brain_* tool calls to the Laravel backend via the shared IDE bridge.

View file

@ -14,9 +14,8 @@ import (
"strings"
"time"
core "dappco.re/go/core"
"dappco.re/go/agent/pkg/agentic"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -28,6 +27,9 @@ func agentName() string {
// DirectSubsystem implements mcp.Subsystem for OpenBrain via direct HTTP calls.
// Unlike Subsystem (which uses the IDE WebSocket bridge), this calls the
// Laravel API directly — suitable for standalone core-mcp usage.
//
// sub := brain.NewDirect()
// sub.RegisterTools(server)
type DirectSubsystem struct {
apiURL string
apiKey string
@ -46,8 +48,8 @@ func NewDirect() *DirectSubsystem {
apiKey := os.Getenv("CORE_BRAIN_KEY")
if apiKey == "" {
home, _ := os.UserHomeDir()
if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil {
apiKey = strings.TrimSpace(data)
if r := fs.Read(filepath.Join(home, ".claude", "brain.key")); r.OK {
apiKey = strings.TrimSpace(r.Value.(string))
}
}
@ -87,21 +89,21 @@ func (s *DirectSubsystem) Shutdown(_ context.Context) error { return nil }
func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body any) (map[string]any, error) {
if s.apiKey == "" {
return nil, coreerr.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil)
return nil, core.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil)
}
var reqBody io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, coreerr.E("brain.apiCall", "marshal request", err)
return nil, core.E("brain.apiCall", "marshal request", err)
}
reqBody = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, s.apiURL+path, reqBody)
if err != nil {
return nil, coreerr.E("brain.apiCall", "create request", err)
return nil, core.E("brain.apiCall", "create request", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
@ -109,22 +111,22 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body
resp, err := s.client.Do(req)
if err != nil {
return nil, coreerr.E("brain.apiCall", "API call failed", err)
return nil, core.E("brain.apiCall", "API call failed", err)
}
defer resp.Body.Close()
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, coreerr.E("brain.apiCall", "read response", err)
return nil, core.E("brain.apiCall", "read response", err)
}
if resp.StatusCode >= 400 {
return nil, coreerr.E("brain.apiCall", fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil)
return nil, core.E("brain.apiCall", fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(respData)), nil)
}
var result map[string]any
if err := json.Unmarshal(respData, &result); err != nil {
return nil, coreerr.E("brain.apiCall", "parse response", err)
return nil, core.E("brain.apiCall", "parse response", err)
}
return result, nil

View file

@ -9,8 +9,6 @@ import (
"net/http/httptest"
"path/filepath"
"testing"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -63,8 +61,8 @@ func TestNewDirect_Good_KeyFromFile(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
keyDir := filepath.Join(tmpHome, ".claude")
require.NoError(t, coreio.Local.EnsureDir(keyDir))
require.NoError(t, coreio.Local.Write(filepath.Join(keyDir, "brain.key"), " file-key-456 \n"))
require.True(t, fs.EnsureDir(keyDir).OK)
require.True(t, fs.Write(filepath.Join(keyDir, "brain.key"), " file-key-456 \n").OK)
sub := NewDirect()
assert.Equal(t, "file-key-456", sub.apiKey)

View file

@ -7,7 +7,7 @@ import (
"fmt"
"net/url"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -75,7 +75,7 @@ type ConversationOutput struct {
func (s *DirectSubsystem) sendMessage(ctx context.Context, _ *mcp.CallToolRequest, input SendInput) (*mcp.CallToolResult, SendOutput, error) {
if input.To == "" || input.Content == "" {
return nil, SendOutput{}, coreerr.E("brain.sendMessage", "to and content are required", nil)
return nil, SendOutput{}, core.E("brain.sendMessage", "to and content are required", nil)
}
result, err := s.apiCall(ctx, "POST", "/v1/messages/send", map[string]any{
@ -116,7 +116,7 @@ func (s *DirectSubsystem) inbox(ctx context.Context, _ *mcp.CallToolRequest, inp
func (s *DirectSubsystem) conversation(ctx context.Context, _ *mcp.CallToolRequest, input ConversationInput) (*mcp.CallToolResult, ConversationOutput, error) {
if input.Agent == "" {
return nil, ConversationOutput{}, coreerr.E("brain.conversation", "agent is required", nil)
return nil, ConversationOutput{}, core.E("brain.conversation", "agent is required", nil)
}
result, err := s.apiCall(ctx, "GET", "/v1/messages/conversation/"+url.PathEscape(input.Agent)+"?me="+url.QueryEscape(agentName()), nil)

View file

@ -6,7 +6,7 @@ import (
"context"
"time"
coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
"forge.lthn.ai/core/mcp/pkg/mcp/ide"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -140,7 +140,7 @@ func (s *Subsystem) brainRemember(_ context.Context, _ *mcp.CallToolRequest, inp
},
})
if err != nil {
return nil, RememberOutput{}, coreerr.E("brain.remember", "failed to send brain_remember", err)
return nil, RememberOutput{}, core.E("brain.remember", "failed to send brain_remember", err)
}
return nil, RememberOutput{
@ -163,7 +163,7 @@ func (s *Subsystem) brainRecall(_ context.Context, _ *mcp.CallToolRequest, input
},
})
if err != nil {
return nil, RecallOutput{}, coreerr.E("brain.recall", "failed to send brain_recall", err)
return nil, RecallOutput{}, core.E("brain.recall", "failed to send brain_recall", err)
}
return nil, RecallOutput{
@ -185,7 +185,7 @@ func (s *Subsystem) brainForget(_ context.Context, _ *mcp.CallToolRequest, input
},
})
if err != nil {
return nil, ForgetOutput{}, coreerr.E("brain.forget", "failed to send brain_forget", err)
return nil, ForgetOutput{}, core.E("brain.forget", "failed to send brain_forget", err)
}
return nil, ForgetOutput{
@ -210,7 +210,7 @@ func (s *Subsystem) brainList(_ context.Context, _ *mcp.CallToolRequest, input L
},
})
if err != nil {
return nil, ListOutput{}, coreerr.E("brain.list", "failed to send brain_list", err)
return nil, ListOutput{}, core.E("brain.list", "failed to send brain_list", err)
}
return nil, ListOutput{

View file

@ -18,9 +18,8 @@ import (
"path/filepath"
"strings"
core "dappco.re/go/core"
"dappco.re/go/agent/pkg/agentic"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// harvestResult tracks what happened during harvest.
@ -81,8 +80,8 @@ func (m *Subsystem) harvestCompleted() string {
// harvestWorkspace checks a single workspace and pushes if ready.
func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json"))
if err != nil {
r := fs.Read(filepath.Join(wsDir, "status.json"))
if !r.OK {
return nil
}
@ -91,7 +90,7 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
Repo string `json:"repo"`
Branch string `json:"branch"`
}
if json.Unmarshal([]byte(data), &st) != nil {
if json.Unmarshal([]byte(r.Value.(string)), &st) != nil {
return nil
}
@ -262,19 +261,19 @@ func pushBranch(srcDir, branch string) error {
cmd.Dir = srcDir
out, err := cmd.CombinedOutput()
if err != nil {
return coreerr.E("harvest.pushBranch", strings.TrimSpace(string(out)), err)
return core.E("harvest.pushBranch", strings.TrimSpace(string(out)), err)
}
return nil
}
// updateStatus updates the workspace status.json.
func updateStatus(wsDir, status, question string) {
data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json"))
if err != nil {
r := fs.Read(filepath.Join(wsDir, "status.json"))
if !r.OK {
return
}
var st map[string]any
if json.Unmarshal([]byte(data), &st) != nil {
if json.Unmarshal([]byte(r.Value.(string)), &st) != nil {
return
}
st["status"] = status
@ -284,5 +283,5 @@ func updateStatus(wsDir, status, question string) {
delete(st, "question") // clear stale question from previous state
}
updated, _ := json.MarshalIndent(st, "", " ")
coreio.Local.Write(filepath.Join(wsDir, "status.json"), string(updated))
fs.Write(filepath.Join(wsDir, "status.json"), string(updated))
}

View file

@ -21,12 +21,17 @@ import (
"sync"
"time"
core "dappco.re/go/core"
"dappco.re/go/agent/pkg/agentic"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// fs provides unrestricted filesystem access (root "/" = no sandbox).
//
// r := fs.Read(filepath.Join(wsRoot, name, "status.json"))
// if r.OK { json.Unmarshal([]byte(r.Value.(string)), &st) }
var fs = agentic.LocalFs()
// ChannelNotifier pushes events to connected MCP sessions.
// Matches the Notifier interface in core/mcp without importing it.
type ChannelNotifier interface {
@ -34,6 +39,10 @@ type ChannelNotifier interface {
}
// Subsystem implements mcp.Subsystem for background monitoring.
//
// mon := monitor.New(monitor.Options{Interval: 2 * time.Minute})
// mon.SetNotifier(notifier)
// mon.Start(ctx)
type Subsystem struct {
server *mcp.Server
notifier ChannelNotifier
@ -222,8 +231,8 @@ func (m *Subsystem) checkCompletions() string {
m.mu.Lock()
seeded := m.completionsSeeded
for _, entry := range entries {
data, err := coreio.Local.Read(entry)
if err != nil {
r := fs.Read(entry)
if !r.OK {
continue
}
var st struct {
@ -231,7 +240,7 @@ func (m *Subsystem) checkCompletions() string {
Repo string `json:"repo"`
Agent string `json:"agent"`
}
if json.Unmarshal([]byte(data), &st) != nil {
if json.Unmarshal([]byte(r.Value.(string)), &st) != nil {
continue
}
@ -291,11 +300,11 @@ func (m *Subsystem) checkInbox() string {
if apiKeyStr == "" {
home, _ := os.UserHomeDir()
keyFile := filepath.Join(home, ".claude", "brain.key")
data, err := coreio.Local.Read(keyFile)
if err != nil {
r := fs.Read(keyFile)
if !r.OK {
return ""
}
apiKeyStr = data
apiKeyStr = r.Value.(string)
}
// Call the API to check inbox
@ -421,7 +430,7 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour
wsRoot := agentic.WorkspaceRoot()
entries, err := filepath.Glob(filepath.Join(wsRoot, "*/status.json"))
if err != nil {
return nil, coreerr.E("monitor.agentStatus", "failed to scan workspaces", err)
return nil, core.E("monitor.agentStatus", "failed to scan workspaces", err)
}
type wsInfo struct {
@ -434,8 +443,8 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour
var workspaces []wsInfo
for _, entry := range entries {
data, err := coreio.Local.Read(entry)
if err != nil {
r := fs.Read(entry)
if !r.OK {
continue
}
var st struct {
@ -444,7 +453,7 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour
Agent string `json:"agent"`
PRURL string `json:"pr_url"`
}
if json.Unmarshal([]byte(data), &st) != nil {
if json.Unmarshal([]byte(r.Value.(string)), &st) != nil {
continue
}
workspaces = append(workspaces, wsInfo{

View file

@ -14,7 +14,6 @@ import (
"time"
"dappco.re/go/agent/pkg/agentic"
coreio "dappco.re/go/core/io"
)
// CheckinResponse is what the API returns for an agent checkin.
@ -53,8 +52,8 @@ func (m *Subsystem) syncRepos() string {
brainKey := os.Getenv("CORE_BRAIN_KEY")
if brainKey == "" {
home, _ := os.UserHomeDir()
if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil {
brainKey = strings.TrimSpace(data)
if r := fs.Read(filepath.Join(home, ".claude", "brain.key")); r.OK {
brainKey = strings.TrimSpace(r.Value.(string))
}
}
if brainKey != "" {