From 877de4325711bceb0d7b6de28ff93ec92e875284 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 22:46:21 +0000 Subject: [PATCH] fix(ax): refine harvest and dispatch naming Co-Authored-By: Virgil --- pkg/agentic/dispatch.go | 13 ++-- pkg/agentic/dispatch_sync_example_test.go | 2 +- pkg/agentic/dispatch_sync_test.go | 6 +- pkg/agentic/dispatch_test.go | 2 +- pkg/agentic/logic_test.go | 14 ++-- pkg/agentic/watch.go | 3 +- pkg/monitor/harvest.go | 82 +++++++++++------------ 7 files changed, 59 insertions(+), 63 deletions(-) diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index bb33a43..60d92c8 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -57,7 +57,7 @@ func (s *PrepSubsystem) registerDispatchTool(server *mcp.Server) { }, s.dispatch) } -// agentCommand returns the command and args for a given agent type. +// command, args, err := agentCommand("codex:review", "Review the last 2 commits via git diff HEAD~2") // Supports model variants: "gemini", "gemini:flash", "codex", "claude", "claude:haiku". func agentCommand(agent, prompt string) (string, []string, error) { commandResult := agentCommandResult(agent, prompt) @@ -161,12 +161,8 @@ func agentCommandResult(agent, prompt string) core.Result { // Override via AGENT_DOCKER_IMAGE env var. const defaultDockerImage = "core-dev" -// containerCommand wraps an agent command to run inside a Docker container. -// All agents run containerised — no bare metal execution. -// agentType is the base agent name (e.g. "local", "codex", "claude"). -// -// cmd, args := containerCommand("local", "codex", []string{"exec", "..."}, repoDir, metaDir) -func containerCommand(agentType, command string, args []string, repoDir, metaDir string) (string, []string) { +// command, args := containerCommand("local", "codex", []string{"exec", "--oss"}, "/srv/.core/workspace/core/go-io/task-5/repo", "/srv/.core/workspace/core/go-io/task-5/.meta") +func containerCommand(command string, args []string, repoDir, metaDir string) (string, []string) { image := core.Env("AGENT_DOCKER_IMAGE") if image == "" { image = defaultDockerImage @@ -405,8 +401,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, workspaceDir string) (int, str fs.Delete(WorkspaceBlockedPath(workspaceDir)) // All agents run containerised - agentBase := core.SplitN(agent, ":", 2)[0] - command, args = containerCommand(agentBase, command, args, repoDir, metaDir) + command, args = containerCommand(command, args, repoDir, metaDir) procSvc, ok := core.ServiceFor[*process.Service](s.Core(), "process") if !ok { diff --git a/pkg/agentic/dispatch_sync_example_test.go b/pkg/agentic/dispatch_sync_example_test.go index f03de79..ad61e1a 100644 --- a/pkg/agentic/dispatch_sync_example_test.go +++ b/pkg/agentic/dispatch_sync_example_test.go @@ -5,7 +5,7 @@ package agentic import core "dappco.re/go/core" func Example_containerCommand() { - cmd, args := containerCommand("codex", "codex", []string{"--model", "gpt-5.4"}, "/workspace", "/meta") + cmd, args := containerCommand("codex", []string{"--model", "gpt-5.4"}, "/workspace", "/meta") core.Println(cmd) core.Println(len(args) > 0) // Output: diff --git a/pkg/agentic/dispatch_sync_test.go b/pkg/agentic/dispatch_sync_test.go index 2d29b45..ea2f9b9 100644 --- a/pkg/agentic/dispatch_sync_test.go +++ b/pkg/agentic/dispatch_sync_test.go @@ -9,19 +9,19 @@ import ( ) func TestDispatchsync_ContainerCommand_Good(t *testing.T) { - cmd, args := containerCommand("codex", "codex", []string{"--model", "gpt-5.4"}, "/workspace", "/meta") + cmd, args := containerCommand("codex", []string{"--model", "gpt-5.4"}, "/workspace", "/meta") assert.Equal(t, "docker", cmd) assert.Contains(t, args, "run") } func TestDispatchsync_ContainerCommand_Bad_UnknownAgent(t *testing.T) { - cmd, args := containerCommand("unknown", "unknown", nil, "/workspace", "/meta") + cmd, args := containerCommand("unknown", nil, "/workspace", "/meta") assert.Equal(t, "docker", cmd) assert.NotEmpty(t, args) } func TestDispatchsync_ContainerCommand_Ugly_EmptyArgs(t *testing.T) { assert.NotPanics(t, func() { - containerCommand("codex", "codex", nil, "", "") + containerCommand("codex", nil, "", "") }) } diff --git a/pkg/agentic/dispatch_test.go b/pkg/agentic/dispatch_test.go index 9679244..f915675 100644 --- a/pkg/agentic/dispatch_test.go +++ b/pkg/agentic/dispatch_test.go @@ -584,7 +584,7 @@ func TestDispatch_ContainerCommand_Bad(t *testing.T) { t.Setenv("DIR_HOME", "/home/dev") // Empty command string — docker still runs, just with no command after image - cmd, args := containerCommand("codex", "", []string{}, "/ws/repo", "/ws/.meta") + cmd, args := containerCommand("", []string{}, "/ws/repo", "/ws/.meta") assert.Equal(t, "docker", cmd) assert.Contains(t, args, "run") // The image should still be present in args diff --git a/pkg/agentic/logic_test.go b/pkg/agentic/logic_test.go index f9a58b8..830bcf7 100644 --- a/pkg/agentic/logic_test.go +++ b/pkg/agentic/logic_test.go @@ -121,7 +121,7 @@ func TestDispatch_ContainerCommand_Good_Codex(t *testing.T) { t.Setenv("AGENT_DOCKER_IMAGE", "") t.Setenv("DIR_HOME", "/home/dev") - cmd, args := containerCommand("codex", "codex", []string{"exec", "--dangerously-bypass-approvals-and-sandbox", "do it"}, "/ws/repo", "/ws/.meta") + cmd, args := containerCommand("codex", []string{"exec", "--dangerously-bypass-approvals-and-sandbox", "do it"}, "/ws/repo", "/ws/.meta") assert.Equal(t, "docker", cmd) assert.Contains(t, args, "run") assert.Contains(t, args, "--rm") @@ -138,7 +138,7 @@ func TestDispatch_ContainerCommand_Good_CustomImage(t *testing.T) { t.Setenv("AGENT_DOCKER_IMAGE", "my-custom-image:latest") t.Setenv("DIR_HOME", "/home/dev") - cmd, args := containerCommand("codex", "codex", []string{"exec"}, "/ws/repo", "/ws/.meta") + cmd, args := containerCommand("codex", []string{"exec"}, "/ws/repo", "/ws/.meta") assert.Equal(t, "docker", cmd) assert.Contains(t, args, "my-custom-image:latest") } @@ -147,7 +147,7 @@ func TestDispatch_ContainerCommand_Good_ClaudeMountsConfig(t *testing.T) { t.Setenv("AGENT_DOCKER_IMAGE", "") t.Setenv("DIR_HOME", "/home/dev") - _, args := containerCommand("claude", "claude", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta") + _, args := containerCommand("claude", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta") joined := strings.Join(args, " ") assert.Contains(t, joined, ".claude:/home/dev/.claude:ro") } @@ -156,7 +156,7 @@ func TestDispatch_ContainerCommand_Good_GeminiMountsConfig(t *testing.T) { t.Setenv("AGENT_DOCKER_IMAGE", "") t.Setenv("DIR_HOME", "/home/dev") - _, args := containerCommand("gemini", "gemini", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta") + _, args := containerCommand("gemini", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta") joined := strings.Join(args, " ") assert.Contains(t, joined, ".gemini:/home/dev/.gemini:ro") } @@ -165,7 +165,7 @@ func TestDispatch_ContainerCommand_Good_CodexNoClaudeMount(t *testing.T) { t.Setenv("AGENT_DOCKER_IMAGE", "") t.Setenv("DIR_HOME", "/home/dev") - _, args := containerCommand("codex", "codex", []string{"exec"}, "/ws/repo", "/ws/.meta") + _, args := containerCommand("codex", []string{"exec"}, "/ws/repo", "/ws/.meta") joined := strings.Join(args, " ") // codex agent must NOT mount .claude config assert.NotContains(t, joined, ".claude:/home/dev/.claude:ro") @@ -175,7 +175,7 @@ func TestDispatch_ContainerCommand_Good_APIKeysPassedByRef(t *testing.T) { t.Setenv("AGENT_DOCKER_IMAGE", "") t.Setenv("DIR_HOME", "/home/dev") - _, args := containerCommand("codex", "codex", []string{"exec"}, "/ws/repo", "/ws/.meta") + _, args := containerCommand("codex", []string{"exec"}, "/ws/repo", "/ws/.meta") joined := strings.Join(args, " ") assert.Contains(t, joined, "OPENAI_API_KEY") assert.Contains(t, joined, "ANTHROPIC_API_KEY") @@ -187,7 +187,7 @@ func TestDispatch_ContainerCommand_Ugly_EmptyDirs(t *testing.T) { t.Setenv("DIR_HOME", "") // Should not panic with empty paths - cmd, args := containerCommand("codex", "codex", []string{"exec"}, "", "") + cmd, args := containerCommand("codex", []string{"exec"}, "", "") assert.Equal(t, "docker", cmd) assert.NotEmpty(t, args) } diff --git a/pkg/agentic/watch.go b/pkg/agentic/watch.go index 4c6ff21..224474c 100644 --- a/pkg/agentic/watch.go +++ b/pkg/agentic/watch.go @@ -190,7 +190,8 @@ func (s *PrepSubsystem) watch(ctx context.Context, request *mcp.CallToolRequest, }, nil } -// findActiveWorkspaces returns workspace names that are running or queued. +// active := s.findActiveWorkspaces() +// if len(active) == 0 { return nil } func (s *PrepSubsystem) findActiveWorkspaces() []string { var active []string for _, entry := range WorkspaceStatusPaths() { diff --git a/pkg/monitor/harvest.go b/pkg/monitor/harvest.go index baedd81..b01d8a0 100644 --- a/pkg/monitor/harvest.go +++ b/pkg/monitor/harvest.go @@ -1,11 +1,10 @@ // SPDX-License-Identifier: EUPL-1.2 -// Harvest completed agent workspaces — push changes back to source repos. +// result := m.harvestWorkspace("/srv/.core/workspace/core/go-io/task-5") +// if result != nil && result.rejected == "" { /* ready-for-review */ } // -// After an agent completes, its commits live in the workspace clone. -// This code pushes the agent's branch to the source repo so the -// changes are available for review. It checks for binaries and -// large files before pushing. +// Completed workspaces are scanned, validated, and marked ready for review. +// The code does not auto-push; review remains an explicit action. package monitor @@ -26,8 +25,8 @@ type harvestResult struct { rejected string // non-empty if rejected (binary, too large, etc.) } -// harvestCompleted scans for completed workspaces and pushes their -// branches back to the source repos. Returns a summary message. +// summary := m.harvestCompleted() +// if summary != "" { core.Print(nil, summary) } func (m *Subsystem) harvestCompleted() string { var harvested []harvestResult @@ -60,7 +59,8 @@ func (m *Subsystem) harvestCompleted() string { return core.Concat("Harvested: ", core.Join(", ", parts...)) } -// harvestWorkspace checks a single workspace and pushes if ready. +// result := m.harvestWorkspace("/srv/.core/workspace/core/go-io/task-5") +// if result != nil && result.rejected == "" { /* ready-for-review */ } func (m *Subsystem) harvestWorkspace(workspaceDir string) *harvestResult { statusResult := fs.Read(agentic.WorkspaceStatusPath(workspaceDir)) if !statusResult.OK { @@ -90,7 +90,7 @@ func (m *Subsystem) harvestWorkspace(workspaceDir string) *harvestResult { return nil } - // Check if there are commits to push + // Check if there are commits ahead of the default branch branch := workspaceStatus.Branch if branch == "" { branch = m.detectBranch(repoDir) @@ -103,10 +103,10 @@ func (m *Subsystem) harvestWorkspace(workspaceDir string) *harvestResult { // Check for unpushed commits unpushed := m.countUnpushed(repoDir, branch) if unpushed == 0 { - return nil // already pushed or no commits + return nil // already on origin or no commits } - // Safety checks before pushing + // Safety checks before marking ready-for-review if reason := m.checkSafety(repoDir); reason != "" { updateStatus(workspaceDir, "rejected", reason) return &harvestResult{repo: workspaceStatus.Repo, branch: branch, rejected: reason} @@ -123,48 +123,48 @@ func (m *Subsystem) harvestWorkspace(workspaceDir string) *harvestResult { return &harvestResult{repo: workspaceStatus.Repo, branch: branch, files: files} } -// gitOutput runs a git command and returns trimmed stdout via Core Process. -func (m *Subsystem) gitOutput(dir string, args ...string) string { - processResult := m.Core().Process().RunIn(context.Background(), dir, "git", args...) +// output := m.gitOutput("/srv/.core/workspace/core/go-io/task-5/repo", "log", "--oneline") +func (m *Subsystem) gitOutput(repoDir string, args ...string) string { + processResult := m.Core().Process().RunIn(context.Background(), repoDir, "git", args...) if !processResult.OK { return "" } return core.Trim(processResult.Value.(string)) } -// gitOK runs a git command and returns true if it exits 0. -func (m *Subsystem) gitOK(dir string, args ...string) bool { - return m.Core().Process().RunIn(context.Background(), dir, "git", args...).OK +// ok := m.gitOK("/srv/.core/workspace/core/go-io/task-5/repo", "rev-parse", "--verify", "main") +func (m *Subsystem) gitOK(repoDir string, args ...string) bool { + return m.Core().Process().RunIn(context.Background(), repoDir, "git", args...).OK } -// detectBranch returns the current branch name. -func (m *Subsystem) detectBranch(srcDir string) string { - return m.gitOutput(srcDir, "rev-parse", "--abbrev-ref", "HEAD") +// branch := m.detectBranch("/srv/.core/workspace/core/go-io/task-5/repo") +func (m *Subsystem) detectBranch(repoDir string) string { + return m.gitOutput(repoDir, "rev-parse", "--abbrev-ref", "HEAD") } -// defaultBranch detects the default branch of the repo (main, master, etc.). -func (m *Subsystem) defaultBranch(srcDir string) string { - if ref := m.gitOutput(srcDir, "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); ref != "" { +// base := m.defaultBranch("/srv/.core/workspace/core/go-io/task-5/repo") +func (m *Subsystem) defaultBranch(repoDir string) string { + if ref := m.gitOutput(repoDir, "symbolic-ref", "refs/remotes/origin/HEAD", "--short"); ref != "" { if core.HasPrefix(ref, "origin/") { return core.TrimPrefix(ref, "origin/") } return ref } for _, branch := range []string{"main", "master"} { - if m.gitOK(srcDir, "rev-parse", "--verify", branch) { + if m.gitOK(repoDir, "rev-parse", "--verify", branch) { return branch } } return "main" } -// countUnpushed returns the number of commits ahead of origin's default branch. -func (m *Subsystem) countUnpushed(srcDir, branch string) int { - base := m.defaultBranch(srcDir) - out := m.gitOutput(srcDir, "rev-list", "--count", core.Concat("origin/", base, "..", branch)) +// ahead := m.countUnpushed("/srv/.core/workspace/core/go-io/task-5/repo", "feature/ax-cleanup") +func (m *Subsystem) countUnpushed(repoDir, branch string) int { + base := m.defaultBranch(repoDir) + out := m.gitOutput(repoDir, "rev-list", "--count", core.Concat("origin/", base, "..", branch)) if out == "" { // Fallback - out2 := m.gitOutput(srcDir, "log", "--oneline", core.Concat(base, "..", branch)) + out2 := m.gitOutput(repoDir, "log", "--oneline", core.Concat(base, "..", branch)) if out2 == "" { return 0 } @@ -181,10 +181,10 @@ func (m *Subsystem) countUnpushed(srcDir, branch string) int { return count } -// checkSafety rejects workspaces with binaries or oversized files. -func (m *Subsystem) checkSafety(srcDir string) string { - base := m.defaultBranch(srcDir) - out := m.gitOutput(srcDir, "diff", "--name-only", core.Concat(base, "...HEAD")) +// reason := m.checkSafety("/srv/.core/workspace/core/go-io/task-5/repo") +func (m *Subsystem) checkSafety(repoDir string) string { + base := m.defaultBranch(repoDir) + out := m.gitOutput(repoDir, "diff", "--name-only", core.Concat(base, "...HEAD")) if out == "" { return "safety check failed: git diff error" } @@ -208,7 +208,7 @@ func (m *Subsystem) checkSafety(srcDir string) string { return core.Sprintf("binary file added: %s", file) } - fullPath := core.JoinPath(srcDir, file) + fullPath := core.JoinPath(repoDir, file) if stat := fs.Stat(fullPath); stat.OK { if info, ok := stat.Value.(interface{ Size() int64 }); ok && info.Size() > 1024*1024 { return core.Sprintf("large file: %s (%d bytes)", file, info.Size()) @@ -219,10 +219,10 @@ func (m *Subsystem) checkSafety(srcDir string) string { return "" } -// countChangedFiles returns the number of files changed vs the default branch. -func (m *Subsystem) countChangedFiles(srcDir string) int { - base := m.defaultBranch(srcDir) - out := m.gitOutput(srcDir, "diff", "--name-only", core.Concat(base, "...HEAD")) +// files := m.countChangedFiles("/srv/.core/workspace/core/go-io/task-5/repo") +func (m *Subsystem) countChangedFiles(repoDir string) int { + base := m.defaultBranch(repoDir) + out := m.gitOutput(repoDir, "diff", "--name-only", core.Concat(base, "...HEAD")) if out == "" { return 0 } @@ -233,9 +233,9 @@ func (m *Subsystem) countChangedFiles(srcDir string) int { return len(lines) } -// pushBranch pushes the agent's branch to origin. -func (m *Subsystem) pushBranch(srcDir, branch string) error { - processResult := m.Core().Process().RunIn(context.Background(), srcDir, "git", "push", "origin", branch) +// _ = m.pushBranch("/srv/.core/workspace/core/go-io/task-5/repo", "feature/ax-cleanup") +func (m *Subsystem) pushBranch(repoDir, branch string) error { + processResult := m.Core().Process().RunIn(context.Background(), repoDir, "git", "push", "origin", branch) if !processResult.OK { if err, ok := processResult.Value.(error); ok { return core.E("harvest.pushBranch", "push failed", err)