> ## Documentation Index
> Fetch the complete documentation index at: https://docs.outerlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build reference

> What a runner does around a build's command: the branch, the item key, repository tokens, the proofs, the judge, host checks and runner status.

This page is the lookup for what a runner does before and after a build's command. [What happens to one build](https://docs.outerlayer.ai/run-work-on-your-machines.md#what-happens-to-one-build) gives the order of the stages. The commands that start and inspect a runner are in [Runner commands](https://docs.outerlayer.ai/reference/cli-runner.md). To fix a build that failed, see [Troubleshoot builds](https://docs.outerlayer.ai/troubleshoot-builds.md).

## The branch a claim names

A claim names the branch that the build works on in each repository the factory connected. The item's own repository is the primary. The build gets the primary's branch as `OUTERLAYER_BRANCH`, and all of them as `OUTERLAYER_REPOSITORIES`. See [The repositories in a build](https://docs.outerlayer.ai/container-builds.md#the-repositories-in-a-build).

In each repository, the branch depends on the item's open pull requests:

| The item has | The branch |
| - | - |
| No open pull request in the repository | `outerlayer/<factory>/<item number>` |
| One open pull request in the repository | The pull request's head branch |
| Anything else | None. OuterLayer refuses the claim. |

OuterLayer writes the factory name in lower case. It replaces each run of characters other than letters, digits, `.`, `_` and `-` with `-`. A name that this changes also gets 8 characters of a hash, so two factories never share a branch. Item 7 of `acme` builds on `outerlayer/acme/7`. Item 7 of `Acme Prod` builds on `outerlayer/acme-prod-<hash>/7`.

The runner checks the branch out before the command starts:

* An `outerlayer/` branch that exists and is not merged is continued.
* An `outerlayer/` branch that is missing or merged starts from the default branch.
* Any other branch must exist on the remote, or the build fails at `provision`.

A container or microVM build may push only that branch. See [The git remote and the build's tokens](https://docs.outerlayer.ai/container-builds.md#the-git-remote-and-the-builds-tokens).

OuterLayer leaves a repository other than the primary out of the claim when its GitHub App installation is suspended. The build then runs in the other repositories.

A hook of your own must not make a `git worktree` on the claim's branch while another worktree has that branch checked out. Git refuses, and the build fails. Make a detached worktree.

### When a claim is refused

A refused claim records a failed build with the code as its reason. The runner logs `#<item> skipped: <code>`. The refusal uses up the build request, except where the table says otherwise. So fix the cause, then ask for a build again.

| Status | Code | Cause | What to do |
| - | - | - | - |
| 422 | `pull_request_from_fork` | The open pull request's head is in another repository. | Close it, or use a branch of the repository. |
| 422 | `pull_request_on_default_branch` | The open pull request's head is the default branch. | Close the pull request. |
| 422 | `pull_request_ambiguous` | The item has two open pull requests in one repository. The refusal names the repository. | Close all but one. |
| 422 | `pull_request_repository_not_connected` | An open pull request is in a repository that the factory has not connected. | [Connect the repository](https://docs.outerlayer.ai/connect-repository.md), or close the pull request. |
| 422 | `repository_not_reachable` | The GitHub App installation that covers the item's repository is suspended. | An owner of the GitHub organization unsuspends the App on GitHub. |
| 409 | `branch_claimed_elsewhere` | Another item's live claim names the same branch in the same repository. | Ask again when that claim ends. |
| 426 | `runner_protocol_too_old` | The runner is too old for this build. The request stays unused. | [Update the runner](https://docs.outerlayer.ai/update-a-runner.md). |
| 503 | `github_unavailable` | GitHub was failing. No build is recorded. | Nothing. The runner claims again on its next poll. |

### Opening the pull request

[`outerlayer work open-pr`](https://docs.outerlayer.ai/reference/cli-work#outerlayer-work-open-pr) opens the pull request from the claim's branch into the default branch. If one is open, it edits that one.

A runner build opens its pull request as the factory's GitHub App, not with a GitHub login on the host. OuterLayer uses a GitHub token that covers only the item's repository, and the build never receives it. In your own session, `outerlayer work open-pr` runs `gh` under your login.

Its refusals in a runner build:

| Status | Code | Cause and what to do |
| - | - | - |
| 400 | `invalid_request_body` | The title is empty, or the body is not text. Give `--title` a value. |
| 403 | `item_key_out_of_scope` | The credential is not this item's key. Run the command from the build that the claim started. |
| 403 | `repository_not_in_claim` | `--repository` names a repository that the claim does not name. Use one from `OUTERLAYER_REPOSITORIES`. |
| 404 | `work_item_not_found` | No work item has that number. |
| 409 | `work_item_withdrawn` | The item was withdrawn. Stop the build. |
| 409 | `claim_has_no_branch` | The claim names no branch. Ask for a build again. |
| 409 | `repository_permissions_pending` | The installation has not accepted the App's permission to write pull requests. An owner accepts it. Then run the command again. |
| 410 | `claim_expired` | The claim has ended. Ask for a build again. |
| 422 | `branch_has_no_commits` | The branch has no commit that the default branch lacks. Push a commit first. |
| 422 | `branch_not_pushed` | The branch is not on the remote. Push it first. |
| 422 | `work_item_has_no_repository`, `repository_not_connected`, `repository_not_in_installation` | The repository is missing, not connected, or not in the App's installation. See [Connect a repository](https://docs.outerlayer.ai/connect-repository.md). |
| 503 | `repository_tokens_unavailable` | OuterLayer has no GitHub App to issue the token. |
| 503 | `github_unavailable` | GitHub was failing. Run the command again in a short time. |

## Item key

The runner gives a build an item key (`olitem_…`) as `OUTERLAYER_API_KEY`, not the runner key. The item key acts only on its own item:

* It can read the item, its threads, checks and changes.
* It can link and sync sessions, and post comments.
* It can declare, open or edit the pull request.
* It can emit artifacts, results and findings.

The key stops working when the claim ends, and at the latest 24 hours after the claim began. So `runner.timeLimitMinutes` is at most 1320 minutes, which is 22 hours.

A container or microVM build holds only a placeholder. The runner adds the real item key to the build's calls to OuterLayer. A build under hooks with no isolation runs as the runner's account. So it can read the runner key and the item key from the runner's files. See [What the keys limit](https://docs.outerlayer.ai/build-security.md#what-the-keys-limit).

The build's sessions upload under the item key while the build runs. The item shows each session and its cost within a minute of each turn. The runner uploads the sessions in full before it ends the claim. When a build is killed, the turns after the last upload are lost.

| Status | Code | Cause |
| - | - | - |
| 403 | `item_key_out_of_scope` | The call is outside what the item key can do, or names another item. |
| 401 | `item_key_expired` | The claim has ended. |
| 401 | `item_key_invalid` | The key fails its signature check. |
| 503 | `item_key_unavailable` | OuterLayer cannot make item keys. The claim records nothing. |

## Repository tokens

Through the factory's GitHub App, OuterLayer issues short-lived GitHub tokens to a build. The permissions the App holds are in [GitHub App permissions and build tokens](https://docs.outerlayer.ai/github-app-and-build-tokens.md).

### Who can get a token

A build gets repository tokens only when a member of your organization asked for it:

* For a build, that is the person who ran `outerlayer work build` or chose **Build** on the Work page.
* For an amend, that is the person whose newest comment or fail on the item started it.

A key counts as a member's only when it belongs to an active member and is not a runner key. OuterLayer checks this on every request, so a member removed from the organization stops at once.

Only the key that holds the item's live claim can ask. A claim that ended gets nothing. A session that you start yourself gets no token.

`outerlayer work build` with a runner key still records the build request, and prints a warning. A container or microVM build of that request fails, because it gets no token.

A build that an automation starts needs no member. It gets tokens only while the automation's setting is on:

| The build | Setting that must be on | Refusal when the setting goes off | Audit event records |
| - | - | - | - |
| Resolves a merge conflict | **Resolve merge conflicts automatically** | `conflict_resolution_off` | `kind` `factory`, `reason` `merge_conflict` |
| Fixes a failed GitHub check | **Fix failed checks** | `check_fixing_off` | `kind` `factory`, `reason` `failed_check` |
| Records out-of-date proofs again | **Record proofs again** | `build_request_not_from_member` | `kind` `factory`, `reason` `proofs_out_of_date` |

For a build that a member asked for, the audit event records `kind` `member`. See [What OuterLayer records about a token](https://docs.outerlayer.ai/github-app-and-build-tokens.md#what-outerlayer-records-about-a-token).

### The two kinds of token

Each token covers one repository that the claim names. It lasts about an hour.

| Access | Permissions on the repository |
| - | - |
| `read` | Contents, issues, pull requests, actions, checks and commit statuses, all read only. |
| `push` | Contents write. Workflows write is added when [the repository allows it](https://docs.outerlayer.ai/github-app-and-build-tokens.md#letting-a-build-change-workflows). |

The runner also asks for a contents-read token on the control plane that governs the item's repository. It uses that token to fetch the build's governed context.

### When a token is refused

| Status | Code | Cause |
| - | - | - |
| 400 | `invalid_request_body` | The request named no valid access, or asked for a push token on the control plane. |
| 401 | `runner_key_signature_required`, `runner_key_signature_invalid`, `runner_key_signature_stale`, `runner_key_signature_replayed` | The request was not signed by the host key that the runner key is bound to. See [When the gateway refuses a signature](https://docs.outerlayer.ai/troubleshoot-builds.md#when-the-gateway-refuses-a-signature). |
| 403 | `forbidden` | The key does not hold `work.claim`. |
| 403 | `claim_not_held` | The caller is not the key that holds the item's newest claim. |
| 403 | `build_request_not_from_member` | No member of the organization asked for this build. |
| 403 | `conflict_resolution_off` | The build resolves a merge conflict, and the factory turned off **Resolve merge conflicts automatically** after the claim. |
| 403 | `conflict_resolution_ended` | The build resolved a merge conflict, and the conflict has ended. OuterLayer issues no further token. |
| 403 | `check_fixing_off` | The build fixes a failed check, and the factory turned off **Fix failed checks** after the claim. |
| 403 | `check_fixing_ended` | The build fixes a failed check that no longer counts. For example, a person pushed first. |
| 403 | `repository_not_in_claim` | The request names a repository that the claim does not name. |
| 404 | `work_item_not_found` | No such work item. |
| 409 | `work_item_withdrawn` | The item was withdrawn. |
| 409 | `repository_permissions_pending` | The installation has not accepted a permission that the token needs. The message names the GitHub organization whose owners must accept it. |
| 410 | `claim_expired` | The item has no live claim. |
| 422 | `work_item_has_no_repository` | The item names no repository. |
| 422 | `repository_not_connected` | The factory has not connected the item's repository. See [Connect a repository](https://docs.outerlayer.ai/connect-repository.md). |
| 422 | `repository_not_in_installation` | The installation does not include the repository. Add it on GitHub. |
| 422 | `no_control_plane` | No control plane governs the item's repository. The build starts with no governed context. |
| 422 | `control_plane_not_in_installation` | No installation of the GitHub App can read the item's control plane. Give the App access to it. |
| 502 | `github_request_failed` | GitHub refused a request for a reason other than load. The build fails. |
| 503 | `repository_tokens_unavailable` | OuterLayer has no GitHub App to issue the token. |
| 503 | `github_unavailable` | GitHub was rate limiting or failing. The runner tries again on its next poll. |

## The proofs stage

When a build's command exits 0, the runner records the item's proofs itself, before the judge stage. An amend gets the same stage. A proof counts only at the pull request's head, so the runner records at the build's final commit.

The stage runs `outerlayer work proofs --record --comment` in the build's own container, microVM or process. It runs again the tests that name each proof that is not current, and emits what they write. See [Record proofs by rerunning tests](https://docs.outerlayer.ai/attach-evidence.md#record-proofs-by-rerunning-tests).

The stage records nothing when every declared proof is current, or when the item declares no proof.

A proof that the stage cannot record stays out of date or missing:

* The item's verdict names the proof.
* The item's general thread gets one comment that names each proof and the reason.
* The build's reason names the checks that do not pass, then the cause, such as `the host's recording left proofs out of date (setup failed: "yarn db:setup" exited with status 1)`. A reason has at most 200 characters, so a long cause is cut. A cause for one proof starts with the criterion's id.

The stage's output is in `proofs.log` in the [build's folder](#files-the-runner-keeps).

### When the proofs are recorded again

The head of a pull request can move after a build ends. For example, a person pushes, or an amend fixes a failed check. The proofs are then out of date at the new head.

The built-in automation **Record proofs again** starts an amend for this. It is on by default. Its switch is in **Settings → Automations**, under **Pull requests and reviews**. The amend starts when all of these are true:

* OuterLayer evaluated the pull request's current head and found a proof from an earlier commit.
* A runner built the item.
* The head has no merge conflict, and no failed check that the runner still tries to fix. The amend for either moves the head again.

When nothing else waits on an agent, this amend runs no agent. The runner records the proofs at the head, then runs the judge and the host checks.

The automation starts once for each head and each set of out-of-date proofs. A recording that fails starts no second amend. The next push, or a change to which proofs are out of date, starts one again.

## The judge stage

A `where: host` check that declares a [`judge`](https://docs.outerlayer.ai/write-a-validator#a-judge-on-your-host) runs in the judge stage. The stage runs when the build's command exits 0, after the proofs stage and before the host checks.

### What the judge reads

The judge is a fresh Claude Code agent with the check's brief and no shell. It gets these inputs:

* each criterion's text and declared proof
* the branch's diff
* the files of the pushed commit

Nothing tells the judge which tests prove a criterion. It searches the commit's files, and starts from the tests that the diff changes. A test needs no comment or title that names the criterion.

The judge reads the pushed commit, never the build's checkout. It does not judge a file that the build changed and did not push.

A claim can name several repositories. The judge then reads one of them. It reads the primary repository when the build pushed there. Otherwise, it reads the first repository of the claim that the build pushed in. The judge fails for a missing branch only when the build pushed in none of the repositories.

For each criterion, the judge gives a result, a reason and the tests that the result rests on. A pass must name at least one [test file](#which-files-count-as-tests) of the pushed commit. The runner records one check item for each criterion, and the item's Criteria tab lists the tests.

The runner asks the judge about at most 12 criteria in one run. For more criteria, it judges them in groups. Each group has its own limit of 60 turns, and its own time limit of `runner.checks.timeLimitMinutes`.

A judge with `subjects: none` has no criteria. It judges the whole change every time.

The judge needs these things:

* A container or microVM build. A build under your own hooks has no judge.
* A build image that holds Claude Code, because the judge runs inside the image.
* The runner's Claude credential.

The stage also runs [the built-in code review](https://docs.outerlayer.ai/code-review.md) when the factory's **Code review** automation is on. The code review never changes the build's outcome.

A repository can keep its own copy of the starter brief at the path the check names. If that copy finds tests by `// proves` comments or reads `citations.json`, the runner uses the brief from its CLI and logs that it did.

> [!WARNING]
>
> The judge runs in the build's own environment, not a new one. A build that can become root, through `sudo` or Docker, can replace programs that the judge uses. Treat the judge's verdict on such a build as the build's own.

### Which files count as tests

A file counts as a test in three cases:

* Its name is a common test name.
* It is a source file in a test folder.
* A runner glob in [the proofs file](https://docs.outerlayer.ai/reference/cli-work.md#the-proofs-file) matches it.

The common test names are:

| Language | File names |
| - | - |
| JavaScript and TypeScript | `*.test.*`, `*.spec.*`, `*.cy.*`, `*_test.js`, `*_test.ts`, and any file under `__tests__/` |
| Python | `test_*.py`, `*_test.py` |
| Go | `*_test.go` |
| Ruby | `*_test.rb`, `test_*.rb`, `*_spec.rb` |
| Java and Kotlin | `*Test`, `*Tests`, `*IT` |
| Groovy and Scala | `*Spec.groovy`, `*Spec.scala`, `*Test.scala` |
| C#, PHP and Swift | `*Test.cs`, `*Tests.cs`, `*Test.php`, `*Tests.php`, `*Tests.swift` |
| Elixir and Dart | `*_test.exs`, `*_test.dart` |
| Rust | Any `.rs` file under `tests/` |
| Shell and Gherkin | `*.bats`, `*.feature` |

Class names match the capital letter, so `Contest.java` is not a test.

A test folder is named `test`, `tests`, `spec`, `__tests__`, `Test` or `Tests`, or has a name that ends in `Test` or `Tests`, such as `UnitTests`. The names are case-sensitive. Each source file in a test folder counts, whatever its name. A source file has the extension of a programming language. Documentation, data and image files do not count.

The test-folder rule skips a file whose path holds one of these folders, in any case:

* Test data: `fixtures`, `fixture`, `__fixtures__`, `testdata`, `test-data`, `snapshots`, `__snapshots__` and `resources`.
* Other code or build output: `node_modules`, `vendor`, `third_party`, `target`, `build`, `dist`, `out`, `obj` and `.git`.

A common test name wins over this rule, so `test/fixtures/wallet_test.rb` counts.

The runner reads the globs from `.outerlayer-proofs.yaml` at the pushed commit. A glob only adds files.

### The result for one criterion

| Case | Result | What to do |
| - | - | - |
| The criterion declares an artifact proof. | `skipped`. The runner does not ask the judge. | Nothing. The artifact proves it on its own row. |
| The judge passed it and named a test file of the pushed commit. | `pass` | Nothing. |
| The judge passed it before, and nothing changed: the criterion's text, its declared proof, the brief and the named test files. | Keeps its pass. The runner does not ask the judge. | Nothing. A change to a helper that a named test imports does not count as a change, so review such a change yourself. |
| The judge passed it but named only files that do not count as tests. | Fails. The reason names up to three files, for example "The judge named `scripts/verify.sh`, which is not read as a test." | Add a runner to the proofs file whose `files` glob matches the test file. |
| The judge passed it but named no file of the pushed commit. | Fails with "The judge named no test file that proves this criterion." | Write a test that proves the criterion. |
| The judge failed it. | `fail`, with the judge's reason. | Fix the test or the code that the reason names. |
| The judge gave no result for it. | Fails with "The judge gave no result for this criterion." | Nothing. The next run asks the judge again. |
| The judge's run for the criterion's group failed. | Fails, with the run's reason. | Fix the cause that the reason names. The other groups keep their results. |

The runner asks the judge again about a criterion that failed before, in every run. A test that proves it can be in any file.

A reviewer can accept a criterion that the judge failed. See [Accept a criterion without proof](https://docs.outerlayer.ai/review-the-work.md#accept-a-criterion-without-proof).

### Why a judge check fails

The runner records the check as a fail, with one of these reasons. The next build or amend of the item runs the judge again. The judge's output is in `judge.log` in the [build's folder](#files-the-runner-keeps).

| Reason | Cause | What to do |
| - | - | - |
| `<n> of <m> criteria not satisfied: <ids>` | The criteria that `<ids>` names failed. Each check item's reason says why. | See [The result for one criterion](#the-result-for-one-criterion). |
| `<n> of <m> check items not satisfied: <ids>`, or `the judge failed the change` | A `subjects: none` judge failed the change, or parts of it. | Read the check's report, and fix what it names. |
| `no criteria are recorded for the item` | The work item has no recorded criteria. | Record them with [`outerlayer emit criteria`](https://docs.outerlayer.ai/reference/cli-emit#outerlayer-emit-criteria). |
| `the brief <path> is not in the definitions repository` | The brief that the check names is not in the repository that holds the checks. That is the item's repository, or its control plane. | Commit the brief to the default branch, or fix `brief` in the validator file. |
| `the image has no Claude Code for the judge` | The build image has no Claude Code. With `: this build ran outside a container or microVM`, the build ran under your own hooks. | Add Claude Code to the image. Under your own hooks, record the check from CI. See [The criteria judge check](https://docs.outerlayer.ai/policy-and-validators.md#the-criteria-judge-check). |
| `the host holds no Claude credential for the judge` | The runner has no Claude credential. | [Give the runner one](https://docs.outerlayer.ai/set-up-a-host.md#give-the-runner-claudes-credential). |
| `the build pushed no branch: the claim names none`, or `the build pushed no branch: <branch> does not exist on <repository>` | The build pushed nothing for the judge to read, in any repository of the claim. | Read the build's session to learn why it did not push. |
| `the judge ran out of time after <n> minutes (runner.checks.timeLimitMinutes)` | The judge ran past the time limit. | Raise `runner.checks.timeLimitMinutes`. |
| `the judge reached the turn limit of 60 turns before it gave a result` | The judge used 60 turns without a result. | Rerun the item. |
| `the judge program changed during the build` | The Claude Code program in the build is not the one that the build started with. | Do not install or update Claude Code during a build. |
| `the branch <branch> could not be read: <why>`, `the item's criteria could not be read: <why>`, `the build's tests could not be fetched: <why>`, `the build's diff could not be read: <why>`, or `the build's repository tokens are no longer held, so the branch cannot be fetched` | The runner could not get the judge's inputs. | Read `<why>`. Check the runner's network path to GitHub and OuterLayer. |
| `the build's processes could not be stopped: <why>`, or `the judge could not be run: <why>` | The runner could not start the judge. | Read `<why>` and `judge.log`. |
| `the judge exited <status> and printed no result`, ``the judge gave no `result:` line``, `the judge's items.json is not JSON`, or `the judge's items.json does not match its shape: <field>: <why>` | The judge ended without an answer that the runner can read. | Rerun the item. In a brief of your own, do not tell the judge how to format its answer. |
| `the definitions could not be read: <why>` | The runner could not read the repository's validator files. It records this fail only on a check that a runner passed before. | Read `<why>`. |

When a `subjects: none` judge cannot run, its result has a failing check item with the id `judge-not-run`. The check item's reason is one of the reasons above.

## Host checks

A repository's `where: host` validators declare commands that the runner runs itself. [Checks that run on your host](https://docs.outerlayer.ai/write-a-validator.md#checks-that-run-on-your-host) gives the file format. The runner runs the commands only when the build's command exits 0, after the judge stage.

Only `builtin:container` and `builtin:vm` builds run host checks. The checks of one build run in a new container or microVM:

* It is made from the build's image, with the build's limits, tunnel destinations and `allowHosts`.
* It holds no OuterLayer key, no address of OuterLayer and no model credential.
* Its git remote serves fetches only.

The runner reads the check definitions from the repository that governs the item, never from the build's checkout. So a build cannot change its own checks. Only the runner can record a result for a host check.

In the new environment, the runner does these steps:

1. Checks out the claim's branch.
2. Installs the runtimes that the branch's version files name, on the universal image or an image built from it. Each check command then gets the variables that `mise env` prints. See [Variables from mise](https://docs.outerlayer.ai/build-image-from-devcontainer.md#variables-from-mise).
3. Runs the recipe's lifecycle commands.
4. Runs each repository's [setup script](https://docs.outerlayer.ai/build-setup.md).
5. Runs each check command, one at a time.

Two variables name paths that a command can use:

| Variable | What it names |
| - | - |
| `OUTERLAYER_EVAL_INPUT` | A read-only folder with the work item, the branch's diff and the build's sessions. See [What a host check can read](https://docs.outerlayer.ai/write-a-validator.md#what-a-host-check-can-read). |
| `OUTERLAYER_CHECK_ITEMS` | A file that the command can write [check items](https://docs.outerlayer.ai/policy-and-validators.md#check-items) to. The runner records them with the result. |

### Why a host check fails

The runner records a check as a fail, with one of these reasons:

| Reason | Cause | What to do |
| - | - | - |
| `exited <status>: <last line>`, or `exited <status> with no output` | The command exited non-zero. | Read the command's log, and fix what failed. |
| `<n> of <m> check items not satisfied: <ids>` | The command exited 0, and its check items file has a failing check item. | Fix what each failing check item names. |
| `its check items file (OUTERLAYER_CHECK_ITEMS) is not JSON` | The command wrote a check items file that is not JSON. | Write one JSON array to the file. |
| `its check items file does not match the check item shape: <field>: <why>` | A check item in the file has a missing or bad field. | Fix the field. See [Check items](https://docs.outerlayer.ai/policy-and-validators.md#check-items). |
| `its check items file is longer than 1048576 bytes` | The check items file is over 1 MiB. | Write fewer check items, or shorter reasons. |
| `ran out of time after <n> minutes (runner.checks.timeLimitMinutes)` | The command ran past the time limit. | Make the command faster, or raise `runner.checks.timeLimitMinutes`. |
| `the check environment setup ran out of time after <n> minutes (runner.checks.timeLimitMinutes)` | The start of the environment, the clone, the runtime install and the lifecycle commands together ran past the time limit. | Raise `runner.checks.timeLimitMinutes`. |
| `the check environment could not be prepared` | The clone, the runtime install, a lifecycle command or a setup script failed in the new environment. A setup script that runs past `runner.build.setupMinutes` fails the same way. | Read `checks/environment.log`. |
| `the host has no clean environment for checks` | The build ran under a provision hook of your own. | Use `builtin:container` or `builtin:vm` hooks, or run the check in CI with `where: ci`. |
| `the build pushed no branch` | The claim's branch does not exist on the repository. | Read the build's session to learn why it did not push. |

If the runner cannot read the definitions, it records nothing and logs why. The check then reads as waiting while the claim is live, and as failed after the claim ends.

### Logs and time limits

Each command's output is in `checks/<name>.log` in the [build's folder](#files-the-runner-keeps). The setup of the environment is in `checks/environment.log`.

`runner.checks.timeLimitMinutes` limits two things: the setup of the environment, and each check command. It is 10 by default. `runner.build.setupMinutes` limits each setup script, as it does in the build. See [the config file](https://docs.outerlayer.ai/reference/config-file.md).

Host checks run the recipe's setup commands once more for each build, and a `builtin:vm` build starts a second microVM. Allow for that time.

## Files the runner keeps

The runner's files are in `<config dir>/runner/`, which is `~/.outerlayer/runner/` by default. Only the runner's account can read them. [Housekeeping](https://docs.outerlayer.ai/run-a-host-as-a-service.md#housekeeping) says what the runner removes and when.

Each build has one folder in `runner/jobs/`, named for the queue, the item and the claim. These are the files to read when a build fails:

| File | What it holds |
| - | - |
| `provision.log`, `cleanup.log`, `report.log` | Each hook's output. For `builtin:container`, `provision.log` holds the output of the clone and of each lifecycle command. |
| `command.log` | The command's output. |
| `proofs.log` | The output of the [proofs stage](#the-proofs-stage). |
| `judge.log` | The output of the [judge stage](#the-judge-stage). |
| `checks/` | Each host check's output as `<name>.log`, and `environment.log` for the setup of the check environment. |
| `sync.log` | The output of the `outerlayer sync` that uploads the build's sessions. |
| `vm-console.log` | The console of a `builtin:vm` build's microVM, up to 8 MiB. |
| `hook-errors.log` | Failures of the agent's session hooks in a container or microVM build. |
| `tunnel.json` | The hosts the build reached, the addresses the tunnel refused, and the destinations the build called. |
| `job.json` | The item, the queue, the branch, the stage and the outcome. It holds no credential. It names the repositories and the people credited on the build's commits, so read it before you share it. |
| `reason` | One line on how the build went, which your cleanup hook or report hook can write. See [The cleanup hook](https://docs.outerlayer.ai/write-your-own-hooks.md#the-cleanup-hook). |

> [!WARNING]
>
> Never share the `item-key` file in a build's folder, or `runner/host-key.pem`. The first holds the build's [item key](#item-key). The second holds the [host key](https://docs.outerlayer.ai/run-a-host-as-a-service.md#the-host-key).

`outerlayer work logs --item <n>` prints the runner's log for a build without a login to the host. See [Find out why a build ended](https://docs.outerlayer.ai/troubleshoot-builds.md#find-out-why-a-build-ended).

## Runner status

The factory's **Settings → Runners** page lists each runner that has checked in, with a status. `GET /v1/hosts` returns the same runners with the status as a code. The call needs the `hosts.read` permission. See [List the factory's runner hosts](https://docs.outerlayer.ai/api-reference/hosts/list-the-factorys-runner-hosts.md) for every field.

A runner has the first status in this table that applies:

| Runners page | `status` | Cause | What to do |
| - | - | - | - |
| **Not seen** | `stale` | The last heartbeat is older than three of the runner's poll intervals. | Check that the runner is running. Run `outerlayer runner status` on the host. |
| **Refused** | `outdated` | The runner's version is below the lowest version that OuterLayer accepts. | [Update the runner](https://docs.outerlayer.ai/update-a-runner.md). |
| **Draining** | `draining` | The runner finishes its builds and takes no new work, after `outerlayer runner stop`. | Wait. See [Let a restart through](https://docs.outerlayer.ai/run-a-host-as-a-service.md#let-a-restart-through). |
| **Outdated** | `outdated` | The runner's [protocol](#runner-protocol) is below the lowest that OuterLayer accepts. | [Update the runner](https://docs.outerlayer.ai/update-a-runner.md). |
| **Failing** | `failing` | The runner's last request for work failed, and no later request succeeded. | Read `lastError` from the API, or the runner's log. |
| **Ready** | `ok` | None of the above. | Nothing. |

A runner that is blocked or low on disk still reads **Ready**, but takes no work. The API shows this in `state`, `blocked` and `lowDisk`. See [When the runner is blocked](https://docs.outerlayer.ai/troubleshoot-builds.md#when-the-runner-is-blocked) and [When the runner holds off for disk](https://docs.outerlayer.ai/troubleshoot-builds.md#when-the-runner-holds-off-for-disk).

## Runner protocol

A runner reports a protocol number to OuterLayer. This number is not the CLI version. When OuterLayer needs a higher number, it refuses the runner's claims, and the runner's status is **Outdated**. [Update the runner](https://docs.outerlayer.ai/update-a-runner.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.