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"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-08-21 18:07:12 -07:00
|
|
|
"go.temporal.io/sdk/workflow"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
// TaskUnitWorkflow executes a single task with retries and judge review.
|
|
|
|
|
// Flow: Worktree → Implementer (retry) → Test → Judge → Commit or Retry
|
2026-08-21 18:07:12 -07:00
|
|
|
func TaskUnitWorkflow(ctx workflow.Context, in TaskUnitInput) (TaskUnitOutput, error) {
|
2026-08-26 15:00:11 -07:00
|
|
|
logger := workflow.GetLogger(ctx)
|
2026-08-21 18:07:12 -07:00
|
|
|
output := TaskUnitOutput{
|
|
|
|
|
TaskID: in.TaskID,
|
2026-08-26 15:00:11 -07:00
|
|
|
Status: "failed",
|
|
|
|
|
Reason: "",
|
|
|
|
|
Branch: fmt.Sprintf("task/%s", in.TaskID),
|
|
|
|
|
Changes: "",
|
2026-08-21 18:07:12 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
activityOpts := workflow.ActivityOptions{
|
|
|
|
|
StartToCloseTimeout: 30 * time.Minute,
|
|
|
|
|
ScheduleToCloseTimeout: 35 * time.Minute,
|
|
|
|
|
}
|
|
|
|
|
actCtx := workflow.WithActivityOptions(ctx, activityOpts)
|
|
|
|
|
|
|
|
|
|
// Add worktree
|
2026-08-21 21:51:41 -07:00
|
|
|
var worktreePath string
|
2026-08-26 15:00:11 -07:00
|
|
|
if err := workflow.ExecuteActivity(actCtx, "GitWorktreeAddActivity", map[string]interface{}{
|
|
|
|
|
"RepoPath": in.TargetRepoPath,
|
|
|
|
|
"TaskID": in.TaskID,
|
|
|
|
|
}).Get(ctx, &worktreePath); err != nil {
|
|
|
|
|
output.Reason = fmt.Sprintf("worktree add failed: %v", err)
|
2026-08-21 21:51:41 -07:00
|
|
|
return output, nil
|
|
|
|
|
}
|
2026-08-21 18:07:12 -07:00
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
logger.Info("task unit started", "task", in.TaskID, "worktree", worktreePath)
|
2026-08-21 21:51:41 -07:00
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
// Retry loop: implementer + test + judge
|
|
|
|
|
maxRetries := 3
|
|
|
|
|
var lessons string
|
|
|
|
|
|
|
|
|
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
|
|
|
|
logger.Info("attempt", "task", in.TaskID, "attempt", attempt)
|
|
|
|
|
|
|
|
|
|
// Call ImplementerActivity with escalating timeout
|
|
|
|
|
timeoutMultiplier := int64(attempt)
|
|
|
|
|
implOpts := workflow.ActivityOptions{
|
|
|
|
|
StartToCloseTimeout: time.Duration(timeoutMultiplier*30) * time.Minute,
|
|
|
|
|
ScheduleToCloseTimeout: time.Duration(timeoutMultiplier*35) * time.Minute,
|
2026-08-21 21:51:41 -07:00
|
|
|
}
|
2026-08-26 15:00:11 -07:00
|
|
|
implCtx := workflow.WithActivityOptions(ctx, implOpts)
|
2026-08-21 21:51:41 -07:00
|
|
|
|
|
|
|
|
var implOutput map[string]interface{}
|
2026-08-26 15:00:11 -07:00
|
|
|
implErr := workflow.ExecuteActivity(implCtx, "ImplementerActivity", map[string]interface{}{
|
|
|
|
|
"Config": in.Config,
|
|
|
|
|
"TaskID": in.TaskID,
|
|
|
|
|
"WorktreePath": worktreePath,
|
|
|
|
|
"Lessons": lessons,
|
|
|
|
|
}).Get(ctx, &implOutput)
|
2026-08-21 21:51:41 -07:00
|
|
|
|
|
|
|
|
if implErr != nil {
|
2026-08-26 15:00:11 -07:00
|
|
|
if attempt < maxRetries {
|
|
|
|
|
logger.Info("implementer failed, will retry", "task", in.TaskID, "attempt", attempt, "error", implErr)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
output.Reason = fmt.Sprintf("implementer exhausted after %d attempts: %v", maxRetries, implErr)
|
2026-08-21 21:51:41 -07:00
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
// Run integration test
|
|
|
|
|
var testOutput map[string]interface{}
|
|
|
|
|
if err := workflow.ExecuteActivity(actCtx, "RunIntegrationTestActivity", map[string]interface{}{
|
|
|
|
|
"WorktreePath": worktreePath,
|
|
|
|
|
"TestCmd": "go test ./...",
|
|
|
|
|
}).Get(ctx, &testOutput); err != nil {
|
|
|
|
|
logger.Info("test failed", "task", in.TaskID, "error", err)
|
|
|
|
|
testOutput = map[string]interface{}{
|
|
|
|
|
"success": false,
|
|
|
|
|
"logs": fmt.Sprintf("test error: %v", err),
|
2026-08-21 21:51:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
// Get diff
|
|
|
|
|
var diff string
|
|
|
|
|
if err := workflow.ExecuteActivity(actCtx, "GitDiffActivity", map[string]interface{}{
|
|
|
|
|
"WorktreePath": worktreePath,
|
|
|
|
|
}).Get(ctx, &diff); err != nil {
|
|
|
|
|
logger.Info("diff failed", "task", in.TaskID, "error", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call JudgeActivity
|
|
|
|
|
var judgeOutput map[string]interface{}
|
|
|
|
|
if err := workflow.ExecuteActivity(actCtx, "JudgeActivity", map[string]interface{}{
|
|
|
|
|
"Config": in.Config,
|
|
|
|
|
"Diff": diff,
|
|
|
|
|
"IntegrationTestLogs": fmt.Sprintf("%v", testOutput),
|
|
|
|
|
}).Get(ctx, &judgeOutput); err != nil {
|
|
|
|
|
logger.Info("judge failed", "task", in.TaskID, "error", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verdict, _ := judgeOutput["Verdict"].(string)
|
|
|
|
|
critique, _ := judgeOutput["Critique"].(string)
|
|
|
|
|
|
2026-08-21 21:51:41 -07:00
|
|
|
if verdict == "pass" {
|
2026-08-26 15:00:11 -07:00
|
|
|
// Commit and success
|
|
|
|
|
if err := workflow.ExecuteActivity(actCtx, "GitCommitActivity", map[string]interface{}{
|
|
|
|
|
"WorktreePath": worktreePath,
|
|
|
|
|
"Message": fmt.Sprintf("%s: implementation", in.TaskID),
|
|
|
|
|
}).Get(ctx, nil); err != nil {
|
|
|
|
|
output.Reason = fmt.Sprintf("commit failed: %v", err)
|
2026-08-21 21:51:41 -07:00
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
output.Status = "success"
|
|
|
|
|
output.Changes = fmt.Sprintf("completed in %d attempt(s)", attempt)
|
|
|
|
|
logger.Info("task success", "task", in.TaskID, "attempt", attempt)
|
2026-08-21 21:51:41 -07:00
|
|
|
return output, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
// Judge failed: append to lessons and retry
|
|
|
|
|
if attempt < maxRetries {
|
|
|
|
|
lessons = fmt.Sprintf("%s\nAttempt %d critique: %s", lessons, attempt, critique)
|
|
|
|
|
logger.Info("judge rejected, appending to lessons and retrying", "task", in.TaskID, "attempt", attempt)
|
|
|
|
|
continue
|
2026-08-21 21:51:41 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
// Exhausted retries after judge failures
|
|
|
|
|
output.Reason = fmt.Sprintf("judge rejected after %d attempts. Last critique: %s", maxRetries, critique)
|
|
|
|
|
logger.Info("judge exhausted retries", "task", in.TaskID)
|
|
|
|
|
return output, nil
|
2026-08-21 21:51:41 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:00:11 -07:00
|
|
|
output.Reason = "exhausted all retry attempts"
|
2026-08-21 18:07:12 -07:00
|
|
|
return output, nil
|
|
|
|
|
}
|