Adds `.core/workspace/db.duckdb` — the permanent record of dispatch cycles described in RFC §15.5. Stats rows persist BEFORE workspace directories are deleted so "what happened in the last 50 dispatches" queries survive cleanup and sync drain. - `workspace_stats.go` — lazy go-store handle for the parent stats DB, build/record/filter/list helpers, report payload projection - `commit.go` — writes a stats row as part of the completion pipeline so every committed dispatch carries forward into the permanent record - `commands_workspace.go` — `workspace/clean` captures stats before deleting, new `workspace/stats` command + `agentic.workspace.stats` action answer the spec's "query on the parent" use case Co-Authored-By: Virgil <virgil@lethean.io>
896 lines
26 KiB
Go
896 lines
26 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// c.Action("agentic.dispatch").Run(ctx, options)
|
|
// c.Actions()
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
|
|
"dappco.re/go/agent/pkg/lib"
|
|
"dappco.re/go/agent/pkg/messages"
|
|
core "dappco.re/go/core"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
if s.Core() != nil {
|
|
entitlement := s.Core().Entitled("agentic.concurrency", 1)
|
|
if !entitlement.Allowed {
|
|
reason := core.Trim(entitlement.Reason)
|
|
if reason == "" {
|
|
reason = "dispatch concurrency not available"
|
|
}
|
|
return core.Result{Value: core.E("agentic.dispatch", reason, nil), OK: false}
|
|
}
|
|
}
|
|
|
|
input := dispatchInputFromOptions(options)
|
|
_, out, err := s.dispatch(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
if s.Core() != nil {
|
|
s.Core().RecordUsage("agentic.dispatch")
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
input := prepInputFromOptions(options)
|
|
_, out, err := s.prepWorkspace(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.status").Run(ctx, core.NewOptions())
|
|
func (s *PrepSubsystem) handleStatus(ctx context.Context, options core.Options) core.Result {
|
|
input := StatusInput{
|
|
Workspace: options.String("workspace"),
|
|
Limit: options.Int("limit"),
|
|
Status: options.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}
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
input := resumeInputFromOptions(options)
|
|
_, out, err := s.resume(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.scan").Run(ctx, core.NewOptions())
|
|
func (s *PrepSubsystem) handleScan(ctx context.Context, options core.Options) core.Result {
|
|
input := scanInputFromOptions(options)
|
|
_, out, err := s.scan(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// WorkspaceStatsInput filters rows returned by agentic.workspace.stats.
|
|
// Empty fields act as wildcards — the same shape used by StatusInput so
|
|
// callers do not need a second filter vocabulary.
|
|
//
|
|
// Usage example: `input := WorkspaceStatsInput{Repo: "go-io", Status: "completed", Limit: 50}`
|
|
type WorkspaceStatsInput struct {
|
|
Repo string `json:"repo,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
// WorkspaceStatsOutput is the envelope returned by agentic.workspace.stats.
|
|
// Rows are unsorted — callers may re-sort by CompletedAt, DurationMS, etc.
|
|
// The count is included so CLI consumers do not need to call len().
|
|
//
|
|
// Usage example: `output := WorkspaceStatsOutput{Count: 3, Rows: rows}`
|
|
type WorkspaceStatsOutput struct {
|
|
Count int `json:"count"`
|
|
Rows []workspaceStatsRecord `json:"rows,omitempty"`
|
|
}
|
|
|
|
// result := c.Action("agentic.workspace.stats").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// core.Option{Key: "status", Value: "completed"},
|
|
// core.Option{Key: "limit", Value: 50},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleWorkspaceStats(_ context.Context, options core.Options) core.Result {
|
|
input := WorkspaceStatsInput{
|
|
Repo: options.String("repo"),
|
|
Status: options.String("status"),
|
|
Limit: options.Int("limit"),
|
|
}
|
|
rows := filterWorkspaceStats(s.listWorkspaceStats(), input.Repo, input.Status, input.Limit)
|
|
return core.Result{
|
|
Value: WorkspaceStatsOutput{Count: len(rows), Rows: rows},
|
|
OK: true,
|
|
}
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
input := watchInputFromOptions(options)
|
|
_, out, err := s.watch(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.prompt").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "slug", Value: "coding"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handlePrompt(_ context.Context, options core.Options) core.Result {
|
|
return lib.Prompt(options.String("slug"))
|
|
}
|
|
|
|
// result := c.Action("agentic.task").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "slug", Value: "bug-fix"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleTask(_ context.Context, options core.Options) core.Result {
|
|
return lib.Task(options.String("slug"))
|
|
}
|
|
|
|
// result := c.Action("agentic.flow").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "slug", Value: "go"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleFlow(_ context.Context, options core.Options) core.Result {
|
|
return lib.Flow(options.String("slug"))
|
|
}
|
|
|
|
// result := c.Action("agentic.persona").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "path", Value: "code/backend-architect"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handlePersona(_ context.Context, options core.Options) core.Result {
|
|
return lib.Persona(options.String("path"))
|
|
}
|
|
|
|
// result := c.Action("agentic.complete").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "workspace", Value: "/srv/.core/workspace/core/go-io/task-42"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleComplete(ctx context.Context, options core.Options) core.Result {
|
|
return s.Core().Task("agent.completion").Run(ctx, s.Core(), options)
|
|
}
|
|
|
|
// input := agentic.CompleteInput{Workspace: "/srv/.core/workspace/core/go-io/task-42"}
|
|
type CompleteInput struct {
|
|
Workspace string `json:"workspace"`
|
|
}
|
|
|
|
// out := agentic.CompleteOutput{Success: true, Workspace: "core/go-io/task-42"}
|
|
type CompleteOutput struct {
|
|
Success bool `json:"success"`
|
|
Workspace string `json:"workspace"`
|
|
}
|
|
|
|
func (s *PrepSubsystem) completeTool(ctx context.Context, _ *mcp.CallToolRequest, input CompleteInput) (*mcp.CallToolResult, CompleteOutput, error) {
|
|
if input.Workspace == "" {
|
|
return nil, CompleteOutput{}, core.E("agentic.complete", "workspace is required", nil)
|
|
}
|
|
|
|
result := s.handleComplete(ctx, core.NewOptions(core.Option{Key: "workspace", Value: input.Workspace}))
|
|
if !result.OK {
|
|
return nil, CompleteOutput{}, resultErrorValue("agentic.complete", result)
|
|
}
|
|
|
|
return nil, CompleteOutput{
|
|
Success: true,
|
|
Workspace: input.Workspace,
|
|
}, nil
|
|
}
|
|
|
|
// result := c.Action("agentic.qa").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleQA(ctx context.Context, options core.Options) core.Result {
|
|
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-qa") {
|
|
return core.Result{Value: true, OK: true}
|
|
}
|
|
workspaceDir := options.String("workspace")
|
|
if workspaceDir == "" {
|
|
return core.Result{Value: core.E("agentic.qa", "workspace is required", nil), OK: false}
|
|
}
|
|
passed := s.runQA(workspaceDir)
|
|
if !passed {
|
|
if result := ReadStatusResult(workspaceDir); result.OK {
|
|
workspaceStatus, ok := workspaceStatusValue(result)
|
|
if ok {
|
|
workspaceStatus.Status = "failed"
|
|
workspaceStatus.Question = "QA check failed — build or tests did not pass"
|
|
writeStatusResult(workspaceDir, workspaceStatus)
|
|
}
|
|
}
|
|
}
|
|
if s.ServiceRuntime != nil {
|
|
result := ReadStatusResult(workspaceDir)
|
|
workspaceStatus, ok := workspaceStatusValue(result)
|
|
repo := ""
|
|
if ok {
|
|
repo = workspaceStatus.Repo
|
|
}
|
|
s.Core().ACTION(messages.QAResult{
|
|
Workspace: WorkspaceName(workspaceDir),
|
|
Repo: repo,
|
|
Passed: passed,
|
|
})
|
|
}
|
|
return core.Result{Value: passed, OK: passed}
|
|
}
|
|
|
|
// result := c.Action("agentic.auto-pr").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleAutoPR(ctx context.Context, options core.Options) core.Result {
|
|
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-pr") {
|
|
return core.Result{OK: true}
|
|
}
|
|
workspaceDir := options.String("workspace")
|
|
if workspaceDir == "" {
|
|
return core.Result{Value: core.E("agentic.auto-pr", "workspace is required", nil), OK: false}
|
|
}
|
|
s.autoCreatePR(workspaceDir)
|
|
|
|
if s.ServiceRuntime != nil {
|
|
result := ReadStatusResult(workspaceDir)
|
|
workspaceStatus, ok := workspaceStatusValue(result)
|
|
if ok && workspaceStatus.PRURL != "" {
|
|
s.Core().ACTION(messages.PRCreated{
|
|
Repo: workspaceStatus.Repo,
|
|
Branch: workspaceStatus.Branch,
|
|
PRURL: workspaceStatus.PRURL,
|
|
PRNum: extractPullRequestNumber(workspaceStatus.PRURL),
|
|
})
|
|
}
|
|
}
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.verify").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleVerify(ctx context.Context, options core.Options) core.Result {
|
|
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-merge") {
|
|
return core.Result{OK: true}
|
|
}
|
|
workspaceDir := options.String("workspace")
|
|
if workspaceDir == "" {
|
|
return core.Result{Value: core.E("agentic.verify", "workspace is required", nil), OK: false}
|
|
}
|
|
s.autoVerifyAndMerge(workspaceDir)
|
|
|
|
if s.ServiceRuntime != nil {
|
|
result := ReadStatusResult(workspaceDir)
|
|
workspaceStatus, ok := workspaceStatusValue(result)
|
|
if ok {
|
|
if workspaceStatus.Status == "merged" {
|
|
s.Core().ACTION(messages.PRMerged{
|
|
Repo: workspaceStatus.Repo,
|
|
PRURL: workspaceStatus.PRURL,
|
|
PRNum: extractPullRequestNumber(workspaceStatus.PRURL),
|
|
})
|
|
} else if workspaceStatus.Question != "" {
|
|
s.Core().ACTION(messages.PRNeedsReview{
|
|
Repo: workspaceStatus.Repo,
|
|
PRURL: workspaceStatus.PRURL,
|
|
PRNum: extractPullRequestNumber(workspaceStatus.PRURL),
|
|
Reason: workspaceStatus.Question,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.ingest").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleIngest(ctx context.Context, options core.Options) core.Result {
|
|
workspaceDir := options.String("workspace")
|
|
if workspaceDir == "" {
|
|
return core.Result{Value: core.E("agentic.ingest", "workspace is required", nil), OK: false}
|
|
}
|
|
s.ingestFindings(workspaceDir)
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.poke").Run(ctx, core.NewOptions())
|
|
func (s *PrepSubsystem) handlePoke(ctx context.Context, _ core.Options) core.Result {
|
|
if s.ServiceRuntime != nil && s.Core().Action("runner.poke").Exists() {
|
|
return s.Core().Action("runner.poke").Run(ctx, core.NewOptions())
|
|
}
|
|
s.Poke()
|
|
return core.Result{OK: true}
|
|
}
|
|
|
|
// result := c.Action("agentic.mirror").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleMirror(ctx context.Context, options core.Options) core.Result {
|
|
input := mirrorInputFromOptions(options)
|
|
_, out, err := s.mirror(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
return s.cmdIssueGet(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := c.Action("agentic.issue.list").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleIssueList(ctx context.Context, options core.Options) core.Result {
|
|
return s.cmdIssueList(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
return s.cmdIssueCreate(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
return s.cmdPRGet(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := c.Action("agentic.pr.list").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handlePRList(ctx context.Context, options core.Options) core.Result {
|
|
return s.cmdPRList(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
return s.cmdPRMerge(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := c.Action("agentic.pr.close").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "_arg", Value: "go-io"},
|
|
// core.Option{Key: "number", Value: "12"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handlePRClose(ctx context.Context, options core.Options) core.Result {
|
|
return s.cmdPRClose(normaliseForgeActionOptions(options))
|
|
}
|
|
|
|
// result := c.Action("agentic.branch.delete").Run(ctx, core.NewOptions(
|
|
//
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// core.Option{Key: "branch", Value: "agent/fix-tests"},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) handleBranchDelete(ctx context.Context, options core.Options) core.Result {
|
|
input := DeleteBranchInput{
|
|
Org: optionStringValue(options, "org"),
|
|
Repo: optionStringValue(options, "repo", "_arg"),
|
|
Branch: optionStringValue(options, "branch"),
|
|
}
|
|
_, out, err := s.deleteBranch(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
input := reviewQueueInputFromOptions(options)
|
|
_, out, err := s.reviewQueue(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := 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, options core.Options) core.Result {
|
|
input := epicInputFromOptions(options)
|
|
_, out, err := s.createEpic(ctx, nil, input)
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: out, OK: true}
|
|
}
|
|
|
|
// result := c.Command("epic").Run(core.NewOptions(
|
|
//
|
|
// core.Option{Key: "repo", Value: "go-io"},
|
|
// core.Option{Key: "title", Value: "Stabilise agent dispatch"},
|
|
// core.Option{Key: "tasks", Value: []string{"Fix the queue race", "Add regression tests"}},
|
|
//
|
|
// ))
|
|
func (s *PrepSubsystem) cmdEpic(options core.Options) core.Result {
|
|
return s.handleEpic(s.commandContext(), options)
|
|
}
|
|
|
|
func dispatchInputFromOptions(options core.Options) DispatchInput {
|
|
return DispatchInput{
|
|
Repo: optionStringValue(options, "repo"),
|
|
Org: optionStringValue(options, "org"),
|
|
Task: optionStringValue(options, "task"),
|
|
Agent: optionStringValue(options, "agent"),
|
|
Template: optionStringValue(options, "template"),
|
|
PlanTemplate: optionStringValue(options, "plan_template", "plan-template"),
|
|
Variables: optionStringMapValue(options, "variables"),
|
|
Persona: optionStringValue(options, "persona"),
|
|
Issue: optionIntValue(options, "issue"),
|
|
PR: optionIntValue(options, "pr"),
|
|
Branch: optionStringValue(options, "branch"),
|
|
Tag: optionStringValue(options, "tag"),
|
|
DryRun: optionBoolValue(options, "dry_run", "dry-run"),
|
|
}
|
|
}
|
|
|
|
func prepInputFromOptions(options core.Options) PrepInput {
|
|
return PrepInput{
|
|
Repo: optionStringValue(options, "repo"),
|
|
Org: optionStringValue(options, "org"),
|
|
Task: optionStringValue(options, "task"),
|
|
Agent: optionStringValue(options, "agent"),
|
|
Issue: optionIntValue(options, "issue"),
|
|
PR: optionIntValue(options, "pr"),
|
|
Branch: optionStringValue(options, "branch"),
|
|
Tag: optionStringValue(options, "tag"),
|
|
Template: optionStringValue(options, "template"),
|
|
PlanTemplate: optionStringValue(options, "plan_template", "plan-template"),
|
|
Variables: optionStringMapValue(options, "variables"),
|
|
Persona: optionStringValue(options, "persona"),
|
|
DryRun: optionBoolValue(options, "dry_run", "dry-run"),
|
|
}
|
|
}
|
|
|
|
func resumeInputFromOptions(options core.Options) ResumeInput {
|
|
return ResumeInput{
|
|
Workspace: optionStringValue(options, "workspace"),
|
|
Answer: optionStringValue(options, "answer"),
|
|
Agent: optionStringValue(options, "agent"),
|
|
DryRun: optionBoolValue(options, "dry_run", "dry-run"),
|
|
}
|
|
}
|
|
|
|
func scanInputFromOptions(options core.Options) ScanInput {
|
|
return ScanInput{
|
|
Org: optionStringValue(options, "org"),
|
|
Labels: optionStringSliceValue(options, "labels"),
|
|
Limit: optionIntValue(options, "limit"),
|
|
}
|
|
}
|
|
|
|
func watchInputFromOptions(options core.Options) WatchInput {
|
|
workspaces := optionStringSliceValue(options, "workspaces")
|
|
if len(workspaces) == 0 {
|
|
if workspace := optionStringValue(options, "workspace"); workspace != "" {
|
|
workspaces = []string{workspace}
|
|
}
|
|
}
|
|
return WatchInput{
|
|
Workspaces: workspaces,
|
|
PollInterval: optionIntValue(options, "poll_interval", "poll-interval"),
|
|
Timeout: optionIntValue(options, "timeout"),
|
|
}
|
|
}
|
|
|
|
func mirrorInputFromOptions(options core.Options) MirrorInput {
|
|
return MirrorInput{
|
|
Repo: optionStringValue(options, "repo"),
|
|
DryRun: optionBoolValue(options, "dry_run", "dry-run"),
|
|
MaxFiles: optionIntValue(options, "max_files", "max-files"),
|
|
}
|
|
}
|
|
|
|
func reviewQueueInputFromOptions(options core.Options) ReviewQueueInput {
|
|
return ReviewQueueInput{
|
|
Limit: optionIntValue(options, "limit"),
|
|
Reviewer: optionStringValue(options, "reviewer"),
|
|
DryRun: optionBoolValue(options, "dry_run", "dry-run"),
|
|
LocalOnly: optionBoolValue(options, "local_only", "local-only"),
|
|
}
|
|
}
|
|
|
|
func epicInputFromOptions(options core.Options) EpicInput {
|
|
return EpicInput{
|
|
Repo: optionStringValue(options, "repo"),
|
|
Org: optionStringValue(options, "org"),
|
|
Title: optionStringValue(options, "title"),
|
|
Body: optionStringValue(options, "body"),
|
|
Tasks: optionStringSliceValue(options, "tasks"),
|
|
Labels: optionStringSliceValue(options, "labels"),
|
|
Dispatch: optionBoolValue(options, "dispatch"),
|
|
Agent: optionStringValue(options, "agent"),
|
|
Template: optionStringValue(options, "template"),
|
|
}
|
|
}
|
|
|
|
func normaliseForgeActionOptions(options core.Options) core.Options {
|
|
normalised := core.NewOptions(options.Items()...)
|
|
if normalised.String("_arg") == "" {
|
|
if repo := optionStringValue(options, "repo"); repo != "" {
|
|
normalised.Set("_arg", repo)
|
|
}
|
|
}
|
|
if number := optionStringValue(options, "number"); number != "" {
|
|
normalised.Set("number", number)
|
|
}
|
|
return normalised
|
|
}
|
|
|
|
func optionStringValue(options core.Options, keys ...string) string {
|
|
for _, key := range keys {
|
|
result := options.Get(key)
|
|
if !result.OK {
|
|
continue
|
|
}
|
|
if value := stringValue(result.Value); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func optionIntValue(options core.Options, keys ...string) int {
|
|
for _, key := range keys {
|
|
result := options.Get(key)
|
|
if !result.OK {
|
|
continue
|
|
}
|
|
switch value := result.Value.(type) {
|
|
case int:
|
|
return value
|
|
case int64:
|
|
return int(value)
|
|
case float64:
|
|
return int(value)
|
|
case string:
|
|
parsed := parseInt(value)
|
|
if parsed != 0 || core.Trim(value) == "0" {
|
|
return parsed
|
|
}
|
|
return parseIntString(value)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func optionBoolValue(options core.Options, keys ...string) bool {
|
|
for _, key := range keys {
|
|
result := options.Get(key)
|
|
if !result.OK {
|
|
continue
|
|
}
|
|
switch value := result.Value.(type) {
|
|
case bool:
|
|
return value
|
|
case string:
|
|
switch core.Lower(core.Trim(value)) {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func optionStringSliceValue(options core.Options, keys ...string) []string {
|
|
for _, key := range keys {
|
|
result := options.Get(key)
|
|
if !result.OK {
|
|
continue
|
|
}
|
|
values := stringSliceValue(result.Value)
|
|
if len(values) > 0 {
|
|
return values
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func optionStringMapValue(options core.Options, keys ...string) map[string]string {
|
|
for _, key := range keys {
|
|
result := options.Get(key)
|
|
if !result.OK {
|
|
continue
|
|
}
|
|
values := stringMapValue(result.Value)
|
|
if len(values) > 0 {
|
|
return values
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func optionAnyValue(options core.Options, keys ...string) any {
|
|
for _, key := range keys {
|
|
result := options.Get(key)
|
|
if !result.OK {
|
|
continue
|
|
}
|
|
return normaliseOptionValue(result.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stringValue(value any) string {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return typed
|
|
case int:
|
|
return core.Sprint(typed)
|
|
case int64:
|
|
return core.Sprint(typed)
|
|
case float64:
|
|
return core.Sprint(int(typed))
|
|
case bool:
|
|
return core.Sprint(typed)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func stringSliceValue(value any) []string {
|
|
switch typed := value.(type) {
|
|
case []string:
|
|
return cleanStrings(typed)
|
|
case []any:
|
|
var values []string
|
|
for _, item := range typed {
|
|
if text := stringValue(item); text != "" {
|
|
values = append(values, text)
|
|
}
|
|
}
|
|
return cleanStrings(values)
|
|
case string:
|
|
trimmed := core.Trim(typed)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
if core.HasPrefix(trimmed, "[") {
|
|
var values []string
|
|
if result := core.JSONUnmarshalString(trimmed, &values); result.OK {
|
|
return cleanStrings(values)
|
|
}
|
|
var generic []any
|
|
if result := core.JSONUnmarshalString(trimmed, &generic); result.OK {
|
|
return stringSliceValue(generic)
|
|
}
|
|
}
|
|
return cleanStrings(core.Split(trimmed, ","))
|
|
default:
|
|
if text := stringValue(value); text != "" {
|
|
return []string{text}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normaliseOptionValue(value any) any {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
trimmed := core.Trim(typed)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
if core.HasPrefix(trimmed, "{") {
|
|
var values map[string]any
|
|
if result := core.JSONUnmarshalString(trimmed, &values); result.OK {
|
|
return values
|
|
}
|
|
}
|
|
if core.HasPrefix(trimmed, "[") {
|
|
var values []any
|
|
if result := core.JSONUnmarshalString(trimmed, &values); result.OK {
|
|
return values
|
|
}
|
|
}
|
|
switch core.Lower(trimmed) {
|
|
case "true":
|
|
return true
|
|
case "false":
|
|
return false
|
|
}
|
|
if parsed := parseInt(trimmed); parsed != 0 || trimmed == "0" {
|
|
return parsed
|
|
}
|
|
return typed
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
func stringMapValue(value any) map[string]string {
|
|
switch typed := value.(type) {
|
|
case map[string]string:
|
|
out := make(map[string]string, len(typed))
|
|
for key, val := range typed {
|
|
if text := core.Trim(val); text != "" {
|
|
out[key] = text
|
|
}
|
|
}
|
|
return out
|
|
case map[string]any:
|
|
out := make(map[string]string, len(typed))
|
|
for key, val := range typed {
|
|
if text := stringValue(val); text != "" {
|
|
out[key] = text
|
|
}
|
|
}
|
|
return out
|
|
case []string:
|
|
out := make(map[string]string, len(typed))
|
|
for _, item := range typed {
|
|
mergeStringMapEntry(out, item)
|
|
}
|
|
return out
|
|
case []any:
|
|
out := make(map[string]string, len(typed))
|
|
for _, item := range typed {
|
|
mergeStringMapEntry(out, stringValue(item))
|
|
}
|
|
return out
|
|
case string:
|
|
trimmed := core.Trim(typed)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
if core.HasPrefix(trimmed, "{") {
|
|
var values map[string]string
|
|
if result := core.JSONUnmarshalString(trimmed, &values); result.OK {
|
|
return stringMapValue(values)
|
|
}
|
|
var generic map[string]any
|
|
if result := core.JSONUnmarshalString(trimmed, &generic); result.OK {
|
|
return stringMapValue(generic)
|
|
}
|
|
}
|
|
out := make(map[string]string)
|
|
for _, pair := range core.Split(trimmed, ",") {
|
|
mergeStringMapEntry(out, pair)
|
|
}
|
|
if len(out) > 0 {
|
|
return out
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mergeStringMapEntry(values map[string]string, entry string) {
|
|
trimmed := core.Trim(entry)
|
|
if trimmed == "" {
|
|
return
|
|
}
|
|
|
|
parts := core.SplitN(trimmed, "=", 2)
|
|
if len(parts) != 2 {
|
|
return
|
|
}
|
|
|
|
key := core.Trim(parts[0])
|
|
value := core.Trim(parts[1])
|
|
if key == "" || value == "" {
|
|
return
|
|
}
|
|
|
|
values[key] = value
|
|
}
|
|
|
|
func cleanStrings(values []string) []string {
|
|
var cleaned []string
|
|
for _, value := range values {
|
|
trimmed := core.Trim(value)
|
|
if trimmed != "" {
|
|
cleaned = append(cleaned, trimmed)
|
|
}
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
// result := c.QUERY(agentic.WorkspaceQuery{Name: "core/go-io/task-42"})
|
|
// result := c.QUERY(agentic.WorkspaceQuery{Status: "blocked"})
|
|
func (s *PrepSubsystem) handleWorkspaceQuery(_ *core.Core, query core.Query) core.Result {
|
|
workspaceQuery, ok := query.(WorkspaceQuery)
|
|
if !ok {
|
|
return core.Result{}
|
|
}
|
|
if workspaceQuery.Name != "" {
|
|
return s.workspaces.Get(workspaceQuery.Name)
|
|
}
|
|
if workspaceQuery.Status != "" {
|
|
var names []string
|
|
s.workspaces.Each(func(name string, workspaceStatus *WorkspaceStatus) {
|
|
if workspaceStatus.Status == workspaceQuery.Status {
|
|
names = append(names, name)
|
|
}
|
|
})
|
|
return core.Result{Value: names, OK: true}
|
|
}
|
|
return core.Result{Value: s.workspaces, OK: true}
|
|
}
|