Cheap Adjacents (Token Conservation, Component 5) Implementation Plan¶
For agentic workers: this plan is executed by parallel implementation subagents driven by the orchestrator. REQUIRED SUB-SKILL: superpowers:subagent-driven-development
Goal: land the four independent, low-effort, high-ROI token-conservation adjacents from component 5 of the token-conservation design (docs/superpowers/specs/2026-07-04-token-conservation-design.md): (1) stretch mrScan/issueScan/refine scan cadence from hourly to every 2-4h; (2) activate the already-live stale-proposal reaper via staleProposalDays=14 AND gate bot-authored brainstorming proposals on human activity so churn stops at source; (3) fix the implementPrompt double-append of platformProblemGuidance+toolingConsumeGuidance (~219 tok/Implement turn-0); (4) add a per-Task cumulative-token runaway backstop for the turn-uncapped implement/issueLifecycle kinds.
Architecture: all code changes are in tatara-operator (Go, controller-runtime operator). All configuration changes are in tatara-helmfile (Project CR values rendered verbatim by the tatara-project chart, charts/tatara-project/templates/project.yaml does toYaml .Values.project.spec). No tatara-cli/wrapper/skills changes. Deploy is GitOps: merge operator main (CI builds+pushes image+chart), then one tatara-helmfile MR bumping the tatara-operator chart version + pinned image.tag AND applying the Project values edits.
Tech Stack: Go 1.26.3, controller-runtime, kubebuilder CRDs, testify + stdlib testing table tests, Prometheus client, Helm/helmfile YAML.
Global Constraints¶
- Newest stable Go; pin the exact minor in go.mod. gofmt + golangci-lint must pass. Wrap errors with %w. Table-driven tests with t.Run.
- KISS. No tech debt; if complex, note rationale in MEMORY.md.
- JSON logs via stdlib log/slog. Expose /metrics Prometheus on every service. Log business actions at INFO with structured fields.
- Charts via 'helm create' then edited, never hand-rolled. Charts cluster-agnostic.
- values.yaml rule: NO plain ENVs, NO lists in values.yaml. camelCase scalar in values.yaml -> kebab-case key in ConfigMap/Secret -> workload consumes via envFrom. List-shaped data goes into a templated ConfigMap read at runtime. (Note: CRD spec fields are exempt from this - they are typed API fields, not helm values; a map[string]string on a CRD spec is fine.)
- Model IDs are authoritative literals: claude-opus-4-8 (Opus), claude-sonnet-5 (Sonnet). Effort enum: low|medium|high|xhigh|max.
- Deploy ONLY via tatara-helmfile GitOps: merge component repo main (CI builds+pushes image/chart), then a tatara-helmfile MR bumping BOTH the chart version AND the pinned image.tag for the release. Never kubectl set-image/patch to ship. Project CR value changes are tatara-helmfile values edits.
- Branch flow: worktree off main -> develop -> merge to component main -> deploy from main only.
Repo roots (absolute): - operator: /Users/szymonri/Documents/tatara/tatara-operator - helmfile: /Users/szymonri/Documents/tatara/tatara-helmfile
Build/test invocation in the operator repo uses mise: mise exec -- go test ./internal/controller/... etc. mise install once in a fresh clone.
Tasks 1, 2, 3 are independent operator code changes (different files, no shared symbols) and MAY be dispatched in parallel. Task 4 (helmfile values) is independent of the operator code except that the maxTaskTokens value it sets requires Task 2's CRD field to exist in the deployed CRD; sequencing is handled in Task 6 (deploy). Task 5 regenerates operator CRD/deepcopy artifacts and MUST run after Tasks 1-3 merge. Task 6 is the deploy, last.
Task 1: Fix implementPrompt double-append of platformProblemGuidance + toolingConsumeGuidance¶
The Implement turn-0 prompt bills platformProblemGuidance twice (planTurnText adds it at turnloop.go:66; lifecyclePhaseGuidance("Implement") adds it again at turnloop.go:88) and toolingConsumeGuidance twice (planTurnText adds it at turnloop.go:66; an explicit base += toolingConsumeGuidance at lifecycle.go:1518). Root-cause fix: split the lifecycle-phase block from its trailing platformProblemGuidance so implementPrompt can append the phase block WITHOUT re-adding the guidance planTurnText already carried, and drop the explicit toolingConsumeGuidance re-append. Other callers (lifecycleTriageText, which does NOT use planTurnText) keep the guidance-carrying lifecyclePhaseGuidance.
Files: - Modify: internal/controller/turnloop.go:77 (split lifecyclePhaseGuidance into lifecyclePhaseBlock + guidance wrapper) - Modify: internal/controller/lifecycle.go:1504 (use block, not guidance) and internal/controller/lifecycle.go:1518 (delete explicit re-append) - Test: internal/controller/lifecycle_test.go (new TestImplementPrompt_GuidanceAppearsExactlyOnce)
Interfaces: - Produces: func lifecyclePhaseBlock(state string) string - the "## Lifecycle phase" block WITHOUT the trailing platformProblemGuidance. - Consumes/preserves: func lifecyclePhaseGuidance(state string) string still returns lifecyclePhaseBlock(state) + platformProblemGuidance (unchanged external behavior for lifecycleTriageText at turnloop.go:162 and directive_platform_problem_test.go:29). - func implementPrompt(task *tatarav1alpha1.Task) string output unchanged except the two duplicate blocks removed.
Steps:
-
Step 1: Write the failing test. Add to
internal/controller/lifecycle_test.go:(// TestImplementPrompt_GuidanceAppearsExactlyOnce guards the token-conservation // double-append fix: the Implement turn-0 prompt must carry platformProblemGuidance // and toolingConsumeGuidance exactly once each (planTurnText already appends both; // lifecyclePhaseGuidance and the old explicit re-append duplicated them). func TestImplementPrompt_GuidanceAppearsExactlyOnce(t *testing.T) { task := &tatarav1alpha1.Task{ ObjectMeta: metav1.ObjectMeta{Name: "task-dedupe", Namespace: testNS}, Spec: tatarav1alpha1.TaskSpec{ ProjectRef: "proj", RepositoryRef: "repo", Goal: "fix the bug", Kind: "issueLifecycle", }, } got := implementPrompt(task) if n := strings.Count(got, "## Platform problems"); n != 1 { t.Errorf("platformProblemGuidance appears %d times, want 1:\n%s", n, got) } if n := strings.Count(got, toolingConsumeSubstr); n != 1 { t.Errorf("toolingConsumeGuidance (%q) appears %d times, want 1:\n%s", toolingConsumeSubstr, n, got) } // The phase block must still be present (fix must not delete it). if !strings.Contains(got, "## Lifecycle phase: Implement") { t.Errorf("implementPrompt missing lifecycle phase block:\n%s", got) } }toolingConsumeSubstr = "## Tooling from the issue"already exists inturnloop_tooling_test.go:16;stringsandmetav1are already imported inlifecycle_test.go.) -
Step 2: Run it, expect FAIL.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestImplementPrompt_GuidanceAppearsExactlyOnce -count=1Expect:platformProblemGuidance appears 2 times, want 1andtoolingConsumeGuidance ("## Tooling from the issue") appears 2 times, want 1,FAIL. -
Step 3: Split the phase block from its guidance in
turnloop.go. Replace the body oflifecyclePhaseGuidance(currentlyturnloop.go:77-89) with a block helper plus a thin guidance wrapper:(Keep the existing doc comment above// lifecyclePhaseBlock returns the "## Lifecycle phase" block WITHOUT the trailing // platformProblemGuidance. Callers that already carry platformProblemGuidance // (implementPrompt, via planTurnText) use this to avoid double-billing it; callers // that do not (lifecycleTriageText) use lifecyclePhaseGuidance, which appends it. func lifecyclePhaseBlock(state string) string { durable := "Only what you post to the issue/MR conversation (comments, the issue_outcome decision) survives to the next run. Any file edits you make in this workspace are discarded and will NOT be restored." switch state { case "Implement", "MRCI", "Merge", "MainCI": durable = "Changes you commit and push to the task branch ARE restored on the next run (the workspace is re-cloned and the branch checked out). Uncommitted file edits are discarded." } return fmt.Sprintf( "\n\n## Lifecycle phase: %s\n"+ "This issue is handled as a multi-phase conversation and you are currently in the %s phase. "+ "The workspace is transient: it is rebuilt by git clone+checkout on every run and nothing on disk carries over between runs by itself. "+ "%s", state, state, durable) } // lifecyclePhaseGuidance is lifecyclePhaseBlock plus platformProblemGuidance, for // callers (lifecycleTriageText) that do not already carry the platform guidance. func lifecyclePhaseGuidance(state string) string { return lifecyclePhaseBlock(state) + platformProblemGuidance }lifecyclePhaseGuidanceatturnloop.go:69-76describing the phase semantics; move it abovelifecyclePhaseBlock.) -
Step 4: Use the block + drop the explicit re-append in
implementPrompt. Ininternal/controller/lifecycle.go: - Change line 1504
base += lifecyclePhaseGuidance("Implement")tobase += lifecyclePhaseBlock("Implement"). -
Delete line 1518
base += toolingConsumeGuidance(the return becomes the immediately-followingreturn base).planTurnTextatlifecycle.go:1476already appendedtoolingConsumeGuidance, so the prompt keeps it exactly once. -
Step 5: Run the new test + the existing prompt tests, expect PASS.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run 'TestImplementPrompt_GuidanceAppearsExactlyOnce|TestLifecycleImplementPlanText|TestToolingConsumeGuidance|TestDirective' -count=1Expect:ok github.com/szymonrychu/tatara-operator/internal/controller. (TestToolingConsumeGuidance_InImplementPromptanddirective_platform_problem_test.gostill pass: the guidance is present once, andlifecyclePhaseGuidance("Implement")still containsplatformProblemGuidancein isolation.) -
Step 6: Commit.
cd /Users/szymonri/Documents/tatara/tatara-operator && git add internal/controller/turnloop.go internal/controller/lifecycle.go internal/controller/lifecycle_test.go && git commit -m "fix: implementPrompt double-appended platform+tooling guidance"
Task 2: Per-Task cumulative-token runaway backstop for implement/issueLifecycle¶
turnCap (task_controller.go:1089) returns (0,false) for implement/issueLifecycle so those kinds run turn-uncapped. Add an opt-in per-Task cumulative-token ceiling that fails the Task when Status.CumulativeTokens (the operator's output-token ledger, accumulated at turncallback.go:276) crosses Project.Spec.Agent.MaxTaskTokens. Gate ONLY those two kinds; every other kind keeps its turn cap and is never token-gated. The threshold is opt-in (0 = disabled, default), set from telemetry in the helmfile values (Task 4); the terminate path already emits operator_task_terminal_total{kind,phase,reason} so no new metric is needed.
Files: - Modify: api/v1alpha1/project_types.go:139 (add MaxTaskTokens to AgentSpec, after Effort) - Modify: internal/controller/task_controller.go:1100 (new taskTokenBudgetExceeded helper after turnCap) and internal/controller/task_controller.go:1002 (wire the check at the turn-cap site) - Test: api/v1alpha1/project_types_test.go (new TestAgentSpec_MaxTaskTokensField) - Test: internal/controller/task_controller_test.go (new TestTaskTokenBudgetExceeded table test - create the file if absent using the package + imports below)
Interfaces: - Produces field: AgentSpec.MaxTaskTokens int64 (json:"maxTaskTokens,omitempty", no kubebuilder default = 0 disabled). - Produces: func taskTokenBudgetExceeded(project *tatarav1alpha1.Project, task *tatarav1alpha1.Task) bool. - Consumes: task.Status.CumulativeTokens int64 (api/v1alpha1/task_types.go:374), project.Spec.Agent.MaxTaskTokens, task.Spec.Kind.
Steps:
-
Step 1: Write the failing field test. Add to
api/v1alpha1/project_types_test.go(mirrors the existingTestBrainstormActivity_StaleProposalDaysFieldat line 230):func TestAgentSpec_MaxTaskTokensField(t *testing.T) { a := v1alpha1.AgentSpec{MaxTaskTokens: 3_000_000} if a.MaxTaskTokens != 3_000_000 { t.Fatalf("MaxTaskTokens = %d, want 3000000", a.MaxTaskTokens) } var z v1alpha1.AgentSpec if z.MaxTaskTokens != 0 { t.Fatalf("MaxTaskTokens zero value = %d, want 0 (backstop disabled)", z.MaxTaskTokens) } } -
Step 2: Run it, expect FAIL (compile error).
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./api/v1alpha1/ -run TestAgentSpec_MaxTaskTokensField -count=1Expect:a.MaxTaskTokens undefined (type v1alpha1.AgentSpec has no field or method MaxTaskTokens),FAIL [build failed]. -
Step 3: Add the field. In
api/v1alpha1/project_types.go, immediately after theEffort stringfield (ends at line 139), add:// MaxTaskTokens is a per-Task cumulative output-token ceiling for the // otherwise turn-uncapped implementation kinds (implement, issueLifecycle): a // runaway backstop, not a cost lever. 0 disables it (the default); opt in via // the Project values. When Status.CumulativeTokens crosses it the Task is // failed with reason TokenBudgetExceeded. TUNE from the component-6 per-kind // token telemetry once a healthy-run distribution is known. // +optional MaxTaskTokens int64 `json:"maxTaskTokens,omitempty"` -
Step 4: Run the field test, expect PASS.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./api/v1alpha1/ -run TestAgentSpec_MaxTaskTokensField -count=1Expect:ok github.com/szymonrychu/tatara-operator/api/v1alpha1. -
Step 5: Write the failing helper table test. Add
internal/controller/task_controller_budget_test.go:package controller import ( "testing" tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1" ) func TestTaskTokenBudgetExceeded(t *testing.T) { mk := func(kind string, limit, used int64) (*tatarav1alpha1.Project, *tatarav1alpha1.Task) { p := &tatarav1alpha1.Project{} p.Spec.Agent.MaxTaskTokens = limit tk := &tatarav1alpha1.Task{} tk.Spec.Kind = kind tk.Status.CumulativeTokens = used return p, tk } cases := []struct { name string kind string limit int64 used int64 want bool }{ {"implement under budget continues", "implement", 1000, 500, false}, {"implement at budget stops", "implement", 1000, 1000, true}, {"implement over budget stops", "implement", 1000, 1500, true}, {"issueLifecycle over budget stops", "issueLifecycle", 1000, 2000, true}, {"review over budget not gated", "review", 1000, 5000, false}, {"triageIssue over budget not gated", "triageIssue", 1000, 5000, false}, {"implement disabled when limit zero", "implement", 0, 9_999_999, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { p, tk := mk(tc.kind, tc.limit, tc.used) if got := taskTokenBudgetExceeded(p, tk); got != tc.want { t.Errorf("taskTokenBudgetExceeded(kind=%s limit=%d used=%d) = %v, want %v", tc.kind, tc.limit, tc.used, got, tc.want) } }) } } -
Step 6: Run it, expect FAIL (undefined).
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestTaskTokenBudgetExceeded -count=1Expect:undefined: taskTokenBudgetExceeded,FAIL [build failed]. -
Step 7: Add the helper. In
internal/controller/task_controller.go, immediately after theturnCapfunction (ends at line 1100), add:// taskTokenBudgetExceeded reports whether an uncapped implementation Task has // burned past its per-Task output-token backstop. Only implement/issueLifecycle // are gated (they run turn-uncapped per turnCap); every other kind keeps the turn // cap and is never token-gated here. A zero MaxTaskTokens disables the backstop. func taskTokenBudgetExceeded(project *tatarav1alpha1.Project, task *tatarav1alpha1.Task) bool { if task.Spec.Kind != "implement" && task.Spec.Kind != "issueLifecycle" { return false } limit := project.Spec.Agent.MaxTaskTokens if limit <= 0 { return false } return task.Status.CumulativeTokens >= limit } -
Step 8: Wire the check at the turn-cap site. In
internal/controller/task_controller.go, immediately after theturnCapblock (theif limit, capped := turnCap(...)...}ending at line 1002), add:(// Per-Task token runaway backstop (token conservation, component 5): the // uncapped implementation kinds fail cleanly when they cross the per-Task // output-token ceiling, so a looping agent cannot burn unbounded tokens. The // terminate path records operator_task_terminal_total{reason=TokenBudgetExceeded}. if taskTokenBudgetExceeded(project, task) { return r.terminate(ctx, task, "Failed", "TokenBudgetExceeded", fmt.Sprintf("cumulative tokens %d reached the per-task budget %d", task.Status.CumulativeTokens, project.Spec.Agent.MaxTaskTokens)) }fmtis already imported intask_controller.go.) -
Step 9: Run helper + package tests, expect PASS.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run 'TestTaskTokenBudgetExceeded' -count=1 && mise exec -- go build ./...Expect:ok github.com/szymonrychu/tatara-operator/internal/controllerand a clean build. -
Step 10: Commit.
cd /Users/szymonri/Documents/tatara/tatara-operator && git add api/v1alpha1/project_types.go api/v1alpha1/project_types_test.go internal/controller/task_controller.go internal/controller/task_controller_budget_test.go && git commit -m "feat: per-task token runaway backstop for implement/issueLifecycle"
Task 3: Gate bot-authored brainstorming proposals on human activity in issueScan¶
A bot-authored brainstorming proposal (carries the brainstorming label, none of approved/implementation/declined) with no comments passes the existing gates: lastTerminalNoLabelTask (projectscan.go:1358) only fires for no-managed-label issues, botHadLastWord (projectscan.go:1376) returns false when there are no comments, and reapEligible (projectscan.go:1383) only fires once the proposal is ALSO stale (> StaleProposalDays). So between fresh and stale a fresh triage Task is spawned every scan cycle - the observed brainstorm churn. Add a label-only predicate plus a human-activity gate that skips fresh Task creation for such a proposal until any human comment exists. This stops the churn from the first cycle; the reaper (Task 4 activates it) still closes it once stale.
Files: - Modify: internal/controller/projectscan.go:517 (new pure isBotBrainstormProposal predicate after isStaleUnengagedProposal) - Modify: internal/controller/projectscan.go:1273 (compute lifecycle labels) and internal/controller/projectscan.go:1382 (new gate in the create loop, after botHadLastWord, before reapEligible) - Test: internal/controller/projectscan_brainstorm_gate_test.go (new)
Interfaces: - Produces: func isBotBrainstormProposal(c candidate, brainstorming, approved, implementation, declined, botLogin string) bool. - Consumes: candidate{repo,number,author,labels,isPR} (projectscan.go), hasLabel/hasAnyLabel (projectscan.go:455,467), lifecycleLabels (labels.go:18), humanCommentAfter (projectscan.go:1940, zero since = "any human comment ever"), r.Metrics.ScanItem (free-form reason string).
Steps:
-
Step 1: Write the failing test. Add
internal/controller/projectscan_brainstorm_gate_test.go:(package controller import ( "context" "testing" "time" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1" "github.com/szymonrychu/tatara-operator/internal/obs" "github.com/szymonrychu/tatara-operator/internal/scm" ) // freshBrainstormIssue is a bot-authored, brainstorming-labelled proposal that is // NOT stale (recent UpdatedAt) so the reaper's stale gate does not fire; only the // human-activity gate should suppress a fresh triage Task. func freshBrainstormIssue() scm.IssueRef { return scm.IssueRef{Repo: "o/r", Number: 7, Author: "tatara-bot", Labels: []string{"tatara-brainstorming"}, UpdatedAt: time.Now().Add(-2 * time.Hour)} } // TestIsBotBrainstormProposal is the pure label predicate. func TestIsBotBrainstormProposal(t *testing.T) { brs, app, impl, dec := "tatara-brainstorming", "tatara-approved", "tatara-implementation", "tatara-declined" cases := []struct { name string c candidate want bool }{ {"bot brainstorming proposal", candidate{repo: "o/r", number: 7, author: "tatara-bot", labels: []string{brs}}, true}, {"human authored", candidate{repo: "o/r", number: 7, author: "szymonrychu", labels: []string{brs}}, false}, {"empty author", candidate{repo: "o/r", number: 7, author: "", labels: []string{brs}}, false}, {"advanced to approved", candidate{repo: "o/r", number: 7, author: "tatara-bot", labels: []string{brs, app}}, false}, {"already declined", candidate{repo: "o/r", number: 7, author: "tatara-bot", labels: []string{brs, dec}}, false}, {"no brainstorming label", candidate{repo: "o/r", number: 7, author: "tatara-bot", labels: []string{"other"}}, false}, {"is a PR", candidate{repo: "o/r", number: 7, author: "tatara-bot", labels: []string{brs}, isPR: true}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := isBotBrainstormProposal(tc.c, brs, app, impl, dec, "tatara-bot"); got != tc.want { t.Errorf("isBotBrainstormProposal = %v, want %v", got, tc.want) } }) } } // TestIssueScan_SkipsBrainstormProposalWithoutHumanActivity: a fresh bot proposal // with no comments must NOT create a triage Task. func TestIssueScan_SkipsBrainstormProposalWithoutHumanActivity(t *testing.T) { proj, repo := seedBackstopProject(t, "bsgate-nohuman") reader := &perRepoFakeReader{issuesByRepo: map[string][]scm.IssueRef{"o/r": {freshBrainstormIssue()}}} // fakeReader.ListIssueComments returns nil -> no human comment ever. r := newScanReconciler(reader) r.Metrics = obs.NewOperatorMetrics(prometheus.NewRegistry()) r.issueScan(context.Background(), proj, reader, []tatarav1alpha1.Repository{repo}, nil, proj.Spec.Scm.Cron.IssueScan) qes := listScanQEs(t, "bsgate-nohuman") require.Empty(t, qes, "issueScan must not triage a bot brainstorming proposal with no human activity") } // TestIssueScan_TriagesBrainstormProposalWithHumanActivity: the same proposal WITH // a human comment must be triaged (one QueuedEvent created). func TestIssueScan_TriagesBrainstormProposalWithHumanActivity(t *testing.T) { proj, repo := seedBackstopProject(t, "bsgate-human") reader := &perRepoFakeReader{ fakeReader: fakeReader{comments: []scm.IssueComment{{Author: "szymonrychu", Body: "please build this", CreatedAt: time.Now()}}}, issuesByRepo: map[string][]scm.IssueRef{"o/r": {freshBrainstormIssue()}}, } r := newScanReconciler(reader) r.Metrics = obs.NewOperatorMetrics(prometheus.NewRegistry()) r.issueScan(context.Background(), proj, reader, []tatarav1alpha1.Repository{repo}, nil, proj.Spec.Scm.Cron.IssueScan) qes := listScanQEs(t, "bsgate-human") require.Len(t, qes, 1, "issueScan must triage a bot brainstorming proposal once a human has engaged") }seedBackstopProject,newScanReconciler,listScanQEs,perRepoFakeReader,fakeReaderall exist in the controller test package;scm.IssueComment{Author,Body,CreatedAt}perbotIsLastCommenterusage. Confirm the exactIssueCommentfield names viainternal/scmbefore writing - useCreatedAtas inprojectscan.go:1979.) -
Step 2: Run it, expect FAIL.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run 'TestIsBotBrainstormProposal|TestIssueScan_.*BrainstormProposal' -count=1Expect:undefined: isBotBrainstormProposal(build failed) - the predicate test - and once the predicate exists but the gate is not wired,TestIssueScan_SkipsBrainstormProposalWithoutHumanActivityfails withShould be empty, but was [<one QE>]. -
Step 3: Add the pure predicate. In
internal/controller/projectscan.go, immediately afterisStaleUnengagedProposal(ends at line 517), add:// isBotBrainstormProposal reports whether the candidate is a bot-authored, open // (non-PR) brainstorming proposal still in the proposal phase: it carries the // brainstorming label and NONE of approved/implementation/declined. It is the // label-only half of the source-of-churn gate - such a proposal must not spawn a // fresh triage Task every scan cycle until a human engages it. Mirrors // isStaleUnengagedProposal's author+label gates without the time / task-liveness // checks (the reaper owns staleness; this owns "never engaged"). func isBotBrainstormProposal(c candidate, brainstorming, approved, implementation, declined, botLogin string) bool { if c.isPR { return false } if c.author == "" || c.author != botLogin { return false } return hasLabel(c.labels, brainstorming) && !hasAnyLabel(c.labels, []string{approved, implementation, declined}) } -
Step 4: Compute lifecycle labels once in issueScan. In
internal/controller/projectscan.go, right aftermanaged := managedPhaseLabels(proj.Spec.Scm)(line 1273), add: -
Step 5: Add the gate in the create loop. In
internal/controller/projectscan.go, in thefor _, c := range eligibleloop, immediately after thebotHadLastWordblock (ends at line 1382) and before thereapEligibleblock (line 1383), add:(// Source-of-churn gate (token conservation, component 5): a bot-authored // brainstorming proposal no human has engaged must not be re-triaged every // scan cycle. The reaper (staleProposalDays) only closes it once it is ALSO // stale; this stops the churn from the first cycle. Any human comment (zero // `since` = ever) clears it. Fail-open when SCM/botLogin/owner-split is // unavailable, matching botHadLastWord and the reactivation gate. if isBotBrainstormProposal(c, brainstorming, approved, implementation, declined, botLogin) { owner, name, cut := strings.Cut(c.repo, "/") if cut && reader != nil && botLogin != "" && !humanCommentAfter(ctx, reader, owner, name, c.number, botLogin, time.Time{}) { r.Metrics.ScanItem("issueScan", "skipped_brainstorm_no_human") l.Info("issueScan: skipped fresh task creation, brainstorming proposal awaiting human engagement", "action", "scan_issue", "resource_id", proj.Name, "issue", fmt.Sprintf("%s#%d", c.repo, c.number)) continue } }strings,time,fmtalready imported inprojectscan.go.) -
Step 6: Run the brainstorm-gate tests + the reap-gate regression, expect PASS.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run 'TestIsBotBrainstormProposal|TestIssueScan_.*BrainstormProposal|TestIssueScan_.*Reap|TestIssueScan_Triages' -count=1Expect:ok github.com/szymonrychu/tatara-operator/internal/controller. (TestIssueScan_TriagesWhenReaperDisableduses a stale issue with reaper off; the new gate only fires for bot brainstorming proposals with no human comment - confirm that test's fixture still yields a Task; the stale issue is bot+brainstorming with no comments, so the new gate WOULD now suppress it too. If that regression test flips to empty, it is CORRECT new behavior: the churn source is closed independent of the reaper. UpdateTestIssueScan_TriagesWhenReaperDisabledto seed a human comment viafakeReader{comments: [...]}so it still asserts a Task is created, preserving its original intent - "reaper disabled -> normal triage" - under the stricter gate.) -
Step 7: Run the full package test to catch collateral, expect PASS.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -count=1Expect:ok. Fix any other scan test that relied on a commentless bot brainstorming proposal being triaged by seeding a human comment (same one-linefakeReader{comments: ...}pattern). -
Step 8: Commit.
cd /Users/szymonri/Documents/tatara/tatara-operator && git add internal/controller/projectscan.go internal/controller/projectscan_brainstorm_gate_test.go internal/controller/projectscan_reap_gate_test.go && git commit -m "feat: gate bot brainstorming proposals on human activity in issueScan"
Task 4: tatara-helmfile Project values - cadence, staleProposalDays, skillsRef pin, maxTaskTokens¶
Apply the four value changes to BOTH Project CRs. The tatara-project chart renders .Values.project.spec verbatim, so every field lands on the Project CR with no chart change. Cadence: stretch issueScan and mrScan from hourly; refine has no own schedule (it fires before each due scan cycle) so it rides the stretched scan cadence automatically - no separate field. brainstorm is out of scope (not listed in the spec) and stays hourly. Chosen cadence (stated per Decision authority; squarely inside the spec's 2-4h band): mrScan every 2h (bot-MR CI/merge progression is slightly more time-sensitive, still webhook-backed), issueScan every 4h.
Files: - Modify: /Users/szymonri/Documents/tatara/tatara-helmfile/values/project-tatara/common.yaml (agent block ~17-23; cron block ~70-92) - Modify: /Users/szymonri/Documents/tatara/tatara-helmfile/values/project-infrastructure/common.yaml (agent block ~17-23; cron block ~56-78)
Interfaces: none (YAML values consumed by the operator: agent.skillsRef -> AgentSpec.SkillsRef -> pod.go:648; agent.maxTaskTokens -> AgentSpec.MaxTaskTokens (Task 2); scm.cron.brainstorm.staleProposalDays -> BrainstormActivity.StaleProposalDays (projectscan.go:2219); scm.cron.{issueScan,mrScan}.schedule).
The pinned skills SHA is the current tatara-agent-skills HEAD: ae4250fd567b6dd0af439aba364460b1ce72d737 (ae4250f feat: add push-CD release workflow tagging semver and bumping wrapper skills ref). Re-verify HEAD at execution time: cd /Users/szymonri/Documents/tatara/tatara-agent-skills && git rev-parse HEAD; use whatever HEAD prints (the pin must be a real commit).
Steps:
-
Step 1: Edit
values/project-tatara/common.yaml. In theagent:block, addskillsRefandmaxTaskTokens(alpha-ordered among the existing scalar keys):In theagent: effort: xhigh image: harbor.szymonrichert.pl/containers/tatara-claude-code-wrapper:8f3d880 # Per-Task cumulative output-token runaway backstop for the turn-uncapped # implement/issueLifecycle kinds. TUNE from the component-6 per-kind token # telemetry; this is a safety ceiling, not a cost lever. maxTaskTokens: 3000000 maxTurnsPerTask: 100 model: claude-opus-4-8 permissionMode: bypassPermissions # Pin the agent-skills install to a SHA (kills the `main` drift; component 5 # / component 4b). Bump deliberately alongside a skills change. skillsRef: ae4250fd567b6dd0af439aba364460b1ce72d737 turnTimeoutSeconds: 2700cron:block, change the two scan schedules and addstaleProposalDaysunderbrainstorm:cron: brainstorm: enabled: true maxOpenProposals: 10 schedule: 0 * * * * # Activate the stale-proposal reaper (operator #199): auto-close bot # proposals with no human engagement after 14 days. staleProposalDays: 14 sources: - docs - memory - internet issueScan: maxPerRepo: 1 # Stretched hourly -> every 4h (token conservation); webhooks cover real # issue activity, so the backstop scan can be less frequent. schedule: 0 */4 * * * mrScan: maxPerRepo: 1 # Stretched hourly -> every 2h (token conservation); webhooks cover MR # events, this is the CI/merge-progression backstop. schedule: 0 */2 * * * refine: closedLookbackDays: 30 -
Step 2: Edit
values/project-infrastructure/common.yaml. Apply the identicalagentadditions (maxTaskTokens: 3000000,skillsRef: <same SHA>) and the identical cron edits (issueScan.schedule: 0 */4 * * *,mrScan.schedule: 0 */2 * * *,brainstorm.staleProposalDays: 14). Keep this project's ownbotEmail/provider: gitlab/repo list untouched. -
Step 3: Verify templating renders the new fields. From the helmfile repo, render the tatara-project releases and grep for each new key:
Expect lines showingcd /Users/szymonri/Documents/tatara/tatara-helmfile && helmfile template 2>/dev/null | grep -E "staleProposalDays|maxTaskTokens|skillsRef|schedule: 0 \*/[24]"staleProposalDays: 14,maxTaskTokens: 3000000,skillsRef: ae4250f...,schedule: 0 */4 * * *,schedule: 0 */2 * * *for BOTHproject-tataraandproject-infrastructure. Ifhelmfile templateneeds selectors/auth locally, fall back tohelm templateon thetatara-projectchart with eachcommon.yamlas-fvalues, asserting the same keys appear underspec:. (Do NOT runhelmfile diff/applylocally against the cluster - that is the deploy pipeline's job in Task 6.) -
Step 4: Commit (do NOT push yet; the push+MR is bundled with the chart/image bump in Task 6).
cd /Users/szymonri/Documents/tatara/tatara-helmfile && git add values/project-tatara/common.yaml values/project-infrastructure/common.yaml && git commit -m "feat: stretch scan cadence, activate reaper, pin skillsRef, token backstop"
Task 5: Regenerate operator CRD + deepcopy artifacts for the new AgentSpec field¶
Task 2 added AgentSpec.MaxTaskTokens; the generated CRD manifests (charts/tatara-operator/crd-bases/tatara.dev_projects.yaml and the mirror under config/crd/) and deepcopy must be regenerated so the deployed CRD accepts the new field (the operator rejects unknown Project spec fields - see the DisallowUnknownFields memory). Run AFTER Tasks 1-3 are merged/rebased onto the feature branch.
Files: - Modify (generated): charts/tatara-operator/crd-bases/tatara.dev_projects.yaml, any config/crd/** mirror, api/v1alpha1/zz_generated.deepcopy.go (int64 scalar = no functional deepcopy delta, but run make generate for consistency).
Steps:
-
Step 1: Regenerate.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- make manifests generate(Makefile targetsmanifests:at line 37 andgenerate:at line 34.) -
Step 2: Confirm the field is in the CRD.
cd /Users/szymonri/Documents/tatara/tatara-operator && grep -n "maxTaskTokens" charts/tatara-operator/crd-bases/tatara.dev_projects.yamlExpect amaxTaskTokens:property underspec.properties.agent.propertieswithtype: integer/format: int64. -
Step 3: Build + full test to confirm nothing else drifted.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go build ./... && mise exec -- go test ./... -count=1Expect: clean build andokfor all packages. -
Step 4: Lint.
cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- gofmt -l internal/ api/ && mise exec -- golangci-lint runExpect: no files listed bygofmt -l,golangci-lintclean. -
Step 5: Commit the generated artifacts.
cd /Users/szymonri/Documents/tatara/tatara-operator && git add charts/ config/ api/v1alpha1/zz_generated.deepcopy.go && git commit -m "chore: regenerate CRD + deepcopy for maxTaskTokens"
Task 6: Deploy - merge operator main, then one tatara-helmfile MR (chart+image bump + values)¶
Per the hard deploy rules. Requesting-code-review + verification-before-completion gate the merge. This is procedural (no unit test); verification is via the deploy diff and post-apply metrics.
Steps:
-
Step 1: Code review + pre-commit before merge. Invoke
superpowers:requesting-code-reviewon the operator branch diff (Tasks 1-3, 5). Fix any critical/high findings. Then, from the operator repo, run the repo's pre-commit/lint gate (mise exec -- golangci-lint run,mise exec -- go test ./... -count=1). -
Step 2: Merge the operator feature branch to
main(worktree ->mainper the branch-flow rule; never deploy from the worktree). Pushingmaintriggers CI to build+push the operator image and chart to harbor. Record the merged operator short SHA asOP_SHA(e.g.git -C /Users/szymonri/Documents/tatara/tatara-operator rev-parse --short main). -
Step 3: Confirm CI published the image+chart. Wait for the operator repo's CI run on
mainto go green (gh run list --repo szymonrychu/tatara-operator --branch main --limit 1). The chart version is0.0.0-g<OP_SHA>and the image tag<OP_SHA>(per the leading-zero-SHA + dual-pin deploy memories). -
Step 4: Build the tatara-helmfile MR. On a branch in
/Users/szymonri/Documents/tatara/tatara-helmfile, on top of the Task 4 commit: - Bump the
tatara-operatorrelease chart version to0.0.0-g<OP_SHA>and the pinned operatorimage.tagto<OP_SHA>invalues/tatara-operator/common.yaml(BOTH, per the dual-pin memory - chart-only leaves the old image running). Use thebump-chart-usage+bump-container-usageskills to locate the exact keys. -
The Task 4 Project values edits ride the same branch/MR.
-
Step 5: Open the MR; review the sticky helmfile diff. Push and open the MR (bot auto-posts a
helmfile diff). Verify the diff shows ONLY: the operator Deployment image<OP_SHA>+ chart version, and the Project CR field changes (schedule,staleProposalDays: 14,skillsRef,maxTaskTokens: 3000000) for both Projects. No unexpected resource churn. -
Step 6: Merge the MR; the pipeline auto-applies. Confirm the apply job is green.
-
Step 7: Verify live. After apply:
- Operator image tag matches
<OP_SHA>:kubectl -n tatara get deploy tatara-operator -o jsonpath='{.spec.template.spec.containers[0].image}'. - Project CRs carry the new fields:
kubectl -n tatara get project tatara -o jsonpath='{.spec.agent.maxTaskTokens} {.spec.agent.skillsRef} {.spec.scm.cron.brainstorm.staleProposalDays} {.spec.scm.cron.issueScan.schedule} {.spec.scm.cron.mrScan.schedule}'(repeat forproject infrastructure). - Reaper active: over the next day watch
operator_task_terminal_total/IssueOutcome("stale-close")and the newScanItem("issueScan","skipped_brainstorm_no_human")counter appear; scan cadence reflects 2h/4h (fewer scan-cycle log lines). -
Backstop wired (not yet expected to fire on healthy runs): watch for
operator_task_terminal_total{reason="TokenBudgetExceeded"}staying at 0 on healthy implement/issueLifecycle runs; a non-zero value on a genuinely long healthy run meansmaxTaskTokens=3000000is too low - re-tune upward via a values-only MR. -
Step 8: Record the outcome in
tatara-operator/MEMORY.mdandtatara-helmfile/MEMORY.md(one dated line each): the four adjacents shipped, the pinned skills SHA, and thatmaxTaskTokens=3000000is a TUNE-from-telemetry placeholder. Move the component-5 item out ofROADMAP.md.