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.
You have a registered agent, a declared world, and a suite bound to CI on the platform. The last thing to do before you push is prove the suite runs green on your machine, using the exact command your GitHub Action will run in CI.
That command is dystopic ci review --platform-suite (the CLI also ships as odyssey, so odyssey ci review --platform-suite is identical). It submits two sides — your PR's base commit and its head commit — as commit-keyed runs, waits for the platform to assemble a pairwise review, and gates on the diff between them. Run it locally and you get the same verdict, the same findings, and the same exit code you'll see on the PR. No surprises after you push.
The one command
From your repo root, on your feature branch:
dystopic ci review --platform-suite \
--base "$(git merge-base origin/main HEAD)" \
--head HEAD \
--repo owner/repo \
--pr-number 123With --platform-suite, the CLI submits no agent config and no scenarios. Instead, the platform resolves the execution recipe (run command, timeout, tools, credentials) from your connected repo's umbrella agent, and sources the bound suite's frozen scenarios server-side — byte-identical to an in-app check of that suite. Nothing about how the run executes lives in your checkout; the SHAs are all the CLI sends.
Four things are worth understanding about those arguments:
--baseis the PR's merge-base, not the base-branch tip.git merge-base origin/main HEADgives you the commit your branch forked from. This is what CI derives too — it's stable across pushes tomain, which is what makes the base side cache-hittable. Passing the base-branch tip instead re-runs the base every timemainmoves.--headdefaults toHEAD, so you can drop it when reviewing your current checkout.--repodefaults to$GITHUB_REPOSITORY. Locally you passowner/repoyourself.--pr-numberdefaults to$PR_NUMBER. Any placeholder integer is fine when iterating locally — it only keys the review's context.
--platform-suite fails closed with a 422 that names what to configure if the repo isn't connected, if the umbrella agent has no run command, or if the suite doesn't resolve to a bound platform suite. Connect the repo and set the gate + suite on the platform first — see Connect your repo on the platform.
What actually happens
dystopic ci review --platform-suite resolves SHAs from your local git and submits them to the platform API. In order:
Resolve the SHAs. --base and --head are resolved against your local checkout with git rev-parse. The base side pins the merge-base commit; the head side pins your current commit. These two SHAs are the only thing the CLI extracts from your repo.
Submit both sides as commit-keyed runs. Each side is POSTed to /api/ci/runs with its SHA and the --platform-suite flag — no config, no scenarios. The platform clones the connected repo at each SHA and runs the umbrella agent's recipe against the bound suite. Submissions are idempotent and cache-keyed: an identical base that already ran is reused rather than re-executed, so re-running a review is cheap.
Create the review with POST /api/ci/reviews, linking the two sides.
Poll for assembly. The CLI polls GET /api/ci/reviews/{id} on a bounded budget (default 20 minutes) while the platform completes the runs, diffs the two sides, runs the reviewer, and evaluates the gate.
Render the verdict and exit with the corresponding code.
The base side is keyed by the merge-base SHA, so once a base commit has been reviewed, later PRs sharing that merge-base hit the cache. This is why the merge-base — not the moving base-branch tip — is the right thing to pass.
Reading the output
A blocked review prints something like:
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
scenario base head Δ trace
─────────────────────────────────────────────────────
refund-denied PASS FAIL regressed https://platform.pipelines.tech/...
findings (1):
[critical] refund-denied — head now issues the refund instead of refusing
The verdict flipped from DENY to APPROVE...
trace: https://platform.pipelines.tech/...
BLOCK: 1 case(s) regressed PASS->FAIL
verdict: blocked — https://platform.pipelines.tech/...The header line carries the gate status — pass, blocked, neutral, or action_required. Below it, the counts line summarizes the diff (intersecting cases, regressions, improvements, findings), then each finding is a scenario the pairwise reviewer flagged as changed, with a severity, a title, and a deep link into the head trace on the platform.
Add --json to get the full machine-readable review object on stdout (the human report moves to stderr). That's the same dump the GitHub Action consumes. For the complete schema of findings, diff subfields, and gate status, see the Review the findings walkthrough and its reference.
Exit codes
The exit code is the contract. It's identical locally and in CI, so echo $? after a local run tells you exactly what your PR check will do.
| Code | Meaning |
|---|---|
| 0 | Gate passed, neutral, or advisory — also a poll-budget timeout (the review is still assembling; the platform finishes it asynchronously). Always safe to merge. |
| 1 | Gate blocked, or an operational failure occurred after runs were submitted. |
| 2 | A usage error or a preflight error — nothing was submitted. |
Exit 2 means no runs were created. A usage or preflight error — an unresolvable --base/--head, a missing required argument — is caught before any cost is incurred. Fix it and re-run; you haven't spent anything.
The gate is platform-authoritative: it lives on the umbrella agent, is RBAC'd and audited, and cannot be weakened or removed by a PR. There is no config in your checkout to delete, so dropping files on head does not disarm the check — a platform-resolved base always evaluates against the live gate and fails closed (red) when it should. See Connect your repo on the platform for how platform gating works.
Iterate before you push
The point of running locally is a fast edit-run loop before your PR ever exists. A few flags make that cheap:
# Only run the first 3 scenarios — fast, cheap smoke test while iterating
dystopic ci review --platform-suite \
--base "$(git merge-base origin/main HEAD)" \
--head HEAD \
--repo owner/repo \
--pr-number 123 \
--sample 3--sample Ntruncates both sides to the firstNscenarios in the suite. Use it to get a signal in seconds while you're fixing your agent, then drop it for the full suite.- Re-running is cheap by default. Submissions are cache-keyed, so an unchanged side is reused, not re-executed. You only pay for what actually changed.
--freshforces real re-execution of both sides (it stamps submissions with a UTC-hour epoch so identical inputs re-run). This costs real runs — reach for it only for drift canaries, not routine iteration.--timeout <seconds>caps the poll budget (default1200). A timeout is not a failure: the CLI exits 0 and prints the review id so you can fetch the result later.
If --base doesn't resolve — a common mistake is passing an empty ref like --base "origin/" — the review silently degrades to head-only floor mode: only your head side runs, no base-vs-head diff, and the gate falls back to min_pass_rate only. Always compute the base with git merge-base so you're actually running the comparison.
Once dystopic ci review --platform-suite exits 0 on the full suite locally, you're ready to commit and open the PR with confidence — the check will land the same verdict.
Next
Your local run just produced a review. Next, learn how to read it in depth — the findings, the diff, the trace deep links, and how to decide what changed and why.
4 · Create a test suite
Author scenarios, a suite, and a gate on the platform, bind the suite to CI, and verify locally with dystopic ci review --platform-suite.
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.