dystopic docs is in beta — content is actively being added.
dystopic
The regression flow

6 · Review the findings

Read the assembled review — gate status, per-case diff, reviewer findings, and trace deep links — and decide what changed and why.

Your local odyssey ci review just produced a review. Now read it. The review is the pairwise object the platform assembles from your PR's base and head commits: a per-case diff, a set of findings the reviewer narrates for the cases that changed behavior, and a single gate status that decides whether the check is green or red.

This page walks the assembled review top to bottom — the verdict header, the per-case diff table, each finding and its trace deep link, and the blocking reasons — so you know exactly what changed between base and head, and why the gate landed where it did.

The review is fetched from /api/ci/reviews/{id}, and its per-case trace and review deep links open on the dashboard at https://platform.pipelines.tech. The merge check itself is branded Dystopic.

The verdict at a glance

A blocked review renders like this (this is the same text report you saw at the end of Run the checks, printed to stdout, or to stderr under --json):

Dystopic — agent regression: blocked
  owner/repo#123 · base a1b2c3d → head e4f5a6b · suite default
  gate: blocks on: constraint regressions, reviewer findings ≥ critical
  cases 10 · regressed 1 (constraint 0) · improved 2 · findings 1

  case            base  head   Δ            trace
  ──────────────────────────────────────────────────────────────
  refund-denied   ✓     ✗     ▼ regressed  https://platform.pipelines.tech/ci/traces/8412
  signup-happy    ✓     ✓     −
  cancel-order    ✗     ✓     ▲ improved

  findings (1):
  [critical] refund-denied — head now issues the refund instead of refusing
      The verdict flipped from DENY to APPROVE; the head agent no longer
      checks the refund-window policy before acting.
      evidence (head):
      | Issued refund of $49.00 for order #8815.
      trace: https://platform.pipelines.tech/ci/traces/8412

  BLOCK: 1 reviewer finding(s) at or above 'critical'

  verdict: blocked — https://platform.pipelines.tech/ci/reviews/5

Read it in four passes: the status header, the diff table, the findings, and the blocking reasons. The rest of this page takes each in turn.

1 · Gate status — the one line that decides the check

The first line carries the gate status. It is one of exactly four values:

StatusMeaningExit code
passEvery gate tier passed.0
neutralThe suite is advisory — no blocking tiers configured, so nothing can fail.0
action_requiredInconclusive or platform-degraded — the reviewer couldn't produce an apples-to-apples verdict (see structural change).0
blockedOne or more gate tiers fired. This is the only status that turns the check red.1

The gate is described on the second line — gate: blocks on: constraint regressions, reviewer findings ≥ critical — reconstructed from your agent's gate config. A suite whose agent has no blocking tiers configured renders gate: advisory (no blocking tiers) and can only ever land neutral.

The gate that enforces this review lives on your umbrella agent — the dashboard gate card, or PUT /api/agents/{id}/gate. It is authoritative: RBAC-guarded and audited, and a PR cannot weaken or remove it. Dropping config on your head commit does not disarm the check — the platform-resolved gate still applies and fails closed (red). See the CI/CD step for how the check resolves.

2 · The diff table — what changed, case by case

The counts line summarizes the diff:

  cases 10 · regressed 1 (constraint 0) · improved 2 · findings 1
  • casesdiff.intersecting: scenarios that ran on both sides (the only ones eligible for the gate).
  • regresseddiff.regressions: cases that flipped PASS → FAIL.
  • constraintdiff.constraint_regressions: cases that newly violated a constraint on head.
  • improveddiff.improvements: cases that flipped FAIL → PASS.
  • findings — the number of reviewer findings (below).

Then the per-case table gives you one row per aligned case. Each side's cell is a verdict, and the Δ column names the transition:

Cell / markerMeaning
The case passed on that side.
The case failed on that side.
Absent or not evaluated on that side (or, in the Δ column, unchanged).
▼ regressedPASS → FAIL.
▲ improvedFAIL → PASS.
new / removedThe scenario exists on only one side.
constraint!Head newly violated a constraint on this case.
[xfail]The scenario is marked xfail — it runs and reports as a known expected failure, but never gates (excluded from the diff, the gate math, and infra thresholds).

An infra failure on a side (a build or run error rather than a real verdict) shows the raw status — e.g. infra: build_failed — instead of a tick.

3 · Findings — the reviewer's narration of what changed

A finding is one changed case that the pairwise reviewer analyzed and narrated. Findings appear only for cases that changed behavior between base and head — an unchanged case gets a table row but no finding.

Each finding carries:

  • case_id — the scenario's name.
  • severitycritical, warning, or nit. This is assigned by the reviewer, not derived from the judge's pass/fail.
  • title — a one-line summary of the change.
  • rationale — the longer explanation of what changed and why.
  • deep_link — a URL into the head trace for that case on the platform.
  [critical] refund-denied — head now issues the refund instead of refusing
      The verdict flipped from DENY to APPROVE; the head agent no longer
      checks the refund-window policy before acting.
      trace: https://platform.pipelines.tech/ci/traces/8412

The deep_link on a finding points to the head trace only. To compare the exact base-side behavior side-by-side, open the full review page (https://platform.pipelines.tech/ci/reviews/<review_id>), which aligns base and head traces per case.

Severity drives how findings surface in CI annotations: when your gate blocks on reviewer_severity, a finding at or above that threshold becomes a hard ::error; every other finding is an advisory ::warning. If no reviewer tier is configured, all findings are warnings. (Blocking reasons themselves are always ::error.)

4 · Blocking reasons — why the gate fired

When the status is blocked, the review lists a blocking_reasons array — one human string per tier that fired:

  BLOCK: 1 reviewer finding(s) at or above 'critical'

Other reasons you'll see, depending on which gate tiers your agent configures:

  • A constraint regression — N case(s) introduced a tracked-constraint violation (Tier-1 regression).
  • A reviewer-severity block — N reviewer finding(s) at or above '<severity>'.
  • A min_pass_rate floor failure — head pass rate X% below floor Y%.
  • A named-scorer block — a deterministic scorer you gate on failed (or never ran) on head.

The exact wording of each reason is assembled server-side and echoed verbatim under BLOCK:. Each reason maps to a tier in your agent's gate config. To change what blocks, edit the gate on the dashboard (or dystopic agents gate set) — see Create a suite for the tier grammar.

Structural and suite changes

If you edited the world between base and head, some cases can't be compared apples-to-apples, and the review says so loudly rather than silently gating on a bad comparison:

  • Scenario rows changed (diff.suite_changed) — specific scenarios whose content differs between sides are excluded from the diff and the gate.
  • Structural change (diff.suite_structurally_changed) — the agent's tools or ledger schema changed between base and head. This isn't apples-to-apples at all, so the reviewer is skipped and the constraint/regression tiers are zeroed — the gate can't fire on a moved goalpost.

Both surface as a NOTE: banner in the report:

  NOTE: suite changed between base and head (2 case(s) excluded from the diff)
  NOTE: tool/ledger schema changed between base and head

A structural change is the intended path for a PR that legitimately reshapes the world. The check is never turned red on the structural diff so a real tool/schema change isn't blocked by a comparison that can't be made — re-run once base and head share a schema to get a real diff again.

Head-only reviews (no base to compare)

When there's no base to diff against — a first PR, a base commit with no prior check to align against, or a push build run with --head-only — the review runs head-only: only your head side runs, so there's no per-case diff and no pairwise reviewer, and the gate falls back to the absolute floor (min_pass_rate) if one is configured.

The report is keyed by branch instead of PR number, and the target line says so:

Dystopic — agent regression: pass
  owner/repo@feature-branch · head e4f5a6b · head-only (absolute gate) · suite default
  gate: blocks on: pass rate < 0.9
  cases 0 · regressed 0 (constraint 0) · improved 0 · findings 0

  verdict: pass — https://platform.pipelines.tech/ci/reviews/5

The status is neutral when no floor is set (nothing to gate on) and blocked only when the floor fails. There's no per-case flip table and no findings — there's nothing to compare against.

If you meant to run a base-vs-head diff but see a head-only report, your --base probably didn't resolve. Passing an empty ref like --base "origin/" silently degrades to head-only. Always compute the base with git merge-base origin/main HEAD.

Fetching the review programmatically

The inline report is usually all you need. When you want the raw object — for scripting, for a dashboard, or to fetch a review that timed out locally — you have three surfaces.

Add --json to any odyssey ci review invocation to get the full machine dump on stdout (the human report moves to stderr, so stdout stays byte-clean JSON):

odyssey ci review \
  --base "$(git merge-base origin/main HEAD)" \
  --head HEAD \
  --repo owner/repo \
  --pr-number 123 \
  --json > review.json

The dump wraps the raw review under a review key, alongside review_id, gate_status, and a report_markdown string (the same Markdown the GitHub Action posts as the PR comment). This is exactly what the GitHub Action consumes.

Fetch an assembled review directly by id:

curl -H "Authorization: Bearer $DYSTOPIC_API_KEY" \
  https://api.pipelines.tech/api/ci/reviews/5

The response carries status (assembled, failed, or skipped), the nested gate (with status and blocking_reasons), findings, diff, and a per-case cases array with aligned base/head verdicts and trace URLs.

The review is the pairwise object. To pull one side's raw per-task results — every task, verdict, and detail for a single check — export that run by its check id:

odyssey runs export 8815 --json results.json --junit results.xml

--json writes the full rollup (paged so no task is clipped); --junit writes a JUnit XML file for CI test-report widgets. This exports one run, not the review — it has no reviewer narration or base-vs-head diff.

If your local poll budget runs out before the platform finishes assembling, that is not a failure — the CLI exits 0 and prints the review_id. The platform finishes asynchronously and posts the check; fetch the review later with the API call above to see the final findings.

Decide what changed and why

Reading a review is a decision, not just a read:

  • Gate blocked on a real regression? A ▼ regressed case with a critical finding is a genuine behavior change. Open the head trace deep link, fix the agent, and re-run.
  • Gate blocked but the change is intended? If the new behavior is correct and the old expectation is stale, update the scenario's expected_outcome so the case reflects the new truth. (Marking a case xfail runs and reports it as a known expected failure but never gates — it's excluded from the diff and the gate math entirely.)
  • ▲ improved cases? Free wins — the reviewer surfaces improvements too, so you can confirm a fix landed everywhere you expected.
  • Suite went neutral on a structural change? That's the world itself changing shape. Land the schema change, then subsequent PRs diff against the new baseline.

Next

You can read a review locally and decide what to do with it. The last step is to make this happen automatically on every PR — a real branded Dystopic check, posted by the platform, with the same findings you just read.