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:
parent
52c3e67692
commit
deaa06a54d
32 changed files with 311 additions and 260 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -1,8 +1,5 @@
|
|||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
.core/
|
||||
docker/.env
|
||||
ui/node_modules
|
||||
# Compiled binaries
|
||||
core-agent
|
||||
mcp
|
||||
*.exe
|
||||
var/
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 != "" {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue