2026-03-16 11:10:33 +00:00
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"time"
2026-03-24 14:46:59 +00:00
"dappco.re/go/agent/pkg/messages"
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"
)
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
type workspaceTracker interface {
2026-03-30 21:11:06 +00:00
TrackWorkspace ( name string , status any )
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
}
2026-03-31 04:33:36 +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 {
2026-03-31 05:28:26 +00:00
Repo string ` json:"repo" `
Org string ` json:"org,omitempty" `
Task string ` json:"task" `
Agent string ` json:"agent,omitempty" `
Template string ` json:"template,omitempty" `
PlanTemplate string ` json:"plan_template,omitempty" `
Variables map [ string ] string ` json:"variables,omitempty" `
Persona string ` json:"persona,omitempty" `
Issue int ` json:"issue,omitempty" `
PR int ` json:"pr,omitempty" `
Branch string ` json:"branch,omitempty" `
Tag string ` json:"tag,omitempty" `
DryRun bool ` json:"dry_run,omitempty" `
2026-03-16 11:10:33 +00:00
}
2026-03-31 04:33:36 +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 )
}
2026-03-30 22:46:21 +00:00
// command, args, err := agentCommand("codex:review", "Review the last 2 commits via git diff HEAD~2")
2026-03-16 11:10:33 +00:00
func agentCommand ( agent , prompt string ) ( string , [ ] string , error ) {
2026-03-30 21:11:06 +00:00
commandResult := agentCommandResult ( agent , prompt )
if ! commandResult . OK {
err , _ := commandResult . Value . ( error )
2026-03-30 07:30:42 +00:00
if err == nil {
err = core . E ( "agentCommand" , "failed to resolve command" , nil )
}
return "" , nil , err
}
2026-03-30 21:11:06 +00:00
result , ok := commandResult . Value . ( agentCommandResultValue )
2026-03-30 07:30:42 +00:00
if ! ok {
return "" , nil , core . E ( "agentCommand" , "invalid command result" , nil )
}
return result . command , result . args , nil
}
type agentCommandResultValue struct {
command string
args [ ] string
}
func agentCommandResult ( agent , prompt string ) core . Result {
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 != "" {
2026-03-26 06:38:02 +00:00
args = append ( args , "-m" , core . Concat ( "gemini-2.5-" , model ) )
2026-03-16 11:10:33 +00:00
}
2026-03-30 07:30:42 +00:00
return core . Result { Value : agentCommandResultValue { command : "gemini" , args : args } , OK : true }
2026-03-16 11:10:33 +00:00
case "codex" :
2026-03-17 17:45:04 +00:00
if model == "review" {
2026-03-30 07:30:42 +00:00
return core . Result { Value : agentCommandResultValue { command : "codex" , args : [ ] string {
2026-03-23 12:53:33 +00:00
"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." ,
2026-03-30 14:01:43 +00:00
} } , OK : true }
2026-03-17 17:45:04 +00:00
}
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 )
2026-03-30 07:30:42 +00:00
return core . Result { Value : agentCommandResultValue { command : "codex" , args : args } , OK : true }
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" ,
2026-03-26 06:38:02 +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 )
}
2026-03-30 07:30:42 +00:00
return core . Result { Value : agentCommandResultValue { command : "claude" , args : args } , OK : true }
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" )
}
2026-03-30 07:30:42 +00:00
return core . Result { Value : agentCommandResultValue { command : "coderabbit" , args : args } , OK : true }
2026-03-16 11:10:33 +00:00
case "local" :
2026-03-23 12:53:33 +00:00
localModel := model
if localModel == "" {
localModel = "devstral-24b"
}
2026-04-02 07:32:23 +00:00
script := localAgentCommandScript ( localModel , prompt )
2026-03-30 07:30:42 +00:00
return core . Result { Value : agentCommandResultValue { command : "sh" , args : [ ] string { "-c" , script } } , OK : true }
2026-03-16 11:10:33 +00:00
default :
2026-03-30 07:30:42 +00:00
return core . Result { Value : core . E ( "agentCommand" , core . Concat ( "unknown agent: " , agent ) , nil ) , OK : false }
2026-03-16 11:10:33 +00:00
}
}
2026-04-02 07:32:23 +00:00
// localAgentCommandScript("devstral-24b", "Review the last 2 commits")
func localAgentCommandScript ( model , prompt string ) string {
builder := core . NewBuilder ( )
builder . WriteString ( "socat TCP-LISTEN:11434,fork,reuseaddr TCP:host.docker.internal:11434 & sleep 0.5" )
builder . WriteString ( " && codex exec --dangerously-bypass-approvals-and-sandbox --oss --local-provider ollama -m " )
builder . WriteString ( shellQuote ( model ) )
builder . WriteString ( " -o ../.meta/agent-codex.log " )
builder . WriteString ( shellQuote ( prompt ) )
return builder . String ( )
}
func shellQuote ( value string ) string {
return core . Concat ( "'" , core . Replace ( value , "'" , "'\\''" ) , "'" )
}
2026-03-23 12:53:33 +00:00
const defaultDockerImage = "core-dev"
2026-03-31 14:54:32 +00:00
// command, args := containerCommand("codex", []string{"exec", "--model", "gpt-5.4"}, "/srv/.core/workspace/core/go-io/task-5", "/srv/.core/workspace/core/go-io/task-5/.meta")
func containerCommand ( command string , args [ ] string , workspaceDir , metaDir string ) ( string , [ ] string ) {
2026-03-23 12:53:33 +00:00
image := core . Env ( "AGENT_DOCKER_IMAGE" )
if image == "" {
image = defaultDockerImage
}
2026-03-30 18:52:15 +00:00
home := HomeDir ( )
2026-03-23 12:53:33 +00:00
dockerArgs := [ ] string {
"run" , "--rm" ,
"--add-host=host.docker.internal:host-gateway" ,
2026-03-31 14:54:32 +00:00
"-v" , core . Concat ( workspaceDir , ":/workspace" ) ,
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
"-v" , core . Concat ( metaDir , ":/workspace/.meta" ) ,
2026-03-31 14:54:32 +00:00
"-w" , "/workspace/repo" ,
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
"-v" , core . Concat ( core . JoinPath ( home , ".codex" ) , ":/home/dev/.codex:ro" ) ,
2026-03-23 12:53:33 +00:00
"-e" , "OPENAI_API_KEY" ,
"-e" , "ANTHROPIC_API_KEY" ,
"-e" , "GEMINI_API_KEY" ,
"-e" , "GOOGLE_API_KEY" ,
"-e" , "TERM=dumb" ,
"-e" , "NO_COLOR=1" ,
"-e" , "CI=true" ,
"-e" , "GIT_USER_NAME=Virgil" ,
"-e" , "GIT_USER_EMAIL=virgil@lethean.io" ,
2026-03-26 07:37:43 +00:00
"-e" , "GONOSUMCHECK=dappco.re/*,forge.lthn.ai/*" ,
"-e" , "GOFLAGS=-mod=mod" ,
2026-03-23 12:53:33 +00:00
}
if command == "claude" {
dockerArgs = append ( dockerArgs ,
2026-03-26 06:38:02 +00:00
"-v" , core . Concat ( core . JoinPath ( home , ".claude" ) , ":/home/dev/.claude:ro" ) ,
2026-03-23 12:53:33 +00:00
)
}
if command == "gemini" {
dockerArgs = append ( dockerArgs ,
2026-03-26 06:38:02 +00:00
"-v" , core . Concat ( core . JoinPath ( home , ".gemini" ) , ":/home/dev/.gemini:ro" ) ,
2026-03-23 12:53:33 +00:00
)
}
2026-03-26 07:37:43 +00:00
quoted := core . NewBuilder ( )
2026-03-31 14:54:32 +00:00
quoted . WriteString ( "if [ ! -d /workspace/repo ]; then echo 'missing /workspace/repo' >&2; exit 1; fi" )
if command != "" {
quoted . WriteString ( "; " )
quoted . WriteString ( command )
for _ , a := range args {
quoted . WriteString ( " '" )
quoted . WriteString ( core . Replace ( a , "'" , "'\\''" ) )
quoted . WriteString ( "'" )
}
2026-03-26 07:37:43 +00:00
}
quoted . WriteString ( "; chmod -R a+w /workspace /workspace/.meta 2>/dev/null; true" )
dockerArgs = append ( dockerArgs , image , "sh" , "-c" , quoted . String ( ) )
2026-03-23 12:53:33 +00:00
return "docker" , dockerArgs
}
2026-03-30 22:54:19 +00:00
// outputFile := agentOutputFile(workspaceDir, "codex")
2026-03-30 21:22:54 +00:00
func agentOutputFile ( workspaceDir , agent string ) string {
2026-03-25 01:11:04 +00:00
agentBase := core . SplitN ( agent , ":" , 2 ) [ 0 ]
2026-03-30 21:22:54 +00:00
return core . JoinPath ( WorkspaceMetaDir ( workspaceDir ) , core . Sprintf ( "agent-%s.log" , agentBase ) )
2026-03-25 01:11:04 +00:00
}
2026-03-30 22:54:19 +00:00
// status, question := detectFinalStatus(repoDir, 0, "completed")
2026-03-30 21:37:15 +00:00
func detectFinalStatus ( repoDir string , exitCode int , processStatus string ) ( string , string ) {
2026-03-25 01:11:04 +00:00
blockedPath := core . JoinPath ( repoDir , "BLOCKED.md" )
2026-03-30 21:11:06 +00:00
if blockedResult := fs . Read ( blockedPath ) ; blockedResult . OK && core . Trim ( blockedResult . Value . ( string ) ) != "" {
return "blocked" , core . Trim ( blockedResult . Value . ( string ) )
2026-03-25 01:11:04 +00:00
}
2026-03-30 21:37:15 +00:00
if exitCode != 0 || processStatus == "failed" || processStatus == "killed" {
2026-03-25 01:11:04 +00:00
question := ""
if exitCode != 0 {
question = core . Sprintf ( "Agent exited with code %d" , exitCode )
}
return "failed" , question
}
return "completed" , ""
}
2026-03-31 04:33:36 +00:00
// backoff := s.trackFailureRate("codex", "failed", time.Now().Add(-30*time.Second))
2026-03-25 01:11:04 +00:00
func ( s * PrepSubsystem ) trackFailureRate ( agent , status string , startedAt time . Time ) bool {
pool := baseAgent ( agent )
if status == "failed" {
elapsed := time . Since ( startedAt )
if elapsed < 60 * time . Second {
s . failCount [ pool ] ++
if s . failCount [ pool ] >= 3 {
2026-04-01 20:12:28 +00:00
backoffDuration := 30 * time . Minute
until := time . Now ( ) . Add ( backoffDuration )
s . backoff [ pool ] = until
2026-04-01 17:54:26 +00:00
s . persistRuntimeState ( )
2026-04-01 20:12:28 +00:00
if s . ServiceRuntime != nil {
s . Core ( ) . ACTION ( messages . RateLimitDetected {
Pool : pool ,
Duration : backoffDuration . String ( ) ,
} )
}
2026-03-25 01:11:04 +00:00
core . Print ( nil , "rate-limit detected for %s — pausing pool for 30 minutes" , pool )
return true
}
} else {
2026-03-31 05:28:26 +00:00
s . failCount [ pool ] = 0
2026-03-25 01:11:04 +00:00
}
} else {
2026-03-31 05:28:26 +00:00
s . failCount [ pool ] = 0
2026-03-25 01:11:04 +00:00
}
2026-04-01 17:54:26 +00:00
s . persistRuntimeState ( )
2026-03-25 01:11:04 +00:00
return false
}
2026-03-30 21:22:54 +00:00
func ( s * PrepSubsystem ) startIssueTracking ( workspaceDir string ) {
2026-03-25 01:11:04 +00:00
if s . forge == nil {
return
}
2026-03-30 21:22:54 +00:00
result := ReadStatusResult ( workspaceDir )
2026-03-30 21:11:06 +00:00
workspaceStatus , ok := workspaceStatusValue ( result )
if ! ok || workspaceStatus . Issue == 0 {
2026-03-25 01:11:04 +00:00
return
}
2026-03-30 21:11:06 +00:00
org := workspaceStatus . Org
2026-03-25 01:11:04 +00:00
if org == "" {
org = "core"
}
2026-03-30 21:11:06 +00:00
s . forge . Issues . StartStopwatch ( context . Background ( ) , org , workspaceStatus . Repo , int64 ( workspaceStatus . Issue ) )
2026-03-25 01:11:04 +00:00
}
2026-03-30 21:22:54 +00:00
func ( s * PrepSubsystem ) stopIssueTracking ( workspaceDir string ) {
2026-03-25 01:11:04 +00:00
if s . forge == nil {
return
}
2026-03-30 21:22:54 +00:00
result := ReadStatusResult ( workspaceDir )
2026-03-30 21:11:06 +00:00
workspaceStatus , ok := workspaceStatusValue ( result )
if ! ok || workspaceStatus . Issue == 0 {
2026-03-25 01:11:04 +00:00
return
}
2026-03-30 21:11:06 +00:00
org := workspaceStatus . Org
2026-03-25 01:11:04 +00:00
if org == "" {
org = "core"
}
2026-03-30 21:11:06 +00:00
s . forge . Issues . StopStopwatch ( context . Background ( ) , org , workspaceStatus . Repo , int64 ( workspaceStatus . Issue ) )
2026-03-25 01:11:04 +00:00
}
2026-03-30 21:22:54 +00:00
func ( s * PrepSubsystem ) broadcastStart ( agent , workspaceDir string ) {
2026-03-30 21:30:49 +00:00
workspaceName := WorkspaceName ( workspaceDir )
2026-03-30 21:22:54 +00:00
result := ReadStatusResult ( workspaceDir )
2026-03-30 21:11:06 +00:00
workspaceStatus , ok := workspaceStatusValue ( result )
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
repo := ""
2026-03-30 19:40:02 +00:00
if ok {
2026-03-30 21:11:06 +00:00
repo = workspaceStatus . Repo
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
}
if s . ServiceRuntime != nil {
s . Core ( ) . ACTION ( messages . AgentStarted {
2026-03-30 21:30:49 +00:00
Agent : agent , Repo : repo , Workspace : workspaceName ,
2026-03-25 01:11:04 +00:00
} )
}
2026-03-30 21:30:49 +00:00
emitStartEvent ( agent , workspaceName )
2026-03-25 01:11:04 +00:00
}
2026-03-30 21:22:54 +00:00
func ( s * PrepSubsystem ) broadcastComplete ( agent , workspaceDir , finalStatus string ) {
2026-03-30 21:30:49 +00:00
workspaceName := WorkspaceName ( workspaceDir )
emitCompletionEvent ( agent , workspaceName , finalStatus )
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
if s . ServiceRuntime != nil {
2026-03-30 21:22:54 +00:00
result := ReadStatusResult ( workspaceDir )
2026-03-30 21:11:06 +00:00
workspaceStatus , ok := workspaceStatusValue ( result )
2026-03-25 01:11:04 +00:00
repo := ""
2026-03-30 19:40:02 +00:00
if ok {
2026-03-30 21:11:06 +00:00
repo = workspaceStatus . Repo
2026-03-25 01:11:04 +00:00
}
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
s . Core ( ) . ACTION ( messages . AgentCompleted {
2026-03-25 01:11:04 +00:00
Agent : agent , Repo : repo ,
2026-03-30 21:30:49 +00:00
Workspace : workspaceName , Status : finalStatus ,
2026-03-25 01:11:04 +00:00
} )
}
}
2026-03-30 21:37:15 +00:00
func ( s * PrepSubsystem ) onAgentComplete ( agent , workspaceDir , outputFile string , exitCode int , processStatus , output string ) {
2026-03-25 01:11:04 +00:00
if output != "" {
fs . Write ( outputFile , output )
}
2026-03-30 21:22:54 +00:00
repoDir := WorkspaceRepoDir ( workspaceDir )
2026-03-30 21:37:15 +00:00
finalStatus , question := detectFinalStatus ( repoDir , exitCode , processStatus )
2026-03-25 01:11:04 +00:00
2026-03-30 21:22:54 +00:00
result := ReadStatusResult ( workspaceDir )
2026-03-30 21:11:06 +00:00
workspaceStatus , ok := workspaceStatusValue ( result )
2026-03-30 19:40:02 +00:00
if ok {
2026-03-30 21:11:06 +00:00
workspaceStatus . Status = finalStatus
workspaceStatus . PID = 0
workspaceStatus . Question = question
2026-03-30 21:22:54 +00:00
writeStatusResult ( workspaceDir , workspaceStatus )
s . TrackWorkspace ( WorkspaceName ( workspaceDir ) , workspaceStatus )
2026-03-25 01:11:04 +00:00
2026-03-30 21:11:06 +00:00
s . trackFailureRate ( agent , finalStatus , workspaceStatus . StartedAt )
2026-03-25 01:11:04 +00:00
}
2026-03-30 21:22:54 +00:00
s . stopIssueTracking ( workspaceDir )
2026-03-25 01:11:04 +00:00
2026-03-30 21:22:54 +00:00
s . broadcastComplete ( agent , workspaceDir , finalStatus )
2026-03-25 01:11:04 +00:00
}
2026-03-30 22:54:19 +00:00
// pid, processID, outputFile, err := s.spawnAgent(agent, prompt, workspaceDir)
2026-03-30 21:22:54 +00:00
func ( s * PrepSubsystem ) spawnAgent ( agent , prompt , workspaceDir string ) ( int , string , string , error ) {
2026-03-16 17:52:55 +00:00
command , args , err := agentCommand ( agent , prompt )
if err != nil {
2026-03-30 16:01:32 +00:00
return 0 , "" , "" , err
2026-03-16 17:52:55 +00:00
}
2026-03-30 21:22:54 +00:00
metaDir := WorkspaceMetaDir ( workspaceDir )
outputFile := agentOutputFile ( workspaceDir , agent )
2026-03-16 17:52:55 +00:00
2026-03-30 21:22:54 +00:00
fs . Delete ( WorkspaceBlockedPath ( workspaceDir ) )
2026-03-21 16:53:55 +00:00
2026-03-31 14:54:32 +00:00
command , args = containerCommand ( command , args , workspaceDir , metaDir )
2026-03-23 12:53:33 +00:00
2026-03-31 05:43:14 +00:00
processResult := s . Core ( ) . Service ( "process" )
if ! processResult . OK {
2026-03-30 16:01:32 +00:00
return 0 , "" , "" , core . E ( "dispatch.spawnAgent" , "process service not registered" , nil )
2026-03-26 06:47:44 +00:00
}
2026-03-31 05:43:14 +00:00
procSvc , ok := processResult . Value . ( * process . Service )
if ! ok {
return 0 , "" , "" , core . E ( "dispatch.spawnAgent" , "process service has unexpected type" , nil )
}
2026-03-29 20:15:58 +00:00
proc , err := procSvc . 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 ,
2026-03-31 14:54:32 +00:00
Dir : workspaceDir ,
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
} )
2026-03-29 20:15:58 +00:00
if err != nil {
2026-03-30 16:01:32 +00:00
return 0 , "" , "" , core . E ( "dispatch.spawnAgent" , core . Concat ( "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-30 16:01:32 +00:00
processID := proc . ID
2026-03-16 17:52:55 +00:00
2026-03-30 21:22:54 +00:00
s . broadcastStart ( agent , workspaceDir )
s . startIssueTracking ( workspaceDir )
2026-03-23 12:53:33 +00:00
2026-03-30 21:22:54 +00:00
monitorAction := core . Concat ( "agentic.monitor." , core . Replace ( WorkspaceName ( workspaceDir ) , "/" , "." ) )
2026-03-30 17:02:28 +00:00
monitor := & agentCompletionMonitor {
service : s ,
agent : agent ,
2026-03-30 21:22:54 +00:00
workspaceDir : workspaceDir ,
2026-03-30 17:02:28 +00:00
outputFile : outputFile ,
process : proc ,
}
s . Core ( ) . Action ( monitorAction , monitor . run )
2026-03-26 07:16:48 +00:00
s . Core ( ) . PerformAsync ( monitorAction , core . NewOptions ( ) )
2026-03-16 17:52:55 +00:00
2026-03-30 16:01:32 +00:00
return pid , processID , outputFile , nil
2026-03-16 17:52:55 +00:00
}
2026-03-30 17:02:28 +00:00
type completionProcess interface {
Done ( ) <- chan struct { }
Info ( ) process . Info
Output ( ) string
}
type agentCompletionMonitor struct {
service * PrepSubsystem
agent string
workspaceDir string
outputFile string
process completionProcess
}
func ( m * agentCompletionMonitor ) run ( _ context . Context , _ core . Options ) core . Result {
if m == nil || m . service == nil {
return core . Result { Value : core . E ( "agentic.monitor" , "service is required" , nil ) , OK : false }
}
if m . process == nil {
return core . Result { Value : core . E ( "agentic.monitor" , "process is required" , nil ) , OK : false }
}
<- m . process . Done ( )
info := m . process . Info ( )
m . service . onAgentComplete ( m . agent , m . workspaceDir , m . outputFile , info . ExitCode , string ( info . Status ) , m . process . Output ( ) )
return core . Result { OK : true }
}
2026-03-30 21:22:54 +00:00
func ( s * PrepSubsystem ) runQA ( workspaceDir string ) bool {
2026-03-25 09:51:57 +00:00
ctx := context . Background ( )
2026-03-30 21:22:54 +00:00
repoDir := WorkspaceRepoDir ( workspaceDir )
2026-03-30 15:48:21 +00:00
process := s . Core ( ) . Process ( )
2026-03-22 14:49:56 +00:00
if fs . IsFile ( core . JoinPath ( repoDir , "go.mod" ) ) {
for _ , args := range [ ] [ ] string {
{ "go" , "build" , "./..." } ,
{ "go" , "vet" , "./..." } ,
{ "go" , "test" , "./..." , "-count=1" , "-timeout" , "120s" } ,
} {
2026-03-30 15:48:21 +00:00
if ! process . RunIn ( ctx , repoDir , args [ 0 ] , args [ 1 : ] ... ) . OK {
2026-03-25 09:51:57 +00:00
core . Warn ( "QA failed" , "cmd" , core . Join ( " " , args ... ) )
2026-03-22 14:49:56 +00:00
return false
}
}
return true
}
if fs . IsFile ( core . JoinPath ( repoDir , "composer.json" ) ) {
2026-03-30 15:48:21 +00:00
if ! process . RunIn ( ctx , repoDir , "composer" , "install" , "--no-interaction" ) . OK {
2026-03-22 14:49:56 +00:00
return false
}
2026-03-30 15:48:21 +00:00
return process . RunIn ( ctx , repoDir , "composer" , "test" ) . OK
2026-03-22 14:49:56 +00:00
}
if fs . IsFile ( core . JoinPath ( repoDir , "package.json" ) ) {
2026-03-30 15:48:21 +00:00
if ! process . RunIn ( ctx , repoDir , "npm" , "install" ) . OK {
2026-03-22 14:49:56 +00:00
return false
}
2026-03-30 15:48:21 +00:00
return process . RunIn ( ctx , repoDir , "npm" , "test" ) . OK
2026-03-22 14:49:56 +00:00
}
return true
}
2026-03-30 22:40:28 +00:00
func ( s * PrepSubsystem ) dispatch ( ctx context . Context , callRequest * mcp . CallToolRequest , input DispatchInput ) ( * mcp . CallToolResult , DispatchOutput , error ) {
2026-03-16 11:10:33 +00:00
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"
}
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 ,
}
2026-03-30 22:40:28 +00:00
_ , prepOut , err := s . prepWorkspace ( ctx , callRequest , prepInput )
2026-03-16 11:10:33 +00:00
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
}
2026-03-30 21:22:54 +00:00
workspaceDir := 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 ,
2026-03-30 21:22:54 +00:00
WorkspaceDir : 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 : prompt ,
2026-03-16 11:10:33 +00:00
} , nil
}
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
if s . ServiceRuntime != nil {
2026-03-30 21:11:06 +00:00
dispatchResult := s . Core ( ) . Action ( "runner.dispatch" ) . Run ( ctx , core . NewOptions (
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
core . Option { Key : "agent" , Value : input . Agent } ,
2026-03-26 11:23:04 +00:00
core . Option { Key : "repo" , Value : input . Repo } ,
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
) )
2026-03-30 21:11:06 +00:00
if ! dispatchResult . OK {
workspaceStatus := & WorkspaceStatus {
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
Status : "queued" ,
Agent : input . Agent ,
Repo : input . Repo ,
Org : input . Org ,
Task : input . Task ,
Branch : prepOut . Branch ,
StartedAt : time . Now ( ) ,
Runs : 0 ,
}
2026-03-30 21:22:54 +00:00
writeStatusResult ( workspaceDir , workspaceStatus )
2026-03-31 05:43:14 +00:00
if runnerResult := s . Core ( ) . Service ( "runner" ) ; runnerResult . OK {
if runnerSvc , ok := runnerResult . Value . ( workspaceTracker ) ; ok {
runnerSvc . TrackWorkspace ( WorkspaceName ( workspaceDir ) , workspaceStatus )
}
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
}
return nil , DispatchOutput {
Success : true ,
Agent : input . Agent ,
Repo : input . Repo ,
2026-03-30 21:22:54 +00:00
WorkspaceDir : workspaceDir ,
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
OutputFile : "queued — at concurrency limit or frozen" ,
} , nil
}
2026-03-16 11:10:33 +00:00
}
2026-03-30 21:22:54 +00:00
pid , processID , outputFile , err := s . spawnAgent ( input . Agent , prompt , workspaceDir )
2026-03-16 11:10:33 +00:00
if err != nil {
return nil , DispatchOutput { } , err
}
2026-03-30 21:11:06 +00:00
workspaceStatus := & WorkspaceStatus {
2026-03-16 11:10:33 +00:00
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 ,
2026-03-30 16:01:32 +00:00
ProcessID : processID ,
2026-03-16 11:10:33 +00:00
StartedAt : time . Now ( ) ,
Runs : 1 ,
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
}
2026-03-30 21:22:54 +00:00
writeStatusResult ( workspaceDir , workspaceStatus )
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
if s . ServiceRuntime != nil {
2026-03-31 05:43:14 +00:00
if runnerResult := s . Core ( ) . Service ( "runner" ) ; runnerResult . OK {
if runnerSvc , ok := runnerResult . Value . ( workspaceTracker ) ; ok {
runnerSvc . TrackWorkspace ( WorkspaceName ( workspaceDir ) , workspaceStatus )
}
feat(runner): extract dispatch runner into independent Core service
Moves concurrency, queue drain, workspace lifecycle, and frozen state
from agentic/prep into pkg/runner/ — a standalone Core service that
communicates via IPC Actions only.
- runner.Register wires Actions: dispatch, status, start, stop, kill, poke
- runner.HandleIPCEvents catches AgentCompleted → ChannelPush + queue poke
- Agentic dispatch asks runner for permission via c.Action("runner.dispatch")
- Dispatch mutex moved to struct-level sync.Mutex (fixes core.Lock init race)
- Registry-based concurrency counting replaces disk scanning
- TrackWorkspace called on both queued and running status writes
- SpawnQueued message added for runner→agentic spawn requests
- ChannelPush message in core/mcp enables any service to push channel events
- 51 new tests covering runner service, queue, and config parsing
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 11:00:47 +00:00
}
}
2026-03-16 11:10:33 +00:00
return nil , DispatchOutput {
Success : true ,
Agent : input . Agent ,
Repo : input . Repo ,
2026-03-30 21:22:54 +00:00
WorkspaceDir : workspaceDir ,
2026-03-16 11:10:33 +00:00
PID : pid ,
OutputFile : outputFile ,
} , nil
}