Files

79 lines
3.5 KiB
Markdown
Raw Permalink Normal View History

# T0.6: TaskUnit Workflow
## Scope
Implement `statemachine/taskunit.go` with retry loops, timeout escalation, and lessons injection.
## Implementation
### File: `statemachine/taskunit.go`
```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/<TaskID>.jsonl) for Planner's review
## Verification
```bash
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 → BaseTimeout*1 fails, Implementer timeout 2nd → BaseTimeout*2 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