summaryrefslogtreecommitdiff
path: root/internal/task/currentattempt.go
blob: 924e9eab5854e3cecada389c1407032b3ec7ba81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package task

// TaskLookup is the minimal interface CurrentAttempt needs: resolving a
// task by ID and finding tasks that depend on it. Both
// internal/executor.Store and internal/scheduler.StoryStore already satisfy
// this structurally, so CurrentAttempt lives here (in the lowest-level
// package both already import) rather than being duplicated in each.
type TaskLookup interface {
	GetTask(id string) (*Task, error)
	ListDependents(taskID string) ([]*Task, error)
}

// maxAttemptChainWalk bounds CurrentAttempt's forward walk as a defense
// against a pathological/cyclic DependsOn chain (task creation doesn't
// reject cycles) -- in practice, no caller creates chains anywhere near
// this long; a much smaller, intentional policy limit (e.g.
// internal/scheduler's own maxFixAttempts) is what actually bounds real
// fix-attempt chains in normal operation.
const maxAttemptChainWalk = 100

// CurrentAttempt resolves the task that currently represents the logical
// position anchored at anchorID: anchorID itself, or -- if it was rejected
// and a fix attempt exists -- the fix attempt, walking forward through
// however many rejections have occurred. A fix-attempt task is identified
// structurally: a task with the same ParentTaskID as the task it supersedes,
// whose sole DependsOn entry is that task's own ID -- the exact shape a
// fix-attempt is spawned with, whether at a story's root (ParentTaskID ==
// "") or at an arbitrary subtask position (ParentTaskID set). This is the
// one place "which task is current" is ever decided; callers resolve
// through this before checking a position's own completion/verdict state,
// never operating on a raw anchor ID directly once a fix-attempt might
// exist. See docs/superpowers/specs/2026-07-09-recursive-arbitrated-review-design.md.
func CurrentAttempt(store TaskLookup, anchorID string) (*Task, error) {
	current, err := store.GetTask(anchorID)
	if err != nil {
		return nil, err
	}
	for i := 0; i < maxAttemptChainWalk; i++ {
		dependents, err := store.ListDependents(current.ID)
		if err != nil {
			return nil, err
		}
		var next *Task
		for _, d := range dependents {
			if d.ParentTaskID == current.ParentTaskID && d.Agent.Role == current.Agent.Role && len(d.DependsOn) == 1 && d.DependsOn[0] == current.ID {
				next = d
				break
			}
		}
		if next == nil {
			return current, nil
		}
		current = next
	}
	return current, nil
}