Run checks
The full mechanics of dystopic ci review --platform-suite — base/head SHA resolution and per-side worktrees, zero-yaml platform-resolved submission, commit-keyed run caching, the --sample/--repeats/--fresh knobs, bounded polling, the 0/1/2 exit-code contract, the named preflight error catalog, and head-only floor degradation when there is no base.
dystopic ci review --platform-suite runs one suite's regression check locally, exactly the way CI runs it: it resolves a base and a head commit, materializes each as a detached git worktree, submits the two sides as commit-keyed CI runs, and polls the assembled review to a gate verdict. Each side submits no agent config and no scenarios — the platform resolves the execution recipe from the connected repo's umbrella agent and sources the bound suite's frozen scenarios server-side, byte-identical to an in-app check. The command is decoupled: it submits and exits with a verdict; the platform drives run completion and posts the branded Dystopic check asynchronously.
The walkthrough in /docs/flow/run-checks is the happy path — run it, read the verdict, iterate before you push. This page is the lookup surface: every flag, the exit-code contract your CI keys off, the full preflight error catalog, and the sharp edges (floor-only degradation, cache reuse, the --fresh cost, platform-resolved gating) that bite when you go off the golden path.
The command
dystopic ci review \
--platform-suite \
--suite default \
--base "$(git merge-base origin/main HEAD)" \
--head HEAD \
--repo owner/name \
--pr-number 123It is also reachable as odyssey ci review — same parser, same behavior. The odyssey front end is the one wired into the GitHub Action.
--base is required unless you pass --head-only. It should be the PR's merge-base, not the base-branch tip — the merge-base is stable across base-branch churn, which is what makes cached runs reuse. The GitHub Action derives it for you; run it by hand with git merge-base origin/main HEAD as above.
--platform-suite needs a connected repo. Because neither side carries a local recipe, the platform must be able to resolve the run command, timeout, tools/ledger, and credential refs from the connected repo's umbrella agent, and to resolve --suite to a bound platform suite. If the repo is unconnected, the umbrella agent has no run command, or the suite doesn't resolve, the submission fails closed with a 422 that names what to configure. See /docs/reference/cicd for the connect-repo mechanics and the zero-yaml resolution contract.
Flags
| Flag | Default | Meaning |
|---|---|---|
--platform-suite | recommended | Zero-yaml mode. Submit no agent config and no scenarios; the platform resolves the execution recipe from the umbrella agent and sources the bound suite's frozen scenarios (byte-identical to an in-app check of that suite). |
--suite <name> | default | The bound platform suite to review. |
--all-suites | off | Review every suite bound to this repo, over the same two worktrees. Exit code is the worst across suites. Ignored when --suite is set. |
--base <ref> | — | Base git sha/ref (the PR's merge-base). Required unless --head-only. |
--head <ref> | HEAD | Head git ref/sha. |
--repo <owner/name> | $GITHUB_REPOSITORY | Repository slug. |
--pr-number <N> | $PR_NUMBER | PR number. Mutually exclusive with --head-only. |
--head-only | off | Submit only the head side and gate on the absolute floor. For push / non-PR contexts. Needs --branch (or $GITHUB_REF_NAME). |
--branch <name> | $GITHUB_REF_NAME | Branch name for --head-only reviews. |
--sample <N> | all scenarios | Truncate a locally-submitted scenario set to the first N rows (cost control). Ignored under --platform-suite, where the platform sources the full frozen scenario set server-side. |
--repeats <N> | suite value, else 1 | Repeats per scenario per side. Overrides the suite's configured value (see Repeats precedence). |
--fresh | off | Force fresh executions instead of reusing cached commit-keyed runs. Costs real runs. |
--baseline-review <id> | — | Client-side drift compare against a previously assembled review. Requires --head-only and --fresh. |
--json | off | Emit the machine dump to stdout (the human report goes to stderr). |
--poll-interval <float> | 5.0 | Initial poll interval in seconds (grows 1.5× up to 30s). |
--timeout <float> | 1200.0 | Poll budget in seconds. Exhaustion is not a failure — the platform finishes async. |
Under the hood
One invocation runs this sequence for each reviewed suite:
Resolve the two SHAs. --head (default HEAD) and --base are each run through git rev-parse <ref> against the repo root. A --head that does not resolve is a usage error → exit 2. A --base that does not resolve is also exit 2 — the message reminds you to check out with fetch-depth: 0 so both commits are present. (Contrast with --head-only, which has no base side at all — see Floor-only degradation.)
Materialize worktrees. Each SHA becomes a detached git worktree via git worktree add --detach under a temp dir (dystopic-wt-<sha7>-…). Both worktrees materialize the repo root; the relative project subdir is resolved via git rev-parse --show-toplevel so a monorepo submits the right side per commit.
Decide the review shape. The (base, head) state maps to a defined outcome (see Decision table): a full base-vs-head diff, or head_only floor mode when there is no base to compare against. Only these proceed to submission.
Preflight. All checks run client-side, before any run row exists (see Preflight error catalog). Any named error halts with exit 2 and submits nothing.
Submit commit-keyed runs. Each side is POSTed to /api/ci/runs — an idempotent get-or-create keyed on (repo, commit_sha, agent config, scenarios, suite). Under --platform-suite each side submits agent-less (the platform resolves the recipe from the umbrella agent, stamping source_git to this side's commit) and with an empty scenario list (the server sources the bound suite's frozen scenarios). Identical submissions reuse an existing run (created: false); this is the cache that makes re-runs cheap.
Create the review. POST /api/ci/reviews links the two sides by their config_hashes, carrying the effective platform gate. The head-only case posts base_config_hash: null — that absence is the server's floor-mode signal.
Poll. GET /api/ci/reviews/{id} until a terminal status (assembled, failed, skipped) or the poll budget runs out. Polling is a convenience; the platform owns completion. On budget exhaustion the command prints the review_id and exits 0.
Cleanup. Both worktrees are removed (git worktree remove --force) — always, even on exception. A worktree that fails to remove warns to stderr and does not fail the run.
Commit-keyed runs, not published agents. dystopic ci review does not create, publish, or archive Agent rows. Each side is a standalone, commit-keyed CI run submitted to /api/ci/runs — idempotent and cacheable per commit. Nothing to leak, nothing to clean up server-side; the only teardown is the two local worktrees.
Cost budget. If the bound suite sets a cost budget, each side is submitted with an integer budget_cents cap. Once a side's finished runs (failures and timeouts included — their spend is real) have consumed it, that side's remaining runs are skipped as cost_capped: excluded from pass-rate math, and a heavily capped side marks the review inconclusive. Configure it with the suite — see /docs/reference/create-suite.
Submission and caching (--sample / --repeats / --fresh)
Under --platform-suite the run body sent to /api/ci/runs for each side carries no recipe and no scenarios:
{
"repo": "owner/name",
"commit_sha": "<sha>",
"repeats": 1,
"suite": "default"
}agentis omitted.CiRunSubmission.agentisOptional; an absentagenttells the server to resolve the execution recipe from the connected repo's umbrella agent, stampingsource_gitto this side's commit.scenariosare submitted empty. No local scenarios are read, so the list is[]. The API accepts an empty list only whensuiteresolves to a bound platform suite (the server sources that suite's frozen scenarios); a submission that does not resolve fails closed with a 422.--sample Nwould truncate a locally-submitted scenario set, but is ignored under--platform-suite— the platform sources the full frozen set server-side.--repeats Nrides therepeatsfield on both sides identically so the comparison stays fair (see Repeats precedence).--freshadds anepochfield — the current UTC-hour bucket. Same identity key + a new epoch = a distinct run, so identical configs re-run for real instead of attaching to a cached run. This costs real runs; it exists for drift canaries and "run it again for real," not routine iteration.
The /api/ci/runs response tells you what happened per run:
{
"config_hash": "<hash of the resolved config>",
"runs": [ { "id": 12345, "created": true } ]
}created: true means a fresh run was minted; created: false means an existing commit-keyed run was reused (a cache hit).
Repeats precedence
The repeats value submitted on both sides is resolved in this order:
- Explicit
--repeats Nalways wins. - Otherwise the bound suite's configured repeats.
- When the suite configures none, the default 1.
Exit-code contract
CI keys off exactly three codes. This is the contract:
| Code | Meaning |
|---|---|
| 0 | Gate pass / neutral / advisory / skip-neutral / poll-budget exhausted (review still assembling server-side) / HTTP 429 quota deferral. Always safe to merge. |
| 1 | Gate blocked; a fail-closed submission (e.g. the server's zero-yaml 422, or an attempt to disarm the platform-authoritative gate from a PR); or an operational failure (an HTTP error mid-flow, or the review assembling with status failed). |
| 2 | A usage error, or a named preflight error with no runs submitted (source_empty, run_command_missing_file, secret_ref_unfulfilled, bad refs). |
Exit 2 means nothing was submitted — the platform never learns the run exists, so it cannot post a check. That is why the GitHub Action posts the terminal check itself for preflight failures. Exit 1 means either the gate blocked, the server refused the submission (fail-closed), or runs were created and an operational failure followed. Do not read exit 1 as "always gate-blocked."
A poll timeout and an HTTP 429 quota deferral are both exit 0 — a bounded local wait or a busy platform must never turn a gated PR red. In both cases the review_id (or quota_deferred: true) is surfaced so the run stays traceable and the check lands async.
The gate is platform-authoritative. It lives on the umbrella agent (dashboard gate card / PUT /api/agents/{id}/gate), is RBAC'd and audited, and cannot be weakened or removed by a PR. Dropping config on head does not disarm the check: a base that the platform resolves fails closed (red) rather than degrading to neutral. See /docs/reference/cicd for the full gate model.
Preflight error catalog
All preflight runs client-side, before any run row is created. Each named error is part of the contract and exits 2 with no submission:
| Error | Condition |
|---|---|
source_empty | The resolved agent source globs matched 0 files (the message carries the active-glob count). |
run_command_missing_file | The run command references a script that will not exist at /home/user/agent/<path> in the sandbox. |
secret_ref_unfulfilled | A from: github credential ref has no env var; the message lists every missing name. |
bad --base / --head ref | The ref does not git rev-parse (check out with fetch-depth: 0). |
The server-side fail-closed 422 (unconnected repo, umbrella agent with no run command, or unresolved suite) is separate from these client preflights — it fires during submission and surfaces as exit 1.
run_command_missing_file in detail
Agent source lands at /home/user/agent/<relative>, but the run command executes from /home/user/workspace. A relative path or a wrong absolute path fails in the sandbox with "file not found." Preflight lints this by splitting the run command into tokens, checking each token ending in a script suffix (.py, .sh, .bash, .js, .mjs, .ts, .rb) against the packaged file set or the local worktree, and suggesting the correct absolute path:
run_command_missing_file: run_command references 'agent.py' but agent source lands at
/home/user/agent/ and the command runs from /home/user/workspace — use the absolute
path /home/user/agent/agent.pyThe lint is skipped for zip and static-git sources (their tree is not local) and for pure-CLI run commands with no agent source at all.
Decision table
Every (base, head) state maps to a defined outcome — never a stack trace. On a connected repo both commits are platform-resolved (the recipe lives on the umbrella agent), so the shape turns only on whether a base is present to diff against:
| Base | Head | Outcome | Exit |
|---|---|---|---|
| resolved | resolved | full diff (both sides submitted) | verdict-driven |
none (first PR / push / --head-only) | resolved | head_only floor mode (flagged on the review) | verdict-driven |
unresolvable --base ref | — | usage error ("Could not resolve --base …") | 2 |
Floor-only degradation (head-only)
When there is no base to compare against — a first PR, an absent base branch, or --head-only for a push context — the command runs the head side alone and enforces only the absolute floor.
In --head-only mode:
--pr-numberis rejected (mutually exclusive); a--branch(or$GITHUB_REF_NAME) is required for the review context.- Only the head side is submitted.
base_config_hash: nullon the review body signals floor mode to the server. - The only gate tier that applies is
block_on_min_pass_rate— the head's absolute pass-rate floor. There is no diff, no reviewer, no severity / constraint / scorer tiering. - With a floor configured and the head below it →
blocked(exit 1). With the floor met or unset → neutral/pass (exit 0).
The same degradation is reached implicitly on the full PR path when there is no base side: the decision table returns head_only and the review is flagged accordingly.
Base footgun. An unresolvable --base errors loudly (exit 2, "Could not resolve --base …"), and passing --base together with --head-only is rejected outright ("--base is meaningless with --head-only", exit 2). When you expect a base-vs-head diff and see floor-only, confirm --base actually resolved to the intended merge-base commit — not the base-branch tip and not an unfetched sha.
Reading the output
Without --json, the command prints a human report to stdout: a header with the gate status, the target line (owner/name#123 · base abc1234 → head def5678), the gate description, a per-scenario table with ▼ regressed / ▲ improved markers and trace links, findings blocks, any banners, and a final verdict line. Trace and review deep links point at the web dashboard at https://platform.pipelines.tech. With --json, the machine dump goes to stdout and that same human report goes to stderr, so stdout stays byte-clean JSON.
Scenarios tagged xfail on the platform suite run and report but never gate — an expected-failure row cannot turn the PR red.
The full review schema (findings, blocking_reasons, diff subfields, structural-change and floor-only degradations) is documented on /docs/reference/review-findings. To fetch an already-created review directly, GET /api/ci/reviews/{id}.
Related
Create a suite
Author scenarios, bind them into a suite, set the gate tiers, and bind the suite to CI — all on the platform.
Review findings
The assembled-review schema — findings, diff subfields, gate status, degradations.
Integrate into CI/CD
Connect your repo, the zero-yaml resolution contract, and how the GitHub Action derives base/head and lets the platform post the Dystopic check.
CLI reference
The full command surface, global flags, and credential/base-URL precedence.
Create a suite: scenarios, gate & CI binding
The authoring reference for scenarios, suites, the gate, and the CI suite binding on the platform — every REST endpoint with its typed SDK method and CLI command, every scenario axis (including multi-turn and human approval), and the gate-tier semantics.
Review findings
The assembled-review schema — findings, diff, gate status and blocking reasons — plus how floor-only and structural-change modes degrade the gate, and how to fetch a review via the CLI, the API, or the dashboard.