2026-03-16 11:10:33 +00:00
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
2026-03-22 14:49:56 +00:00
"os/exec"
2026-03-17 05:56:22 +00:00
"syscall"
2026-03-16 11:10:33 +00:00
"time"
2026-03-22 03:41:07 +00:00
core "dappco.re/go/core"
2026-03-22 01:27:48 +00:00
"dappco.re/go/core/process"
2026-03-16 11:10:33 +00:00
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// DispatchInput is the input for agentic_dispatch.
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
//
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// input := agentic.DispatchInput{Repo: "go-io", Task: "Fix the failing tests", Agent: "codex", Issue: 15}
2026-03-16 11:10:33 +00:00
type DispatchInput struct {
Repo string ` json:"repo" ` // Target repo (e.g. "go-io")
Org string ` json:"org,omitempty" ` // Forge org (default "core")
Task string ` json:"task" ` // What the agent should do
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
Agent string ` json:"agent,omitempty" ` // "codex" (default), "claude", "gemini"
2026-03-16 11:10:33 +00:00
Template string ` json:"template,omitempty" ` // "conventions", "security", "coding" (default)
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
PlanTemplate string ` json:"plan_template,omitempty" ` // Plan template slug
2026-03-16 11:10:33 +00:00
Variables map [ string ] string ` json:"variables,omitempty" ` // Template variable substitution
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
Persona string ` json:"persona,omitempty" ` // Persona slug
Issue int ` json:"issue,omitempty" ` // Forge issue number → workspace: task-{num}/
PR int ` json:"pr,omitempty" ` // PR number → workspace: pr-{num}/
Branch string ` json:"branch,omitempty" ` // Branch → workspace: {branch}/
Tag string ` json:"tag,omitempty" ` // Tag → workspace: {tag}/ (immutable)
2026-03-16 11:10:33 +00:00
DryRun bool ` json:"dry_run,omitempty" ` // Preview without executing
}
// DispatchOutput is the output for agentic_dispatch.
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
//
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// out := agentic.DispatchOutput{Success: true, Agent: "codex", Repo: "go-io", WorkspaceDir: ".core/workspace/core/go-io/task-15"}
2026-03-16 11:10:33 +00:00
type DispatchOutput struct {
Success bool ` json:"success" `
Agent string ` json:"agent" `
Repo string ` json:"repo" `
WorkspaceDir string ` json:"workspace_dir" `
Prompt string ` json:"prompt,omitempty" `
PID int ` json:"pid,omitempty" `
OutputFile string ` json:"output_file,omitempty" `
}
func ( s * PrepSubsystem ) registerDispatchTool ( server * mcp . Server ) {
mcp . AddTool ( server , & mcp . Tool {
Name : "agentic_dispatch" ,
Description : "Dispatch a subagent (Gemini, Codex, or Claude) to work on a task. Preps a sandboxed workspace first, then spawns the agent inside it. Templates: conventions, security, coding." ,
} , s . dispatch )
}
// agentCommand returns the command and args for a given agent type.
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// Supports model variants: "gemini", "gemini:flash", "codex", "claude", "claude:haiku".
2026-03-16 11:10:33 +00:00
func agentCommand ( agent , prompt string ) ( string , [ ] string , error ) {
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
parts := core . SplitN ( agent , ":" , 2 )
2026-03-16 11:10:33 +00:00
base := parts [ 0 ]
model := ""
if len ( parts ) > 1 {
model = parts [ 1 ]
}
switch base {
case "gemini" :
args := [ ] string { "-p" , prompt , "--yolo" , "--sandbox" }
if model != "" {
args = append ( args , "-m" , "gemini-2.5-" + model )
}
return "gemini" , args , nil
case "codex" :
2026-03-17 17:45:04 +00:00
if model == "review" {
2026-03-23 12:53:33 +00:00
// Use exec with bypass — codex review subcommand has its own sandbox that blocks shell
// No -o flag — stdout captured by process output, ../.meta path unreliable in sandbox
return "codex" , [ ] string {
"exec" ,
"--dangerously-bypass-approvals-and-sandbox" ,
"Review the last 2 commits via git diff HEAD~2. Check for bugs, security issues, missing tests, naming issues. Report pass/fail with specifics. Do NOT make changes." ,
} , nil
2026-03-17 17:45:04 +00:00
}
2026-03-23 12:53:33 +00:00
// Container IS the sandbox — let codex run unrestricted inside it
feat: devops plugin, CLI commands, Codex dispatch fixes, AX sweep
DevOps plugin (5 skills):
- install-core-agent, repair-core-agent, merge-workspace,
update-deps, clean-workspaces
CLI commands: version, check, extract for diagnostics.
Codex dispatch: --skip-git-repo-check, removed broken
--model-reasoning-effort, --sandbox workspace-write via
--full-auto. Workspace template extracts to wsDir not srcDir.
AX sweep (Codex-generated): sanitise.go extracted from prep/plan,
mirror.go JSON parsing via encoding/json, setup/config.go URL
parsing via net/url, strings/fmt imports eliminated from setup.
CODEX.md template updated with Env/Path patterns.
Review workspace template with audit-only PROMPT.md.
Marketplace updated with devops plugin.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:30:27 +00:00
args := [ ] string {
"exec" ,
2026-03-23 12:53:33 +00:00
"--dangerously-bypass-approvals-and-sandbox" ,
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
"-o" , "../.meta/agent-codex.log" ,
feat: devops plugin, CLI commands, Codex dispatch fixes, AX sweep
DevOps plugin (5 skills):
- install-core-agent, repair-core-agent, merge-workspace,
update-deps, clean-workspaces
CLI commands: version, check, extract for diagnostics.
Codex dispatch: --skip-git-repo-check, removed broken
--model-reasoning-effort, --sandbox workspace-write via
--full-auto. Workspace template extracts to wsDir not srcDir.
AX sweep (Codex-generated): sanitise.go extracted from prep/plan,
mirror.go JSON parsing via encoding/json, setup/config.go URL
parsing via net/url, strings/fmt imports eliminated from setup.
CODEX.md template updated with Env/Path patterns.
Review workspace template with audit-only PROMPT.md.
Marketplace updated with devops plugin.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:30:27 +00:00
}
if model != "" {
2026-03-22 15:00:49 +00:00
args = append ( args , "--model" , model )
feat: devops plugin, CLI commands, Codex dispatch fixes, AX sweep
DevOps plugin (5 skills):
- install-core-agent, repair-core-agent, merge-workspace,
update-deps, clean-workspaces
CLI commands: version, check, extract for diagnostics.
Codex dispatch: --skip-git-repo-check, removed broken
--model-reasoning-effort, --sandbox workspace-write via
--full-auto. Workspace template extracts to wsDir not srcDir.
AX sweep (Codex-generated): sanitise.go extracted from prep/plan,
mirror.go JSON parsing via encoding/json, setup/config.go URL
parsing via net/url, strings/fmt imports eliminated from setup.
CODEX.md template updated with Env/Path patterns.
Review workspace template with audit-only PROMPT.md.
Marketplace updated with devops plugin.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:30:27 +00:00
}
2026-03-22 15:00:49 +00:00
args = append ( args , prompt )
feat: devops plugin, CLI commands, Codex dispatch fixes, AX sweep
DevOps plugin (5 skills):
- install-core-agent, repair-core-agent, merge-workspace,
update-deps, clean-workspaces
CLI commands: version, check, extract for diagnostics.
Codex dispatch: --skip-git-repo-check, removed broken
--model-reasoning-effort, --sandbox workspace-write via
--full-auto. Workspace template extracts to wsDir not srcDir.
AX sweep (Codex-generated): sanitise.go extracted from prep/plan,
mirror.go JSON parsing via encoding/json, setup/config.go URL
parsing via net/url, strings/fmt imports eliminated from setup.
CODEX.md template updated with Env/Path patterns.
Review workspace template with audit-only PROMPT.md.
Marketplace updated with devops plugin.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:30:27 +00:00
return "codex" , args , nil
2026-03-16 11:10:33 +00:00
case "claude" :
2026-03-17 04:12:54 +00:00
args := [ ] string {
"-p" , prompt ,
"--output-format" , "text" ,
2026-03-17 17:45:04 +00:00
"--dangerously-skip-permissions" ,
2026-03-17 04:12:54 +00:00
"--no-session-persistence" ,
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
"--append-system-prompt" , "SANDBOX: You are restricted to the current directory only. " +
"Do NOT use absolute paths. Do NOT navigate outside this repository." ,
2026-03-17 04:12:54 +00:00
}
2026-03-16 11:10:33 +00:00
if model != "" {
args = append ( args , "--model" , model )
}
return "claude" , args , nil
2026-03-17 17:45:04 +00:00
case "coderabbit" :
args := [ ] string { "review" , "--plain" , "--base" , "HEAD~1" }
if model != "" {
args = append ( args , "--type" , model )
}
if prompt != "" {
args = append ( args , "--config" , "CLAUDE.md" )
}
return "coderabbit" , args , nil
2026-03-16 11:10:33 +00:00
case "local" :
2026-03-23 12:53:33 +00:00
// Local model via codex --oss → Ollama. Default model: devstral-24b
// socat proxies localhost:11434 → host.docker.internal:11434
// because codex hardcodes localhost check for Ollama.
localModel := model
if localModel == "" {
localModel = "devstral-24b"
}
script := core . Sprintf (
` socat TCP-LISTEN:11434,fork,reuseaddr TCP:host.docker.internal:11434 & sleep 0.5 && codex exec --dangerously-bypass-approvals-and-sandbox --oss --local-provider ollama -m %s -o ../.meta/agent-codex.log %q ` ,
localModel , prompt ,
)
return "sh" , [ ] string { "-c" , script } , nil
2026-03-16 11:10:33 +00:00
default :
2026-03-22 03:41:07 +00:00
return "" , nil , core . E ( "agentCommand" , "unknown agent: " + agent , nil )
2026-03-16 11:10:33 +00:00
}
}
2026-03-23 12:53:33 +00:00
// defaultDockerImage is the container image for agent dispatch.
// 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 ) {
image := core . Env ( "AGENT_DOCKER_IMAGE" )
if image == "" {
image = defaultDockerImage
}
home := core . Env ( "DIR_HOME" )
dockerArgs := [ ] string {
"run" , "--rm" ,
// Host access for Ollama (local models)
"--add-host=host.docker.internal:host-gateway" ,
// Workspace: repo + meta
"-v" , repoDir + ":/workspace" ,
"-v" , metaDir + ":/workspace/.meta" ,
"-w" , "/workspace" ,
// Auth: agent configs only — NO SSH keys, git push runs on host
"-v" , core . JoinPath ( home , ".codex" ) + ":/root/.codex:ro" ,
// API keys — passed by name, Docker resolves from host env
"-e" , "OPENAI_API_KEY" ,
"-e" , "ANTHROPIC_API_KEY" ,
"-e" , "GEMINI_API_KEY" ,
"-e" , "GOOGLE_API_KEY" ,
// Agent environment
"-e" , "TERM=dumb" ,
"-e" , "NO_COLOR=1" ,
"-e" , "CI=true" ,
"-e" , "GIT_USER_NAME=Virgil" ,
"-e" , "GIT_USER_EMAIL=virgil@lethean.io" ,
// Local model access — Ollama on host
"-e" , "OLLAMA_HOST=http://host.docker.internal:11434" ,
}
// Mount Claude config if dispatching claude agent
if command == "claude" {
dockerArgs = append ( dockerArgs ,
"-v" , core . JoinPath ( home , ".claude" ) + ":/root/.claude:ro" ,
)
}
// Mount Gemini config if dispatching gemini agent
if command == "gemini" {
dockerArgs = append ( dockerArgs ,
"-v" , core . JoinPath ( home , ".gemini" ) + ":/root/.gemini:ro" ,
)
}
dockerArgs = append ( dockerArgs , image , command )
dockerArgs = append ( dockerArgs , args ... )
return "docker" , dockerArgs
}
// spawnAgent launches an agent inside a Docker container.
// The repo/ directory is mounted at /workspace, agent runs sandboxed.
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// Output is captured and written to .meta/agent-{agent}.log on completion.
func ( s * PrepSubsystem ) spawnAgent ( agent , prompt , wsDir string ) ( int , string , error ) {
2026-03-16 17:52:55 +00:00
command , args , err := agentCommand ( agent , prompt )
if err != nil {
return 0 , "" , err
}
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
repoDir := core . JoinPath ( wsDir , "repo" )
metaDir := core . JoinPath ( wsDir , ".meta" )
2026-03-22 14:57:24 +00:00
// Use base agent name for log file — colon in variants breaks paths
agentBase := core . SplitN ( agent , ":" , 2 ) [ 0 ]
outputFile := core . JoinPath ( metaDir , core . Sprintf ( "agent-%s.log" , agentBase ) )
2026-03-16 17:52:55 +00:00
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// Clean up stale BLOCKED.md from previous runs
fs . Delete ( core . JoinPath ( repoDir , "BLOCKED.md" ) )
2026-03-21 16:53:55 +00:00
2026-03-23 12:53:33 +00:00
// All agents run containerised
command , args = containerCommand ( agentBase , command , args , repoDir , metaDir )
2026-03-16 17:52:55 +00:00
proc , err := process . StartWithOptions ( context . Background ( ) , process . RunOptions {
test(brain): add unit tests for recall, remember, messaging
Coverage: 5.3% → 92.8%. Tests cover DirectSubsystem (apiCall, remember,
recall, forget via httptest), messaging (sendMessage, inbox, conversation,
parseMessages, toInt), BrainProvider (gin handlers, routes, describe,
status), Subsystem bridge-backed handlers, and RegisterTools.
Also fixes build error in dispatch.go (removed KillGroup, Timeout,
GracePeriod fields no longer in process.RunOptions).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-21 13:40:20 +00:00
Command : command ,
Args : args ,
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
Dir : repoDir ,
test(brain): add unit tests for recall, remember, messaging
Coverage: 5.3% → 92.8%. Tests cover DirectSubsystem (apiCall, remember,
recall, forget via httptest), messaging (sendMessage, inbox, conversation,
parseMessages, toInt), BrainProvider (gin handlers, routes, describe,
status), Subsystem bridge-backed handlers, and RegisterTools.
Also fixes build error in dispatch.go (removed KillGroup, Timeout,
GracePeriod fields no longer in process.RunOptions).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-21 13:40:20 +00:00
Detach : true ,
2026-03-16 17:52:55 +00:00
} )
if err != nil {
2026-03-22 03:41:07 +00:00
return 0 , "" , core . E ( "dispatch.spawnAgent" , "failed to spawn " + agent , err )
2026-03-16 17:52:55 +00:00
}
2026-03-17 17:45:04 +00:00
proc . CloseStdin ( )
2026-03-16 17:52:55 +00:00
pid := proc . Info ( ) . PID
2026-03-22 16:19:13 +00:00
// Notify monitor directly — no filesystem polling
if s . onComplete != nil {
st , _ := readStatus ( wsDir )
repo := ""
if st != nil {
repo = st . Repo
}
s . onComplete . AgentStarted ( agent , repo , core . PathBase ( wsDir ) )
}
emitStartEvent ( agent , core . PathBase ( wsDir ) ) // audit log
2026-03-22 15:14:14 +00:00
2026-03-23 12:53:33 +00:00
// Start Forge stopwatch on the issue (time tracking)
if st , _ := readStatus ( wsDir ) ; st != nil && st . Issue > 0 {
org := st . Org
if org == "" {
org = "core"
}
s . forge . Issues . StartStopwatch ( context . Background ( ) , org , st . Repo , int64 ( st . Issue ) )
}
2026-03-16 17:52:55 +00:00
go func ( ) {
2026-03-17 05:56:22 +00:00
ticker := time . NewTicker ( 5 * time . Second )
defer ticker . Stop ( )
for {
select {
2026-03-17 17:45:04 +00:00
case <- proc . Done ( ) :
goto done
2026-03-17 05:56:22 +00:00
case <- ticker . C :
2026-03-17 17:45:04 +00:00
if err := syscall . Kill ( pid , 0 ) ; err != nil {
goto done
2026-03-17 05:56:22 +00:00
}
}
}
2026-03-17 17:45:04 +00:00
done :
2026-03-16 17:52:55 +00:00
if output := proc . Output ( ) ; output != "" {
2026-03-22 03:41:07 +00:00
fs . Write ( outputFile , output )
2026-03-16 17:52:55 +00:00
}
2026-03-17 17:45:04 +00:00
finalStatus := "completed"
exitCode := proc . Info ( ) . ExitCode
procStatus := proc . Info ( ) . Status
2026-03-17 19:35:15 +00:00
question := ""
2026-03-17 17:45:04 +00:00
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
blockedPath := core . JoinPath ( repoDir , "BLOCKED.md" )
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
if r := fs . Read ( blockedPath ) ; r . OK && core . Trim ( r . Value . ( string ) ) != "" {
2026-03-17 17:45:04 +00:00
finalStatus = "blocked"
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
question = core . Trim ( r . Value . ( string ) )
2026-03-17 17:45:04 +00:00
} else if exitCode != 0 || procStatus == "failed" || procStatus == "killed" {
finalStatus = "failed"
2026-03-17 19:35:15 +00:00
if exitCode != 0 {
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
question = core . Sprintf ( "Agent exited with code %d" , exitCode )
2026-03-17 17:45:04 +00:00
}
2026-03-16 17:52:55 +00:00
}
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
if st , stErr := readStatus ( wsDir ) ; stErr == nil {
2026-03-17 19:35:15 +00:00
st . Status = finalStatus
st . PID = 0
st . Question = question
writeStatus ( wsDir , st )
}
2026-03-22 16:19:13 +00:00
emitCompletionEvent ( agent , core . PathBase ( wsDir ) , finalStatus ) // audit log
2026-03-17 03:05:26 +00:00
2026-03-23 16:08:08 +00:00
// Rate-limit detection: if agent failed fast (<60s), track consecutive failures
pool := baseAgent ( agent )
if finalStatus == "failed" {
if st , _ := readStatus ( wsDir ) ; st != nil {
elapsed := time . Since ( st . StartedAt )
if elapsed < 60 * time . Second {
s . failCount [ pool ] ++
if s . failCount [ pool ] >= 3 {
s . backoff [ pool ] = time . Now ( ) . Add ( 30 * time . Minute )
core . Print ( nil , "rate-limit detected for %s — pausing pool for 30 minutes" , pool )
}
} else {
s . failCount [ pool ] = 0 // slow failure = real failure, reset count
}
}
} else {
s . failCount [ pool ] = 0 // success resets count
}
2026-03-23 12:53:33 +00:00
// Stop Forge stopwatch on the issue (time tracking)
if st , _ := readStatus ( wsDir ) ; st != nil && st . Issue > 0 {
org := st . Org
if org == "" {
org = "core"
}
s . forge . Issues . StopStopwatch ( context . Background ( ) , org , st . Repo , int64 ( st . Issue ) )
}
2026-03-22 16:19:13 +00:00
// Push notification directly — no filesystem polling
2026-03-17 17:45:04 +00:00
if s . onComplete != nil {
2026-03-22 16:19:13 +00:00
stNow , _ := readStatus ( wsDir )
repoName := ""
if stNow != nil {
repoName = stNow . Repo
}
s . onComplete . AgentCompleted ( agent , repoName , core . PathBase ( wsDir ) , finalStatus )
2026-03-17 17:45:04 +00:00
}
if finalStatus == "completed" {
2026-03-22 14:49:56 +00:00
// Run QA before PR — if QA fails, mark as failed, don't PR
if ! s . runQA ( wsDir ) {
finalStatus = "failed"
question = "QA check failed — build or tests did not pass"
if st , stErr := readStatus ( wsDir ) ; stErr == nil {
st . Status = finalStatus
st . Question = question
writeStatus ( wsDir , st )
}
} else {
s . autoCreatePR ( wsDir )
s . autoVerifyAndMerge ( wsDir )
}
2026-03-17 17:45:04 +00:00
}
2026-03-17 04:19:48 +00:00
2026-03-16 17:52:55 +00:00
s . ingestFindings ( wsDir )
2026-03-23 12:53:33 +00:00
s . Poke ( )
2026-03-16 17:52:55 +00:00
} ( )
return pid , outputFile , nil
}
2026-03-22 14:49:56 +00:00
// runQA runs build + test checks on the repo after agent completion.
// Returns true if QA passes, false if build or tests fail.
func ( s * PrepSubsystem ) runQA ( wsDir string ) bool {
repoDir := core . JoinPath ( wsDir , "repo" )
// Detect language and run appropriate checks
if fs . IsFile ( core . JoinPath ( repoDir , "go.mod" ) ) {
// Go: build + vet + test
for _ , args := range [ ] [ ] string {
{ "go" , "build" , "./..." } ,
{ "go" , "vet" , "./..." } ,
{ "go" , "test" , "./..." , "-count=1" , "-timeout" , "120s" } ,
} {
cmd := exec . Command ( args [ 0 ] , args [ 1 : ] ... )
cmd . Dir = repoDir
if err := cmd . Run ( ) ; err != nil {
core . Warn ( "QA failed" , "cmd" , core . Join ( " " , args ... ) , "err" , err )
return false
}
}
return true
}
if fs . IsFile ( core . JoinPath ( repoDir , "composer.json" ) ) {
// PHP: composer install + test
install := exec . Command ( "composer" , "install" , "--no-interaction" )
install . Dir = repoDir
if err := install . Run ( ) ; err != nil {
return false
}
test := exec . Command ( "composer" , "test" )
test . Dir = repoDir
return test . Run ( ) == nil
}
if fs . IsFile ( core . JoinPath ( repoDir , "package.json" ) ) {
// Node: npm install + test
install := exec . Command ( "npm" , "install" )
install . Dir = repoDir
if err := install . Run ( ) ; err != nil {
return false
}
test := exec . Command ( "npm" , "test" )
test . Dir = repoDir
return test . Run ( ) == nil
}
// Unknown language — pass QA (no checks to run)
return true
}
2026-03-16 11:10:33 +00:00
func ( s * PrepSubsystem ) dispatch ( ctx context . Context , req * mcp . CallToolRequest , input DispatchInput ) ( * mcp . CallToolResult , DispatchOutput , error ) {
if input . Repo == "" {
2026-03-22 03:41:07 +00:00
return nil , DispatchOutput { } , core . E ( "dispatch" , "repo is required" , nil )
2026-03-16 11:10:33 +00:00
}
if input . Task == "" {
2026-03-22 03:41:07 +00:00
return nil , DispatchOutput { } , core . E ( "dispatch" , "task is required" , nil )
2026-03-16 11:10:33 +00:00
}
if input . Org == "" {
input . Org = "core"
}
if input . Agent == "" {
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
input . Agent = "codex"
2026-03-16 11:10:33 +00:00
}
if input . Template == "" {
input . Template = "coding"
}
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// Step 1: Prep workspace — clone + build prompt
2026-03-16 11:10:33 +00:00
prepInput := PrepInput {
Repo : input . Repo ,
Org : input . Org ,
Issue : input . Issue ,
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
PR : input . PR ,
Branch : input . Branch ,
Tag : input . Tag ,
2026-03-16 11:10:33 +00:00
Task : input . Task ,
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
Agent : input . Agent ,
2026-03-16 11:10:33 +00:00
Template : input . Template ,
PlanTemplate : input . PlanTemplate ,
Variables : input . Variables ,
Persona : input . Persona ,
}
_ , prepOut , err := s . prepWorkspace ( ctx , req , prepInput )
if err != nil {
2026-03-22 03:41:07 +00:00
return nil , DispatchOutput { } , core . E ( "dispatch" , "prep workspace failed" , err )
2026-03-16 11:10:33 +00:00
}
wsDir := prepOut . WorkspaceDir
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
prompt := prepOut . Prompt
2026-03-16 11:10:33 +00:00
if input . DryRun {
return nil , DispatchOutput {
Success : true ,
Agent : input . Agent ,
Repo : input . Repo ,
WorkspaceDir : wsDir ,
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
Prompt : prompt ,
2026-03-16 11:10:33 +00:00
} , nil
}
// Step 2: Check per-agent concurrency limit
if ! s . canDispatchAgent ( input . Agent ) {
writeStatus ( wsDir , & WorkspaceStatus {
Status : "queued" ,
Agent : input . Agent ,
Repo : input . Repo ,
Org : input . Org ,
Task : input . Task ,
2026-03-17 04:19:48 +00:00
Branch : prepOut . Branch ,
2026-03-16 11:10:33 +00:00
StartedAt : time . Now ( ) ,
Runs : 0 ,
} )
return nil , DispatchOutput {
Success : true ,
Agent : input . Agent ,
Repo : input . Repo ,
WorkspaceDir : wsDir ,
OutputFile : "queued — waiting for a slot" ,
} , nil
}
refactor(agentic): workspace = clone, prompt replaces files
Major simplification of the dispatch model:
- Workspace dir: .core/workspace/{org}/{repo}/{pr|task|branch|tag}/
- Clone into repo/ (not src/), metadata in .meta/
- One of issue, pr, branch, or tag required for dispatch
- All context (brain, consumers, git log, wiki, plan) assembled
into prompt string — no TODO.md, PROMPT.md, CONTEXT.md files
- Resume detection: skip clone if repo/.git exists
- Default agent changed to codex
- spawnAgent drops srcDir param, runs from repo/
- No --skip-git-repo-check (repo/ IS a git repo)
- All downstream files: srcDir → repoDir
Track PRs, not workspace iterations.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 13:41:59 +00:00
// Step 3: Spawn agent in repo/ directory
pid , outputFile , err := s . spawnAgent ( input . Agent , prompt , wsDir )
2026-03-16 11:10:33 +00:00
if err != nil {
return nil , DispatchOutput { } , err
}
writeStatus ( wsDir , & WorkspaceStatus {
Status : "running" ,
Agent : input . Agent ,
Repo : input . Repo ,
Org : input . Org ,
Task : input . Task ,
2026-03-17 04:19:48 +00:00
Branch : prepOut . Branch ,
2026-03-16 11:10:33 +00:00
PID : pid ,
StartedAt : time . Now ( ) ,
Runs : 1 ,
} )
return nil , DispatchOutput {
Success : true ,
Agent : input . Agent ,
Repo : input . Repo ,
WorkspaceDir : wsDir ,
PID : pid ,
OutputFile : outputFile ,
} , nil
}