Skip to main content

Headless runs in CI

omnicoreagent run runs one instruction and exits. Nobody is there to approve a call or top up a budget, so you say up front how those are answered; the exit code names how the run ended, and the evidence is written to files. By the end of this page you will have an agent that opens a pull request from a CI job, the run’s record read back from its files, and an outcome recorded once the tests have passed. It needs only the core install: no server.
Every output on this page is what the command printed when it was run. The model’s wording, the ids and the token counts will differ on your run.

An agent file

The command loads a Python file that defines an agent variable or a create_agent() function, the same file omniserve run takes. This agent lists changes and may open a pull request, and its policy says a person approves pull requests:
The SQLite store keeps the run’s record after the command exits, so a later step can read it and record its outcome (SQLite needs pip install "omnicoreagent[postgres]", which brings SQLAlchemy). With the default in-memory store the record is gone when the command ends.

Run it

The model asked to open the pull request, the policy asked for a person, and by default (--approval-mode stop) the command leaves the run waiting and exits 3. out/result.json says what it is waiting for:
A person can approve it later from any process that shares the store (resolve_approval, then resume: Approvals). In CI, decide instead. An approvals file approves pull requests and denies anything else the policy asks about:
The run paused at the pull request, the CLI approved it by rule 0, and the run resumed and finished: two traces, one run. The last line is on stderr. usage is the whole run’s, both segments counted (three model calls: two before the pause, one after), the same totals trajectory.json holds.

How it works

1

Load the agent, run the instruction

The run is tagged headless, approval-mode:<mode> and budget-mode:<mode>, plus your --tags, and recorded as entered from the headless surface. MCP servers the agent names are connected first.
2

Answer each pause by the mode you chose

When the run pauses for approvals, each one is decided by --approval-mode through the same resolve_approval a person would use, with the approver omnicoreagent-cli and a note naming the mode or rule, and the run is resumed. This repeats up to --max-approval-rounds (20) times. When a budget runs out, --budget-mode deny denies the top-up through deny_budget; the CLI never grants money.
3

One deadline for all of it

--timeout covers the whole run, pauses and resumes included. When it passes, the run is stopped and recorded as timeout, not cancelled.
4

Write the evidence, exit with the state

result.json and trajectory.json go to --output-dir; the answer (or with --json, the whole result.json) to stdout; one status line to stderr. Reading the evidence never changes the outcome: if the trajectory cannot be read, evidence_error says why.

Approval modes

A rule matches on tool_name, capability, or both, and has a decision (approve or deny) and an optional note. The file’s default is approve, deny (when omitted) or stop.

Exit codes

What it writes

The traces themselves are also in the agent’s telemetry store (./workspace/telemetry/ by default), and the run’s record in its memory store.

Read the run afterwards

trajectory.json needs no agent to read:
The approval’s status is used: its decision was applied when the run resumed. decision says which decision it was, approve or deny; a denied approval is used too once the run resumes, and its call was not made. --provenance keys the trace knows (environment, trial_id, case_id, evaluation_id, deployment_id, application_version, adapter, …) are set directly; any other key, like build, goes under external_ids. What every field means is in Read a run.

Record the outcome after the tests

The run said it opened a pull request; whether the change was any good is known when the tests run. A later step records it on the run, with no model key needed:
The run is now one training record, both segments in it, with its outcome:
More in Outcomes and training records.

In a CI job

A GitHub Actions job: run the agent, keep its evidence whatever happened, run the tests, record the outcome. The steps share one machine, so the SQLite file and the workspace carry from step to step; across jobs or machines, point DATABASE_URL at a shared database and keep the workspace with it.
--instruction-file reads the instruction from a file (or - for stdin), which keeps a long one out of the workflow. With --budget-mode deny, a run that runs out of budget ends (exit 1) instead of waiting (exit 4) for a top-up nobody will give.

Options

The command line reference is generated from the code.

When things go wrong

--timeout 2 against a model call that takes several seconds. The run is recorded as timeout in its trace and its record, and result.json has "error": "run exceeded its deadline". Give it the time a real run needs. The deadline starts once the agent is set up (its tools, MCP servers and model client loaded), so a cold start does not spend it.
With --budget-mode deny and a request budget of one model call:
termination_reason is budget_exhausted, and cli_decisions holds the denial. Raise the budget (Budgets).
The model key is not in the step’s environment. In GitHub Actions, set it from a secret, as above.
Nothing ran; the message says why:
An agent file that raises on import, or defines neither agent nor create_agent(), is exit 2 as well.
The later step cannot find the run: the agent keeps its records in memory (the default), or the step points at a different store. Give the agent a durable store, as agent.py does, and run both steps against it.

Next

Read a run

Every field of the trajectory the command writes.

Outcomes and training records

What the outcome is for, and how to export runs.

Observability

Traces, stores and exporters.

Harbor and Terminal-Bench

The same headless run, on benchmark tasks.