agent/pkg/agentic/actions.go
Snider 03e5934607 feat(agent): RFC §15.5 parent workspace stats store
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>
2026-04-14 13:41:07 +01:00

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}
}