Overview
Thedorgu remediation command group is how you work with RemediationAction resources. These are the fixes the Dorgu Operator proposes after it detects and diagnoses an incident. You list them, read the proposed plan as a diff, then approve or reject.
Approving does two things: it records your decision on the RemediationAction, and where Dorgu is allowed to, it heals the running workload. The operator re-checks the approval on the live cluster and issues a short-lived apply ticket. It applies the ticket to the ApplicationPersona (desired state), and the CLI patches the Deployment with your credentials, using exactly what the ticket says. See How Dorgu decides for the path and Self-healing for the loop.
All subcommands shell out to
kubectl, so kubectl must be on your PATH, and your current context, or the global --context flag, must point at the target cluster. Every kubectl call a command makes uses the same --kubeconfig and --context, so a command never reads one cluster and writes another. The alias dorgu rem works everywhere dorgu remediation does.Exit codes
Lifecycle
Two condition reasons are worth knowing, both on the
Applied condition. AdvisoryOnly accompanies Acknowledged. PreconditionRejected marks a plan the executor refused before touching the cluster, and GateRefusedAtApproval marks one the gate refused when you approved it. Neither starts the 30-minute failure cooldown, since nothing in the cluster actually went wrong.
remediation list
Synopsis
RemediationAction resources. By default it shows only active ones: Pending, Approved, Ticketed, Applying, Verifying, and Failed. Use --all to include Completed, Acknowledged, RolledBack, Rejected, and Expired (a superseded proposal is Expired).
Flags
Example output
PLAN: rule-based is what a default install produces. It reads ai-anthropic where you have turned the planner on.
Output columns
The GUARDRAIL column
It appears only when there is something to put in it, sitting besideSTEPS. On a cluster where no guardrail has ruled on anything, the column is absent and the list prints exactly what it printed before.
rejected (a field refused outright) over clamped (a value substituted) over derived (a value Dorgu sized itself), with - when no guardrail ruled.
It is a pointer, not the record. One word cannot carry a field-by-field account, and it does not try to. Read dorgu remediation diff for what was actually decided.
Requires operator v0.11.0 with CLI v0.12.0. Against an older operator the underlying spec.steps[].safety field is absent, the column never appears, and --json gains no safety key.
There is no severity here.
RemediationAction carries no severity field, so this command cannot filter or sort by it. The blank SEVERITY column and the --severity flag that matched nothing were both removed. Filter by --phase, and read severity from the linked incident with dorgu incidents describe, where --severity does work.Examples
remediation diff
Synopsis
Flags
Example output
[order] type (risk; mode): description, with the rationale indented underneath, an optional Run: command, and a unified diff of prePatchState → patch where the step carries one.
There is no
Severity: row. RemediationAction carries no severity field. Read severity from the linked incident with dorgu incidents describe.Explanation: and Plan summary: say different things: the summary is the root cause (why it broke), the explanation is the shape of the response (what the plan will and will not do for you). They used to render the same paragraph twice; where an older operator wrote both fields identically, the CLI now prints it once.The Workload: and Owner: header, and the Deployment change block
Two blocks in that output are about the thing that is actually failing, rather than about the persona.
Workload: and Owner: name the live Deployment the operator observed and who reconciles it. The persona is what Dorgu records; the workload is what runs, and on a brownfield cluster the two rarely share a name (persona frontend over Deployment frontend-podinfo). Owner: reads unmanaged (nothing reconciles it, so Dorgu may patch it) when Dorgu may write, and otherwise names the owner: Helm release "frontend" in namespace apps. Both lines are omitted when the operator resolved no workload at all.
Deployment change is a workload-to-workload diff, built from workloadRef.observedResources. The old diff read the persona on both sides, which is why a fix that silently introduced a CPU limit the container never had showed nothing at all:
Adding a key the workload does not set is now refused at proposal time, so the
(adds a key this workload does not set) marker should not appear on a v0.9.0 operator. It stays in the renderer because objects proposed by an older operator are still readable, and an added key is exactly the thing that must not be invisible.On an owned workload
The header names the owner, and the suggested actions change:approve is not offered, because that command would be declined.
Pending, that action block is gone, so the Deployment change block carries the fact instead: Dorgu did not apply this: <owner> owns this Deployment.
Guardrail verdicts
When one of Dorgu’s guardrails has ruled on a field,diff prints what it decided under its own heading, per step, between the rationale and the diff that reflects it.
applying nothing is stated rather than left to inference, because a missing word is not a fact a reader should have to notice.
The verdicts appear in three places, each of them the last screen before a decision:
Printed once, not twice. The operator writes a guarded step’s
description as its own sentence followed by each safety message verbatim, so that a client which knows nothing of the field still delivers the verdict. The CLI takes those messages back out of the description, so the verdict appears once, under the heading, with the numbers beside it, rather than twice, three lines apart.spec.steps[].safety is absent, no block is printed, and the step renders exactly as it did before. Full schema: step safety fields. How the verdicts are produced: guardrail verdicts as data.
Ready-to-run commands on advisory steps
An advisory step is one Dorgu will not carry out for you. Where a single command does the job, the step carries it anddiff prints it under the rationale:
kubectl set image yourself.
Requires operator v0.8.0 and CLI v0.9.0 or newer. Older objects simply carry no command, and no Run: line is printed.
On an owned workload, only read-only commands are printed
A command that writes to a Deployment that Helm or ArgoCD owns takes field ownership from them and breaks their next apply, so it is not something to hand over. On an owned workload the CLI prints only commands it can positively classify as read-only:kubectl logs and kubectl get events matter most on the workloads Dorgu will not patch, because reading is the whole of what is left to hand over. Anything on an unmanaged workload is printed unchanged.
Classification is positive, not inferred from the absence of a write verb: a command whose verb matches nothing at all is refused rather than assumed harmless. The verb is found by scanning for the first token that is a known kubectl subcommand, so kubectl -n apps patch ... cannot hide patch behind a flag argument.
The operator already strips workload-writing commands before it stores the object. The CLI establishes read-only-ness again anyway, because the command is model-authored and the CLI reads
RemediationAction objects straight out of the cluster, where an older operator or anything with permission to create the CRD could have written a kubectl patch. The shell-metacharacter check still runs first, on owned and unmanaged workloads alike.Advisory plans
Some plans have nothing the CLI can apply, typically anotification action or a plan whose every step is advisory. Those say so plainly and offer only reject:
Acknowledged. Every plan for a workload that Helm, Argo CD, Flux or kustomize owns is of this kind from operator v0.13.1: it is advisory only, and approving it changes nothing in the cluster, not the workload and not the ApplicationPersona. It used to be offered approve as its suggested next action, and following that advice failed the remediation and put the app into a 30-minute cooldown.
Only
persona-update steps can ever be auto. The Kubernetes API server enforces this with a CEL validation rule on the CRD, so no plan, whether AI-written or not, can mark a workload change as auto-executable.RemediationAction objects that carry a single spec.action instead of an ordered plan render a single Proposed change: diff rather than a Plan (n steps): block.
remediation approve
Synopsis
Pending remediation and, when Dorgu owns the write, heals the workload.
What approval actually does
1
The CLI preflights the workload change
Before anything is written: the protected-context guard, the remediation plan, the ownership check, the target Deployment, the container, and the patch. If any of it fails, nothing is approved. On an owned workload this is where the command stops: it prints the refusal and exits
4, having written neither a Deployment patch nor a status patch.2
The CLI records your decision
It patches the RemediationAction status subresource to
phase: Approved, with approvedBy: cli-user and the current timestamp. The write carries the resourceVersion the CLI read. If anything wrote to the action in between, the CLI re-reads it. A status-only write is retried on the new version. If the spec changed since you read the plan (its generation moved), the CLI refuses the retry and tells you to re-run dorgu remediation diff, because the approval would land on a plan you never saw. If someone else’s approval landed first and stands (Approved, Ticketed, or Applying with a ticket), it says already approved by another actor, writes nothing, and exits 0 without healing. Run dorgu remediation heal to apply it. “Approved” is printed only after a short settle, once the operator still has the approval standing. If the operator expired or superseded the action in that moment, the CLI says what it became and exits 1.3
The operator re-checks the approval and writes a ticket
The operator runs its gate again on the live cluster: the same workload, an unedited spec, current policy, current guardrail arithmetic. If it clears, it writes
status.applyTicket (the Deployment’s UID and generation, the container, the exact patch, every verdict, and an expiry 10 minutes out) and the phase becomes Ticketed. If it refuses, the action ends Failed with GateRefusedAtApproval and nothing is applied. See the approval-time re-check.4
The operator applies the ticket to the ApplicationPersona
It applies the ticket’s patch to the persona’s
spec, the app’s desired-state record, then moves the action to Applying. It never touches your Deployment, and it cannot: its ClusterRole has no write verbs on apps/deployments. See security and permissions.5
The CLI waits for the ticket, then heals the workload
The CLI waits up to 30 seconds for the ticket (a spinner on a terminal, one line when piped). It uses only a ticket the operator issued at or after your approval was recorded, with at most ten minutes of life. It then patches the matching Deployment with your credentials, with the ticket’s change in the ticket’s container, and nothing from the plan you previewed. If the ticket differs from the preview, it says so. Use
--no-heal to skip this.6
The CLI records the patch
A
WorkloadPatched condition is stamped on the RemediationAction naming the Deployment, container, and fields it set, so the record and the cluster agree. The phase is already Approved or later by this point, so only the condition is written. See what the heal records. The operator uses that condition when it decides heal credit.Expired, and the sentence follows the Expired condition’s reason:
An approval of a proposal that predates the gate is shown as the gate’s refusal (
NoProposalRecord, exit 4) with a Note: line carrying the operator’s own account, and not as “ended in phase Expired”.
When the operator’s gate refuses the approval, the CLI shows the reason the operator persisted and exits 4:
BlastRadius, WorkloadGone, NoProposalRecord, SpecChanged, SelfHealingMode or Concurrent. It is also on the action as the reason of its GateRefused condition.
Advisory steps (restart, scale, config-change, manual, and any persona-update that is not a resource change) are printed as numbered manual instructions. They are never executed.
On an owned workload
Flags
--heal and --no-heal are mutually exclusive.
--next takes the oldest pending remediation, so the longest-waiting incident goes first; ties break on namespace and name, making the pick reproducible. It previously ranked by severity, which RemediationAction does not carry. Every candidate tied, so the winner was effectively arbitrary.Examples
Recording a decision without a patch
--no-heal is how you say “I have read this, I agree, and I will apply it myself”. It records phase: Approved, lets the operator re-check, issue a ticket and patch the persona spec, skips the Deployment patch, and exits 0.
It is the intended path in three situations: an owned workload, a GitOps-managed persona, and any time you would rather apply the change through your own pipeline.
The CLI warns that the persona and the running workload will disagree until you do apply it. Heed that: the operator’s verification window will run against a workload that has not changed yet.
The target Deployment is resolved before the approval is recorded. If the workload cannot be found, nothing is approved and nothing is changed, and the error lists the Deployments that are present. Approving first and failing to resolve afterwards used to leave the persona at the new limits, the workload at the old ones, and a 10-minute verification window running over a change that never landed.
the remediation is Approved but <ns>/<deployment> was NOT patched; the persona and the workload now disagree. --no-heal warns about the same divergence up front.
That is the divergence where the workload did not change. Since CLI v0.12.0 there is a second, opposite one: the workload changed but the record could not be written. Both exit non-zero, and both name which way round they run, because they call for different responses. See when the record cannot be written.
Only Pending remediations can be approved. Anything else exits with an error naming the current phase.
remediation reject
Synopsis
Rejected. Works from Pending or Approved: any other phase is refused.
Flags
RejectionReason status condition, written in the same patch that sets the phase, so a rejection never exists without it. The condition’s reason is the CamelCase of the flag (WrongDiagnosis, TooRisky, and so on) and its message is the note. Nothing parses the note.
A rejection holds the same fix for the same incident for one hour. A rejection of a milder shape does not hold a strictly worse shape of the same fault. See How Dorgu decides.
Rejecting is also a safety gate: dorgu remediation heal refuses to run against a rejected remediation.
remediation heal
Synopsis
approve runs this for you unless you passed --no-heal, so reach for heal when you deferred the apply, or when a previous heal failed and you want to retry it.
heal is not an approval. It applies the operator’s apply ticket, not the plan, and a ticket exists only for an action a human approved. Healing a Pending remediation is refused: approve it first. It works only while the operator still holds the action in Approved, Ticketed or Applying, and the ticket lives 10 minutes. Applying is the operator’s verification window, so after approve --no-heal you have that long. Once the action is Verifying or Completed, heal is refused, because the operator’s verdict would be about a workload nobody changed. Apply the change yourself instead.
Within those limits it is idempotent, which is what makes re-running it the fix for a heal that patched but could not record.
Flags
How the workload is found
1
Ownership
The first gate.
spec.workloadRef.managedBy must be unmanaged. Everything else, including unknown and an absent workloadRef, is declined with the refusal and exit code 4. Nothing after this step runs.2
Ticket
The action must carry a ticket the operator issued at or after the approval on record (
status.applyTicket.issuedAt), unexpired, with a lifetime of at most ten minutes, in phase Ticketed or the Applying the operator moves to in the same pass. A ticket already sitting on the action, one issued before the approval, one with no issue time, and one on an action already Verifying or Completed are ignored. Without a usable ticket nothing is patched.3
Namespace
The persona’s namespace (
spec.personaRef.namespace), falling back to the remediation’s own namespace. Never anywhere else.4
Deployment
Every Deployment in that namespace is a candidate, and the CLI walks the same ordered chain the operator uses, taking the first rung that matches exactly one Deployment: the
app.kubernetes.io/name label, then the app label, then metadata.name, then spec.selector.matchLabels. All against the persona’s spec.name.No label is required on the Deployment object. Helm, kustomize, and most hand-written YAML label the pod template only; that resolves on the last rung. Zero matches and an ambiguous rung are both errors, and both list the Deployments actually present in the namespace and point at --workload. See discovery.Whatever this resolves to, and however it was resolved, it must agree with spec.workloadRef.name. A mismatch is refused rather than patched.5
Container
The container the operator observed, unless
--container names another. Failing that, the only container if there is one, otherwise the container whose name matches the app. Anything else asks for --container.6
Patch
A strategic-merge patch that sets exactly the
resources.limits and resources.requests fields the ticket carries (cpu and memory only), in the ticket’s container, and nothing broader. The Deployment is matched on the UID and metadata.generation the ticket names, so a status or scale write does not invalidate a good ticket, and a spec change does. It runs under the field manager dorgu rather than kubectl’s default kubectl-patch, so the entry it creates is distinguishable from a kubectl patch you ran yourself.7
Release
The
dorgu entry is then removed from metadata.managedFields and the object is read back to confirm it is gone, so a heal leaves no ownership footprint and a later server-side apply does not conflict with it. If the removal fails, the heal still succeeded and the CLI says loudly what is left behind and how to clear it. New in CLI v0.11.0. See Dorgu leaves no field manager behind.8
Record
The
RemediationAction is updated to say what just happened, so the record and the cluster agree. New in CLI v0.12.0. See what the heal records.What the heal records
After a patch the cluster accepted, and only after one, the CLI stamps aWorkloadPatched condition on the RemediationAction. It names the namespace, the Deployment, the container, and each field that was set.
heal no longer approves anything, so the only thing it writes is the condition.
The condition matters beyond the record: the operator reads it when it decides whether Dorgu is credited with the heal. A recovery with no WorkloadPatched (a --no-heal, a declined prompt, a patch that changed nothing) is recorded as self-resolved, not as a Dorgu heal. See heal credit.
The write is a read-modify-write, because conditions are keyed by type and adding one means sending the whole list. Every other writer’s condition goes back as raw JSON byte for byte, the CLI’s own prior marker is replaced rather than appended, the
resourceVersion is sent as a precondition, and a conflict is retried against fresh state three times before it is reported. Round-tripping the operator’s conditions through a struct would silently drop fields the CLI does not know about.When the record cannot be written
The workload is patched by then, so nothing is undone. But the command exits1 and says which way round the disagreement runs, because exiting 0 there would reproduce the defect above:
Ticketed or Applying. After that, the heal is refused and the record stays incomplete. The Deployment is patched either way, so check it with kubectl get deploy.
Note that the success line still prints, because the heal genuinely succeeded. It is the record that failed, and the two are reported separately.
No paste-ready
kubectl patch is offered, on purpose. The only patch that would work replaces the whole conditions list, so handing one over would have you overwrite the operator’s own conditions to fix a record bug. Re-running the heal is both shorter and safe: it is idempotent, and it records the change on the way through.Safety gates
Protected contexts
Beforeapprove writes an approval, and before heal patches a workload, the CLI checks the name of the kube-context it is about to write through and refuses if the name contains a protected pattern. An approval is refused with or without --no-heal, because approving is itself a change: it tells the operator to update the persona. The context it checks is the global --context value when you pass one, and the kubeconfig’s current-context otherwise, so the name it checks is always the context the patch goes to.
The patterns come from DORGU_PROTECTED_CONTEXTS: comma-separated, case-insensitive substrings. The default is prod. An unset or blank value falls back to prod rather than turning the guard off, because an empty variable is far more often an accident than a decision to heal production.
1 and names the context and the pattern it matched. It opens with the command you ran, so the message is never about something you did not ask for:
dorgu report does not count it. Every other CLI decline is recorded as a Warning Event on the RemediationAction.
The ownership gate runs before the plan is resolved against the cluster, and an advisory-only plan never reaches it: there is no workload change to refuse, so an advisory remediation behaves identically on owned and unmanaged workloads.
Next steps
Ownership model
Why a Helm or ArgoCD-owned workload is declined, and what to do instead
Self-healing
How detection, diagnosis, planning, and verification fit together
AI setup
Turn on the AI planner with your own Anthropic key
dorgu incidents
Inspect the IncidentMemory a remediation was proposed for
CRD reference
Full RemediationAction schema, including
steps[] and rollback