From Issue to PR¶
Follow one issue through the whole machine and you will know what happens at every point between clicking "Submit" and seeing the change deployed, and where you can step in. This is the general walkthrough; Watch One Run is the same journey as one real run in tatara's own repository, with the thread quoted.
The eight states, up front¶
Where a piece of work stands is Task.status.state, a field you can watch with kubectl get tasks. It is a closed eight-value enum, and the whole page below is a tour of it:
| State | What is happening |
|---|---|
new | Triage. No pod runs |
refined | The approval gate. A pod is up, writing a plan and looking for a maintainer comment to cite |
under-implementation | Code is being written. A pod is up |
awaiting-review | A review pod is reading the diff |
merged | The merge phase. The operator is walking the Task's merge order. No pod |
deployed | Merged, waiting for the deploy to land. No pod |
done | Terminal, success |
rejected | Terminal, stopped |
Two things worth fixing in your head before you start. parked and failed are not states: parking is a separate flag, status.parkReason, that a Task carries alongside whatever state it is in. And merged and deployed name the phase, not the milestone - a Task at merged has an accepted approval and an operator working its merge order, not necessarily a merged pull request.
Twenty-five transitions connect those eight states, and six guards constrain them. The Task state machine tabulates every edge, guard, and park reason; this page does not repeat them.
The operator also projects a small, per-project-configurable set of labels onto the SCM issue as a read-only mirror of Issue.status.status and Task.status.state, so you can follow along without cluster access. The labels are a one-way projection, never a control input: nothing in the operator ever reads a label to decide what happens next. One you will see named throughout the runbooks is tatara-parked, applied whenever a Task carries any parkReason - see Approval Gates for the full projection rule.
Since the #521 lifecycle redesign: clarify is implement
This page describes the platform after the #521 lifecycle redesign folded the clarify agent kind into implement (both as the running agent and as the Task origin) and replaced the old 16-member status.stage with an 8-member status.state plus the orthogonal status.parkReason flag.
Step 1 - You open an issue¶
You open a GitHub issue in any repository enrolled in your tatara Project. The title and body are your only inputs, and tatara reads them verbatim.
Task state: (none yet)
If Project.spec.scm.reporterLogins is populated, the issue author must be the bot, a maintainer, or an allowed reporter, or the event is dropped at intake. Left empty (the shipped default), any author's issue is accepted. Either way, opening an issue does not itself grant anything - it only gets a Task minted.
This page follows the full path - issue opened, implement's approval-gate conversation runs first.
Step 2 - The operator mints a Task¶
The operator sees the new open issue in an enrolled repository, mirrors it as an Issue custom resource, and mints a Task custom resource with spec.kind: implement that owns it (SweepIssueKind - the origin role clarify used to play). The Task is the durable, project-scoped unit that carries state across the whole implementation stream - every Issue and MergeRequest it owns, plus status.notes, an append-only journal of plans, handoffs, and free-text continuation state every pod reads at turn 0.
apiVersion: tatara.dev/v1alpha1
kind: Task
metadata:
name: myproject-implement-2026-07-12-a3f9d # <project>-<kind>-<date>-<uid5>
spec:
projectRef: myproject
kind: implement
goal: "Support dark mode in the dashboard"
status:
state: new
Task state: new - no pod runs here; the operator classifies the origin and, by spec.kind, drives the transition to the matching agent state.
Step 3 - implement reads the issue¶
new transitions to refined, and the operator schedules a pod named imp-myproject-<repo>-i<issue> - a container running tatara-claude-code-wrapper with tatara-cli as its MCP server, TATARA_KIND=implement. The pod:
- Clones the repository.
- Reads the operator-rendered context bundle: the Issue, its comments, and any prior
status.notesfrom an earlier pod on this same Task - built fresh every turn, there is no resume mode. - Loads the code knowledge graph from tatara-memory.
- Presents everything to the agent and waits for a decision.
At this point implement has read-only access to the repository and issue - it does not write code yet. It is also a live polling pod: the operator re-spawns it on every new comment on an owned issue, not just once. See Implement for the full workflow.
Step 4 - implement decides: should we do this?¶
The agent reads the issue and the codebase and calls submit_outcome(action=...).
Path A - rejected¶
The issue is out of scope, already fixed elsewhere, or not actionable. The agent calls submit_outcome(action=rejected, reason=...). The operator posts the reason as a comment and closes the issue.
Task state: rejected - then the issue closes.
Path B - discuss¶
The issue needs clarification, a design choice, or human input. The agent calls submit_outcome(action=discuss, reason=...), posting its questions, and the pod tears down (no cost while waiting).
Task state: refined (waiting; pod-less until the next comment)
The Task keeps re-spawning implement on every new non-bot comment on the thread until a maintainer's comment satisfies the approval grammar (see Path C) or the idle budget (ConversationIdleDefault, 60m by default, once a pod is up) elapses with no approval, at which point the Task parks awaiting-human.
The agent judges meaning; only the operator's independent structural check grants anything
Even when implement itself concludes the issue is ready and calls submit_outcome(action=approved, approval_citations=...), the agent's judgment is informational: the operator independently re-verifies each cited comment against the thread, plus the pinned plan note's hash. A citation whose comment does not exist, whose author is not a verified non-bot maintainer, or whose quote does not genuinely occur in the body the operator holds means no approval, regardless of what the agent decided. This applies uniformly - a bot-authored brainstorm proposal and a human-filed issue are gated identically; there is no fast path for either. See Approval Gates.
Path C - approved¶
The agent decides this is worth building and calls submit_outcome(action=approved, reason=..., plan_note_id=..., approval_citations=[{id, quote}, ...]), citing who approved and why for every live Issue it owns that a maintainer has actually commented on - an Issue with no maintainer comment has nothing to cite and is not required to carry one, provided the eventual change stays under Project.spec.autoApproveMaxSignificance (off | patch | minor | major, default off, and an empty value also reads as off). That field is a severity ceiling on the auto-approve carve-out, not an on/off switch, and the ceiling is only checked later, against the level the agent declares in step 5 - a maintainer-cited approval is never severity-limited. plan_note_id names the plan the operator will hash and re-check before any code is written. The operator independently re-reads its own mirror and checks, for every live Issue this Task owns that has a citation to check: does the cited comment exist, is its author in maintainerLogins and never the bot, does the quoted text genuinely occur in the comment body, and has it not already been consumed? There is no requirement that the cited comment be the thread's most recent maintainer comment - the agent, not the operator, is responsible for reading whether a later comment withdraws an earlier approval.
- Every live owned Issue passes: the operator stamps
Issue.status.approvalon each and movesTask.status.statetounder-implementation. - Any live owned Issue fails: the Task parks
identity-unverified(HTTP 200, not an error, and nothing is posted to the issue thread - the refusal lives only in the operator's logs, notes and metrics). The next non-bot comment on any owned thread un-parks the Task, spawning a freshimplementpod that submits its own fresh citation.
Task state: under-implementation (only once the citation and plan-hash checks pass for every live owned Issue; otherwise parked(identity-unverified))
Step 5 - implement writes the code¶
Once the approval gate grants, refined transitions to under-implementation and the same pod - or its next turn on this Task - picks up the coding work (still TATARA_KIND=implement). It may work across every repo the Task owns MRs in - a run is not scoped to just the repo the issue was filed in.
Task state: under-implementation
The agent:
- Re-reads the issue and its conversation thread from the context bundle.
- Queries the code knowledge graph for relevant context.
- Plans the change - for large or cross-repo work it tiers out sub-agents (see subagent tiering).
- Writes code, commits, and pushes to branch
task/myproject-implement-2026-07-12-a3f9d. - Calls
submit_outcome(action=submitted, title=..., body=..., change_significance=..., merge_order=[...])-change_significance(major/minor/patch, driving the semver tag on push-CD repos) is required and, once set, can only be raised by a later reviewer, never lowered.merge_orderis required whenever the Task's MRs span more than one repo.
change_significance does not exist on the wire until this call, so this is also the first point where an auto-approved Issue's ceiling can be checked: if any live owned Issue was granted only through the autoApproveMaxSignificance carve-out, the operator compares the declared level against that ceiling and refuses the call with over-auto-approve-ceiling if it is over. A maintainer-cited approval carries no such ceiling.
Opening the merge request is itself gated on approval. mr_write(action=open) and this same submit_outcome(action=submitted) call refuse with 409 {reason: "approval-required"} while any live owned Issue still carries no approval evidence, naming each blocking issue and what it needs. There is no path from an unapproved issue to a PR - work done before the gate grants cannot be carried forward by anything. See Approval Gates for the mechanics.
The operator then opens the pull request, referencing the owned issue. If the agent instead calls submit_outcome(action=declined, decline_reason=...) - for example, the fix already shipped on a sibling branch - the Task parks implement-declined and no PR is opened.
Since tatara-operator#594, step 5's submit_outcome is itself gated: if the Task already owns an open MR with red CI, a real base conflict, or an unanswered request_changes, the call is refused outright with a structured 409 before anything is written, and the agent's prompt tells it to wait for its own pipeline (bounded ~20 minutes) and resubmit. See CI readiness gate.
A pod that never becomes Ready within 5 minutes of creation is respawned automatically, uncapped (maxPodRecreations is deprecated with zero effect - see Runbooks); a pod that runs past agentPodTTLSeconds is stopped with a guaranteed handoff note in status.notes and a fresh pod picks up the same Task. maxTurnsPerPod (implement's per-pod turn cap, with implement itself exempt) and the Task-lifetime maxTurnsPerTask that used to bound that exemption are both deprecated with zero effect; what bounds a runaway Task now is the 24h residency cap.
Step 6 - under-implementation -> awaiting-review¶
submit_outcome(action=submitted) with at least one owned MR open moves the Task straight to awaiting-review - there is no separate CI-polling state the Task sits in; CI status is read as part of the review and merge sequence itself.
Task state: awaiting-review
Step 7 - review approves, the operator merges¶
There is no separate review Task. The same Task moves to awaiting-review, the implement pod is torn down, and a review pod comes up against the opened PR (see PR / MR Review). It reads the diff read-only and calls submit_outcome(verdict=...):
verdict=approve- the outcome call writes nothing to the forge. It records a pending review on each ownedMergeRequestCR, carrying the SHAs the agent reports having reviewed. The operator posts the review afterwards, on the next reconcile: aCOMMENT-type review under the bot identity with the verdict in its body (GitHub 422s a self-authoredAPPROVEorREQUEST_CHANGESeither way, since there is only one bot identity). Once every owned MR's pending review has landed, the Task moves tomerged.verdict=request_changes- the Task returns tounder-implementationwith the review's findings as context. It is the same Task, with a fresh implement pod: the review pod is torn down, not run alongside.maxReviewRoundsis deprecated with zero effect, so theawaiting-reviewtounder-implementationcycle carries no round count at all (see the residency cap for the backstop that replaced it). The same review-approveoutcome is also subject to the CI readiness gate above - a red or unresolved MR refuses the approval outright.
Task state: awaiting-review until a verdict lands, then merged or back to under-implementation.
At merged the operator walks Task.spec.mergeOrder sequentially: for each repo it re-reads the live head, merges only if it still matches reviewedSHA and CI is green, and sends the Task back to awaiting-review if the head moved underneath it (bounded by headMoveReentries, cap 3, parking at head-moving). See Merge and Deploy for the full sequence - this is an operator action end to end; no MCP tool exposes merge, and auto-merge is never armed on a tatara-opened PR.
Step 8 - Deploy and delivery¶
Once every repo in mergeOrder is merged, merged moves to deployed - still pod-less. When every owned MR shows merged and the release has actually landed, the operator closes every owned issue with a citing comment and moves the Task to done.
Task state: deployed, then done.
MergeRequest.status.significance (set from the implement Task's change_significance, only ever raised by a reviewer) drives the semver tag the release job cuts. See semver push-CD for the tag-cut-to-cluster-apply chain.
Delivery is not documented per-change. A Task delivered in the last 24 hours becomes eligible for the next nightly documentation batch Task for its project, which covers everything delivered since the last run in one PR - not one documentation pipeline per merged change.
Full sequence diagram¶
sequenceDiagram
participant Dev as Developer
participant M as Maintainer
participant GH as GitHub
participant Op as tatara-operator
participant Pod as Agent Pod
participant Rev as review Pod
Dev->>GH: Open issue #42
GH-->>Op: webhook
Op->>Op: mint Task (kind=implement), state=new
Op->>Op: state=refined
Note over Op,Pod: implement pod running (approval-gate turn)
Op->>Pod: schedule implement pod
Pod->>GH: fetch issue + comments
Pod-->>Op: submit_outcome(action=approved, no citation yet)
Op->>Op: verifyApprovalScope: no comment to cite; park(identity-unverified)
M->>GH: Comment: "go ahead, I approve!"
GH-->>Op: webhook (comment event)
Op->>Op: sync comment mirror; un-park (no grant here)
Note over Op,Pod: fresh implement pod running
Op->>Pod: schedule implement pod
Pod->>GH: fetch refreshed comments
Pod-->>Op: submit_outcome(action=approved, plan_note_id=..., approval_citations=[{id, quote}])
Op->>Op: verify M in maintainerLogins, quote occurs verbatim, not previously consumed, plan hash matches
Op->>Op: Issue.status.approval stamped; state=under-implementation
Note over Op,Pod: same pod (or its next turn) writes code
Pod->>GH: clone repo, write code, commit, push
Pod-->>Op: submit_outcome(action=submitted, change_significance=minor)
Op->>GH: open PR
Op->>Op: state=awaiting-review
Op->>Rev: schedule review pod
Note over Op,Rev: review pod running
Rev->>GH: read PR diff (read-only)
Rev-->>Op: submit_outcome(verdict=approve, reviewed_shas=[...])
Op->>GH: read live PR head
Op->>GH: post COMMENT review (verdict in body)
Op->>Op: state=merged
Op->>GH: Merge(expectedHeadSHA=reviewedSHA)
Op->>Op: state=deployed
Op->>GH: close issue #42, citing the release
Op->>Op: state=done What to do when a Task is Parked¶
A Task parks (with a specific parkReason) when the operator cannot proceed without human input: the citation check found nothing to grant, the merge could not complete, CI never went green within the state deadline, or the agent explicitly declined. A park is not a failure and not a state - the Task stays exactly where it was and un-parks from there.
Not every park reason posts a comment explaining itself. identity-unverified posts nothing to the issue thread: the refusal is visible only in the operator's own logs, Task.status.notes, and the operator_approval_refused_total metric, never on the thread a maintainer is watching. Where the operator does post an explanatory comment for other park reasons, the comment turn-taking gate can still withhold a repeat one - for example, a Task that keeps parking on the same unanswered thread stops re-commenting after the first note - but for identity-unverified there was never a first note to begin with.
Your options:
- Comment on the issue. For
awaiting-humanoridentity-unverified, any non-bot comment un-parks the Task, spawning a fresh agent pod to read it, as appropriate to the park reason. - Comment as a maintainer to give the next
implementpod something unambiguous to cite as approval - there is no configured phrase to match, only the agent's judgment and the operator's independent verification of that citation. - Fix the underlying problem (a failing test, for example) and comment to resume; approval already recorded earlier in the same Task is not re-consumed.
You are not the only way out. Every park except backlog-sweep ages out on a 7-day retention window and is then reaped, and a Task parked under a reason nothing un-parks can be collected and its issue re-minted automatically at most three times before it becomes a real dead end sitting in the backlog with the tatara-parked label on it. Nothing waits indefinitely, and nothing retries forever.
Where to go next¶
- Watch One Run - this journey as one real nine-hour run, including the review round that sent the fix back and the rework it forced.
- The Task state machine - every edge, guard, and park reason, in full.
- The Agentic Operating Model - the model behind the walkthrough, including the two human gates and the security boundary.