AX Quality Gates (RFC-025):
- Eliminate os/exec from all test + production code (12+ files)
- Eliminate encoding/json from all test files (15 files, 66 occurrences)
- Eliminate os from all test files except TestMain (Go runtime contract)
- Eliminate path/filepath, net/url from all files
- String concat: 39 violations replaced with core.Concat()
- Test naming AX-7: 264 test functions renamed across all 6 packages
- Example test 1:1 coverage complete
Core Features Adopted:
- Task Composition: agent.completion pipeline (QA → PR → Verify → Ingest → Poke)
- PerformAsync: completion pipeline runs with WaitGroup + progress tracking
- Config: agents.yaml loaded once, feature flags (auto-qa/pr/merge/ingest)
- Named Locks: c.Lock("drain") for queue serialisation
- Registry: workspace state with cross-package QUERY access
- QUERY: c.QUERY(WorkspaceQuery{Status: "running"}) for cross-service queries
- Action descriptions: 25+ Actions self-documenting
- Data mounts: prompts/tasks/flows/personas/workspaces via c.Data()
- Content Actions: agentic.prompt/task/flow/persona callable via IPC
- Drive endpoints: forge + brain registered with tokens
- Drive REST helpers: DriveGet/DrivePost/DriveDo for Drive-aware HTTP
- HandleIPCEvents: auto-discovered by WithService (no manual wiring)
- Entitlement: frozen-queue gate on write Actions
- CLI dispatch: workspace dispatch wired to real dispatch method
- CLI: --quiet/-q and --debug/-d global flags
- CLI: banner, version, check (with service/action/command counts), env
- main.go: minimal — 5 services + c.Run(), no os import
- cmd tests: 84.2% coverage (was 0%)
Co-Authored-By: Virgil <virgil@lethean.io>
365 lines
11 KiB
Go
365 lines
11 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// Named Action handlers for the agentic service.
|
|
// Each handler adapts (ctx, Options) → Result to call the existing MCP tool method.
|
|
// Registered during OnStartup — the Action registry IS the capability map.
|
|
//
|
|
// c.Action("agentic.dispatch").Run(ctx, opts)
|
|
// c.Actions() // all registered capabilities
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
|
|
"dappco.re/go/agent/pkg/messages"
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// --- Dispatch & Workspace ---
|
|
|
|
// handleDispatch dispatches a subagent to work on a repo task.
|
|
//
|
|
// r := c.Action("agentic.dispatch").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// core.Option{Key: "task", Value: "Fix tests"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleDispatch(ctx context.Context, opts core.Options) core.Result {
|
|
input := DispatchInput{
|
|
Repo: opts.String("repo"),
|
|
Task: opts.String("task"),
|
|
Agent: opts.String("agent"),
|
|
Issue: opts.Int("issue"),
|
|
}
|
|
_, out, err := s.dispatch(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// handlePrep prepares a workspace without dispatching an agent.
|
|
//
|
|
// r := c.Action("agentic.prep").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// core.Option{Key: "issue", Value: 42},
|
|
// ))
|
|
func (s *PrepSubsystem) handlePrep(ctx context.Context, opts core.Options) core.Result {
|
|
input := PrepInput{
|
|
Repo: opts.String("repo"),
|
|
Org: opts.String("org"),
|
|
Issue: opts.Int("issue"),
|
|
}
|
|
_, out, err := s.prepWorkspace(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// handleStatus lists workspace statuses.
|
|
//
|
|
// r := c.Action("agentic.status").Run(ctx, core.NewOptions())
|
|
func (s *PrepSubsystem) handleStatus(ctx context.Context, opts core.Options) core.Result {
|
|
input := StatusInput{
|
|
Workspace: opts.String("workspace"),
|
|
Limit: opts.Int("limit"),
|
|
Status: opts.String("status"),
|
|
}
|
|
_, out, err := s.status(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// handleResume resumes a blocked workspace.
|
|
//
|
|
// r := c.Action("agentic.resume").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "core/go-io/task-5"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleResume(ctx context.Context, opts core.Options) core.Result {
|
|
input := ResumeInput{
|
|
Workspace: opts.String("workspace"),
|
|
Answer: opts.String("answer"),
|
|
}
|
|
_, out, err := s.resume(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// handleScan scans forge repos for actionable issues.
|
|
//
|
|
// r := c.Action("agentic.scan").Run(ctx, core.NewOptions())
|
|
func (s *PrepSubsystem) handleScan(ctx context.Context, opts core.Options) core.Result {
|
|
input := ScanInput{
|
|
Org: opts.String("org"),
|
|
Limit: opts.Int("limit"),
|
|
}
|
|
_, out, err := s.scan(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// handleWatch watches a workspace for completion.
|
|
//
|
|
// r := c.Action("agentic.watch").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "core/go-io/task-5"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleWatch(ctx context.Context, opts core.Options) core.Result {
|
|
input := WatchInput{}
|
|
_, out, err := s.watch(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// --- Pipeline ---
|
|
|
|
// handleQA runs build+test on a completed workspace.
|
|
//
|
|
// r := c.Action("agentic.qa").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleQA(ctx context.Context, opts core.Options) core.Result {
|
|
// Feature flag gate — skip QA if disabled
|
|
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-qa") {
|
|
return core.Result{Value: true, OK: true}
|
|
}
|
|
wsDir := opts.String("workspace")
|
|
if wsDir == "" {
|
|
return core.Result{Value: core.E("agentic.qa", "workspace is required", nil), OK: false}
|
|
}
|
|
passed := s.runQA(wsDir)
|
|
if !passed {
|
|
if st, err := ReadStatus(wsDir); err == nil {
|
|
st.Status = "failed"
|
|
st.Question = "QA check failed — build or tests did not pass"
|
|
writeStatus(wsDir, st)
|
|
}
|
|
}
|
|
// Emit QA result for observability (monitor picks this up)
|
|
if s.ServiceRuntime != nil {
|
|
st, _ := ReadStatus(wsDir)
|
|
repo := ""
|
|
if st != nil {
|
|
repo = st.Repo
|
|
}
|
|
s.Core().ACTION(messages.QAResult{
|
|
Workspace: core.PathBase(wsDir),
|
|
Repo: repo,
|
|
Passed: passed,
|
|
})
|
|
}
|
|
return core.Result{Value: passed, OK: passed}
|
|
}
|
|
|
|
// handleAutoPR creates a PR for a completed workspace.
|
|
//
|
|
// r := c.Action("agentic.auto-pr").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleAutoPR(ctx context.Context, opts core.Options) core.Result {
|
|
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-pr") {
|
|
return core.Result{OK: true}
|
|
}
|
|
wsDir := opts.String("workspace")
|
|
if wsDir == "" {
|
|
return core.Result{Value: core.E("agentic.auto-pr", "workspace is required", nil), OK: false}
|
|
}
|
|
s.autoCreatePR(wsDir)
|
|
|
|
// Emit PRCreated for observability
|
|
if s.ServiceRuntime != nil {
|
|
if st, err := ReadStatus(wsDir); err == nil && st.PRURL != "" {
|
|
s.Core().ACTION(messages.PRCreated{
|
|
Repo: st.Repo,
|
|
Branch: st.Branch,
|
|
PRURL: st.PRURL,
|
|
PRNum: extractPRNumber(st.PRURL),
|
|
})
|
|
}
|
|
}
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// handleVerify verifies and auto-merges a PR.
|
|
//
|
|
// r := c.Action("agentic.verify").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleVerify(ctx context.Context, opts core.Options) core.Result {
|
|
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-merge") {
|
|
return core.Result{OK: true}
|
|
}
|
|
wsDir := opts.String("workspace")
|
|
if wsDir == "" {
|
|
return core.Result{Value: core.E("agentic.verify", "workspace is required", nil), OK: false}
|
|
}
|
|
s.autoVerifyAndMerge(wsDir)
|
|
|
|
// Emit merge/review events for observability
|
|
if s.ServiceRuntime != nil {
|
|
if st, err := ReadStatus(wsDir); err == nil {
|
|
if st.Status == "merged" {
|
|
s.Core().ACTION(messages.PRMerged{
|
|
Repo: st.Repo,
|
|
PRURL: st.PRURL,
|
|
PRNum: extractPRNumber(st.PRURL),
|
|
})
|
|
} else if st.Question != "" {
|
|
s.Core().ACTION(messages.PRNeedsReview{
|
|
Repo: st.Repo,
|
|
PRURL: st.PRURL,
|
|
PRNum: extractPRNumber(st.PRURL),
|
|
Reason: st.Question,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// handleIngest creates issues from agent findings.
|
|
//
|
|
// r := c.Action("agentic.ingest").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleIngest(ctx context.Context, opts core.Options) core.Result {
|
|
wsDir := opts.String("workspace")
|
|
if wsDir == "" {
|
|
return core.Result{Value: core.E("agentic.ingest", "workspace is required", nil), OK: false}
|
|
}
|
|
s.ingestFindings(wsDir)
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// handlePoke drains the dispatch queue.
|
|
//
|
|
// r := c.Action("agentic.poke").Run(ctx, core.NewOptions())
|
|
func (s *PrepSubsystem) handlePoke(ctx context.Context, opts core.Options) core.Result {
|
|
s.Poke()
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// handleMirror mirrors agent branches to GitHub.
|
|
//
|
|
// r := c.Action("agentic.mirror").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleMirror(ctx context.Context, opts core.Options) core.Result {
|
|
input := MirrorInput{
|
|
Repo: opts.String("repo"),
|
|
}
|
|
_, out, err := s.mirror(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// --- Forge ---
|
|
|
|
// handleIssueGet retrieves a forge issue.
|
|
//
|
|
// r := c.Action("agentic.issue.get").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// core.Option{Key: "number", Value: "42"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleIssueGet(ctx context.Context, opts core.Options) core.Result {
|
|
return s.cmdIssueGet(opts)
|
|
}
|
|
|
|
// handleIssueList lists forge issues.
|
|
//
|
|
// r := c.Action("agentic.issue.list").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleIssueList(ctx context.Context, opts core.Options) core.Result {
|
|
return s.cmdIssueList(opts)
|
|
}
|
|
|
|
// handleIssueCreate creates a forge issue.
|
|
//
|
|
// r := c.Action("agentic.issue.create").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
// core.Option{Key: "title", Value: "Bug report"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleIssueCreate(ctx context.Context, opts core.Options) core.Result {
|
|
return s.cmdIssueCreate(opts)
|
|
}
|
|
|
|
// handlePRGet retrieves a forge PR.
|
|
//
|
|
// r := c.Action("agentic.pr.get").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
// core.Option{Key: "number", Value: "12"},
|
|
// ))
|
|
func (s *PrepSubsystem) handlePRGet(ctx context.Context, opts core.Options) core.Result {
|
|
return s.cmdPRGet(opts)
|
|
}
|
|
|
|
// handlePRList lists forge PRs.
|
|
//
|
|
// r := c.Action("agentic.pr.list").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
// ))
|
|
func (s *PrepSubsystem) handlePRList(ctx context.Context, opts core.Options) core.Result {
|
|
return s.cmdPRList(opts)
|
|
}
|
|
|
|
// handlePRMerge merges a forge PR.
|
|
//
|
|
// r := c.Action("agentic.pr.merge").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
// core.Option{Key: "number", Value: "12"},
|
|
// ))
|
|
func (s *PrepSubsystem) handlePRMerge(ctx context.Context, opts core.Options) core.Result {
|
|
return s.cmdPRMerge(opts)
|
|
}
|
|
|
|
// --- Review ---
|
|
|
|
// handleReviewQueue runs CodeRabbit review on a workspace.
|
|
//
|
|
// r := c.Action("agentic.review-queue").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "workspace", Value: "core/go-io/task-5"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleReviewQueue(ctx context.Context, opts core.Options) core.Result {
|
|
input := ReviewQueueInput{
|
|
Limit: opts.Int("limit"),
|
|
Reviewer: opts.String("reviewer"),
|
|
DryRun: opts.Bool("dry_run"),
|
|
}
|
|
_, out, err := s.reviewQueue(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// --- Epic ---
|
|
|
|
// handleEpic creates an epic (multi-repo task breakdown).
|
|
//
|
|
// r := c.Action("agentic.epic").Run(ctx, core.NewOptions(
|
|
// core.Option{Key: "task", Value: "Update all repos to v0.8.0"},
|
|
// ))
|
|
func (s *PrepSubsystem) handleEpic(ctx context.Context, opts core.Options) core.Result {
|
|
input := EpicInput{
|
|
Repo: opts.String("repo"),
|
|
Org: opts.String("org"),
|
|
Title: opts.String("title"),
|
|
Body: opts.String("body"),
|
|
}
|
|
_, out, err := s.createEpic(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|