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 }