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