From a90ac76f3ccf0bf1259b7ef45e5f331d2b4ec26f Mon Sep 17 00:00:00 2001 From: Agent Date: Thu, 9 Jul 2026 19:45:21 +0000 Subject: refactor(scheduler): migrate story fix loop off RootTaskID re-pointing onto CurrentAttempt resolution --- internal/scheduler/story_orchestrator.go | 36 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) (limited to 'internal/scheduler/story_orchestrator.go') diff --git a/internal/scheduler/story_orchestrator.go b/internal/scheduler/story_orchestrator.go index c3d5215..8061b35 100644 --- a/internal/scheduler/story_orchestrator.go +++ b/internal/scheduler/story_orchestrator.go @@ -221,9 +221,9 @@ func (o *StoryOrchestrator) processStory(ctx context.Context, st *story.Story) { return } - root, err := o.Store.GetTask(st.RootTaskID) + root, err := task.CurrentAttempt(o.Store, st.RootTaskID) if err != nil { - o.logf("story orchestrator: get root task", "storyID", st.ID, "rootTaskID", st.RootTaskID, "error", err) + o.logf("story orchestrator: resolve current root attempt", "storyID", st.ID, "rootTaskID", st.RootTaskID, "error", err) return } root = o.autoAccept(st, root) @@ -495,24 +495,27 @@ const maxFixAttempts = 3 // structural discoverability/audit trail — the rejected root is already // COMPLETED, so this dependency is immediately satisfied), whose // instructions carry the original story spec/acceptance criteria plus the -// arbitration's rejection reasoning, then re-points st.RootTaskID at the new -// task and resets st.Status to IN_PROGRESS. The very next tick re-enters -// processStory's normal Builder->Evaluators->Arbitration flow against the -// new root, completely unchanged — no new pipeline, the existing one -// re-entered. +// arbitration's rejection reasoning, then resets st.Status to IN_PROGRESS. +// st.RootTaskID is never written here — it is an immutable anchor set once +// at story creation; every caller that needs "the task currently +// representing the story's root" resolves it via task.CurrentAttempt, which +// walks forward through however many fix-attempt links exist. The very next +// tick re-enters processStory's normal Builder->Evaluators->Arbitration flow +// against whatever CurrentAttempt now resolves to — completely unchanged, no +// new pipeline, the existing one re-entered. // // Idempotency is structural, mirroring every other stage in this file: it // looks for an existing builder-role dependent of the rejected root before // spawning a new one, so calling this repeatedly (or after a restart between -// "task spawned" and "story updated") never spawns duplicates. The +// "task spawned" and "story status reset") never spawns duplicates. The // maxFixAttempts cap is only checked in the "spawn a new one" branch — a -// dependent that was already committed to being spawned still gets -// re-pointed to, regardless of the cap, since refusing to do so would leave -// a task dangling with nothing tracking it. +// dependent that was already committed to being spawned still gets used, +// regardless of the cap, since refusing to do so would leave a task +// dangling with nothing tracking it. func (o *StoryOrchestrator) ensureFixAttempt(ctx context.Context, st *story.Story) { - oldRoot, err := o.Store.GetTask(st.RootTaskID) + oldRoot, err := task.CurrentAttempt(o.Store, st.RootTaskID) if err != nil { - o.logf("story orchestrator: fix attempt: get rejected root task", "storyID", st.ID, "rootTaskID", st.RootTaskID, "error", err) + o.logf("story orchestrator: fix attempt: resolve current root attempt", "storyID", st.ID, "rootTaskID", st.RootTaskID, "error", err) return } @@ -547,10 +550,9 @@ func (o *StoryOrchestrator) ensureFixAttempt(ctx context.Context, st *story.Stor fix = nt } - st.RootTaskID = fix.ID st.Status = "IN_PROGRESS" if err := o.Store.UpdateStory(st); err != nil { - o.logf("story orchestrator: fix attempt: repoint story root", "storyID", st.ID, "error", err) + o.logf("story orchestrator: fix attempt: reset story status", "storyID", st.ID, "error", err) } } @@ -626,9 +628,9 @@ func (o *StoryOrchestrator) fixAttemptDepth(root *task.Task) int { // (auto-accepted from READY exactly like every other task in this pipeline), // hands off to finalizeRetro to emit KindRetroCaptured. func (o *StoryOrchestrator) processRetro(ctx context.Context, st *story.Story) { - root, err := o.Store.GetTask(st.RootTaskID) + root, err := task.CurrentAttempt(o.Store, st.RootTaskID) if err != nil { - o.logf("story orchestrator: retro: get root task", "storyID", st.ID, "rootTaskID", st.RootTaskID, "error", err) + o.logf("story orchestrator: retro: resolve current root attempt", "storyID", st.ID, "rootTaskID", st.RootTaskID, "error", err) return } -- cgit v1.2.3