dystopic docs is in beta — content is actively being added.
dystopic
Reference

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 123

It 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

FlagDefaultMeaning
--platform-suiterecommendedZero-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>defaultThe bound platform suite to review.
--all-suitesoffReview 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>HEADHead git ref/sha.
--repo <owner/name>$GITHUB_REPOSITORYRepository slug.
--pr-number <N>$PR_NUMBERPR number. Mutually exclusive with --head-only.
--head-onlyoffSubmit 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_NAMEBranch name for --head-only reviews.
--sample <N>all scenariosTruncate 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 1Repeats per scenario per side. Overrides the suite's configured value (see Repeats precedence).
--freshoffForce 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.
--jsonoffEmit the machine dump to stdout (the human report goes to stderr).
--poll-interval <float>5.0Initial poll interval in seconds (grows 1.5× up to 30s).
--timeout <float>1200.0Poll 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"
}
  • agent is omitted. CiRunSubmission.agent is Optional; an absent agent tells the server to resolve the execution recipe from the connected repo's umbrella agent, stamping source_git to this side's commit.
  • scenarios are submitted empty. No local scenarios are read, so the list is []. The API accepts an empty list only when suite resolves 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 N would truncate a locally-submitted scenario set, but is ignored under --platform-suite — the platform sources the full frozen set server-side.
  • --repeats N rides the repeats field on both sides identically so the comparison stays fair (see Repeats precedence).
  • --fresh adds an epoch field — 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:

  1. Explicit --repeats N always wins.
  2. Otherwise the bound suite's configured repeats.
  3. When the suite configures none, the default 1.

Exit-code contract

CI keys off exactly three codes. This is the contract:

CodeMeaning
0Gate pass / neutral / advisory / skip-neutral / poll-budget exhausted (review still assembling server-side) / HTTP 429 quota deferral. Always safe to merge.
1Gate 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).
2A 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:

ErrorCondition
source_emptyThe resolved agent source globs matched 0 files (the message carries the active-glob count).
run_command_missing_fileThe run command references a script that will not exist at /home/user/agent/<path> in the sandbox.
secret_ref_unfulfilledA from: github credential ref has no env var; the message lists every missing name.
bad --base / --head refThe 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.py

The 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:

BaseHeadOutcomeExit
resolvedresolvedfull diff (both sides submitted)verdict-driven
none (first PR / push / --head-only)resolvedhead_only floor mode (flagged on the review)verdict-driven
unresolvable --base refusage 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-number is rejected (mutually exclusive); a --branch (or $GITHUB_REF_NAME) is required for the review context.
  • Only the head side is submitted. base_config_hash: null on 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}.