Files
Test 11a1c1196a Add testable task descriptions: T0.1 through T0.9
Each task includes:
- Scope: what to build
- Implementation: code sketches + details
- Verification: concrete test criteria
- Done criteria: acceptance checklist
2026-08-20 22:41:54 -07:00

3.5 KiB

T0.6: TaskUnit Workflow

Scope

Implement statemachine/taskunit.go with retry loops, timeout escalation, and lessons injection.

Implementation

File: statemachine/taskunit.go

func TaskUnitWorkflow(ctx workflow.Context, in TaskUnitInput) (TaskUnitOutput, error)
    // 1. Call GitWorktreeAddActivity(ctx, {RepoPath, TaskID}) → get worktree path
    // 2. Lessons file init: try ReadLessonsActivity, may be empty on first run
    // 3. Retry loop:
    //    timeoutAttempt := 1
    //    for judgeAttempt := 1; judgeAttempt <= in.MaxJudgeRetries; judgeAttempt++
    //
    //    SetupActivity options:
    //      StartToCloseTimeout: in.BaseTimeout * time.Duration(timeoutAttempt)
    //      HeartbeatTimeout: (in.BaseTimeout * time.Duration(timeoutAttempt)) / 4
    //      RetryPolicy: {MaximumAttempts: 1} // NO retries; we manage them in the loop
    //
    //    Call ImplementerActivity(ctx, {Lessons: lessons, ...})
    //    If isStartToCloseTimeout(err):
    //        timeoutAttempt++
    //        judgeAttempt-- // don't consume a judge retry on timeout
    //        continue // next loop iteration has longer timeout
    //    If err != nil:
    //        return TaskUnitOutput{Verdict: "fail", Critique: err.Error()}, nil
    //
    //    Call RunIntegrationTestActivity(ctx, {WorktreePath, TestCmd})
    //    If integration test fails:
    //        // Some tasks don't have tests; pass if no test defined
    //
    //    Call JudgeActivity(ctx, {Diff, IntegrationTestResult})
    //    If judge.Verdict == "pass":
    //        Call GitCommitActivity(ctx, {WorktreePath, "T0.x: implementation"})
    //        return TaskUnitOutput{Verdict: "pass", Branch: "task/T0.x"}
    //    Else:
    //        Call UpdateLessonsActivity(ctx, {Critique})
    //        lessons = ReadLessonsActivity() // updated lessons for next attempt
    //        continue // next judgeAttempt with lessons injected
    //    End retries
    //
    // 4. If we exit loop without pass: return fail verdict

Retry Logic Detail

TimeoutAttempt vs JudgeAttempt:

  • TimeoutAttempt: activity ran out of time, next attempt has longer StartToCloseTimeout
  • JudgeAttempt: activity finished but output wrong, lessons injected, duration doesn't change
  • They're independent counters so timeout escalation doesn't consume judge retries

Lessons injection:

  • Before each ImplementerActivity, render prompt with Lessons appended: "Known errors from prior attempts:\n{lessons}"
  • On judge failure, append new lesson to lessons file
  • Lessons persist within TaskUnitWorkflow (shared memory)
  • Lessons also written to disk (tasks/.orchestrator/lessons/.jsonl) for Planner's review

Verification

cd /Users/rockliang/workplace/Poimen/workflows
go test -v ./tests -run TestTaskUnit

# Test file: tests/taskunit_workflow_test.go

Test cases (mocked activities):

  1. Pass on first try: Implementer→TestPass→JudgePass → return pass
  2. Fail then pass after lesson: Implementer→TestPass→JudgeFail (append lesson) → Implementer (lessons injected)→TestPass→JudgePass → return pass
  3. Retries exhausted: Implementer→JudgeFail 3 times → return fail
  4. Timeout escalation: Implementer timeout 1st → BaseTimeout1 fails, Implementer timeout 2nd → BaseTimeout2 succeeds → continue

Done Criteria

  • go test ./tests -run TestTaskUnit passes all 4 cases
  • Split timeout/judge-fail counters work correctly
  • Lessons inject into prompt without error
  • No infinite loops on mocked failures