# Checks

> Declare your checks once. Agents run them until they pass, and the pull request opens green.

Checks are the commands a change has to pass before it merges: lint, tests, a build. You declare them once, in `.ryph/checks.yml`. Ryph runs them on the commits agents ask about, hands back what failed, and reports the result to GitHub, GitLab or Radicle.

Ryph also remembers every check that passed, and on which commit. When an agent fixes a failure, only the checks that still need an answer run. When its pull request opens, the results are already there.

One change from a first commit to a green pull request:

1. Ada has changed the refund page. It commits, and asks Ryph to run just lint, to catch the quick mistakes first.
2. Ryph reads the checks from main and clones Ada’s exact commit into a check pod. Lint passes.
3. Ada asks for every check on the same commit. Lint already passed on it, so it’s inherited from that run. Test and e2e run side by side, with their own Postgres.
4. The test check fails. Ada gets the failure as its next message, with the last lines of the check’s output.
5. Ada fixes the bug and commits again. A new commit has nothing to inherit, so every check runs.
6. Every check passes on the new commit.
7. Ada submits its patch, and Ryph opens PR #214. Same commit, same checks, same environment, so it joins the suite that just passed.
8. The pull request opens with every check already green, and nothing ran twice. A ruleset can require ryph / test before anyone merges.

## Agents work until it's green

An agent checks its own work as it goes. It commits, calls `ryph__run_checks`, and stops. When the checks finish, the result arrives as its next message, with the last lines of output from every check that failed. It fixes the cause, commits, and asks again.

While it works, an agent can ask for only the checks its change is most likely to break. Before it submits, it runs all of them. Its prompt lists what each repository's checks run, and tells it to make a failing test pass by fixing the code.

A run that asked for checks stays open until the results arrive. If the agent finishes early, it gets one more turn to deal with them.

## Passes carry forward

Ryph groups check results into suites. A suite is identified by what was checked:

- the commit
- the version of `.ryph/checks.yml` it was read from
- the environment image the checks ran in
- the network access and services they had

When a new suite starts on a commit, every check that already passed on that commit, under the same checks file, environment and access, is copied into it with its log. It shows as **Inherited**, and only the remaining checks start. If every check already passed, the suite passes straight away without starting a container.

Only passes carry forward. A failed check always runs again, since asking again is how you retry a flaky test. **Run again**, on the suite's page in Ryph, or a re-run on GitHub, runs everything from scratch. GitLab has no re-run button for Ryph's checks, so there you run them again from Ryph.

Every suite is listed under **Checks** in the sidebar, newest first, with filters for repository, state and who asked. A repository's page under **Repos** shows its declared checks in the **Checks** section.

One change from first try to pull request:

| What the agent does | Tool | Commit | lint | test | e2e |
| --- | --- | --- | --- | --- | --- |
| Runs the test it’s most likely to break | `ryph__run_checks, test only` | `a1c9e3f` | Not asked | Ran, failed | Not asked |
| Fixes the code and asks again | `ryph__run_checks, test only` | `b72d0a4` | Not asked | Ran, passed | Not asked |
| Runs every check before submitting | `ryph__run_checks` | `b72d0a4` | Ran, passed | Inherited | Ran, passed |
| Submits, and the pull request opens | `ryph__submit_patch` | `b72d0a4` | Reported | Reported | Reported |

**4 checks ran, where 8 were asked for.** The failure ran again on the new commit, since asking again is how a fix gets checked. Passes carried forward, and the pull request got its statuses from the suite that had already passed.

## The pull request opens green

When an agent submits its patch, Ryph asks for the full suite on its commit. That's the suite the agent has just run, so the request joins it. Ryph reports the results as soon as the pull request opens: a check run per check on GitHub, named `ryph / <name>`, a commit status of the same name on GitLab, or a job run on the commit on Radicle.

Reviewers see green checks the moment the pull request appears. Nothing waits on a second CI run that would only confirm what's already known.

People's pull requests get the same checks. Every push to a pull request, GitLab merge request or Radicle patch is checked on its newest commit, as long as its author, and whoever pushed, can command agents. A newer push cancels the checks of the one before. A commit someone already checked from their own machine opens green, too.

## Why Ryph can vouch for the result

Usually, CI runs the full suite again even when an agent says its tests passed. That's reasonable: the agent ran them in its own sandbox, where it could have edited a test, skipped a suite, tested uncommitted changes, or misread the output.

Ryph can reuse its results because it controls everything that decides them:

| Ryph controls   | How                                                                                                                                                                                                           |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The commit      | Work leaves a run only as a git bundle sent to Ryph's forge. Ryph reads the commit hash from the bundle itself and verifies it again before publishing, so the commit that was checked is the one that ships. |
| The checks file | `.ryph/checks.yml` is read from the branch the change is going into. A patch that edits its own checks is still judged by the old ones.                                                                       |
| The environment | Checks run on the repository's environment image, pinned by digest, with the services the base branch declares. A patch that edits `.ryph/Dockerfile` can't swap the tools its tests run with.                |
| The container   | Each suite clones the exact commit into a fresh container with no credentials and nothing from the run in it. A check passes when Ryph sees its command exit 0.                                               |
| The report      | Ryph posts results as its own GitHub App or GitLab account, or signs them with your Radicle node's key. A GitHub ruleset can require `ryph / test` to come from the Ryph app specifically.                    |

So a passing check means the commands the base branch declares exited 0 against this commit, in the base's environment, in a container the agent never touched. That stays true wherever the same commit turns up again, so the result can be reused.

> [!WARNING]
> **The limits of a pass**
>
> The command comes from the base branch, but the code it runs, such as the tests and the scripts in package.json, comes from the commit. A patch can weaken its own tests and then pass honestly. Ryph flags a change that touches the files a check [watches](#flag-changes-to-the-tests-themselves), or anything in `.ryph/`. Reviewing the change is still up to you.

## Set up checks

### Turn them on

The `ryph` binary on Docker and [Docker Compose](https://ryph.ai/docs/docker-compose.md) run checks by default. On [Kubernetes](https://ryph.ai/docs/kubernetes.md), set `checks.enabled: true` in the Helm values. Anywhere else, set `RYPH_CHECKS=on`.

To show results on pull requests, Ryph's GitHub App needs the **Checks: write** permission. Without it, checks still run and agents still get their results, but GitHub doesn't show them.

### Declare your checks

Add `.ryph/checks.yml` to your default branch:

```yaml
setup:
  - run: npm ci
keep: [node_modules]
checks:
  - name: lint
    run: npm run lint
  - name: test
    run: npm test
    timeout: 20m
    watch: [package.json, 'test/**']
  - name: e2e
    run: npm run e2e
    services: [postgres]
```

| Key                 | What it does                                                                                                                                                            |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup[].run`       | Commands run in order before the checks, such as installing dependencies. Up to 16.                                                                                     |
| `keep`              | Ignored files from the environment image that each check keeps, such as a build cache. See [below](#keep-build-output-warm).                                            |
| `checks[].name`     | Lowercase letters, digits and dashes, up to 40 characters. GitHub and GitLab show it as `ryph / <name>`, so renaming a check breaks rulesets that require it.           |
| `checks[].run`      | One shell command. It passes when it exits 0.                                                                                                                           |
| `checks[].timeout`  | Like `90s`, `20m` or `1h`. Defaults to 15 minutes.                                                                                                                      |
| `checks[].services` | Services from `.ryph/services.yml` the check needs. Checks that name the same service share it.                                                                         |
| `checks[].watch`    | Files that decide what the check tests, such as its test files and config. A change that touches them gets flagged. See [below](#flag-changes-to-the-tests-themselves). |
| `checks[].pod`      | `own` runs the check alone in its own pod, with only the services it names. Up to 3 per file.                                                                           |
| `checks[].size`     | A size class for a check with `pod: own`. The deployment lists the classes checks may use in `RYPH_CHECKS_OWN_SIZES`.                                                   |

Up to 32 checks run side by side, one container each. The file can also be called `.ryph/checks.yaml`. Unknown keys are refused with a message that says what's wrong.

Since the checks file is read from the branch a change goes into, checks apply to pull requests opened once the file is on their target branch.

### Flag changes to the tests themselves

Ryph reads each check's command from the base branch, but the command runs the commit's code. `npm test` runs whatever the commit's `package.json` says `test` is, against the commit's test files. So a patch can delete a failing test, add `.skip`, or rewrite the `test` script, and the check passes.

To make edits like that easy to spot, list the files that decide what a check actually tests under `watch`:

```yaml
checks:
  - name: test
    run: npm test
    watch: [package.json, 'test/**', '**/*.test.ts', vitest.config.ts]
  - name: lint
    run: npm run lint
    watch: [package.json, eslint.config.js]
```

When a change touches any of them, the check's result on GitHub carries a **Watched paths changed** warning that lists them, and the agent's results say so too. A reviewer then knows to read those files closely. Everything in `.ryph/` is watched for every check.

Patterns match the whole path from the repository root. `*` and `?` match within one directory, and `**` matches any number of directories.

### Give checks a database

A check that needs Postgres or Redis names a service from `.ryph/services.yml`:

```yaml
services:
  postgres:
    uses: postgres@18
    env:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_test
    port: 5432
```

Services start once setup has finished, so setup can't use them. Running services needs `RYPH_SERVICES=on` on the deployment.

### Keep build output warm

When the repository's environment image already has the code checked out and built, each check starts from that checkout, moved to the commit under test. `keep` lists what the check keeps of the image's ignored files. Everything else ignored is removed.

Only keep what your build tool rebuilds when the sources change. Otherwise a check can pass on output built from older code.

| Toolchain                  | `keep`                          |
| -------------------------- | ------------------------------- |
| Rust                       | `[target]`                      |
| npm, pnpm, yarn, bun, Deno | `[node_modules]`                |
| Python with `uv sync`      | `[.venv]`                       |
| Go                         | `[]`, its caches live elsewhere |
| Maven, Gradle, pip         | `[]`                            |

Without `keep`, the default is `[node_modules, target, .venv]`. Run your install in `setup`, so a change to the lockfile is picked up.

### Require checks on GitHub

In a branch ruleset, add `ryph / <name>` for each check you want as a required status check, and set its source to the Ryph app. A pull request then can't merge until that check has passed on its newest commit.

### Require checks on GitLab

Ryph's commit statuses join the merge request's pipeline as an external stage. Turn on **Pipelines must succeed** in the project's merge request settings, and a merge request can't merge until they've passed.

### Ask from your own machine

`ryph checks run` asks a deployment to run the checks on your current commit, on the same machines a pull request's checks run on. When you then open the pull request on that commit, it opens green.

```sh
ryph checks run
ryph checks run --check test
```

It needs `RYPH_URL` and `RYPH_TOKEN`, an API token. Mint one in Ryph under **Account settings** → **API tokens** → **Mint a token**, and tick `checks:request` and `repos:read`. `ryph checks wait <id>` and `ryph checks status <id>` follow a suite you started. For a coding agent in your terminal, `ryph mcp` serves the same as the `checks_run` and `checks_status` tools.

If you run Ryph on your laptop, it can [send its checks to a deployment](https://ryph.ai/docs/try-locally.md#run-checks-on-a-deployment) and have them run there.

### Settings

| Variable                             | Default | What it does                                                                             |
| ------------------------------------ | ------- | ---------------------------------------------------------------------------------------- |
| `RYPH_CHECKS`                        | `off`   | `on` runs checks. On by default for the `ryph` binary on Docker and both Compose setups. |
| `RYPH_CHECKS_CONCURRENCY`            | `4`     | Suites running at once across the deployment. `2` for the `ryph` binary on Docker.       |
| `RYPH_CHECKS_TIMEOUT_MAX`            | `3600`  | The longest timeout, in seconds, any check can have.                                     |
| `RYPH_CHECKS_MAX_PER_RUN`            | `60`    | How many suites one run can ask for.                                                     |
| `RYPH_CHECKS_CHANGE_REQUESTS`        | `on`    | Check every push to a pull request. `off` checks only agents' work.                      |
| `RYPH_CHECKS_LOCAL`                  | `on`    | Let people ask for checks from their own machine.                                        |
| `RYPH_CHECKS_MAX_LOCAL_PER_USER`     | `4`     | Checks one person can have going from their machine at once.                             |
| `RYPH_CHECKS_LOCAL_UPLOADS_PER_HOUR` | `30`    | Uploads one person can make for those checks in an hour. Pushed commits don't count.     |
| `RYPH_CHECKS_SIZE`                   | none    | The size class check pods get. Without it, they get the repository's.                    |
| `RYPH_CHECKS_OWN_SIZES`              | none    | Size classes a check with `pod: own` may name, comma-separated.                          |
| `RYPH_CHECKS_CPUS`                   | `auto`  | On Docker, the most cores one check can use. `auto` is half of what Docker has.          |
| `RYPH_CHECKS_MEMORY`                 | `auto`  | On Docker, the most memory one check can use. `auto` is half of what Docker has.         |
