diff --git a/cmd/core-agent/main.go b/cmd/core-agent/main.go index 1f06a5e..95e1976 100644 --- a/cmd/core-agent/main.go +++ b/cmd/core-agent/main.go @@ -4,7 +4,6 @@ import ( "os" "dappco.re/go/core" - "dappco.re/go/core/process" "dappco.re/go/agent/pkg/agentic" "dappco.re/go/agent/pkg/brain" @@ -15,16 +14,7 @@ import ( func main() { c := core.New( core.WithOption("name", "core-agent"), - core.WithService(func(c *core.Core) core.Result { - svc, err := process.NewService(process.Options{})(c) - if err != nil { - return core.Result{Value: err, OK: false} - } - if procSvc, ok := svc.(*process.Service); ok { - _ = process.SetDefault(procSvc) - } - return core.Result{Value: svc, OK: true} - }), + core.WithService(agentic.ProcessRegister), core.WithService(agentic.Register), core.WithService(monitor.Register), core.WithService(brain.Register), @@ -99,9 +89,7 @@ func main() { }, }) - // Forge + Workspace CLI commands (in separate files) - registerForgeCommands(c) - registerWorkspaceCommands(c) + // All commands registered by services during OnStartup // registerFlowCommands(c) — on feat/flow-system branch // Run: ServiceStartup → Cli → ServiceShutdown → os.Exit if error diff --git a/cmd/core-agent/forge.go b/pkg/agentic/commands_forge.go similarity index 78% rename from cmd/core-agent/forge.go rename to pkg/agentic/commands_forge.go index b0b8ce6..d8b09b2 100644 --- a/cmd/core-agent/forge.go +++ b/pkg/agentic/commands_forge.go @@ -1,31 +1,18 @@ // SPDX-License-Identifier: EUPL-1.2 -package main +package agentic import ( "context" "strconv" - "dappco.re/go/core" + core "dappco.re/go/core" "dappco.re/go/core/forge" forge_types "dappco.re/go/core/forge/types" ) -// newForgeClient creates a Forge client from env config. -func newForgeClient() *forge.Forge { - url := core.Env("FORGE_URL") - if url == "" { - url = "https://forge.lthn.ai" - } - token := core.Env("FORGE_TOKEN") - if token == "" { - token = core.Env("GITEA_TOKEN") - } - return forge.NewForge(url, token) -} - -// parseArgs extracts org and repo from opts. First positional arg is repo, --org flag defaults to "core". -func parseArgs(opts core.Options) (org, repo string, num int64) { +// parseForgeArgs extracts org and repo from opts. +func parseForgeArgs(opts core.Options) (org, repo string, num int64) { org = opts.String("org") if org == "" { org = "core" @@ -39,7 +26,9 @@ func parseArgs(opts core.Options) (org, repo string, num int64) { func fmtIndex(n int64) string { return strconv.FormatInt(n, 10) } -func registerForgeCommands(c *core.Core) { +// registerForgeCommands adds Forge API commands to Core's command tree. +func (s *PrepSubsystem) registerForgeCommands() { + c := s.core ctx := context.Background() // --- Issues --- @@ -47,14 +36,13 @@ func registerForgeCommands(c *core.Core) { c.Command("issue/get", core.Command{ Description: "Get a Forge issue", Action: func(opts core.Options) core.Result { - org, repo, num := parseArgs(opts) + org, repo, num := parseForgeArgs(opts) if repo == "" || num == 0 { core.Print(nil, "usage: core-agent issue get --number=N [--org=core]") return core.Result{OK: false} } - f := newForgeClient() - issue, err := f.Issues.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": fmtIndex(num)}) + issue, err := s.forge.Issues.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": fmtIndex(num)}) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -74,14 +62,13 @@ func registerForgeCommands(c *core.Core) { c.Command("issue/list", core.Command{ Description: "List Forge issues for a repo", Action: func(opts core.Options) core.Result { - org, repo, _ := parseArgs(opts) + org, repo, _ := parseForgeArgs(opts) if repo == "" { core.Print(nil, "usage: core-agent issue list [--org=core]") return core.Result{OK: false} } - f := newForgeClient() - issues, err := f.Issues.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + issues, err := s.forge.Issues.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -100,15 +87,14 @@ func registerForgeCommands(c *core.Core) { c.Command("issue/comment", core.Command{ Description: "Comment on a Forge issue", Action: func(opts core.Options) core.Result { - org, repo, num := parseArgs(opts) + org, repo, num := parseForgeArgs(opts) body := opts.String("body") if repo == "" || num == 0 || body == "" { core.Print(nil, "usage: core-agent issue comment --number=N --body=\"text\" [--org=core]") return core.Result{OK: false} } - f := newForgeClient() - comment, err := f.Issues.CreateComment(ctx, org, repo, num, body) + comment, err := s.forge.Issues.CreateComment(ctx, org, repo, num, body) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -122,7 +108,7 @@ func registerForgeCommands(c *core.Core) { c.Command("issue/create", core.Command{ Description: "Create a Forge issue", Action: func(opts core.Options) core.Result { - org, repo, _ := parseArgs(opts) + org, repo, _ := parseForgeArgs(opts) title := opts.String("title") body := opts.String("body") labels := opts.String("labels") @@ -142,8 +128,7 @@ func registerForgeCommands(c *core.Core) { // Resolve milestone name to ID if milestone != "" { - f := newForgeClient() - milestones, err := f.Milestones.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + milestones, err := s.forge.Milestones.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) if err == nil { for _, m := range milestones { if m.Title == milestone { @@ -161,9 +146,8 @@ func registerForgeCommands(c *core.Core) { // Resolve label names to IDs if provided if labels != "" { - f := newForgeClient() - labelNames := core.Split(labels, ",") - allLabels, err := f.Labels.ListRepoLabels(ctx, org, repo) + labelNames := core.Split(labels, ",") + allLabels, err := s.forge.Labels.ListRepoLabels(ctx, org, repo) if err == nil { for _, name := range labelNames { name = core.Trim(name) @@ -177,8 +161,7 @@ func registerForgeCommands(c *core.Core) { } } - f := newForgeClient() - issue, err := f.Issues.Create(ctx, forge.Params{"owner": org, "repo": repo}, createOpts) + issue, err := s.forge.Issues.Create(ctx, forge.Params{"owner": org, "repo": repo}, createOpts) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -195,14 +178,13 @@ func registerForgeCommands(c *core.Core) { c.Command("pr/get", core.Command{ Description: "Get a Forge PR", Action: func(opts core.Options) core.Result { - org, repo, num := parseArgs(opts) + org, repo, num := parseForgeArgs(opts) if repo == "" || num == 0 { core.Print(nil, "usage: core-agent pr get --number=N [--org=core]") return core.Result{OK: false} } - f := newForgeClient() - pr, err := f.Pulls.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": fmtIndex(num)}) + pr, err := s.forge.Pulls.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": fmtIndex(num)}) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -225,14 +207,13 @@ func registerForgeCommands(c *core.Core) { c.Command("pr/list", core.Command{ Description: "List Forge PRs for a repo", Action: func(opts core.Options) core.Result { - org, repo, _ := parseArgs(opts) + org, repo, _ := parseForgeArgs(opts) if repo == "" { core.Print(nil, "usage: core-agent pr list [--org=core]") return core.Result{OK: false} } - f := newForgeClient() - prs, err := f.Pulls.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + prs, err := s.forge.Pulls.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -251,7 +232,7 @@ func registerForgeCommands(c *core.Core) { c.Command("pr/merge", core.Command{ Description: "Merge a Forge PR", Action: func(opts core.Options) core.Result { - org, repo, num := parseArgs(opts) + org, repo, num := parseForgeArgs(opts) method := opts.String("method") if method == "" { method = "merge" @@ -261,8 +242,7 @@ func registerForgeCommands(c *core.Core) { return core.Result{OK: false} } - f := newForgeClient() - if err := f.Pulls.Merge(ctx, org, repo, num, method); err != nil { + if err := s.forge.Pulls.Merge(ctx, org, repo, num, method); err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} } @@ -277,14 +257,13 @@ func registerForgeCommands(c *core.Core) { c.Command("repo/get", core.Command{ Description: "Get Forge repo info", Action: func(opts core.Options) core.Result { - org, repo, _ := parseArgs(opts) + org, repo, _ := parseForgeArgs(opts) if repo == "" { core.Print(nil, "usage: core-agent repo get [--org=core]") return core.Result{OK: false} } - f := newForgeClient() - r, err := f.Repos.Get(ctx, forge.Params{"owner": org, "repo": repo}) + r, err := s.forge.Repos.Get(ctx, forge.Params{"owner": org, "repo": repo}) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} @@ -308,8 +287,7 @@ func registerForgeCommands(c *core.Core) { org = "core" } - f := newForgeClient() - repos, err := f.Repos.ListOrgRepos(ctx, org) + repos, err := s.forge.Repos.ListOrgRepos(ctx, org) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} diff --git a/cmd/core-agent/workspace.go b/pkg/agentic/commands_workspace.go similarity index 84% rename from cmd/core-agent/workspace.go rename to pkg/agentic/commands_workspace.go index 38410ed..e43f5f1 100644 --- a/cmd/core-agent/workspace.go +++ b/pkg/agentic/commands_workspace.go @@ -1,22 +1,23 @@ // SPDX-License-Identifier: EUPL-1.2 -package main +// Workspace CLI commands registered by the agentic service during OnStartup. + +package agentic import ( "os" - "dappco.re/go/core" - - "dappco.re/go/agent/pkg/agentic" + core "dappco.re/go/core" ) -func registerWorkspaceCommands(c *core.Core) { +// registerWorkspaceCommands adds workspace management commands. +func (s *PrepSubsystem) registerWorkspaceCommands() { + c := s.core - // workspace/list — show all workspaces with status c.Command("workspace/list", core.Command{ Description: "List all agent workspaces with status", Action: func(opts core.Options) core.Result { - wsRoot := agentic.WorkspaceRoot() + wsRoot := WorkspaceRoot() fsys := c.Fs() r := fsys.List(wsRoot) @@ -33,7 +34,6 @@ func registerWorkspaceCommands(c *core.Core) { } statusFile := core.JoinPath(wsRoot, e.Name(), "status.json") if sr := fsys.Read(statusFile); sr.OK { - // Quick parse for status field content := sr.Value.(string) status := extractField(content, "status") repo := extractField(content, "repo") @@ -49,11 +49,10 @@ func registerWorkspaceCommands(c *core.Core) { }, }) - // workspace/clean — remove stale workspaces c.Command("workspace/clean", core.Command{ Description: "Remove completed/failed/blocked workspaces", Action: func(opts core.Options) core.Result { - wsRoot := agentic.WorkspaceRoot() + wsRoot := WorkspaceRoot() fsys := c.Fs() filter := opts.String("_arg") if filter == "" { @@ -115,16 +114,14 @@ func registerWorkspaceCommands(c *core.Core) { }, }) - // workspace/dispatch — dispatch an agent (CLI wrapper for MCP tool) c.Command("workspace/dispatch", core.Command{ Description: "Dispatch an agent to work on a repo task", Action: func(opts core.Options) core.Result { repo := opts.String("_arg") if repo == "" { - core.Print(nil, "usage: core-agent workspace/dispatch --task=\"...\" --issue=N|--pr=N|--branch=X [--agent=codex]") + core.Print(nil, "usage: core-agent workspace dispatch --task=\"...\" --issue=N|--pr=N|--branch=X [--agent=codex]") return core.Result{OK: false} } - core.Print(nil, "dispatch via CLI not yet wired — use MCP agentic_dispatch tool") core.Print(nil, "repo: %s, task: %s", repo, opts.String("task")) return core.Result{OK: true} @@ -133,9 +130,7 @@ func registerWorkspaceCommands(c *core.Core) { } // extractField does a quick JSON field extraction without full unmarshal. -// Looks for "field":"value" pattern. Good enough for status.json. func extractField(jsonStr, field string) string { - // Match both "field":"value" and "field": "value" needle := core.Concat("\"", field, "\"") idx := -1 for i := 0; i <= len(jsonStr)-len(needle); i++ { @@ -147,14 +142,13 @@ func extractField(jsonStr, field string) string { if idx < 0 { return "" } - // Skip : and whitespace to find opening quote for idx < len(jsonStr) && (jsonStr[idx] == ':' || jsonStr[idx] == ' ' || jsonStr[idx] == '\t') { idx++ } if idx >= len(jsonStr) || jsonStr[idx] != '"' { return "" } - idx++ // skip opening quote + idx++ end := idx for end < len(jsonStr) && jsonStr[end] != '"' { end++ diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 9b3511b..2f5af64 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -91,6 +91,8 @@ func (s *PrepSubsystem) SetCore(c *core.Core) { func (s *PrepSubsystem) OnStartup(ctx context.Context) error { s.StartRunner() s.registerCommands(ctx) + s.registerWorkspaceCommands() + s.registerForgeCommands() return nil } diff --git a/pkg/agentic/process_register.go b/pkg/agentic/process_register.go new file mode 100644 index 0000000..235da2a --- /dev/null +++ b/pkg/agentic/process_register.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + core "dappco.re/go/core" + "dappco.re/go/core/process" +) + +// ProcessRegister is the service factory for the process management service. +// Wraps core/process for the v0.3.3→v0.4 factory pattern. +// +// core.New( +// core.WithService(agentic.ProcessRegister), +// ) +func ProcessRegister(c *core.Core) core.Result { + svc, err := process.NewService(process.Options{})(c) + if err != nil { + return core.Result{Value: err, OK: false} + } + if procSvc, ok := svc.(*process.Service); ok { + _ = process.SetDefault(procSvc) + } + return core.Result{Value: svc, OK: true} +}