summaryrefslogtreecommitdiff
path: root/internal/task/currentattempt.go
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator.local>2026-07-09 04:39:05 +0000
committerClaudomator Agent <agent@claudomator.local>2026-07-09 04:39:05 +0000
commit4ba335c8eacb3f9e20bccce1643b2e5320d5239d (patch)
tree397433d1db8564b3818f6b73744567662c15c9d0 /internal/task/currentattempt.go
parent884da0ba74f622dc5c41cde25710718762321ae8 (diff)
feat(task,executor): add CurrentAttempt resolution, wire maybeUnblockParent to resolve through it
Diffstat (limited to 'internal/task/currentattempt.go')
-rw-r--r--internal/task/currentattempt.go56
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
+}