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/5Read 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:
| Status | Meaning | Exit code |
|---|---|---|
| pass | Every gate tier passed. | 0 |
| neutral | The suite is advisory — no blocking tiers configured, so nothing can fail. | 0 |
| action_required | Inconclusive or platform-degraded — the reviewer couldn't produce an apples-to-apples verdict (see structural change). | 0 |
| blocked | One 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- cases —
diff.intersecting: scenarios that ran on both sides (the only ones eligible for the gate). - regressed —
diff.regressions: cases that flipped PASS → FAIL. - constraint —
diff.constraint_regressions: cases that newly violated a constraint on head. - improved —
diff.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 / marker | Meaning |
|---|---|
✓ | The case passed on that side. |
✗ | The case failed on that side. |
− | Absent or not evaluated on that side (or, in the Δ column, unchanged). |
▼ regressed | PASS → FAIL. |
▲ improved | FAIL → PASS. |
new / removed | The 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.severity—critical,warning, ornit. 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/8412The 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_ratefloor 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 headA 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/5The 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.jsonThe 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/5The 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
▼ regressedcase with acriticalfinding 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_outcomeso the case reflects the new truth. (Marking a casexfailruns and reports it as a known expected failure but never gates — it's excluded from the diff and the gate math entirely.) ▲ improvedcases? 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.
5 · Run the checks
Run your bound suite as a base-vs-head diff with dystopic ci review --platform-suite — the same exit codes CI uses — and iterate before you push.
7 · Integrate into CI/CD
Connect your repo on the platform for zero-yaml, platform-authoritative CI, wire the GitHub Action and the Dystopic App, and land a branded Dystopic check on every PR.