2026-08-21 15:58:46 -07:00
|
|
|
package statemachine
|
|
|
|
|
|
2026-08-21 18:07:12 -07:00
|
|
|
import (
|
2026-08-21 21:51:41 -07:00
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-08-21 18:07:12 -07:00
|
|
|
"go.temporal.io/sdk/workflow"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// OrchestratorWorkflow orchestrates multi-agent work on a target repository.
|
|
|
|
|
func OrchestratorWorkflow(ctx workflow.Context, in OrchestratorInput) (OrchestratorOutput, error) {
|
2026-08-21 21:51:41 -07:00
|
|
|
output := OrchestratorOutput{
|
|
|
|
|
MilestoneComplete: false,
|
|
|
|
|
Done: false,
|
2026-08-21 18:07:12 -07:00
|
|
|
LastError: "",
|
2026-08-21 21:51:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 1: Clone the repository
|
|
|
|
|
cloneErr := workflow.ExecuteActivity(
|
|
|
|
|
ctx,
|
|
|
|
|
"CloneRepoActivity",
|
|
|
|
|
map[string]interface{}{
|
|
|
|
|
"RemoteURL": in.RemoteURL,
|
|
|
|
|
"TargetRepoPath": in.TargetRepoPath,
|
|
|
|
|
},
|
|
|
|
|
).Get(ctx, nil)
|
|
|
|
|
|
|
|
|
|
if cloneErr != nil {
|
|
|
|
|
output.LastError = fmt.Sprintf("Clone failed: %v", cloneErr)
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Read tasks from board.md
|
|
|
|
|
tasksToRun, err := readTasksFromBoard(in.TargetRepoPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
output.LastError = fmt.Sprintf("Failed to read tasks: %v", err)
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(tasksToRun) == 0 {
|
|
|
|
|
output.LastError = "No tasks found in board.md"
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 3: Process each task
|
|
|
|
|
completedTasks := 0
|
|
|
|
|
for _, task := range tasksToRun {
|
|
|
|
|
taskID := task["id"].(string)
|
|
|
|
|
taskDesc := task["description"].(string)
|
|
|
|
|
|
|
|
|
|
// taskID will be used for worktree and branch
|
|
|
|
|
|
|
|
|
|
// Add worktree
|
|
|
|
|
var worktreePath string
|
|
|
|
|
wtErr := workflow.ExecuteActivity(
|
|
|
|
|
ctx,
|
|
|
|
|
"GitWorktreeAddActivity",
|
|
|
|
|
map[string]interface{}{
|
|
|
|
|
"RepoPath": in.TargetRepoPath,
|
|
|
|
|
"TaskID": taskID,
|
|
|
|
|
},
|
|
|
|
|
).Get(ctx, &worktreePath)
|
|
|
|
|
|
|
|
|
|
if wtErr != nil {
|
|
|
|
|
continue // Skip this task on error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call implementer to generate code
|
|
|
|
|
var implOutput map[string]interface{}
|
|
|
|
|
implErr := workflow.ExecuteActivity(
|
|
|
|
|
ctx,
|
|
|
|
|
"ImplementerActivity",
|
|
|
|
|
map[string]interface{}{
|
|
|
|
|
"TaskID": taskID,
|
|
|
|
|
"Description": taskDesc,
|
|
|
|
|
"WorktreePath": worktreePath,
|
|
|
|
|
"Prompt": PromptSpec{
|
|
|
|
|
TemplateRef: "implementer/default.tmpl",
|
|
|
|
|
Model: ModelSpec{
|
|
|
|
|
ModelID: in.Config.RolePrompts["implementer"].Model.ModelID,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
).Get(ctx, &implOutput)
|
|
|
|
|
|
|
|
|
|
if implErr != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Commit changes
|
|
|
|
|
commitErr := workflow.ExecuteActivity(
|
|
|
|
|
ctx,
|
|
|
|
|
"GitCommitActivity",
|
|
|
|
|
map[string]interface{}{
|
|
|
|
|
"WorktreePath": worktreePath,
|
|
|
|
|
"Message": fmt.Sprintf("%s: implementation", taskID),
|
|
|
|
|
},
|
|
|
|
|
).Get(ctx, nil)
|
|
|
|
|
|
|
|
|
|
if commitErr == nil {
|
|
|
|
|
completedTasks++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 4: Push to remote
|
|
|
|
|
pushErr := workflow.ExecuteActivity(
|
|
|
|
|
ctx,
|
|
|
|
|
"GitPushActivity",
|
|
|
|
|
map[string]interface{}{
|
|
|
|
|
"RepoPath": in.TargetRepoPath,
|
|
|
|
|
},
|
|
|
|
|
).Get(ctx, nil)
|
|
|
|
|
|
|
|
|
|
if pushErr != nil {
|
|
|
|
|
output.LastError = fmt.Sprintf("Push failed: %v", pushErr)
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 5: Squash merge all task branches
|
|
|
|
|
branches := make([]string, len(tasksToRun))
|
|
|
|
|
for i, task := range tasksToRun {
|
|
|
|
|
branches[i] = fmt.Sprintf("task/%s", task["id"].(string))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mergeErr := workflow.ExecuteActivity(
|
|
|
|
|
ctx,
|
|
|
|
|
"GitSquashMergeActivity",
|
|
|
|
|
map[string]interface{}{
|
|
|
|
|
"RepoPath": in.TargetRepoPath,
|
|
|
|
|
"Branches": branches,
|
|
|
|
|
"Message": fmt.Sprintf("%s: squash merge all tasks", in.Milestone),
|
|
|
|
|
},
|
|
|
|
|
).Get(ctx, nil)
|
|
|
|
|
|
|
|
|
|
if mergeErr != nil {
|
|
|
|
|
output.LastError = fmt.Sprintf("Merge failed: %v", mergeErr)
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Success!
|
|
|
|
|
output.MilestoneComplete = true
|
|
|
|
|
output.Done = true
|
|
|
|
|
output.LastError = fmt.Sprintf("Completed %d tasks successfully", completedTasks)
|
|
|
|
|
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readTasksFromBoard reads tasks from tasks/board.md
|
|
|
|
|
func readTasksFromBoard(repoPath string) ([]map[string]interface{}, error) {
|
|
|
|
|
boardPath := filepath.Join(repoPath, "tasks", "board.md")
|
|
|
|
|
|
|
|
|
|
content, err := ioutil.ReadFile(boardPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines := strings.Split(string(content), "\n")
|
|
|
|
|
var tasks []map[string]interface{}
|
|
|
|
|
|
|
|
|
|
for _, line := range lines {
|
|
|
|
|
// Parse markdown table rows: | T1 | Description | [ ] | ...
|
|
|
|
|
if strings.HasPrefix(strings.TrimSpace(line), "|") && !strings.Contains(line, "---|") && !strings.Contains(line, "ID") {
|
|
|
|
|
parts := strings.Split(line, "|")
|
|
|
|
|
if len(parts) >= 4 {
|
|
|
|
|
id := strings.TrimSpace(parts[1])
|
|
|
|
|
desc := strings.TrimSpace(parts[2])
|
|
|
|
|
|
|
|
|
|
if id != "" && desc != "" {
|
|
|
|
|
tasks = append(tasks, map[string]interface{}{
|
|
|
|
|
"id": id,
|
|
|
|
|
"description": desc,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tasks, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isPiStreamTimeout checks if an error is a 504 stream timeout from Pi command
|
|
|
|
|
func isPiStreamTimeout(err error) bool {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return strings.Contains(err.Error(), "PiStreamTimeout")
|
2026-08-21 18:07:12 -07:00
|
|
|
}
|