diff options
Diffstat (limited to 'internal/task/currentattempt.go')
| -rw-r--r-- | internal/task/currentattempt.go | 56 |
1 files changed, 56 insertions, 0 deletions
diff --git a/internal/task/currentattempt.go b/internal/task/currentattempt.go new file mode 100644 index 0000000..24eed10 --- /dev/null +++ b/internal/task/currentattempt.go @@ -0,0 +1,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 && len(d.DependsOn) == 1 && d.DependsOn[0] == current.ID { + next = d + break + } + } + if next == nil { + return current, nil + } + current = next + } + return current, nil +} |
