fix(ax): continue workspace naming cleanup

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 21:22:54 +00:00
parent e82112024c
commit 1cc8fb50e1
25 changed files with 277 additions and 277 deletions

View file

@ -116,12 +116,12 @@ func (commands appCommandSet) check(_ core.Options) core.Result {
core.Print(nil, " agents: %s (MISSING)", agentsPath)
}
wsRoot := agentic.WorkspaceRoot()
if fs.IsDir(wsRoot) {
workspaceRoot := agentic.WorkspaceRoot()
if fs.IsDir(workspaceRoot) {
statusFiles := agentic.WorkspaceStatusPaths()
core.Print(nil, " workspace: %s (%d workspaces)", wsRoot, len(statusFiles))
core.Print(nil, " workspace: %s (%d workspaces)", workspaceRoot, len(statusFiles))
} else {
core.Print(nil, " workspace: %s (MISSING)", wsRoot)
core.Print(nil, " workspace: %s (MISSING)", workspaceRoot)
}
core.Print(nil, " services: %d registered", len(commands.core.Services()))

View file

@ -183,31 +183,31 @@ func (s *PrepSubsystem) handleQA(ctx context.Context, options core.Options) core
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-qa") {
return core.Result{Value: true, OK: true}
}
wsDir := options.String("workspace")
if wsDir == "" {
workspaceDir := options.String("workspace")
if workspaceDir == "" {
return core.Result{Value: core.E("agentic.qa", "workspace is required", nil), OK: false}
}
passed := s.runQA(wsDir)
passed := s.runQA(workspaceDir)
if !passed {
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
workspaceStatus, ok := workspaceStatusValue(result)
if ok {
workspaceStatus.Status = "failed"
workspaceStatus.Question = "QA check failed — build or tests did not pass"
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
}
}
}
// Emit QA result for observability (monitor picks this up)
if s.ServiceRuntime != nil {
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
repo := ""
if ok {
repo = workspaceStatus.Repo
}
s.Core().ACTION(messages.QAResult{
Workspace: WorkspaceName(wsDir),
Workspace: WorkspaceName(workspaceDir),
Repo: repo,
Passed: passed,
})
@ -224,15 +224,15 @@ func (s *PrepSubsystem) handleAutoPR(ctx context.Context, options core.Options)
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-pr") {
return core.Result{OK: true}
}
wsDir := options.String("workspace")
if wsDir == "" {
workspaceDir := options.String("workspace")
if workspaceDir == "" {
return core.Result{Value: core.E("agentic.auto-pr", "workspace is required", nil), OK: false}
}
s.autoCreatePR(wsDir)
s.autoCreatePR(workspaceDir)
// Emit PRCreated for observability
if s.ServiceRuntime != nil {
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if ok && workspaceStatus.PRURL != "" {
s.Core().ACTION(messages.PRCreated{
@ -255,15 +255,15 @@ func (s *PrepSubsystem) handleVerify(ctx context.Context, options core.Options)
if s.ServiceRuntime != nil && !s.Config().Enabled("auto-merge") {
return core.Result{OK: true}
}
wsDir := options.String("workspace")
if wsDir == "" {
workspaceDir := options.String("workspace")
if workspaceDir == "" {
return core.Result{Value: core.E("agentic.verify", "workspace is required", nil), OK: false}
}
s.autoVerifyAndMerge(wsDir)
s.autoVerifyAndMerge(workspaceDir)
// Emit merge/review events for observability
if s.ServiceRuntime != nil {
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if ok {
if workspaceStatus.Status == "merged" {
@ -291,11 +291,11 @@ func (s *PrepSubsystem) handleVerify(ctx context.Context, options core.Options)
// core.Option{Key: "workspace", Value: "/path/to/workspace"},
// ))
func (s *PrepSubsystem) handleIngest(ctx context.Context, options core.Options) core.Result {
wsDir := options.String("workspace")
if wsDir == "" {
workspaceDir := options.String("workspace")
if workspaceDir == "" {
return core.Result{Value: core.E("agentic.ingest", "workspace is required", nil), OK: false}
}
s.ingestFindings(wsDir)
s.ingestFindings(workspaceDir)
return core.Result{OK: true}
}

View file

@ -11,15 +11,15 @@ import (
// autoCreatePR pushes the agent's branch and creates a PR on Forge
// if the agent made any commits beyond the initial clone.
func (s *PrepSubsystem) autoCreatePR(wsDir string) {
result := ReadStatusResult(wsDir)
func (s *PrepSubsystem) autoCreatePR(workspaceDir string) {
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok || workspaceStatus.Branch == "" || workspaceStatus.Repo == "" {
return
}
ctx := context.Background()
repoDir := WorkspaceRepoDir(wsDir)
repoDir := WorkspaceRepoDir(workspaceDir)
process := s.Core().Process()
// PRs target dev — agents never merge directly to main
@ -44,13 +44,13 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) {
// Push the branch to forge
forgeRemote := core.Sprintf("ssh://git@forge.lthn.ai:2223/%s/%s.git", org, workspaceStatus.Repo)
if !process.RunIn(ctx, repoDir, "git", "push", forgeRemote, workspaceStatus.Branch).OK {
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
workspaceStatusUpdate, ok := workspaceStatusValue(result)
if !ok {
return
}
workspaceStatusUpdate.Question = "PR push failed"
writeStatusResult(wsDir, workspaceStatusUpdate)
writeStatusResult(workspaceDir, workspaceStatusUpdate)
}
return
}
@ -64,25 +64,25 @@ func (s *PrepSubsystem) autoCreatePR(wsDir string) {
prURL, _, err := s.forgeCreatePR(ctx, org, workspaceStatus.Repo, workspaceStatus.Branch, base, title, body)
if err != nil {
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
workspaceStatusUpdate, ok := workspaceStatusValue(result)
if !ok {
return
}
workspaceStatusUpdate.Question = core.Sprintf("PR creation failed: %v", err)
writeStatusResult(wsDir, workspaceStatusUpdate)
writeStatusResult(workspaceDir, workspaceStatusUpdate)
}
return
}
// Update status with PR URL
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
workspaceStatusUpdate, ok := workspaceStatusValue(result)
if !ok {
return
}
workspaceStatusUpdate.PRURL = prURL
writeStatusResult(wsDir, workspaceStatusUpdate)
writeStatusResult(workspaceDir, workspaceStatusUpdate)
}
}

View file

@ -152,11 +152,11 @@ func (s *PrepSubsystem) cmdPrep(options core.Options) core.Result {
}
func (s *PrepSubsystem) cmdStatus(_ core.Options) core.Result {
wsRoot := WorkspaceRoot()
workspaceRoot := WorkspaceRoot()
fsys := s.Core().Fs()
listResult := fsys.List(wsRoot)
listResult := fsys.List(workspaceRoot)
if !listResult.OK {
core.Print(nil, "no workspaces found at %s", wsRoot)
core.Print(nil, "no workspaces found at %s", workspaceRoot)
return core.Result{OK: true}
}

View file

@ -22,9 +22,9 @@ func (s *PrepSubsystem) cmdWorkspaceList(_ core.Options) core.Result {
statusFiles := WorkspaceStatusPaths()
count := 0
for _, sf := range statusFiles {
wsDir := core.PathDir(sf)
wsName := WorkspaceName(wsDir)
result := ReadStatusResult(wsDir)
workspaceDir := core.PathDir(sf)
wsName := WorkspaceName(workspaceDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok {
continue
@ -39,7 +39,7 @@ func (s *PrepSubsystem) cmdWorkspaceList(_ core.Options) core.Result {
}
func (s *PrepSubsystem) cmdWorkspaceClean(options core.Options) core.Result {
wsRoot := WorkspaceRoot()
workspaceRoot := WorkspaceRoot()
fsys := s.Core().Fs()
filter := options.String("_arg")
if filter == "" {
@ -50,9 +50,9 @@ func (s *PrepSubsystem) cmdWorkspaceClean(options core.Options) core.Result {
var toRemove []string
for _, sf := range statusFiles {
wsDir := core.PathDir(sf)
wsName := WorkspaceName(wsDir)
result := ReadStatusResult(wsDir)
workspaceDir := core.PathDir(sf)
wsName := WorkspaceName(workspaceDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok {
continue
@ -85,7 +85,7 @@ func (s *PrepSubsystem) cmdWorkspaceClean(options core.Options) core.Result {
}
for _, name := range toRemove {
path := core.JoinPath(wsRoot, name)
path := core.JoinPath(workspaceRoot, name)
fsys.DeleteAll(path)
core.Print(nil, " removed %s", name)
}

View file

@ -17,8 +17,8 @@ import (
// After this, the workspace go.work includes ./repo and all ./dep-* dirs,
// giving the agent everything needed to build and test.
//
// s.cloneWorkspaceDeps(ctx, wsDir, repoDir, "core")
func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir, org string) {
// s.cloneWorkspaceDeps(ctx, workspaceDir, repoDir, "core")
func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, workspaceDir, repoDir, org string) {
goModPath := core.JoinPath(repoDir, "go.mod")
r := fs.Read(goModPath)
if !r.OK {
@ -48,14 +48,14 @@ func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir,
// Clone each dependency
var cloned []string
for _, dep := range deps {
depDir := core.JoinPath(wsDir, dep.dir)
depDir := core.JoinPath(workspaceDir, dep.dir)
if fs.IsDir(core.JoinPath(depDir, ".git")) {
cloned = append(cloned, dep.dir) // already cloned (resume)
continue
}
repoURL := forgeSSHURL(org, dep.repo)
if result := process.RunIn(ctx, wsDir, "git", "clone", "--depth=1", repoURL, dep.dir); result.OK {
if result := process.RunIn(ctx, workspaceDir, "git", "clone", "--depth=1", repoURL, dep.dir); result.OK {
cloned = append(cloned, dep.dir)
}
}
@ -69,7 +69,7 @@ func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir,
b.WriteString(core.Concat("\t./", dir, "\n"))
}
b.WriteString(")\n")
fs.Write(core.JoinPath(wsDir, "go.work"), b.String())
fs.Write(core.JoinPath(workspaceDir, "go.work"), b.String())
}
}

View file

@ -233,9 +233,9 @@ func containerCommand(agentType, command string, args []string, repoDir, metaDir
// --- spawnAgent: decomposed into testable steps ---
// agentOutputFile returns the log file path for an agent's output.
func agentOutputFile(wsDir, agent string) string {
func agentOutputFile(workspaceDir, agent string) string {
agentBase := core.SplitN(agent, ":", 2)[0]
return core.JoinPath(WorkspaceMetaDir(wsDir), core.Sprintf("agent-%s.log", agentBase))
return core.JoinPath(WorkspaceMetaDir(workspaceDir), core.Sprintf("agent-%s.log", agentBase))
}
// detectFinalStatus reads workspace state after agent exit to determine outcome.
@ -278,11 +278,11 @@ func (s *PrepSubsystem) trackFailureRate(agent, status string, startedAt time.Ti
}
// startIssueTracking starts a Forge stopwatch on the workspace's issue.
func (s *PrepSubsystem) startIssueTracking(wsDir string) {
func (s *PrepSubsystem) startIssueTracking(workspaceDir string) {
if s.forge == nil {
return
}
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok || workspaceStatus.Issue == 0 {
return
@ -295,11 +295,11 @@ func (s *PrepSubsystem) startIssueTracking(wsDir string) {
}
// stopIssueTracking stops a Forge stopwatch on the workspace's issue.
func (s *PrepSubsystem) stopIssueTracking(wsDir string) {
func (s *PrepSubsystem) stopIssueTracking(workspaceDir string) {
if s.forge == nil {
return
}
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok || workspaceStatus.Issue == 0 {
return
@ -312,9 +312,9 @@ func (s *PrepSubsystem) stopIssueTracking(wsDir string) {
}
// broadcastStart emits IPC + audit events for agent start.
func (s *PrepSubsystem) broadcastStart(agent, wsDir string) {
wsName := WorkspaceName(wsDir)
result := ReadStatusResult(wsDir)
func (s *PrepSubsystem) broadcastStart(agent, workspaceDir string) {
wsName := WorkspaceName(workspaceDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
repo := ""
if ok {
@ -329,11 +329,11 @@ func (s *PrepSubsystem) broadcastStart(agent, wsDir string) {
}
// broadcastComplete emits IPC + audit events for agent completion.
func (s *PrepSubsystem) broadcastComplete(agent, wsDir, finalStatus string) {
wsName := WorkspaceName(wsDir)
func (s *PrepSubsystem) broadcastComplete(agent, workspaceDir, finalStatus string) {
wsName := WorkspaceName(workspaceDir)
emitCompletionEvent(agent, wsName, finalStatus)
if s.ServiceRuntime != nil {
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
repo := ""
if ok {
@ -348,34 +348,34 @@ func (s *PrepSubsystem) broadcastComplete(agent, wsDir, finalStatus string) {
// onAgentComplete handles all post-completion logic for a spawned agent.
// Called from the monitoring goroutine after the process exits.
func (s *PrepSubsystem) onAgentComplete(agent, wsDir, outputFile string, exitCode int, procStatus, output string) {
func (s *PrepSubsystem) onAgentComplete(agent, workspaceDir, outputFile string, exitCode int, procStatus, output string) {
// Save output
if output != "" {
fs.Write(outputFile, output)
}
repoDir := WorkspaceRepoDir(wsDir)
repoDir := WorkspaceRepoDir(workspaceDir)
finalStatus, question := detectFinalStatus(repoDir, exitCode, procStatus)
// Update workspace status (disk + registry)
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if ok {
workspaceStatus.Status = finalStatus
workspaceStatus.PID = 0
workspaceStatus.Question = question
writeStatusResult(wsDir, workspaceStatus)
s.TrackWorkspace(WorkspaceName(wsDir), workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
s.TrackWorkspace(WorkspaceName(workspaceDir), workspaceStatus)
// Rate-limit tracking
s.trackFailureRate(agent, finalStatus, workspaceStatus.StartedAt)
}
// Forge time tracking
s.stopIssueTracking(wsDir)
s.stopIssueTracking(workspaceDir)
// Broadcast completion
s.broadcastComplete(agent, wsDir, finalStatus)
s.broadcastComplete(agent, workspaceDir, finalStatus)
// Run completion pipeline via PerformAsync for successful agents.
// Gets ActionTaskStarted/Completed broadcasts + WaitGroup integration for graceful shutdown.
@ -383,7 +383,7 @@ func (s *PrepSubsystem) onAgentComplete(agent, wsDir, outputFile string, exitCod
// c.PerformAsync("agentic.complete", options) → runs agent.completion Task in background
if finalStatus == "completed" && s.ServiceRuntime != nil {
s.Core().PerformAsync("agentic.complete", core.NewOptions(
core.Option{Key: "workspace", Value: wsDir},
core.Option{Key: "workspace", Value: workspaceDir},
))
}
}
@ -391,18 +391,18 @@ func (s *PrepSubsystem) onAgentComplete(agent, wsDir, outputFile string, exitCod
// spawnAgent launches an agent inside a Docker container.
// The repo/ directory is mounted at /workspace, agent runs sandboxed.
// Output is captured and written to .meta/agent-{agent}.log on completion.
func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, string, error) {
func (s *PrepSubsystem) spawnAgent(agent, prompt, workspaceDir string) (int, string, string, error) {
command, args, err := agentCommand(agent, prompt)
if err != nil {
return 0, "", "", err
}
repoDir := WorkspaceRepoDir(wsDir)
metaDir := WorkspaceMetaDir(wsDir)
outputFile := agentOutputFile(wsDir, agent)
repoDir := WorkspaceRepoDir(workspaceDir)
metaDir := WorkspaceMetaDir(workspaceDir)
outputFile := agentOutputFile(workspaceDir, agent)
// Clean up stale BLOCKED.md from previous runs
fs.Delete(WorkspaceBlockedPath(wsDir))
fs.Delete(WorkspaceBlockedPath(workspaceDir))
// All agents run containerised
agentBase := core.SplitN(agent, ":", 2)[0]
@ -426,16 +426,16 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, st
pid := proc.Info().PID
processID := proc.ID
s.broadcastStart(agent, wsDir)
s.startIssueTracking(wsDir)
s.broadcastStart(agent, workspaceDir)
s.startIssueTracking(workspaceDir)
// Register a one-shot Action that monitors this agent, then run it via PerformAsync.
// PerformAsync tracks it in Core's WaitGroup — ServiceShutdown waits for it.
monitorAction := core.Concat("agentic.monitor.", core.Replace(WorkspaceName(wsDir), "/", "."))
monitorAction := core.Concat("agentic.monitor.", core.Replace(WorkspaceName(workspaceDir), "/", "."))
monitor := &agentCompletionMonitor{
service: s,
agent: agent,
workspaceDir: wsDir,
workspaceDir: workspaceDir,
outputFile: outputFile,
process: proc,
}
@ -453,7 +453,7 @@ type completionProcess interface {
// agentCompletionMonitor waits for a spawned process to finish, then finalises the workspace.
//
// monitor := &agentCompletionMonitor{service: s, agent: "codex", workspaceDir: wsDir, outputFile: outputFile, process: proc}
// monitor := &agentCompletionMonitor{service: s, agent: "codex", workspaceDir: workspaceDir, outputFile: outputFile, process: proc}
// s.Core().Action("agentic.monitor.core.go-io.task-5", monitor.run)
type agentCompletionMonitor struct {
service *PrepSubsystem
@ -479,9 +479,9 @@ func (m *agentCompletionMonitor) run(_ context.Context, _ core.Options) core.Res
// 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 {
func (s *PrepSubsystem) runQA(workspaceDir string) bool {
ctx := context.Background()
repoDir := WorkspaceRepoDir(wsDir)
repoDir := WorkspaceRepoDir(workspaceDir)
process := s.Core().Process()
if fs.IsFile(core.JoinPath(repoDir, "go.mod")) {
@ -552,7 +552,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
return nil, DispatchOutput{}, core.E("dispatch", "prep workspace failed", err)
}
wsDir := prepOut.WorkspaceDir
workspaceDir := prepOut.WorkspaceDir
prompt := prepOut.Prompt
if input.DryRun {
@ -560,7 +560,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
Success: true,
Agent: input.Agent,
Repo: input.Repo,
WorkspaceDir: wsDir,
WorkspaceDir: workspaceDir,
Prompt: prompt,
}, nil
}
@ -584,22 +584,22 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
StartedAt: time.Now(),
Runs: 0,
}
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
if runnerSvc, ok := core.ServiceFor[workspaceTracker](s.Core(), "runner"); ok {
runnerSvc.TrackWorkspace(WorkspaceName(wsDir), workspaceStatus)
runnerSvc.TrackWorkspace(WorkspaceName(workspaceDir), workspaceStatus)
}
return nil, DispatchOutput{
Success: true,
Agent: input.Agent,
Repo: input.Repo,
WorkspaceDir: wsDir,
WorkspaceDir: workspaceDir,
OutputFile: "queued — at concurrency limit or frozen",
}, nil
}
}
// Step 3: Spawn agent in repo/ directory
pid, processID, outputFile, err := s.spawnAgent(input.Agent, prompt, wsDir)
pid, processID, outputFile, err := s.spawnAgent(input.Agent, prompt, workspaceDir)
if err != nil {
return nil, DispatchOutput{}, err
}
@ -616,11 +616,11 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
StartedAt: time.Now(),
Runs: 1,
}
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
// Track in runner's registry (runner owns workspace state)
if s.ServiceRuntime != nil {
if runnerSvc, ok := core.ServiceFor[workspaceTracker](s.Core(), "runner"); ok {
runnerSvc.TrackWorkspace(WorkspaceName(wsDir), workspaceStatus)
runnerSvc.TrackWorkspace(WorkspaceName(workspaceDir), workspaceStatus)
}
}
@ -628,7 +628,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
Success: true,
Agent: input.Agent,
Repo: input.Repo,
WorkspaceDir: wsDir,
WorkspaceDir: workspaceDir,
PID: pid,
OutputFile: outputFile,
}, nil

View file

@ -56,14 +56,14 @@ func (s *PrepSubsystem) DispatchSync(ctx context.Context, input DispatchSyncInpu
return DispatchSyncResult{Err: core.E("agentic.DispatchSync", "prep failed", nil)}
}
wsDir := prepOut.WorkspaceDir
workspaceDir := prepOut.WorkspaceDir
prompt := prepOut.Prompt
core.Print(nil, " workspace: %s", wsDir)
core.Print(nil, " workspace: %s", workspaceDir)
core.Print(nil, " branch: %s", prepOut.Branch)
// Spawn agent directly — no queue, no concurrency check
pid, processID, _, err := s.spawnAgent(input.Agent, prompt, wsDir)
pid, processID, _, err := s.spawnAgent(input.Agent, prompt, workspaceDir)
if err != nil {
return DispatchSyncResult{Err: core.E("agentic.DispatchSync", "spawn agent failed", err)}
}
@ -87,7 +87,7 @@ func (s *PrepSubsystem) DispatchSync(ctx context.Context, input DispatchSyncInpu
case <-ticker.C:
if pid > 0 && !ProcessAlive(runtime, processID, pid) {
// Process exited — read final status
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
st, ok := workspaceStatusValue(result)
if !ok {
err, _ := result.Value.(error)

View file

@ -21,33 +21,33 @@ func (s *PrepSubsystem) HandleIPCEvents(c *core.Core, msg core.Message) core.Res
case messages.AgentCompleted:
// Ingest findings (feature-flag gated)
if c.Config().Enabled("auto-ingest") {
if wsDir := resolveWorkspace(ev.Workspace); wsDir != "" {
s.ingestFindings(wsDir)
if workspaceDir := resolveWorkspace(ev.Workspace); workspaceDir != "" {
s.ingestFindings(workspaceDir)
}
}
case messages.SpawnQueued:
// Runner asks agentic to spawn a queued workspace
wsDir := resolveWorkspace(ev.Workspace)
if wsDir == "" {
workspaceDir := resolveWorkspace(ev.Workspace)
if workspaceDir == "" {
break
}
prompt := core.Concat("TASK: ", ev.Task, "\n\nResume from where you left off. Read CODEX.md for conventions. Commit when done.")
pid, processID, outputFile, err := s.spawnAgent(ev.Agent, prompt, wsDir)
pid, processID, outputFile, err := s.spawnAgent(ev.Agent, prompt, workspaceDir)
if err != nil {
break
}
// Update status with real PID
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
workspaceStatus, ok := workspaceStatusValue(result)
if !ok {
break
}
workspaceStatus.PID = pid
workspaceStatus.ProcessID = processID
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
if runnerSvc, ok := core.ServiceFor[workspaceTracker](c, "runner"); ok {
runnerSvc.TrackWorkspace(WorkspaceName(wsDir), workspaceStatus)
runnerSvc.TrackWorkspace(WorkspaceName(workspaceDir), workspaceStatus)
}
}
_ = outputFile
@ -59,10 +59,10 @@ func (s *PrepSubsystem) HandleIPCEvents(c *core.Core, msg core.Message) core.Res
// SpawnFromQueue spawns an agent in a pre-prepped workspace.
// Called by runner.Service via ServiceFor interface matching.
//
// spawnResult := prep.SpawnFromQueue("codex", prompt, wsDir)
// spawnResult := prep.SpawnFromQueue("codex", prompt, workspaceDir)
// pid := spawnResult.Value.(int)
func (s *PrepSubsystem) SpawnFromQueue(agent, prompt, wsDir string) core.Result {
pid, _, _, err := s.spawnAgent(agent, prompt, wsDir)
func (s *PrepSubsystem) SpawnFromQueue(agent, prompt, workspaceDir string) core.Result {
pid, _, _, err := s.spawnAgent(agent, prompt, workspaceDir)
if err != nil {
return core.Result{
Value: core.E("agentic.SpawnFromQueue", "failed to spawn queued agent", err),
@ -75,8 +75,8 @@ func (s *PrepSubsystem) SpawnFromQueue(agent, prompt, wsDir string) core.Result
//
// resolveWorkspace("core/go-io/task-5") → "/Users/snider/Code/.core/workspace/core/go-io/task-5"
func resolveWorkspace(name string) string {
wsRoot := WorkspaceRoot()
path := core.JoinPath(wsRoot, name)
workspaceRoot := WorkspaceRoot()
path := core.JoinPath(workspaceRoot, name)
if fs.IsDir(path) {
return path
}
@ -87,14 +87,14 @@ func resolveWorkspace(name string) string {
// Scans running/completed workspaces for a matching repo+branch combination.
func findWorkspaceByPR(repo, branch string) string {
for _, path := range WorkspaceStatusPaths() {
wsDir := core.PathDir(path)
statusResult := ReadStatusResult(wsDir)
workspaceDir := core.PathDir(path)
statusResult := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(statusResult)
if !ok {
continue
}
if workspaceStatus.Repo == repo && workspaceStatus.Branch == branch {
return wsDir
return workspaceDir
}
}
return ""

View file

@ -8,14 +8,14 @@ import (
core "dappco.re/go/core"
)
func (s *PrepSubsystem) ingestFindings(wsDir string) {
statusResult := ReadStatusResult(wsDir)
func (s *PrepSubsystem) ingestFindings(workspaceDir string) {
statusResult := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(statusResult)
if !ok || workspaceStatus.Status != "completed" {
return
}
logFiles := workspaceLogFiles(wsDir)
logFiles := workspaceLogFiles(workspaceDir)
if len(logFiles) == 0 {
return
}

View file

@ -26,7 +26,7 @@ func LocalFs() *core.Fs { return fs }
// WorkspaceRoot returns the root directory for agent workspaces.
// Checks CORE_WORKSPACE env var first, falls back to HomeDir()/Code/.core/workspace.
//
// wsDir := core.JoinPath(agentic.WorkspaceRoot(), "core", "go-io", "task-42")
// workspaceDir := core.JoinPath(agentic.WorkspaceRoot(), "core", "go-io", "task-42")
func WorkspaceRoot() string {
return core.JoinPath(CoreRoot(), "workspace")
}
@ -41,20 +41,20 @@ func WorkspaceStatusPaths() []string {
// WorkspaceStatusPath returns the status file for a workspace directory.
//
// path := agentic.WorkspaceStatusPath("/srv/.core/workspace/core/go-io/task-5")
func WorkspaceStatusPath(wsDir string) string {
return core.JoinPath(wsDir, "status.json")
func WorkspaceStatusPath(workspaceDir string) string {
return core.JoinPath(workspaceDir, "status.json")
}
// WorkspaceName extracts the unique workspace name from a full path.
// Given /Users/snider/Code/.core/workspace/core/go-io/dev → core/go-io/dev
//
// name := agentic.WorkspaceName("/Users/snider/Code/.core/workspace/core/go-io/dev")
func WorkspaceName(wsDir string) string {
func WorkspaceName(workspaceDir string) string {
root := WorkspaceRoot()
name := core.TrimPrefix(wsDir, root)
name := core.TrimPrefix(workspaceDir, root)
name = core.TrimPrefix(name, "/")
if name == "" {
return core.PathBase(wsDir)
return core.PathBase(workspaceDir)
}
return name
}
@ -83,8 +83,8 @@ func HomeDir() string {
return core.Env("DIR_HOME")
}
func workspaceStatusPaths(wsRoot string) []string {
if wsRoot == "" {
func workspaceStatusPaths(workspaceRoot string) []string {
if workspaceRoot == "" {
return nil
}
@ -122,7 +122,7 @@ func workspaceStatusPaths(wsRoot string) []string {
}
}
walk(wsRoot, 0)
walk(workspaceRoot, 0)
sort.Strings(paths)
return paths
}
@ -130,56 +130,56 @@ func workspaceStatusPaths(wsRoot string) []string {
// WorkspaceRepoDir returns the checked-out repo directory for a workspace.
//
// repoDir := agentic.WorkspaceRepoDir("/srv/.core/workspace/core/go-io/task-5")
func WorkspaceRepoDir(wsDir string) string {
return core.JoinPath(wsDir, "repo")
func WorkspaceRepoDir(workspaceDir string) string {
return core.JoinPath(workspaceDir, "repo")
}
func workspaceRepoDir(wsDir string) string {
return WorkspaceRepoDir(wsDir)
func workspaceRepoDir(workspaceDir string) string {
return WorkspaceRepoDir(workspaceDir)
}
// WorkspaceMetaDir returns the metadata directory for a workspace.
//
// metaDir := agentic.WorkspaceMetaDir("/srv/.core/workspace/core/go-io/task-5")
func WorkspaceMetaDir(wsDir string) string {
return core.JoinPath(wsDir, ".meta")
func WorkspaceMetaDir(workspaceDir string) string {
return core.JoinPath(workspaceDir, ".meta")
}
func workspaceMetaDir(wsDir string) string {
return WorkspaceMetaDir(wsDir)
func workspaceMetaDir(workspaceDir string) string {
return WorkspaceMetaDir(workspaceDir)
}
// WorkspaceBlockedPath returns the BLOCKED.md path for a workspace.
//
// blocked := agentic.WorkspaceBlockedPath("/srv/.core/workspace/core/go-io/task-5")
func WorkspaceBlockedPath(wsDir string) string {
return core.JoinPath(WorkspaceRepoDir(wsDir), "BLOCKED.md")
func WorkspaceBlockedPath(workspaceDir string) string {
return core.JoinPath(WorkspaceRepoDir(workspaceDir), "BLOCKED.md")
}
func workspaceBlockedPath(wsDir string) string {
return WorkspaceBlockedPath(wsDir)
func workspaceBlockedPath(workspaceDir string) string {
return WorkspaceBlockedPath(workspaceDir)
}
// WorkspaceAnswerPath returns the ANSWER.md path for a workspace.
//
// answer := agentic.WorkspaceAnswerPath("/srv/.core/workspace/core/go-io/task-5")
func WorkspaceAnswerPath(wsDir string) string {
return core.JoinPath(WorkspaceRepoDir(wsDir), "ANSWER.md")
func WorkspaceAnswerPath(workspaceDir string) string {
return core.JoinPath(WorkspaceRepoDir(workspaceDir), "ANSWER.md")
}
func workspaceAnswerPath(wsDir string) string {
return WorkspaceAnswerPath(wsDir)
func workspaceAnswerPath(workspaceDir string) string {
return WorkspaceAnswerPath(workspaceDir)
}
// WorkspaceLogFiles returns captured agent log files for a workspace.
//
// logs := agentic.WorkspaceLogFiles("/srv/.core/workspace/core/go-io/task-5")
func WorkspaceLogFiles(wsDir string) []string {
return core.PathGlob(core.JoinPath(WorkspaceMetaDir(wsDir), "agent-*.log"))
func WorkspaceLogFiles(workspaceDir string) []string {
return core.PathGlob(core.JoinPath(WorkspaceMetaDir(workspaceDir), "agent-*.log"))
}
func workspaceLogFiles(wsDir string) []string {
return WorkspaceLogFiles(wsDir)
func workspaceLogFiles(workspaceDir string) []string {
return WorkspaceLogFiles(workspaceDir)
}
// PlansRoot returns the root directory for agent plans.

View file

@ -51,15 +51,15 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
return nil, CreatePROutput{}, core.E("createPR", "no Forge token configured", nil)
}
wsDir := core.JoinPath(WorkspaceRoot(), input.Workspace)
repoDir := WorkspaceRepoDir(wsDir)
workspaceDir := core.JoinPath(WorkspaceRoot(), input.Workspace)
repoDir := WorkspaceRepoDir(workspaceDir)
if !fs.IsDir(core.JoinPath(repoDir, ".git")) {
return nil, CreatePROutput{}, core.E("createPR", core.Concat("workspace not found: ", input.Workspace), nil)
}
// Read workspace status for repo, branch, issue context
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok {
err, _ := result.Value.(error)
@ -126,7 +126,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in
// Update status with PR URL
workspaceStatus.PRURL = prURL
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
// Comment on issue if tracked
if workspaceStatus.Issue > 0 {

View file

@ -234,13 +234,13 @@ func (s *PrepSubsystem) hydrateWorkspaces() {
s.workspaces = core.NewRegistry[*WorkspaceStatus]()
}
for _, path := range WorkspaceStatusPaths() {
wsDir := core.PathDir(path)
result := ReadStatusResult(wsDir)
workspaceDir := core.PathDir(path)
result := ReadStatusResult(workspaceDir)
st, ok := workspaceStatusValue(result)
if !ok {
continue
}
s.workspaces.Set(WorkspaceName(wsDir), st)
s.workspaces.Set(WorkspaceName(workspaceDir), st)
}
}
@ -362,17 +362,17 @@ func workspaceDir(org, repo string, input PrepInput) (string, error) {
}
return "", err
}
wsDir, ok := r.Value.(string)
if !ok || wsDir == "" {
workspaceDir, ok := r.Value.(string)
if !ok || workspaceDir == "" {
return "", core.E("workspaceDir", "invalid workspace directory result", nil)
}
return wsDir, nil
return workspaceDir, nil
}
// workspaceDirResult resolves the workspace path and returns core.Result.
//
// r := workspaceDirResult("core", "go-io", PrepInput{Issue: 15})
// if r.OK { wsDir := r.Value.(string) }
// if r.OK { workspaceDir := r.Value.(string) }
func workspaceDirResult(org, repo string, input PrepInput) core.Result {
orgName := core.ValidateName(org)
if !orgName.OK {
@ -421,14 +421,14 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
}
return nil, PrepOutput{}, err
}
wsDir, ok := wsDirResult.Value.(string)
if !ok || wsDir == "" {
workspaceDir, ok := wsDirResult.Value.(string)
if !ok || workspaceDir == "" {
return nil, PrepOutput{}, core.E("prepWorkspace", "invalid workspace path", nil)
}
repoDir := workspaceRepoDir(wsDir)
metaDir := workspaceMetaDir(wsDir)
out := PrepOutput{WorkspaceDir: wsDir, RepoDir: repoDir}
repoDir := workspaceRepoDir(workspaceDir)
metaDir := workspaceMetaDir(workspaceDir)
out := PrepOutput{WorkspaceDir: workspaceDir, RepoDir: repoDir}
// Source repo path — org and repo were validated by workspaceDirResult.
repoPath := core.JoinPath(s.codePath, input.Org, input.Repo)
@ -467,7 +467,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
}
// Extract default workspace template (go.work etc.)
if result := lib.ExtractWorkspace("default", wsDir, &lib.WorkspaceData{
if result := lib.ExtractWorkspace("default", workspaceDir, &lib.WorkspaceData{
Repo: input.Repo,
Branch: "",
Task: input.Task,
@ -515,7 +515,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
lang := detectLanguage(repoPath)
if lang == "php" {
if r := lib.WorkspaceFile("default", "CODEX-PHP.md.tmpl"); r.OK {
codexPath := core.JoinPath(wsDir, "CODEX.md")
codexPath := core.JoinPath(workspaceDir, "CODEX.md")
fs.Write(codexPath, r.Value.(string))
}
}
@ -523,11 +523,11 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
// Clone workspace dependencies — Core modules needed to build the repo.
// Reads go.mod, finds dappco.re/go/core/* imports, clones from Forge,
// and updates go.work so the agent can build inside the workspace.
s.cloneWorkspaceDeps(ctx, wsDir, repoDir, input.Org)
s.cloneWorkspaceDeps(ctx, workspaceDir, repoDir, input.Org)
// Clone ecosystem docs into .core/reference/ so agents have full documentation.
// The docs site (core.help) has architecture guides, specs, and API references.
docsDir := core.JoinPath(wsDir, ".core", "reference", "docs")
docsDir := core.JoinPath(workspaceDir, ".core", "reference", "docs")
if !fs.IsDir(docsDir) {
docsRepo := core.JoinPath(s.codePath, input.Org, "docs")
if fs.IsDir(core.JoinPath(docsRepo, ".git")) {
@ -537,7 +537,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
// Copy RFC specs from plans repo into workspace specs/ folder.
// Maps repo name to plans directory: go-io → core/go/io/, go-process → core/go/process/, etc.
s.copyRepoSpecs(wsDir, input.Repo)
s.copyRepoSpecs(workspaceDir, input.Repo)
// Build the rich prompt with all context
out.Prompt, out.Memories, out.Consumers = s.buildPrompt(ctx, input, out.Branch, repoPath)
@ -554,7 +554,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
//
// s.copyRepoSpecs("/tmp/ws", "go-io") // copies plans/core/go/io/**/RFC*.md → /tmp/ws/specs/
// s.copyRepoSpecs("/tmp/ws", "core-bio") // copies plans/core/php/bio/**/RFC*.md → /tmp/ws/specs/
func (s *PrepSubsystem) copyRepoSpecs(wsDir, repo string) {
func (s *PrepSubsystem) copyRepoSpecs(workspaceDir, repo string) {
fs := (&core.Fs{}).NewUnrestricted()
// Plans repo base — look for it relative to codePath
@ -587,7 +587,7 @@ func (s *PrepSubsystem) copyRepoSpecs(wsDir, repo string) {
// Glob RFC*.md at each depth level (root, 1 deep, 2 deep, 3 deep).
// Preserves subdirectory structure: specDir/pkg/sub/RFC.md → specs/pkg/sub/RFC.md
specsDir := core.JoinPath(wsDir, "specs")
specsDir := core.JoinPath(workspaceDir, "specs")
fs.EnsureDir(specsDir)
patterns := []string{

View file

@ -327,8 +327,8 @@ func (s *PrepSubsystem) drainQueue() {
// Returns true if a task was spawned, false if nothing to do.
func (s *PrepSubsystem) drainOne() bool {
for _, statusPath := range WorkspaceStatusPaths() {
wsDir := core.PathDir(statusPath)
result := ReadStatusResult(wsDir)
workspaceDir := core.PathDir(statusPath)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok || workspaceStatus.Status != "queued" {
continue
@ -357,7 +357,7 @@ func (s *PrepSubsystem) drainOne() bool {
prompt := core.Concat("TASK: ", workspaceStatus.Task, "\n\nResume from where you left off. Read CODEX.md for conventions. Commit when done.")
pid, processID, _, err := s.spawnAgent(workspaceStatus.Agent, prompt, wsDir)
pid, processID, _, err := s.spawnAgent(workspaceStatus.Agent, prompt, workspaceDir)
if err != nil {
continue
}
@ -366,8 +366,8 @@ func (s *PrepSubsystem) drainOne() bool {
workspaceStatus.PID = pid
workspaceStatus.ProcessID = processID
workspaceStatus.Runs++
writeStatusResult(wsDir, workspaceStatus)
s.TrackWorkspace(WorkspaceName(wsDir), workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
s.TrackWorkspace(WorkspaceName(workspaceDir), workspaceStatus)
return true
}

View file

@ -43,8 +43,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
return nil, ResumeOutput{}, core.E("resume", "workspace is required", nil)
}
wsDir := core.JoinPath(WorkspaceRoot(), input.Workspace)
repoDir := WorkspaceRepoDir(wsDir)
workspaceDir := core.JoinPath(WorkspaceRoot(), input.Workspace)
repoDir := WorkspaceRepoDir(workspaceDir)
// Verify workspace exists
if !fs.IsDir(core.JoinPath(repoDir, ".git")) {
@ -52,7 +52,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
}
// Read current status
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok {
err, _ := result.Value.(error)
@ -71,7 +71,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
// Write ANSWER.md if answer provided
if input.Answer != "" {
answerPath := workspaceAnswerPath(wsDir)
answerPath := workspaceAnswerPath(workspaceDir)
content := core.Sprintf("# Answer\n\n%s\n", input.Answer)
if writeResult := fs.Write(answerPath, content); !writeResult.OK {
err, _ := writeResult.Value.(error)
@ -96,7 +96,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
}
// Spawn agent via go-process
pid, processID, _, err := s.spawnAgent(agent, prompt, wsDir)
pid, processID, _, err := s.spawnAgent(agent, prompt, workspaceDir)
if err != nil {
return nil, ResumeOutput{}, err
}
@ -107,13 +107,13 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu
workspaceStatus.ProcessID = processID
workspaceStatus.Runs++
workspaceStatus.Question = ""
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
return nil, ResumeOutput{
Success: true,
Workspace: input.Workspace,
Agent: agent,
PID: pid,
OutputFile: agentOutputFile(wsDir, agent),
OutputFile: agentOutputFile(workspaceDir, agent),
}, nil
}

View file

@ -26,8 +26,8 @@ import (
// WorkspaceStatus represents the current state of an agent workspace.
//
// result := ReadStatusResult(wsDir)
// if result.OK && result.Value.(*WorkspaceStatus).Status == "completed" { autoCreatePR(wsDir) }
// result := ReadStatusResult(workspaceDir)
// if result.OK && result.Value.(*WorkspaceStatus).Status == "completed" { autoCreatePR(workspaceDir) }
type WorkspaceStatus struct {
Status string `json:"status"` // running, completed, blocked, failed
Agent string `json:"agent"` // gemini, claude, codex
@ -56,8 +56,8 @@ type WorkspaceQuery struct {
Status string // filter by status (empty = all)
}
func writeStatus(wsDir string, status *WorkspaceStatus) error {
r := writeStatusResult(wsDir, status)
func writeStatus(workspaceDir string, status *WorkspaceStatus) error {
r := writeStatusResult(workspaceDir, status)
if !r.OK {
err, _ := r.Value.(error)
if err == nil {
@ -72,12 +72,12 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error {
//
// result := writeStatusResult("/srv/core/workspace/core/go-io/task-5", &WorkspaceStatus{Status: "running"})
// if result.OK { return }
func writeStatusResult(wsDir string, status *WorkspaceStatus) core.Result {
func writeStatusResult(workspaceDir string, status *WorkspaceStatus) core.Result {
if status == nil {
return core.Result{Value: core.E("writeStatus", "status is required", nil), OK: false}
}
status.UpdatedAt = time.Now()
statusPath := WorkspaceStatusPath(wsDir)
statusPath := WorkspaceStatusPath(workspaceDir)
if r := fs.WriteAtomic(statusPath, core.JSONMarshalString(status)); !r.OK {
err, _ := r.Value.(error)
if err == nil {
@ -94,14 +94,14 @@ func writeStatusResult(wsDir string, status *WorkspaceStatus) core.Result {
//
// result := ReadStatusResult("/path/to/workspace")
// if result.OK { workspaceStatus := result.Value.(*WorkspaceStatus) }
func ReadStatusResult(wsDir string) core.Result {
r := fs.Read(WorkspaceStatusPath(wsDir))
func ReadStatusResult(workspaceDir string) core.Result {
r := fs.Read(WorkspaceStatusPath(workspaceDir))
if !r.OK {
err, _ := r.Value.(error)
if err == nil {
return core.Result{Value: core.E("ReadStatusResult", "status not found", nil), OK: false}
}
return core.Result{Value: core.E("ReadStatusResult", core.Concat("status not found for ", wsDir), err), OK: false}
return core.Result{Value: core.E("ReadStatusResult", core.Concat("status not found for ", workspaceDir), err), OK: false}
}
var s WorkspaceStatus
if parseResult := core.JSONUnmarshalString(r.Value.(string), &s); !parseResult.OK {
@ -177,10 +177,10 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
var out StatusOutput
for _, statusPath := range statusFiles {
wsDir := core.PathDir(statusPath)
name := WorkspaceName(wsDir)
workspaceDir := core.PathDir(statusPath)
name := WorkspaceName(workspaceDir)
result := ReadStatusResult(wsDir)
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok {
out.Total++
@ -191,19 +191,19 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
// If status is "running", check whether the managed process is still alive.
if workspaceStatus.Status == "running" && (workspaceStatus.ProcessID != "" || workspaceStatus.PID > 0) {
if !ProcessAlive(runtime, workspaceStatus.ProcessID, workspaceStatus.PID) {
blockedPath := workspaceBlockedPath(wsDir)
blockedPath := workspaceBlockedPath(workspaceDir)
if r := fs.Read(blockedPath); r.OK {
workspaceStatus.Status = "blocked"
workspaceStatus.Question = core.Trim(r.Value.(string))
} else {
if len(workspaceLogFiles(wsDir)) == 0 {
if len(workspaceLogFiles(workspaceDir)) == 0 {
workspaceStatus.Status = "failed"
workspaceStatus.Question = "Agent process died (no output log)"
} else {
workspaceStatus.Status = "completed"
}
}
writeStatusResult(wsDir, workspaceStatus)
writeStatusResult(workspaceDir, workspaceStatus)
}
}

View file

@ -16,14 +16,14 @@ import (
// For deeper review (security, conventions), dispatch a separate task:
//
// agentic_dispatch repo=go-crypt template=verify persona=engineering/engineering-security-engineer
func (s *PrepSubsystem) autoVerifyAndMerge(wsDir string) {
result := ReadStatusResult(wsDir)
func (s *PrepSubsystem) autoVerifyAndMerge(workspaceDir string) {
result := ReadStatusResult(workspaceDir)
workspaceStatus, ok := workspaceStatusValue(result)
if !ok || workspaceStatus.PRURL == "" || workspaceStatus.Repo == "" {
return
}
repoDir := WorkspaceRepoDir(wsDir)
repoDir := WorkspaceRepoDir(workspaceDir)
org := workspaceStatus.Org
if org == "" {
org = "core"
@ -36,13 +36,13 @@ func (s *PrepSubsystem) autoVerifyAndMerge(wsDir string) {
// markMerged is a helper to avoid repeating the status update.
markMerged := func() {
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
st2, ok := workspaceStatusValue(result)
if !ok {
return
}
st2.Status = "merged"
writeStatusResult(wsDir, st2)
writeStatusResult(workspaceDir, st2)
}
}
@ -66,13 +66,13 @@ func (s *PrepSubsystem) autoVerifyAndMerge(wsDir string) {
// Both attempts failed — flag for human review
s.flagForReview(org, workspaceStatus.Repo, prNum, mergeOutcome)
if result := ReadStatusResult(wsDir); result.OK {
if result := ReadStatusResult(workspaceDir); result.OK {
workspaceStatusUpdate, ok := workspaceStatusValue(result)
if !ok {
return
}
workspaceStatusUpdate.Question = "Flagged for review — auto-merge failed after retry"
writeStatusResult(wsDir, workspaceStatusUpdate)
writeStatusResult(workspaceDir, workspaceStatusUpdate)
}
}

View file

@ -269,30 +269,30 @@ type WorkspaceData struct {
// Repo: "go-io", Task: "fix tests", Agent: "codex",
// })
// core.Println(r.OK)
func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) core.Result {
func ExtractWorkspace(templateName, targetDir string, data *WorkspaceData) core.Result {
if result := ensureMounted(); !result.OK {
if err, ok := result.Value.(error); ok {
return core.Result{
Value: core.E("lib.ExtractWorkspace", core.Concat("mount workspace template ", tmplName), err),
Value: core.E("lib.ExtractWorkspace", core.Concat("mount workspace template ", templateName), err),
OK: false,
}
}
return core.Result{
Value: core.E("lib.ExtractWorkspace", core.Concat("mount workspace template ", tmplName), nil),
Value: core.E("lib.ExtractWorkspace", core.Concat("mount workspace template ", templateName), nil),
OK: false,
}
}
r := workspaceFS.Sub(tmplName)
r := workspaceFS.Sub(templateName)
if !r.OK {
if err, ok := r.Value.(error); ok {
return core.Result{
Value: core.E("lib.ExtractWorkspace", core.Concat("template not found: ", tmplName), err),
Value: core.E("lib.ExtractWorkspace", core.Concat("template not found: ", templateName), err),
OK: false,
}
}
return core.Result{
Value: core.E("lib.ExtractWorkspace", core.Concat("template not found: ", tmplName), nil),
Value: core.E("lib.ExtractWorkspace", core.Concat("template not found: ", templateName), nil),
OK: false,
}
}
@ -300,12 +300,12 @@ func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) core.Resu
if !result.OK {
if err, ok := result.Value.(error); ok {
return core.Result{
Value: core.E("lib.ExtractWorkspace", core.Concat("extract workspace template ", tmplName), err),
Value: core.E("lib.ExtractWorkspace", core.Concat("extract workspace template ", templateName), err),
OK: false,
}
}
return core.Result{
Value: core.E("lib.ExtractWorkspace", core.Concat("extract workspace template ", tmplName), nil),
Value: core.E("lib.ExtractWorkspace", core.Concat("extract workspace template ", templateName), nil),
OK: false,
}
}
@ -317,11 +317,11 @@ func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) core.Resu
//
// r := lib.WorkspaceFile("default", "CODEX-PHP.md.tmpl")
// if r.OK { content := r.Value.(string) }
func WorkspaceFile(tmplName, filename string) core.Result {
func WorkspaceFile(templateName, filename string) core.Result {
if result := ensureMounted(); !result.OK {
return result
}
r := workspaceFS.Sub(tmplName)
r := workspaceFS.Sub(templateName)
if !r.OK {
return r
}

View file

@ -32,8 +32,8 @@ func (m *Subsystem) harvestCompleted() string {
var harvested []harvestResult
for _, entry := range agentic.WorkspaceStatusPaths() {
wsDir := core.PathDir(entry)
result := m.harvestWorkspace(wsDir)
workspaceDir := core.PathDir(entry)
result := m.harvestWorkspace(workspaceDir)
if result != nil {
harvested = append(harvested, *result)
}
@ -61,8 +61,8 @@ func (m *Subsystem) harvestCompleted() string {
}
// harvestWorkspace checks a single workspace and pushes if ready.
func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
statusResult := fs.Read(agentic.WorkspaceStatusPath(wsDir))
func (m *Subsystem) harvestWorkspace(workspaceDir string) *harvestResult {
statusResult := fs.Read(agentic.WorkspaceStatusPath(workspaceDir))
if !statusResult.OK {
return nil
}
@ -85,7 +85,7 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
return nil
}
repoDir := agentic.WorkspaceRepoDir(wsDir)
repoDir := agentic.WorkspaceRepoDir(workspaceDir)
if !fs.IsDir(repoDir) {
return nil
}
@ -108,7 +108,7 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
// Safety checks before pushing
if reason := m.checkSafety(repoDir); reason != "" {
updateStatus(wsDir, "rejected", reason)
updateStatus(workspaceDir, "rejected", reason)
return &harvestResult{repo: workspaceStatus.Repo, branch: branch, rejected: reason}
}
@ -118,7 +118,7 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult {
// Mark ready for review — do NOT auto-push.
// Pushing is a high-impact mutation that should happen during
// explicit review (/review command), not silently in the background.
updateStatus(wsDir, "ready-for-review", "")
updateStatus(workspaceDir, "ready-for-review", "")
return &harvestResult{repo: workspaceStatus.Repo, branch: branch, files: files}
}
@ -247,9 +247,9 @@ func (m *Subsystem) pushBranch(srcDir, branch string) error {
// updateStatus rewrites status.json after a harvest decision.
//
// updateStatus(wsDir, "ready-for-review", "")
func updateStatus(wsDir, status, question string) {
statusResult := fs.Read(agentic.WorkspaceStatusPath(wsDir))
// updateStatus(workspaceDir, "ready-for-review", "")
func updateStatus(workspaceDir, status, question string) {
statusResult := fs.Read(agentic.WorkspaceStatusPath(workspaceDir))
if !statusResult.OK {
return
}
@ -267,7 +267,7 @@ func updateStatus(wsDir, status, question string) {
} else {
delete(workspaceStatus, "question") // clear stale question from previous state
}
statusPath := agentic.WorkspaceStatusPath(wsDir)
statusPath := agentic.WorkspaceStatusPath(workspaceDir)
if writeResult := fs.WriteAtomic(statusPath, core.JSONMarshalString(workspaceStatus)); !writeResult.OK {
if err, ok := writeResult.Value.(error); ok {
core.Warn("monitor.updateStatus: failed to write status", "path", statusPath, "reason", err)

View file

@ -19,7 +19,7 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// readResult := fs.Read(core.JoinPath(wsRoot, name, "status.json"))
// readResult := fs.Read(core.JoinPath(workspaceRoot, name, "status.json"))
// if text, ok := resultString(readResult); ok { _ = core.JSONUnmarshalString(text, &workspaceStatus) }
var fs = agentic.LocalFs()

View file

@ -68,8 +68,8 @@ func CoreRoot() string {
//
// result := ReadStatusResult("/srv/core/workspace/core/go-io/task-5")
// if result.OK { workspaceStatus := result.Value.(*WorkspaceStatus) }
func ReadStatusResult(wsDir string) core.Result {
statusResult := agentic.ReadStatusResult(wsDir)
func ReadStatusResult(workspaceDir string) core.Result {
statusResult := agentic.ReadStatusResult(workspaceDir)
if !statusResult.OK {
err, _ := statusResult.Value.(error)
if err == nil {
@ -93,7 +93,7 @@ func ReadStatusResult(wsDir string) core.Result {
//
// result := runner.WriteStatus("/srv/core/workspace/core/go-io/task-5", &runner.WorkspaceStatus{Status: "running", Agent: "codex"})
// core.Println(result.OK)
func WriteStatus(wsDir string, status *WorkspaceStatus) core.Result {
func WriteStatus(workspaceDir string, status *WorkspaceStatus) core.Result {
if status == nil {
return core.Result{Value: core.E("runner.WriteStatus", "status is required", nil), OK: false}
}
@ -103,7 +103,7 @@ func WriteStatus(wsDir string, status *WorkspaceStatus) core.Result {
return core.Result{Value: core.E("runner.WriteStatus", "status conversion failed", nil), OK: false}
}
agenticStatus.UpdatedAt = time.Now()
if writeResult := fs.WriteAtomic(agentic.WorkspaceStatusPath(wsDir), core.JSONMarshalString(agenticStatus)); !writeResult.OK {
if writeResult := fs.WriteAtomic(agentic.WorkspaceStatusPath(workspaceDir), core.JSONMarshalString(agenticStatus)); !writeResult.OK {
err, _ := writeResult.Value.(error)
if err == nil {
return core.Result{Value: core.E("runner.WriteStatus", "failed to write status", nil), OK: false}

View file

@ -220,8 +220,8 @@ func (s *Service) drainQueue() {
func (s *Service) drainOne() bool {
for _, statusPath := range agentic.WorkspaceStatusPaths() {
wsDir := core.PathDir(statusPath)
statusResult := ReadStatusResult(wsDir)
workspaceDir := core.PathDir(statusPath)
statusResult := ReadStatusResult(workspaceDir)
if !statusResult.OK {
continue
}
@ -251,7 +251,7 @@ func (s *Service) drainOne() bool {
// Ask agentic to spawn — runner owns the gate,
// agentic owns the actual process launch.
// Workspace name is relative path from workspace root (e.g. "core/go-ai/dev")
wsName := agentic.WorkspaceName(wsDir)
wsName := agentic.WorkspaceName(workspaceDir)
core.Info("drainOne: found queued workspace", "workspace", wsName, "agent", workspaceStatus.Agent)
// Spawn directly — agentic is a Core service, use ServiceFor to get it
@ -259,7 +259,7 @@ func (s *Service) drainOne() bool {
continue
}
type spawner interface {
SpawnFromQueue(agent, prompt, wsDir string) core.Result
SpawnFromQueue(agent, prompt, workspaceDir string) core.Result
}
agenticService, ok := core.ServiceFor[spawner](s.Core(), "agentic")
if !ok {
@ -267,7 +267,7 @@ func (s *Service) drainOne() bool {
continue
}
prompt := core.Concat("TASK: ", workspaceStatus.Task, "\n\nResume from where you left off. Read CODEX.md for conventions. Commit when done.")
spawnResult := agenticService.SpawnFromQueue(workspaceStatus.Agent, prompt, wsDir)
spawnResult := agenticService.SpawnFromQueue(workspaceStatus.Agent, prompt, workspaceDir)
if !spawnResult.OK {
core.Error("drainOne: spawn failed", "workspace", wsName, "reason", core.Sprint(spawnResult.Value))
continue
@ -282,7 +282,7 @@ func (s *Service) drainOne() bool {
workspaceStatus.Status = "running"
workspaceStatus.PID = pid
workspaceStatus.Runs++
if writeResult := WriteStatus(wsDir, workspaceStatus); !writeResult.OK {
if writeResult := WriteStatus(workspaceDir, workspaceStatus); !writeResult.OK {
core.Error("drainOne: failed to write workspace status", "workspace", wsName, "reason", core.Sprint(writeResult.Value))
continue
}

View file

@ -52,19 +52,19 @@ type configValue struct {
//
// r := setup.GenerateBuildConfig("/srv/repos/agent", setup.TypeGo)
// if r.OK { content := r.Value.(string) }
func GenerateBuildConfig(path string, projType ProjectType) core.Result {
func GenerateBuildConfig(path string, projectType ProjectType) core.Result {
name := core.PathBase(path)
sections := []configSection{
{
Key: "project",
Values: []configValue{
{Key: "name", Value: name},
{Key: "type", Value: string(projType)},
{Key: "type", Value: string(projectType)},
},
},
}
switch projType {
switch projectType {
case TypeGo, TypeWails:
sections = append(sections, configSection{
Key: "build",
@ -99,10 +99,10 @@ func GenerateBuildConfig(path string, projType ProjectType) core.Result {
//
// r := setup.GenerateTestConfig(setup.TypeGo)
// if r.OK { content := r.Value.(string) }
func GenerateTestConfig(projType ProjectType) core.Result {
func GenerateTestConfig(projectType ProjectType) core.Result {
var sections []configSection
switch projType {
switch projectType {
case TypeGo, TypeWails:
sections = []configSection{
{

View file

@ -11,8 +11,8 @@ import (
// ProjectType records what setup detected in a repository path.
//
// projType := setup.Detect("/srv/repos/agent")
// if projType == setup.TypeGo { /* generate Go defaults */ }
// projectType := setup.Detect("/srv/repos/agent")
// if projectType == setup.TypeGo { /* generate Go defaults */ }
type ProjectType string
const (
@ -28,12 +28,12 @@ var fs = (&core.Fs{}).NewUnrestricted()
// Detect inspects a repository path and returns the primary project type.
//
// projType := setup.Detect("./repo")
// projectType := setup.Detect("./repo")
func Detect(path string) ProjectType {
base := absolutePath(path)
checks := []struct {
file string
projType ProjectType
file string
projectType ProjectType
}{
{"wails.json", TypeWails},
{"go.mod", TypeGo},
@ -42,7 +42,7 @@ func Detect(path string) ProjectType {
}
for _, candidate := range checks {
if fs.IsFile(core.JoinPath(base, candidate.file)) {
return candidate.projType
return candidate.projectType
}
}
return TypeUnknown
@ -55,8 +55,8 @@ func DetectAll(path string) []ProjectType {
base := absolutePath(path)
var projectTypes []ProjectType
checks := []struct {
file string
projType ProjectType
file string
projectType ProjectType
}{
{"go.mod", TypeGo},
{"composer.json", TypePHP},
@ -65,7 +65,7 @@ func DetectAll(path string) []ProjectType {
}
for _, candidate := range checks {
if fs.IsFile(core.JoinPath(base, candidate.file)) {
projectTypes = append(projectTypes, candidate.projType)
projectTypes = append(projectTypes, candidate.projectType)
}
}
return projectTypes

View file

@ -28,45 +28,45 @@ func (s *Service) Run(options Options) core.Result {
}
options.Path = absolutePath(options.Path)
projType := Detect(options.Path)
projectType := Detect(options.Path)
allTypes := DetectAll(options.Path)
core.Print(nil, "Project: %s", core.PathBase(options.Path))
core.Print(nil, "Type: %s", projType)
core.Print(nil, "Type: %s", projectType)
if len(allTypes) > 1 {
core.Print(nil, "Also: %v (polyglot)", allTypes)
}
var tmplName string
var templateName string
if options.Template != "" {
templateResult := resolveTemplateName(options.Template, projType)
templateResult := resolveTemplateName(options.Template, projectType)
if !templateResult.OK {
return templateResult
}
tmplName = templateResult.Value.(string)
if !templateExists(tmplName) {
templateName = templateResult.Value.(string)
if !templateExists(templateName) {
return core.Result{
Value: core.E("setup.Run", core.Concat("template not found: ", tmplName), nil),
Value: core.E("setup.Run", core.Concat("template not found: ", templateName), nil),
OK: false,
}
}
}
// Generate .core/ config files
if result := setupCoreDir(options, projType); !result.OK {
if result := setupCoreDir(options, projectType); !result.OK {
return result
}
// Scaffold from dir template if requested
if tmplName != "" {
return s.scaffoldTemplate(options, projType, tmplName)
if templateName != "" {
return s.scaffoldTemplate(options, projectType, templateName)
}
return core.Result{Value: options.Path, OK: true}
}
// setupCoreDir creates .core/ with build.yaml and test.yaml.
func setupCoreDir(options Options, projType ProjectType) core.Result {
func setupCoreDir(options Options, projectType ProjectType) core.Result {
coreDir := core.JoinPath(options.Path, ".core")
if options.DryRun {
@ -83,7 +83,7 @@ func setupCoreDir(options Options, projType ProjectType) core.Result {
}
// build.yaml
buildConfig := GenerateBuildConfig(options.Path, projType)
buildConfig := GenerateBuildConfig(options.Path, projectType)
if !buildConfig.OK {
err, _ := buildConfig.Value.(error)
return core.Result{
@ -96,7 +96,7 @@ func setupCoreDir(options Options, projType ProjectType) core.Result {
}
// test.yaml
testConfig := GenerateTestConfig(projType)
testConfig := GenerateTestConfig(projectType)
if !testConfig.OK {
err, _ := testConfig.Value.(error)
return core.Result{
@ -112,37 +112,37 @@ func setupCoreDir(options Options, projType ProjectType) core.Result {
}
// scaffoldTemplate extracts a dir template into the target path.
func (s *Service) scaffoldTemplate(options Options, projType ProjectType, tmplName string) core.Result {
core.Print(nil, "Template: %s", tmplName)
func (s *Service) scaffoldTemplate(options Options, projectType ProjectType, templateName string) core.Result {
core.Print(nil, "Template: %s", templateName)
data := &lib.WorkspaceData{
Repo: core.PathBase(options.Path),
Branch: "main",
Task: core.Sprintf("Initialise %s project tooling.", projType),
Task: core.Sprintf("Initialise %s project tooling.", projectType),
Agent: "setup",
Language: string(projType),
Language: string(projectType),
Prompt: "This workspace was scaffolded by pkg/setup. Review the repository and continue from the generated context files.",
Flow: formatFlow(projType),
Flow: formatFlow(projectType),
RepoDescription: s.DetectGitRemote(options.Path),
BuildCmd: defaultBuildCommand(projType),
TestCmd: defaultTestCommand(projType),
BuildCmd: defaultBuildCommand(projectType),
TestCmd: defaultTestCommand(projectType),
}
if options.DryRun {
core.Print(nil, "Would extract workspace/%s to %s", tmplName, options.Path)
core.Print(nil, " Template found: %s", tmplName)
core.Print(nil, "Would extract workspace/%s to %s", templateName, options.Path)
core.Print(nil, " Template found: %s", templateName)
return core.Result{Value: options.Path, OK: true}
}
if result := lib.ExtractWorkspace(tmplName, options.Path, data); !result.OK {
if result := lib.ExtractWorkspace(templateName, options.Path, data); !result.OK {
if err, ok := result.Value.(error); ok {
return core.Result{
Value: core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", tmplName), err),
Value: core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", templateName), err),
OK: false,
}
}
return core.Result{
Value: core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", tmplName), nil),
Value: core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", templateName), nil),
OK: false,
}
}
@ -171,7 +171,7 @@ func writeConfig(path, content string, options Options) core.Result {
return core.Result{Value: path, OK: true}
}
func resolveTemplateName(name string, projType ProjectType) core.Result {
func resolveTemplateName(name string, projectType ProjectType) core.Result {
if name == "" {
return core.Result{
Value: core.E("setup.resolveTemplateName", "template is required", nil),
@ -180,7 +180,7 @@ func resolveTemplateName(name string, projType ProjectType) core.Result {
}
if name == "auto" {
switch projType {
switch projectType {
case TypeGo, TypeWails, TypePHP, TypeNode, TypeUnknown:
return core.Result{Value: "default", OK: true}
}
@ -205,8 +205,8 @@ func templateExists(name string) bool {
return false
}
func defaultBuildCommand(projType ProjectType) string {
switch projType {
func defaultBuildCommand(projectType ProjectType) string {
switch projectType {
case TypeGo, TypeWails:
return "go build ./..."
case TypePHP:
@ -218,8 +218,8 @@ func defaultBuildCommand(projType ProjectType) string {
}
}
func defaultTestCommand(projType ProjectType) string {
switch projType {
func defaultTestCommand(projectType ProjectType) string {
switch projectType {
case TypeGo, TypeWails:
return "go test ./..."
case TypePHP:
@ -231,13 +231,13 @@ func defaultTestCommand(projType ProjectType) string {
}
}
func formatFlow(projType ProjectType) string {
func formatFlow(projectType ProjectType) string {
builder := core.NewBuilder()
builder.WriteString("- Build: `")
builder.WriteString(defaultBuildCommand(projType))
builder.WriteString(defaultBuildCommand(projectType))
builder.WriteString("`\n")
builder.WriteString("- Test: `")
builder.WriteString(defaultTestCommand(projType))
builder.WriteString(defaultTestCommand(projectType))
builder.WriteString("`")
return builder.String()
}