- paths.go: resolve relative workspace_root against $HOME/Code so workspaces land in the conventional location regardless of launch cwd (MCP stdio vs CLI) - dispatch.go: container mounts use /home/agent (matches DEV_USER), plus runtime-aware dispatch (apple/docker/podman) with GPU toggle per RFC §15.5 - queue.go / runner/queue.go: DispatchConfig adds Runtime/Image/GPU fields; AgentIdentity parsing for the agents: block (RFC §10/§11) - pr.go / commands_forge.go / actions.go: agentic_delete_branch tool + branch/delete CLI (RFC §7) - brain/tools.go / provider.go: Org + IndexedAt fields on Memory (RFC §4) - config/agents.yaml: document new dispatch fields, fix identity table - tests: dispatch_runtime_test.go (21), expanded pr_test.go + queue_test.go, new CLI fixtures for branch/delete and pr/list Co-Authored-By: Virgil <virgil@lethean.io>
270 lines
7.2 KiB
Go
270 lines
7.2 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
iofs "io/fs"
|
|
"slices"
|
|
"strconv"
|
|
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// fsEntry matches the fs.DirEntry methods used by workspace scanning.
|
|
// Avoids importing io/fs — core.Fs.List() returns []iofs.DirEntry internally.
|
|
//
|
|
// entry, ok := item.(fsEntry)
|
|
// if ok { core.Print(nil, "%s isDir=%v", entry.Name(), entry.IsDir()) }
|
|
type fsEntry interface {
|
|
Name() string
|
|
IsDir() bool
|
|
}
|
|
|
|
// r := fs.Read("/etc/hostname")
|
|
// if r.OK { core.Print(nil, "%s", r.Value.(string)) }
|
|
var fs = (&core.Fs{}).NewUnrestricted()
|
|
|
|
var workspaceRootOverride string
|
|
|
|
// setWorkspaceRootOverride("/srv/.core/workspace") // absolute — used as-is
|
|
// setWorkspaceRootOverride(".core/workspace") // relative — resolved to $HOME/Code/.core/workspace
|
|
// setWorkspaceRootOverride("") // unset — WorkspaceRoot() falls back to CoreRoot()+"/workspace"
|
|
func setWorkspaceRootOverride(root string) {
|
|
root = core.Trim(root)
|
|
if root != "" && !core.PathIsAbs(root) {
|
|
// Resolve relative paths against $HOME/Code — the convention.
|
|
// Without this, workspaces resolve against the binary's cwd which
|
|
// varies by launch context (MCP stdio vs CLI vs dispatch worker).
|
|
root = core.JoinPath(HomeDir(), "Code", root)
|
|
}
|
|
workspaceRootOverride = root
|
|
}
|
|
|
|
// f := agentic.LocalFs()
|
|
// r := f.Read("/tmp/agent-status.json")
|
|
func LocalFs() *core.Fs { return fs }
|
|
|
|
// workspaceDir := core.JoinPath(agentic.WorkspaceRoot(), "core", "go-io", "task-42")
|
|
func WorkspaceRoot() string {
|
|
if root := core.Trim(workspaceRootOverride); root != "" {
|
|
return root
|
|
}
|
|
return core.JoinPath(CoreRoot(), "workspace")
|
|
}
|
|
|
|
// paths := agentic.WorkspaceStatusPaths()
|
|
func WorkspaceStatusPaths() []string {
|
|
return workspaceStatusPaths(WorkspaceRoot())
|
|
}
|
|
|
|
// path := agentic.WorkspaceStatusPath("/srv/.core/workspace/core/go-io/task-5")
|
|
func WorkspaceStatusPath(workspaceDir string) string {
|
|
return core.JoinPath(workspaceDir, "status.json")
|
|
}
|
|
|
|
// name := agentic.WorkspaceName("/Users/snider/Code/.core/workspace/core/go-io/dev")
|
|
func WorkspaceName(workspaceDir string) string {
|
|
root := WorkspaceRoot()
|
|
name := core.TrimPrefix(workspaceDir, root)
|
|
name = core.TrimPrefix(name, "/")
|
|
if name == "" {
|
|
return core.PathBase(workspaceDir)
|
|
}
|
|
return name
|
|
}
|
|
|
|
// root := agentic.CoreRoot()
|
|
func CoreRoot() string {
|
|
if root := core.Env("CORE_WORKSPACE"); root != "" {
|
|
return root
|
|
}
|
|
return core.JoinPath(HomeDir(), "Code", ".core")
|
|
}
|
|
|
|
// home := agentic.HomeDir()
|
|
func HomeDir() string {
|
|
if home := core.Env("CORE_HOME"); home != "" {
|
|
return home
|
|
}
|
|
if home := core.Env("HOME"); home != "" {
|
|
return home
|
|
}
|
|
return core.Env("DIR_HOME")
|
|
}
|
|
|
|
func workspaceStatusPaths(workspaceRoot string) []string {
|
|
if workspaceRoot == "" {
|
|
return nil
|
|
}
|
|
|
|
var paths []string
|
|
seen := make(map[string]bool)
|
|
|
|
var walk func(dir string, depth int)
|
|
walk = func(dir string, depth int) {
|
|
r := fs.List(dir)
|
|
if !r.OK {
|
|
return
|
|
}
|
|
|
|
statusPath := core.JoinPath(dir, "status.json")
|
|
if fs.IsFile(statusPath) {
|
|
if depth == 1 || depth == 3 || (fs.IsDir(core.JoinPath(dir, "repo")) && fs.IsDir(core.JoinPath(dir, ".meta"))) {
|
|
if !seen[statusPath] {
|
|
seen[statusPath] = true
|
|
paths = append(paths, statusPath)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, name := range listDirNames(r) {
|
|
child := core.JoinPath(dir, name)
|
|
if fs.IsDir(child) {
|
|
walk(child, depth+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(workspaceRoot, 0)
|
|
slices.Sort(paths)
|
|
return paths
|
|
}
|
|
|
|
// listDirNames extracts entry names from a core.Fs.List() Result.
|
|
// core.Fs.List() returns []iofs.DirEntry — type-assert directly.
|
|
//
|
|
// r := fs.List("/path/to/dir")
|
|
// names := listDirNames(r) // ["file.go", "subdir", "README.md"]
|
|
func listDirNames(r core.Result) []string {
|
|
if !r.OK || r.Value == nil {
|
|
return nil
|
|
}
|
|
entries, ok := r.Value.([]iofs.DirEntry)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
names := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
names = append(names, entry.Name())
|
|
}
|
|
return names
|
|
}
|
|
|
|
// listDirEntries extracts fsEntry values from a core.Fs.List() Result.
|
|
// core.Fs.List() returns []iofs.DirEntry — type-assert directly.
|
|
//
|
|
// r := fs.List("/path/to/dir")
|
|
// for _, entry := range listDirEntries(r) { core.Print(nil, "%s", entry.Name()) }
|
|
func listDirEntries(r core.Result) []fsEntry {
|
|
if !r.OK || r.Value == nil {
|
|
return nil
|
|
}
|
|
entries, ok := r.Value.([]iofs.DirEntry)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
result := make([]fsEntry, 0, len(entries))
|
|
for _, entry := range entries {
|
|
result = append(result, entry)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// repoDir := agentic.WorkspaceRepoDir("/srv/.core/workspace/core/go-io/task-5")
|
|
func WorkspaceRepoDir(workspaceDir string) string {
|
|
return core.JoinPath(workspaceDir, "repo")
|
|
}
|
|
|
|
func workspaceRepoDir(workspaceDir string) string {
|
|
return WorkspaceRepoDir(workspaceDir)
|
|
}
|
|
|
|
// metaDir := agentic.WorkspaceMetaDir("/srv/.core/workspace/core/go-io/task-5")
|
|
func WorkspaceMetaDir(workspaceDir string) string {
|
|
return core.JoinPath(workspaceDir, ".meta")
|
|
}
|
|
|
|
func workspaceMetaDir(workspaceDir string) string {
|
|
return WorkspaceMetaDir(workspaceDir)
|
|
}
|
|
|
|
// blocked := agentic.WorkspaceBlockedPath("/srv/.core/workspace/core/go-io/task-5")
|
|
func WorkspaceBlockedPath(workspaceDir string) string {
|
|
return core.JoinPath(WorkspaceRepoDir(workspaceDir), "BLOCKED.md")
|
|
}
|
|
|
|
func workspaceBlockedPath(workspaceDir string) string {
|
|
return WorkspaceBlockedPath(workspaceDir)
|
|
}
|
|
|
|
// answer := agentic.WorkspaceAnswerPath("/srv/.core/workspace/core/go-io/task-5")
|
|
func WorkspaceAnswerPath(workspaceDir string) string {
|
|
return core.JoinPath(WorkspaceRepoDir(workspaceDir), "ANSWER.md")
|
|
}
|
|
|
|
func workspaceAnswerPath(workspaceDir string) string {
|
|
return WorkspaceAnswerPath(workspaceDir)
|
|
}
|
|
|
|
// logs := agentic.WorkspaceLogFiles("/srv/.core/workspace/core/go-io/task-5")
|
|
func WorkspaceLogFiles(workspaceDir string) []string {
|
|
return core.PathGlob(core.JoinPath(WorkspaceMetaDir(workspaceDir), "agent-*.log"))
|
|
}
|
|
|
|
func workspaceLogFiles(workspaceDir string) []string {
|
|
return WorkspaceLogFiles(workspaceDir)
|
|
}
|
|
|
|
// plansDir := agentic.PlansRoot()
|
|
func PlansRoot() string {
|
|
return core.JoinPath(CoreRoot(), "plans")
|
|
}
|
|
|
|
// name := agentic.AgentName() // "cladius" on Snider's Mac, "charon" elsewhere
|
|
func AgentName() string {
|
|
if name := core.Env("AGENT_NAME"); name != "" {
|
|
return name
|
|
}
|
|
h := core.Lower(core.Env("HOSTNAME"))
|
|
if core.Contains(h, "snider") || core.Contains(h, "studio") || core.Contains(h, "mac") {
|
|
return "cladius"
|
|
}
|
|
return "charon"
|
|
}
|
|
|
|
// base := s.DefaultBranch("/srv/Code/core/go-io/repo")
|
|
func (s *PrepSubsystem) DefaultBranch(repoDir string) string {
|
|
ctx := context.Background()
|
|
process := s.Core().Process()
|
|
if r := process.RunIn(ctx, repoDir, "git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); r.OK {
|
|
ref := core.Trim(r.Value.(string))
|
|
if core.HasPrefix(ref, "origin/") {
|
|
return core.TrimPrefix(ref, "origin/")
|
|
}
|
|
return ref
|
|
}
|
|
for _, branch := range []string{"main", "master"} {
|
|
if process.RunIn(ctx, repoDir, "git", "rev-parse", "--verify", branch).OK {
|
|
return branch
|
|
}
|
|
}
|
|
return "main"
|
|
}
|
|
|
|
// org := agentic.GitHubOrg() // "dAppCore"
|
|
func GitHubOrg() string {
|
|
if org := core.Env("GITHUB_ORG"); org != "" {
|
|
return org
|
|
}
|
|
return "dAppCore"
|
|
}
|
|
|
|
func parseInt(value string) int {
|
|
n, err := strconv.Atoi(core.Trim(value))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|