package statemachine import ( "fmt" "io/ioutil" "path/filepath" "strings" "time" "go.temporal.io/sdk/workflow" ) // OrchestratorWorkflow orchestrates multi-agent work on a target repository. // Reconciliation loop: Plan → Dispatch → Review → Update → Repeat func OrchestratorWorkflow(ctx workflow.Context, in OrchestratorInput) (OrchestratorOutput, error) { logger := workflow.GetLogger(ctx) output := OrchestratorOutput{ MilestoneComplete: false, Done: false, LastError: "", } // Clone repo once at start activityOpts := workflow.ActivityOptions{ StartToCloseTimeout: 10 * time.Minute, ScheduleToCloseTimeout: 15 * time.Minute, } ctxWithOpts := workflow.WithActivityOptions(ctx, activityOpts) if err := workflow.ExecuteActivity(ctxWithOpts, "CloneRepoActivity", map[string]interface{}{ "RemoteURL": in.RemoteURL, "TargetRepoPath": in.TargetRepoPath, }).Get(ctx, nil); err != nil { output.LastError = fmt.Sprintf("Clone failed: %v", err) return output, nil } // Prepare skills once if len(in.Config.Skills) > 0 { if err := workflow.ExecuteActivity(ctxWithOpts, "PrepareSkillsActivity", map[string]interface{}{ "Skills": in.Config.Skills, "StreamTimeout": 30 * time.Second, "Provider": in.PiProvider, }).Get(ctx, nil); err != nil { logger.Info("skill preparation failed (continuing anyway)", "error", err) } } // Main reconciliation loop var paused bool var completedTasks int cycleCount := 0 completedBranches := []string{} for cycleCount < in.MaxCyclesBeforeCAN { cycleCount++ logger.Info("orchestrator cycle", "cycle", cycleCount) // Read board state bordState, _ := readTasksFromBoard(in.TargetRepoPath) // Call PlanningActivity to decide what to dispatch implOpts := workflow.ActivityOptions{ StartToCloseTimeout: 30 * time.Minute, ScheduleToCloseTimeout: 35 * time.Minute, } implCtx := workflow.WithActivityOptions(ctx, implOpts) var planOutput map[string]interface{} if err := workflow.ExecuteActivity(implCtx, "PlanningActivity", map[string]interface{}{ "Config": in.Config, "BoardState": fmt.Sprintf("%v", bordState), "RepoPath": in.TargetRepoPath, "Milestone": in.Milestone, }).Get(ctx, &planOutput); err != nil { logger.Info("planning failed", "error", err) break } // Extract tasks to dispatch var tasksToDispatch []interface{} if tasks, ok := planOutput["TasksToDispatch"]; ok { tasksToDispatch = tasks.([]interface{}) } if len(tasksToDispatch) == 0 { logger.Info("no tasks to dispatch, milestone complete") output.MilestoneComplete = true break } // Fan-out: Start TaskUnit workflows for each task logger.Info("dispatching task units", "count", len(tasksToDispatch)) var childWFs []workflow.Future for _, taskIDRaw := range tasksToDispatch { taskID := taskIDRaw.(string) if paused { logger.Info("skipping dispatch due to pause signal", "task", taskID) continue } // Child workflow: TaskUnitWorkflow childOpts := workflow.ChildWorkflowOptions{ WorkflowID: fmt.Sprintf("%s-%s-cycle%d", in.Milestone, taskID, cycleCount), } childCtx := workflow.WithChildOptions(ctx, childOpts) taskUnitInput := TaskUnitInput{ TaskID: taskID, RemoteURL: in.RemoteURL, TargetRepoPath: in.TargetRepoPath, Milestone: in.Milestone, Config: in.Config, DryRun: in.DryRun, } future := workflow.ExecuteChildWorkflow(childCtx, TaskUnitWorkflow, taskUnitInput) childWFs = append(childWFs, future) } // Fan-in: Wait for all TaskUnits to complete logger.Info("waiting for task units", "count", len(childWFs)) for _, future := range childWFs { var taskOutput TaskUnitOutput if err := future.Get(ctx, &taskOutput); err != nil { logger.Info("task unit failed", "task", taskOutput.TaskID, "error", err) } else if taskOutput.Status == "success" { completedTasks++ completedBranches = append(completedBranches, taskOutput.Branch) logger.Info("task unit succeeded", "task", taskOutput.TaskID) } } // Board update: Call PlanningActivity again to update board + commit if err := workflow.ExecuteActivity(implCtx, "PlanningActivity", map[string]interface{}{ "Config": in.Config, "BoardState": fmt.Sprintf("%v", bordState), "RepoPath": in.TargetRepoPath, "Milestone": in.Milestone, }).Get(ctx, nil); err != nil { logger.Info("board update failed", "error", err) } // Push completed branches if err := workflow.ExecuteActivity(ctxWithOpts, "GitPushActivity", map[string]interface{}{ "RepoPath": in.TargetRepoPath, }).Get(ctx, nil); err != nil { logger.Info("push failed", "error", err) } // Squash merge completed branches when milestone ready if output.MilestoneComplete && len(completedBranches) > 0 { if err := workflow.ExecuteActivity(ctxWithOpts, "GitSquashMergeActivity", map[string]interface{}{ "RepoPath": in.TargetRepoPath, "Branches": completedBranches, "Message": fmt.Sprintf("%s: squash merge completed tasks", in.Milestone), }).Get(ctx, nil); err != nil { logger.Info("squash merge failed", "error", err) } } } // Use continue-as-new if hit cycle cap if cycleCount >= in.MaxCyclesBeforeCAN { logger.Info("cycle cap reached, continuing as new", "cycles", cycleCount) nextInput := in nextInput.CycleCount = cycleCount return output, workflow.NewContinueAsNewError(ctx, OrchestratorWorkflow, nextInput) } // Success output.Done = true output.LastError = fmt.Sprintf("Completed %d tasks in %d cycles", completedTasks, cycleCount) 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 }