Issue-lifecycle M1 - D babysit (MRCI / Merge / MainCI / give-up) - Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Implement the poll-and-react babysit states so a bot PR drives itself to a merged, main-green close: MRCI polls CI and reacts, Merge merges and detects the 405 conflict, MainCI polls the merge commit, give-up parks on deadline/iterations. This milestone kills the live selfImprove ... merge -> 405 merge conflicts controller-runtime backoff loop.
Architecture: Per spec. Builds on M0's reconcileLifecycle skeleton + states. The Implement state (M0) gains a re-entry context so fix/resolve/ re-implement turns are fed the failing checks / conflict. Context-guard handover on MainCI failure is M3; M1 re-implements under the maxLifecycleIterations backstop only.
Tech stack: Go, envtest, table-driven. Repo: tatara-operator. Same worktree as M0.
File structure¶
internal/scm/scm.go-SCMReader.GetCommitCIStatus;IssueCommenttype stub (full use M2).internal/scm/github.go,internal/scm/gitlab.go- implementGetCommitCIStatus.internal/scm/fake_test.go(or the controller test fakes) - extend the fake reader/writer.internal/controller/lifecycle.go- MRCI, Merge, MainCI handlers; give-up; Implement re-entry context.internal/controller/lifecycle_test.go- state tests + the 405 regression.api/v1alpha1/task_types.go- addimplementContextstatus field (re-entry prompt detail).
Task 1: GetCommitCIStatus SCM reader cap¶
Files: internal/scm/scm.go, github.go, gitlab.go; Test internal/scm/*_test.go.
- Step 1: Failing tests (httptest server) that
GetCommitCIStatus(ctx, owner, repo, sha)returns"success"|"failure"|"pending"|""from: GitHub combined status + check-runs on the commit; GitLab pipelines for the sha. MirrorderiveGHCIStatus/glCIStatuslogic (reuse those helpers against the commit SHA rather than the PR head). - Step 2: Run - FAIL.
- Step 3: Implement
GetCommitCIStatuson both providers (reuse the existing CI-derivation helpers, pointed at the commit SHA endpoint) and add it to theSCMReaderinterface. Updatewire.goconstructions and the test fakes to satisfy the interface. - Step 4: Test PASS.
- Step 5: Commit
feat(scm): GetCommitCIStatus reader cap (github+gitlab).
Task 2: Implement re-entry context field + prompt¶
Files: api/v1alpha1/task_types.go, internal/controller/lifecycle.go, turnloop.go; Test.
- Step 1: Failing test that the Implement turn prompt includes a re-entry context block when
Status.ImplementContextis set (e.g. "CI failed:") and is the plain plan prompt when empty. - Step 2: Run - FAIL.
- Step 3: Add
implementContextstring toTaskStatus(make manifests generate). In the Implement handler, whenImplementContext != ""build the plan turn text with that block appended (extendplanTurnTextor wrap it), and clearImplementContextafter the run starts so the next fresh entry is clean. - Step 4: Test PASS.
- Step 5: Commit
feat(controller): Implement re-entry context for fix/resolve turns.
Task 3: MRCI poll state¶
Files: internal/controller/lifecycle.go; Test lifecycle_test.go.
- Step 1: Failing tests (table) for the MRCI handler reading
GetPRState(prNumber).CIStatus: - first entry sets
DeadlineAt = now + babysitDeadlineMinutesif unset. pending->ctrl.Result{RequeueAfter: pollRequeue}, state stays MRCI, observetatara_mrci_wait_secondsonly on exit.success-> transition toMerge.failure-> setImplementContextto the failing-checks summary, transition toImplement(fix turn), incrementlifecycleIterations.""(no CI) -> transition toMerge(nothing to wait on; merge gate decides).now > DeadlineAt-> give-up:Commenton the PR (deadline message), transition toParked, incrementtatara_lifecycle_giveup_total{reason="deadline"}.- Step 2: Run - FAIL.
- Step 3: Implement the MRCI handler per the table. Authorship gate first (reuse
GetPRState.Author == botLogin; on mismatch -> Parked with a comment,giveup{reason="not-bot-authored"}). UsesetLifecycleStatefor transitions. - Step 4: Test PASS.
- Step 5: Commit
feat(controller): lifecycle MRCI poll-and-react state.
Task 4: Merge state + 405-conflict regression¶
Files: internal/controller/lifecycle.go; Test lifecycle_test.go.
- Step 1: Failing tests:
mergeAllowedtrue +Mergeok -> recordMergeCommitSHA(from the merge response or a follow-upGetPRState), transition toMainCI.mergeAllowedfalse -> stay/park per policy (afterApproval always true, so this is the autoMergeOnGreenCI-not-green case: requeue under deadline, NOT error).- REGRESSION:
Mergereturns*scm.HTTPError{Status:405}-> setImplementContextto a resolve-conflict instruction ("Merge conflict on<headBranch>; rebase<defaultBranch>, resolve, push"), transition toImplement, incrementlifecycleIterations. Must NOT return the error to controller-runtime (asserterr == niland state ==Implement). - other
Mergeerror ->ctrl.Result{RequeueAfter: pollRequeue}under deadline (transient), no infinite loop (deadline -> Parked). - Step 2: Run - FAIL.
- Step 3: Implement the Merge handler:
mergeAllowed(reuse), thenwriter.Merge. Inspect the error witherrors.As(&scm.HTTPError):Status==405(or body contains "conflict") -> resolve-conflict re-entry; else transient requeue underDeadlineAt; on deadline -> Parked (comment + leave open). On success capture the merge SHA (extendMergeto return the SHA, or callGetPRState/a commits lookup) intoStatus.MergeCommitSHA. - Step 4: Test PASS (especially the 405 regression).
- Step 5: Commit
fix(controller): Merge 405-conflict spawns resolve turn instead of error-looping.
Task 5: MainCI poll state + close¶
Files: internal/controller/lifecycle.go; Test.
- Step 1: Failing tests for the MainCI handler reading
GetCommitCIStatus(MergeCommitSHA): - first entry sets a fresh
DeadlineAt = now + babysitDeadlineMinutes. pending-> requeue.success-> post a closing comment on the issue (reusewriter.Comment),CloseIssue(idempotent ifCloses #Nalready closed it - swallow a not-found/already-closed), transition toDone, observetatara_lifecycle_seconds.failure-> setImplementContext("main pipeline failed after merge:; re-implement on <headBranch>"), transition toImplement, incrementlifecycleIterations. (Context-guard handover is M3.)now > DeadlineAt-> Parked (comment),giveup{reason="deadline"}.- Step 2: Run - FAIL.
- Step 3: Implement the MainCI handler per the table.
- Step 4: Test PASS.
- Step 5: Commit
feat(controller): lifecycle MainCI poll + close on green.
Task 6: maxLifecycleIterations backstop¶
Files: internal/controller/lifecycle.go; Test.
- Step 1: Failing test that when entering Implement with
lifecycleIterations >= maxLifecycleIterations(default 10) the handler parks instead (comment +giveup{reason="maxIterations"}-> Parked), never spawning another agent run. - Step 2: Run - FAIL.
- Step 3: Implement the backstop check at the top of the Implement handler (before spawning the pod). Use
project.Spec.Agent.MaxLifecycleIterations(default 10). - Step 4: Test PASS.
- Step 5: Commit
feat(controller): lifecycle iteration backstop parks runaway loops.
Task 7: Closes #N on the lifecycle MR body¶
Files: internal/controller/lifecycle.go (or writeback.go body helper); Test.
- Step 1: Failing test that an issue-keyed lifecycle Task's opened MR body contains
Closes #<issueNumber>(the issue's own repo only - reuse the primary-repo scoping rule), and a PR-entry lifecycle Task (no source issue) does not. - Step 2: Run - FAIL.
- Step 3: Implement a lifecycle-aware MR body: extend the open-change body builder used by the Implement state to append
Closes #NwhenSource.Number > 0 && !Source.IsPRand the repo is the issue's own repo. (This is theCloseslogic that does NOT exist on main today - build it here, scoped, not fromfeat/scm-auto-merge.) - Step 4: Test PASS.
- Step 5: Commit
feat(controller): lifecycle MR body Closes the linked issue.
Task 8: Full suite + lint + the live-loop assertion¶
Files: all M1 tests.
- Step 1:
make test(with-race) green; the 405 regression (Task 4) is the explicit live-loop guard. - Step 2:
gofmt -lempty,golangci-lint runclean,helm lint charts/*clean. - Step 3: Re-run
make manifests generate; confirm no CRD drift uncommitted. - Step 4: Commit any fmt/manifest deltas
chore: regen + lint after M1.
Self-review checklist¶
- No reconcile path returns an error to controller-runtime for a merge conflict (the 405 regression test proves it). The old
writeBackSelfImproveerror-return path is no longer reachable for lifecycle tasks. - Every poll state has a
DeadlineAtand a give-up -> Parked transition. -
lifecycleIterationsis incremented on every Implement re-entry; the backstop parks at the cap. -
ImplementContextis cleared after consumption (no stale fix prompt on a later fresh entry). - MainCI close is idempotent vs
Closes #N. - Full suite green
-race; gofmt/golangci/helm-lint clean; no CRD drift.