Documentation Index
Fetch the complete documentation index at: https://docs.ctrlplane.dev/llms.txt
Use this file to discover all available pages before exploring further.
| Category | Status | Created | Author |
|---|---|---|---|
| Engine | Draft | 2026-05-12 | Aditya Choudhari |
Summary
A job agent’sPlan returns a single (current, proposed) diff today, stored
in one result row per agent invocation. Generalize so an agent can return
multiple labeled diffs per invocation, distinguished by a new kind column.
This unblocks #1075 (surface the rendered ArgoCD Application CR alongside the
existing rendered manifest diff) and gives future agents a uniform shape for
shipping multiple kinds of output.
Motivation
Issue #1075 asks for the rendered ArgoCD Application CR to be shown in plan output. The CR is already computed by the planner today (proposedApp at
argocd_plan.go:111) — used to create a temp Application, then discarded
after the downstream manifest diff is extracted.
More broadly, there’s no shape in the current model for an agent to express
“I have multiple distinct diffs the deployer should review.” The Plannable
interface and deployment_plan_target_result are 1:1.
Proposal
Storage
Add akind column. Each agent invocation produces N rows, one per kind.
''; new rows specify a kind explicitly
or fall back to ''. Section vocabulary is agent-defined — the schema
doesn’t enumerate kinds.
Plan interface
Plannable.Plan returns []PlanResult. Each result carries a Kind.
manifest (existing flow) and cr
(marshal proposedApp + fetch current Application from ArgoCD by name). TFC
emits one (plan). Agents that don’t implement Plannable are skipped as
today. All kinds for an invocation complete together; if any is incomplete,
the agent returns CompletedAt == nil and the worker requeues per the
existing pattern.
Stage-2 controller
The work-queue item still represents one agent invocation. Stage-1 inserts one row (the work item’s anchor). Stage-2 callsPlan, gets []PlanResult,
writes the first result into the anchor row and inserts additional rows for
the remaining kinds. Validation runs once across all rows for the invocation.
Validation: one run per invocation against flat input
Validation runs once per agent invocation, not per row. The OPA input is built by combining all kind-rows into a flat shape:input.manifest.proposed / input.cr.proposed. No
DB-layer routing, no per-rule applies_to_kind declaration. Reserved
top-level keys — agent_type, deployment, environment, resource,
proposed_version, current_version — are documented; agents shouldn’t use
these as section names.
Violations attach to the anchor row (the original stage-1 row). The
deployment_plan_target_result_validation schema is unchanged.
Aggregation: kind → agent → target
Rows are kind-level. Aggregates are agent-level. Target totals roll up over agents — same as today, just one extra rollup step.| Field | Rule across the agent’s kinds |
|---|---|
status | worst kind wins (errored > computing > unsupported > completed) |
hasChanges | OR across kinds |
aggregateResults then runs over agent states. Existing target-level counts
(Total, Completed, Errored, Changed, …) keep their semantics — they
just source from agent rollups instead of raw rows.
UI
- Plan results table: the existing Changes column shows the total
+N -Msummed across all kinds for the row. A new column surfaces the number of diff kinds for that row (e.g.2for an ArgoCD row producing manifest + CR), so the deployer knows there’s more than one diff behind the row before clicking in. - Detail modal: when a row is opened, the modal contains a select whose options are populated dynamically from whatever kinds the agent returned for that release target. Picking an option renders that kind’s diff. Single-kind rows show the select with one option (or hide it entirely).
GitHub check rendering
formatAgentSection iterates the agent’s kinds and renders each as its own
labeled diff block. aggregate.checkTitle follows the worst-kind / OR
rules above. Existing MaybeUpdateTargetCheck flow is otherwise unchanged.
Migration
- One ALTER adds
kind TEXT NOT NULL DEFAULT ''. - Existing rows keep
kind=''. The renderer maps''→ “Manifest” for legacy display (existing rows are all manifest diffs by construction). - No production validation rules depend on the current flat OPA input shape, so the input restructure has no rule breakage.
- Other Plannable agents (TFC, TestRunner) wrap their existing single
PlanResultin a one-element slice and setKindto a chosen string.
Out of scope
- Plan triggers other than version publish (no
deployment_plansnapshot rework, no resource/environment plan kinds). - Stage-1 controller fan-out, variable resolution, release-target snapshot.
- Anything beyond plan-result diff content.
Open Questions
- Per-kind vs invocation-level violation attribution. Validation runs once per invocation with all sections visible, so a violation logically describes the whole invocation. UI displays violations at the agent level. Could be revisited if rule authors want to tag violations with a specific kind for per-section display (e.g. “this denial is about the CR, not the manifest”). Default: invocation-level for v1.
-
Backfill legacy
kind=''rows or leave them. Backfilling existing rows tokind='manifest'is more honest but costs a single UPDATE. Leaving them as''works but requires the renderer to special-case legacy. Lean: leave as''; rows drain quickly viaexpires_at.