Discovery-phase research harness 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: Ship two wrapper agent skills (tatara-deep-research, tatara-research-followup) plus the MCP/operator plumbing they need, so the autonomous loop's discovery phase does deep cross-platform research and keeps research-issue conversations alive until a human approves - all through the tatara MCP server, issues staying in discovery.
Architecture: Three repos, three layers. Wrapper ships the two baked SKILL.md workflows. tatara-cli adds a task-scoped comment MCP tool. tatara-operator adds the POST /tasks/{t}/comment REST handler + a Task.Status.PendingComments queue drained to SCM on reconcile, and nudges the brainstorm + triage turn prompts to invoke the skills by name.
Tech Stack: Go (newest stable per repo go.mod), controller-runtime + chi (operator), cobra + stdio MCP (cli), Claude Code SKILL.md trees (wrapper). Table-driven Go tests. Conventional commits.
Spec¶
docs/superpowers/specs/2026-06-13-discovery-research-harness-design.md
File structure (what changes)¶
tatara-claude-code-wrapper (worktree off main): - Create: templates/skills/tatara-deep-research/SKILL.md - Create: templates/skills/tatara-research-followup/SKILL.md - Create/Modify: a Go test asserting both skills are present + valid frontmatter - Modify (serial tail, after cli tagged): Dockerfile TATARA_CLI_VERSION pin + the build-time tool-guard list (add comment)
tatara-cli (worktree off main): - Modify: internal/mcp/tools.go - add comment to OperatorTools(); fix stale propose_issue description - Modify: internal/mcp/tools_test.go - test comment registration + build mapping
tatara-operator (worktree off main): - Modify: api/v1alpha1/task_types.go - add PendingComments []string to TaskStatus - Regenerate: make manifests generate (CRD yaml + deepcopy) -> config/crd/... and chart crds/ - Modify: internal/restapi/server.go - route POST /tasks/{t}/comment - Modify: internal/restapi/handlers.go - postComment handler + issueCommentReq - Modify: internal/restapi/*_test.go - handler test - Modify: internal/controller/lifecycle.go - drain PendingComments in reconcileLifecycle - Modify: internal/controller/lifecycle_*_test.go - drain test - Modify: internal/controller/turnloop.go:58 - nudge lifecycleTriageText to invoke tatara-research-followup - Modify: internal/controller/projectscan.go:772 - nudge brainstorm goal to invoke tatara-deep-research - Modify: internal/controller/*_test.go - prompt-nudge assertions
Cross-repo ordering¶
- Lanes A (wrapper skills A1-A3), B (cli), C (operator) are INDEPENDENT - implement in parallel worktrees.
- Serial tail A4: after cli
commentmerges + is tagged, bump the wrapper Dockerfile cli pin + guard, then merge wrapper. - Skill names referenced by operator (C5/C6) must equal the wrapper skill dir names exactly:
tatara-deep-research,tatara-research-followup.
LANE A - tatara-claude-code-wrapper¶
Task A1: tatara-deep-research skill¶
Files: - Create: templates/skills/tatara-deep-research/SKILL.md
- Step 1: Create the skill file with exactly this content:
---
name: tatara-deep-research
description: Use on an autonomous platform-research turn (the brainstorm task kind) to discover ONE high-leverage improvement for the tatara platform and open a discovery-phase issue via the propose_issue MCP tool. Researches deeply across the whole platform using the tatara-memory knowledge/code graph plus the on-disk repo, scores leverage against platform and per-repo goals, and files a single well-formed issue that stays in discovery (never self-implemented).
---
# tatara deep research
Discover and propose ONE high-leverage improvement issue per run. All
input and output go through the `tatara` MCP server. You never use git or
gh; you never open an issue yourself - `propose_issue` does that under the
bot identity.
## Hard constraints
- ONE issue per run. The brainstorm task completes after a single proposal.
- Stay in discovery. Do NOT request implementation. Embed the literal
marker `<!-- tatara-authored -->` in the issue body and never set a
trigger label - the operator holds tatara-authored ideas in
conversation until a human approves.
- Every proposal must respect the platform's 14 hard rules (read the
on-disk `CLAUDE.md`), or the loop that later implements it will reject
it. KISS; no tech debt; charts cluster-agnostic; conventional commits;
newest stable Go; JSON slog + INFO business logging + /metrics.
- Communication only via `tatara` MCP tools.
## Workflow
Create a TodoWrite item per numbered step.
1. **Orient on goals.** Read the on-disk `ROADMAP.md`, `MEMORY.md`, and
`CLAUDE.md` of the task's repo (the platform goal, the repo charter,
the hard rules). Then use the memory MCP tools for the wider picture:
`query` (mode global or hybrid) for "tatara platform goal" and "open
roadmap themes"; `describe` for an overview of the target repo.
2. **Map current state.** Use the code-graph tools to find where the
system is fragile or under-optimized, repo-scoped where useful:
`code_stats`, `code_important` (high-PageRank entities = load-bearing
code), `code_communities` (subsystem clustering), `code_bridges`
(coupling/risk), and `code_cross_repo` (cross-repo edges - the pod has
only one repo on disk, so cross-repo understanding MUST come from the
graph). Then READ the actual on-disk code for the strongest candidate
area to confirm what the graph suggested.
3. **Score leverage.** Rank candidate improvements by impact in this
order: (a) reliability/observability of the LIVE autonomous loop
(it is dogfooding in production and surfaces real bugs); (b) un-built
but planned loop features; (c) the Phase-9 SOTA backlog; (d) deploy
debt. Respect gates: do NOT propose downstream memory ranking/reranker
work before the memory retrieval-quality eval harness exists. Pick the
single highest-leverage, well-scoped item.
4. **Dedup.** Call `task_list` and review the repo's open issues/tasks to
avoid duplicating an existing proposal or the operator's own brainstorm
output. If a similar idea is already open, pick the next-best candidate
instead.
5. **Compose ONE proposal.** Write:
- Title: imperative, specific (e.g. "Add per-item ingest timeout to the
memory ingest worker").
- Body: Problem (what hurts, why it matters to the platform/repo goal);
Evidence (`file:line` references and concrete graph findings from
steps 1-2); Proposed approach (KISS, respecting the hard rules);
Scope boundary (what is in and explicitly out); Open questions for
the maintainer. Append the literal line `<!-- tatara-authored -->`.
6. **File it.** Call `propose_issue` with `title`, `body`, `kind`
(`improvement` or `bug`), and `repo` (the repo slug; `project` defaults
from env). Do not set any trigger/approval label. Then stop - the
brainstorm task is complete.
## Anti-patterns
- Proposing more than one issue in a run.
- Proposing vague "improve X" issues with no `file:line` evidence.
- Requesting implementation / setting a trigger label (breaks discovery).
- Proposing memory ranking work before the eval-harness gate.
- Reading only the on-disk repo and ignoring the cross-repo graph.
-
Step 2: Verify frontmatter parses (covered by Task A3 test).
-
Step 3: Commit
git add templates/skills/tatara-deep-research/SKILL.md
git commit -m "feat: add tatara-deep-research discovery skill"
Task A2: tatara-research-followup skill¶
Files: - Create: templates/skills/tatara-research-followup/SKILL.md
- Step 1: Create the skill file with exactly this content:
---
name: tatara-research-followup
description: Use when continuing an existing discovery/research issue conversation on an issueLifecycle Triage or Conversation turn. Read the issue thread and task state, research the gaps with the tatara-memory graph and on-disk code, post substantive design comments via the comment MCP tool, refine the proposal into a concrete design, and push toward human approval - never self-approving. Idle quietly when there is nothing new to add.
---
# tatara research follow-up
Keep a discovery-phase issue conversation alive and move it toward an
approvable design. All input and output go through the `tatara` MCP
server. You never use git or gh.
## Hard constraints
- NEVER self-approve. If THIS issue is tatara-authored, only a human's
approval comment may lead to implementation - you only discuss and
refine. End the turn with `issue_outcome(discuss)`, never
`issue_outcome(implement)` on an unapproved tatara-authored issue.
- Silence over noise. If there is no human input and nothing genuinely
new to add, post nothing and let the conversation idle.
- One focused turn. Communication only via `tatara` MCP tools.
## Workflow
Create a TodoWrite item per numbered step.
1. **Load context.** Call `task_get` (task=env `TATARA_TASK`) for the task
status and lifecycle state. Read the issue body and the full comment
thread (the turn prompt includes the thread). Extract: open questions,
maintainer asks, unresolved design decisions, and whether a human has
engaged.
2. **Research the gaps.** Use the memory MCP tools (`query`, `describe`,
and the `code_*` family incl. `code_cross_repo`) plus the on-disk code
to answer the specific questions raised and to deepen any thin part of
the proposal. The pod has one repo on disk; use the graph for
cross-repo facts.
3. **Respond in-thread** with the `comment` MCP tool (task=env
`TATARA_TASK`, body=...). Post focused comments, not one wall of text:
- Answer each maintainer question with evidence (`file:line`, graph
findings).
- Refine the proposal into a concrete design: architecture,
components, data flow, error handling, testing, plus an
implementation outline.
- Surface remaining decisions for the maintainer.
4. **Drive to approval.** When the design is converged AND a human has
engaged in the thread, post a short summary of the agreed design and
explicitly ask the maintainer for the approval signal (an approval
comment / the approval label). Do not approve it yourself.
5. **Idle discipline.** If nothing new is warranted, do not comment.
6. **Close the turn.** Call `issue_outcome` with action `discuss` (supply
a one-line status as `comment`) to hold the issue in Conversation. Use
action `close` ONLY if the idea is clearly dead AND a human concurred
in the thread. You MUST call `issue_outcome` before finishing.
## Anti-patterns
- Calling `issue_outcome(implement)` on a tatara-authored issue without a
human approval comment.
- Posting one giant comment instead of focused, answerable ones.
- Commenting with no new research when the thread is waiting on the human.
- Making code changes or opening PRs in this turn.
- Step 2: Commit
git add templates/skills/tatara-research-followup/SKILL.md
git commit -m "feat: add tatara-research-followup discovery skill"
Task A3: Skill-presence test¶
Files: - Test: internal/bootstrap/skills_test.go (extend if it exists, else create)
- Step 1: Write the failing test. First check for an existing skills test:
grep -rn "templates/skills\|installSkills\|func Test" internal/bootstrap/skills_test.go. If none exists, createinternal/bootstrap/skills_test.go:
package bootstrap
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestDiscoverySkillsPresentAndValid(t *testing.T) {
root := "../../templates/skills"
for _, name := range []string{"tatara-deep-research", "tatara-research-followup"} {
path := filepath.Join(root, name, "SKILL.md")
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
s := string(b)
if !strings.HasPrefix(s, "---\n") {
t.Fatalf("%s: missing YAML frontmatter", name)
}
end := strings.Index(s[4:], "\n---")
if end < 0 {
t.Fatalf("%s: unterminated frontmatter", name)
}
fm := s[4 : 4+end]
if !strings.Contains(fm, "name: "+name) {
t.Fatalf("%s: frontmatter name does not match dir", name)
}
if !strings.Contains(fm, "description:") {
t.Fatalf("%s: frontmatter missing description", name)
}
body := s[4+end+4:]
if len(strings.TrimSpace(body)) == 0 {
t.Fatalf("%s: empty body", name)
}
}
}
- Step 2: Run it - expect PASS (the SKILL.md files from A1/A2 exist).
Run: cd tatara-claude-code-wrapper && go test ./internal/bootstrap/ -run TestDiscoverySkills -v Expected: PASS. (If it fails on the relative path, adjust root to match the test's package dir.)
- Step 3: Commit
git add internal/bootstrap/skills_test.go
git commit -m "test: assert discovery skills present with valid frontmatter"
Task A4 (SERIAL TAIL - after Lane B cli is merged + tagged): bump cli pin + guard¶
Files: - Modify: Dockerfile
-
Step 1: Find the cli pin + the build-time tool guard:
grep -n "TATARA_CLI_VERSION\|propose_issue\|review_verdict\|pr_outcome\|issue_outcome" Dockerfile -
Step 2: Set
TATARA_CLI_VERSIONto the new cli tag from Lane B. In the guard line/stage that asserts the operator tools exist, addcommentto the checked list (same form as the existingpropose_issue/issue_outcomeentries). -
Step 3: Build the guard stage locally to verify the pinned cli exposes
comment. Run:docker build --target <guard-stage-name> .(use the stage name found in step 1; the wrapper map calls it the cli test-guard stage). Expected: build succeeds (guard passes). -
Step 4: Commit
LANE B - tatara-cli¶
Task B1: comment MCP tool (TDD)¶
Files: - Modify: internal/mcp/tools.go (inside OperatorTools(), after the issue_outcome entry ~line 510) - Test: internal/mcp/tools_test.go
- Step 1: Write the failing test. Mirror the existing operator-tool tests in
tools_test.go(find one:grep -n "issue_outcome\|propose_issue\|func Test" internal/mcp/tools_test.go). Add:
func TestCommentToolBuildsTaskScopedPost(t *testing.T) {
t.Setenv("TATARA_TASK", "task-xyz")
var tool Tool
for _, x := range OperatorTools() {
if x.Name == "comment" {
tool = x
}
}
if tool.Name != "comment" {
t.Fatal("comment tool not registered in OperatorTools")
}
if tool.Target != TargetOperator {
t.Fatalf("comment tool target = %v, want TargetOperator", tool.Target)
}
method, path, body, err := tool.Build(map[string]any{"body": "design note"})
if err != nil {
t.Fatalf("build: %v", err)
}
if method != http.MethodPost {
t.Fatalf("method = %s, want POST", method)
}
if path != "/tasks/task-xyz/comment" {
t.Fatalf("path = %s, want /tasks/task-xyz/comment", path)
}
m, ok := body.(map[string]any)
if !ok || m["body"] != "design note" {
t.Fatalf("body = %#v, want {body: design note}", body)
}
}
func TestCommentToolRequiresBody(t *testing.T) {
t.Setenv("TATARA_TASK", "task-xyz")
var tool Tool
for _, x := range OperatorTools() {
if x.Name == "comment" {
tool = x
}
}
if _, _, _, err := tool.Build(map[string]any{}); err == nil {
t.Fatal("expected error when body missing")
}
}
net/http is imported in the test file (it is used by sibling tests). -
Step 2: Run - expect FAIL (
comment tool not registered). Run:cd tatara-cli && go test ./internal/mcp/ -run TestCommentTool -vExpected: FAIL. -
Step 3: Implement. In
internal/mcp/tools.go, add this entry to theOperatorTools()return slice, immediately after theissue_outcomeentry (before the closing}):
op("comment", "Post a free-form comment on the current task's linked issue (answer maintainer questions, post design notes). The operator posts it under the bot identity on the next reconcile and does NOT change the issue's lifecycle state. Use this to keep a discovery conversation alive; use issue_outcome to set the outcome.",
`{"type":"object","properties":{"task":{"type":"string"},"body":{"type":"string"}},"required":["body"]}`,
func(a map[string]any) (string, string, any, error) {
tk := argOrEnv(a, "task", "TATARA_TASK")
if tk == "" {
return "", "", nil, fmt.Errorf("task required")
}
if argString(a, "body") == "" {
return "", "", nil, fmt.Errorf("body required")
}
body := map[string]any{"body": a["body"]}
return http.MethodPost, "/tasks/" + url.PathEscape(tk) + "/comment", body, nil
}),
OperatorTools doc comment count from "13" to "14" (line ~284). -
Step 4: Run - expect PASS. Run:
cd tatara-cli && go test ./internal/mcp/ -run TestCommentTool -vExpected: PASS. -
Step 5: Commit
git add internal/mcp/tools.go internal/mcp/tools_test.go
git commit -m "feat: add task-scoped comment MCP tool"
Task B2: Fix stale propose_issue description (boy-scout)¶
Files: - Modify: internal/mcp/tools.go:390
-
Step 1: Replace the
propose_issuedescription string. Change:"Propose a new SCM issue for a deferred bug or improvement; created behind the awaiting-approval label until a human approves."to:"Propose a new SCM issue (bug or improvement). The operator opens it under the bot identity as an idea-labelled discovery issue; it stays in discussion until a human approves. Embed <!-- tatara-authored --> in the body to keep it in discovery." -
Step 2: Run the package tests - expect PASS (no behavior change). Run:
cd tatara-cli && go test ./internal/mcp/ -vExpected: PASS. -
Step 3: Commit
Task B3: Release a new cli tag (after B1/B2 reviewed + merged to main)¶
- After merge to main + green pipeline, tag the next version (check latest:
git tag --sort=-v:refname | head -1), e.g.git tag vX.Y.Z && git push origin vX.Y.Z. Record the tag for Lane A Task A4.
LANE C - tatara-operator¶
Task C1: Add PendingComments to TaskStatus + regenerate¶
Files: - Modify: api/v1alpha1/task_types.go (TaskStatus, ~line 101)
- Step 1: Add the field to the
TaskStatusstruct (nearIssueOutcome):
// PendingComments are free-form comments queued by the agent via the
// comment MCP tool, posted to the task's linked issue on the next
// reconcile and then cleared. Does not change the lifecycle state.
PendingComments []string `json:"pendingComments,omitempty"`
-
Step 2: Regenerate CRDs + deepcopy. Run:
cd tatara-operator && make manifests generateExpected:config/crd/...Task CRD gainspendingComments;zz_generated.deepcopy.goupdates. If the chart vendors CRDs (charts/*/crds/), copy the regenerated CRD there (checkgrep -rn "pendingComments\|issueOutcome" charts/*/crds/ config/crd/). -
Step 3: Build - expect PASS. Run:
cd tatara-operator && go build ./...Expected: success. -
Step 4: Commit
git add api/v1alpha1/task_types.go config/crd charts zz_generated.deepcopy.go
git commit -m "feat: add Task.Status.PendingComments queue"
Task C2: POST /tasks/{t}/comment REST handler (TDD)¶
Files: - Modify: internal/restapi/handlers.go (after issueOutcome, ~line 380) - Modify: internal/restapi/server.go (route, after line 56) - Test: the restapi handler test file (find: grep -rln "issueOutcome\|func Test" internal/restapi/*_test.go)
- Step 1: Write the failing test. Mirror the existing
issueOutcomehandler test. Add a test that POSTs{"body":"hello"}to/tasks/{t}/commentfor a Task withSpec.Source.IssueRefset, asserts 200, and asserts the Task'sStatus.PendingCommentscontains"hello"; plus a 400 case for empty body and a 409 for a Task with no Source. Use the same test harness (fake client + chi router) theissueOutcometest uses. Example assertion core:
func TestPostComment_QueuesComment(t *testing.T) {
task := newTestTask("t1") // helper used by sibling tests; Spec.Kind issueLifecycle
task.Spec.Source = &tatarav1alpha1.TaskSource{IssueRef: "owner/repo#5"}
srv, c := newTestServer(t, task) // mirror the issueOutcome test setup
rr := doJSON(t, srv, http.MethodPost, "/tasks/t1/comment", map[string]any{"body": "hello"})
if rr.Code != http.StatusOK {
t.Fatalf("code = %d, want 200; body=%s", rr.Code, rr.Body.String())
}
var got tatarav1alpha1.Task
_ = c.Get(context.Background(), client.ObjectKey{Namespace: testNS, Name: "t1"}, &got)
if len(got.Status.PendingComments) != 1 || got.Status.PendingComments[0] != "hello" {
t.Fatalf("PendingComments = %#v, want [hello]", got.Status.PendingComments)
}
}
newTestTask/newTestServer/doJSON/testNS) to whatever the sibling issueOutcome test actually uses. -
Step 2: Run - expect FAIL (no route/handler). Run:
cd tatara-operator && go test ./internal/restapi/ -run TestPostComment -vExpected: FAIL (404 / undefined). -
Step 3: Implement the handler in
handlers.go:
type issueCommentReq struct {
Body string `json:"body"`
}
func (s *Server) postComment(w http.ResponseWriter, r *http.Request) {
var req issueCommentReq
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
return
}
if req.Body == "" {
writeError(w, http.StatusBadRequest, "body required")
return
}
var t tatarav1alpha1.Task
if err := s.c.Get(r.Context(), client.ObjectKey{Namespace: s.ns, Name: chi.URLParam(r, "t")}, &t); err != nil {
writeClientErr(w, err)
return
}
if t.Spec.Source == nil || t.Spec.Source.IssueRef == "" {
writeError(w, http.StatusConflict, "comment requires a task linked to an issue")
return
}
t.Status.PendingComments = append(t.Status.PendingComments, req.Body)
if err := s.c.Status().Update(r.Context(), &t); err != nil {
writeClientErr(w, err)
return
}
writeJSON(w, http.StatusOK, toTaskDTO(t))
}
- Step 4: Register the route in
server.go, after line 56 (/tasks/{t}/issue-outcome):
-
Step 5: Run - expect PASS. Run:
cd tatara-operator && go test ./internal/restapi/ -run TestPostComment -vExpected: PASS. -
Step 6: Commit
git add internal/restapi/handlers.go internal/restapi/server.go internal/restapi/*_test.go
git commit -m "feat: POST /tasks/{t}/comment queues an agent comment"
Task C3: Drain PendingComments in reconcileLifecycle (TDD)¶
Files: - Modify: internal/controller/lifecycle.go (reconcileLifecycle, insert after the project fetch at line 243, before the spawn-gate block at line 245) - Test: internal/controller/lifecycle_*_test.go (use the fake SCMWriter with a Comment method already present in lifecycle_test.go:47)
- Step 1: Write the failing test. Add a test that builds a lifecycle Task with
Status.PendingComments = ["a","b"]andSpec.Source.IssueRefset, runsreconcileLifecyclewith a fake writer that recordsCommentcalls, and asserts: both comments were posted to the issue ref in order, and the Task'sStatus.PendingCommentsis cleared afterward. Mirror the existing lifecycle reconcile tests (they wirescmContextvia the reconciler's fake reader/writer). Core assertion:
func TestReconcileLifecycle_DrainsPendingComments(t *testing.T) {
task := newLifecycleTask(t, "Triage") // helper used by sibling lifecycle tests
task.Spec.Source = &tatarav1alpha1.TaskSource{IssueRef: "owner/repo#7"}
task.Status.PendingComments = []string{"first", "second"}
r, writer := newLifecycleReconciler(t, task) // mirror sibling setup; writer records Comment calls
if _, err := r.reconcileLifecycle(context.Background(), task); err != nil {
t.Fatalf("reconcile: %v", err)
}
if got := writer.comments("owner/repo#7"); len(got) != 2 || got[0] != "first" || got[1] != "second" {
t.Fatalf("posted comments = %#v, want [first second]", got)
}
// reload + assert cleared (the reconcile persists via Status().Update)
var reloaded tatarav1alpha1.Task
_ = r.Get(context.Background(), client.ObjectKey{Namespace: task.Namespace, Name: task.Name}, &reloaded)
if len(reloaded.Status.PendingComments) != 0 {
t.Fatalf("PendingComments not cleared: %#v", reloaded.Status.PendingComments)
}
}
lifecycle_test.go:47 already has Comment(ctx, _, issueRef, body); extend it to record bodies per issueRef if it does not already. -
Step 2: Run - expect FAIL (comments not drained). Run:
cd tatara-operator && go test ./internal/controller/ -run TestReconcileLifecycle_DrainsPendingComments -vExpected: FAIL. -
Step 3: Implement the drain. In
reconcileLifecycle, insert immediately after the project fetch (after line 243), before the// Memory + concurrency gatesblock:
// Drain agent-queued free-form comments (from the comment MCP tool) to the
// linked issue before anything else, then clear and requeue.
if len(task.Status.PendingComments) > 0 && task.Spec.Source != nil && task.Spec.Source.IssueRef != "" {
_, _, writer, token, _, err := r.scmContext(ctx, task)
if err != nil {
r.Metrics.ReconcileResult("Task", "error")
return ctrl.Result{}, fmt.Errorf("lifecycle drain comments: %w", err)
}
for _, c := range task.Status.PendingComments {
if cerr := writer.Comment(ctx, token, task.Spec.Source.IssueRef, c); cerr != nil {
r.Metrics.ReconcileResult("Task", "error")
return ctrl.Result{}, fmt.Errorf("lifecycle drain comment: %w", cerr)
}
l.Info("lifecycle: agent comment posted",
"action", "scm_agent_comment", "resource_id", task.Name)
}
task.Status.PendingComments = nil
if err := r.Status().Update(ctx, task); err != nil {
r.Metrics.ReconcileResult("Task", "error")
return ctrl.Result{}, fmt.Errorf("lifecycle clear comments: %w", err)
}
return ctrl.Result{Requeue: true}, nil
}
scmContext destructuring matches lifecycle.go:666; writer.Comment matches the backstop call at lifecycle.go:711.) -
Step 4: Run - expect PASS. Run:
cd tatara-operator && go test ./internal/controller/ -run TestReconcileLifecycle_DrainsPendingComments -vExpected: PASS. -
Step 5: Commit
git add internal/controller/lifecycle.go internal/controller/lifecycle_test.go
git commit -m "feat: drain queued agent comments to the linked issue on reconcile"
Task C4: Nudge triage prompt to invoke the follow-up skill (TDD)¶
Files: - Modify: internal/controller/turnloop.go:58 (lifecycleTriageText) - Test: internal/controller/lifecycle_m2_test.go (has TestBuildTriagePrompt_*)
- Step 1: Write the failing test. Add to
lifecycle_m2_test.go:
func TestLifecycleTriageText_NamesFollowupSkill(t *testing.T) {
task := &tatarav1alpha1.Task{}
task.Spec.Source = &tatarav1alpha1.TaskSource{IssueRef: "o/r#1", URL: "http://x"}
got := lifecycleTriageText(task, "T", "B")
if !strings.Contains(got, "tatara-research-followup") {
t.Fatalf("triage prompt does not invoke tatara-research-followup skill:\n%s", got)
}
}
-
Step 2: Run - expect FAIL. Run:
cd tatara-operator && go test ./internal/controller/ -run TestLifecycleTriageText_NamesFollowupSkill -vExpected: FAIL. -
Step 3: Implement. In
lifecycleTriageText, add a line to the prompt (e.g. before "Your job:") instructing skill use:
"Invoke the `tatara-research-followup` skill, which defines how to research the gap, post design comments via the comment tool, and decide the outcome.\n\n"+
fmt.Sprintf format string at an appropriate point (after the issue body block, before "Your job:"). Keep the existing args order intact. -
Step 4: Run - expect PASS (and confirm sibling
TestBuildTriagePrompt_*still pass). Run:cd tatara-operator && go test ./internal/controller/ -run "TestLifecycleTriageText_NamesFollowupSkill|TestBuildTriagePrompt" -vExpected: PASS. -
Step 5: Commit
git add internal/controller/turnloop.go internal/controller/lifecycle_m2_test.go
git commit -m "feat: triage prompt invokes the tatara-research-followup skill"
Task C5: Nudge brainstorm goal to invoke the deep-research skill (TDD)¶
Files: - Modify: internal/controller/projectscan.go:772 (the brainstorm goal string) - Test: internal/controller/projectscan_*_test.go (find brainstorm tests: grep -rln "brainstorm\|Propose a single" internal/controller/*_test.go)
-
Step 1: Inspect the goal construction at
projectscan.go:772:goal := "Propose a single, well-defined issue for repo " + slug. -
Step 2: Write the failing test. Add a test asserting the brainstorm goal text contains
tatara-deep-research. If the goal is built inline, refactor it to a small helperbrainstormGoal(slug string) stringso it is unit-testable, and test the helper:
func TestBrainstormGoal_NamesDeepResearchSkill(t *testing.T) {
g := brainstormGoal("tatara-cli")
if !strings.Contains(g, "tatara-deep-research") {
t.Fatalf("brainstorm goal does not invoke tatara-deep-research skill: %s", g)
}
if !strings.Contains(g, "tatara-cli") {
t.Fatalf("brainstorm goal lost the repo slug: %s", g)
}
}
-
Step 3: Run - expect FAIL. Run:
cd tatara-operator && go test ./internal/controller/ -run TestBrainstormGoal -vExpected: FAIL. -
Step 4: Implement. Replace the inline goal with a helper and call it at line 772:
func brainstormGoal(slug string) string {
return "Invoke the `tatara-deep-research` skill to research the platform deeply and propose a single, " +
"well-defined discovery issue for repo " + slug + ". The skill defines how to research via the " +
"tatara-memory graph and on-disk code, score leverage, dedup, and file exactly one issue via propose_issue."
}
goal := brainstormGoal(slug). -
Step 5: Run - expect PASS. Run:
cd tatara-operator && go test ./internal/controller/ -run TestBrainstormGoal -vExpected: PASS. -
Step 6: Commit
git add internal/controller/projectscan.go internal/controller/projectscan_run_test.go
git commit -m "feat: brainstorm goal invokes the tatara-deep-research skill"
Task C6: Operator full-suite gate¶
- Step 1: Run the full operator test suite. Run:
cd tatara-operator && go test ./... && go vet ./...Expected: PASS. Fix any breakage from C1-C5 (e.g. fake writers needing the recording helper).
Integration & landing (per repo)¶
For EACH repo (wrapper, cli, operator), in its worktree:
- Run
superpowers:requesting-code-reviewon the diff; fix all critical/high findings. - Run the repo's linters + tests green: Go repos
golangci-lint run && go test ./...; wrapper alsohelm lint charts/*and helm-unittest if present. - Merge the worktree branch back to that repo's
main(hard rule 10), push. - Nurse the main CI pipeline until green. Re-run on transient eviction (a uniform ~18min "operation was canceled" = control-plane node flap, NOT a test failure - see global memory). Watch for ARC stale-listener stuck queues. Keep fixing real failures until green.
- Clean up the worktree.
Ordering: land operator (C) and cli (B) first; tag cli (B3); then do wrapper A4 (cli pin bump) and land wrapper (A). Deploy stays GATED (infra helmfile) - out of scope.
Self-review notes (author)¶
- Spec coverage: skill 1 -> A1; skill 2 -> A2; comment capability -> B1 + C2 + C3; discovery-stay -> A1 marker + existing self-approve guard (no new code); cross-platform research -> A1 step 2; full design conversation -> A2; invocation wiring -> C4 + C5; cli pin/guard -> A4; stale desc boy-scout -> B2. All covered.
- Type consistency:
PendingComments []stringdefined C1, written C2, drained C3.commenttool name consistent across B1/A4/C2 path. Skill dir names consistent A1/A2 vs C4/C5. - No placeholders: helper names in operator tests (
newTestTasketc.) are explicitly flagged "adapt to sibling test harness" because the exact harness names vary by file; the implementer has the sibling test open.