# Onus > Onus turns a pull request into a short report of changes in meaning. ## The map for coding agents (MCP) URL: https://onushq.com/docs/agents The map for coding agents: onus mcp, its tools, many agents and worktrees # The map for coding agents (MCP) `onus mcp` gives a coding agent the codebase map as MCP tools: where things are, what depends on what, which tests cover what, who owns it, and what a change means before it is committed. Every answer is computed from the code, never generated, and carries the file and line it comes from. It also lets the agent hand its work in the way Onus expects: run the tests in an environment, submit the change with that evidence, see its lane and the judge's verdict, and ask for more access with evidence instead of working around its token. Nothing it offers lets an agent approve itself. **What helps agents today is `onus_check`.** We tested whether the map makes coding agents faster or more correct when they implement a feature ([evaluation](https://github.com/onushq/onus/blob/main/docs/evaluation/2026-10-07-map-for-agents.md)). It did not, measurably: agents found code with text search just as well and rarely called the navigation tools. They did call `onus_check` before finishing, and Phase 2 now focuses on making it precise. The navigation tools below are experimental. ## Connect an agent Claude Code: ```text claude mcp add onus -- onus mcp ``` Any MCP client that starts servers over stdio: run `onus mcp` in the repository (or pass `--repo `). The map is the map of the worktree the server is started in, as it is on disk now, saved but not committed changes included. Clients that connect over HTTP (streamable HTTP): run `onus mcp --http 127.0.0.1:8765` and point the client at `http://127.0.0.1:8765/`. Only requests naming a loopback host are answered, which keeps other machines and DNS rebinding out; `--allow-host ` adds a host for a network you trust. The tools describe your code and can run containers, and there is no authentication. Every tool answers the same over stdio and HTTP, and to every client. ## Tools The check: - `onus_check`: the changes in meaning between a commit (default `HEAD`) and the worktree now, as the same rows as a pull request report, preceded by a short checklist of what to verify before finishing: third-party APIs the repository has never used (with the pinned version), implementations and test doubles of a changed interface that were not updated, committed secrets and broken boundary rules. Run it before finishing a task. - `onus_impact`: before a change, what breaks if a symbol, file or component is removed, renamed, changes its signature, gains a required member, or only changes behavior. One site per file (file, line, how it is used; for a new required member only the implementations, found in the source text), the tests that exercise it, and what static analysis cannot see. - `onus_invariants`: the invariants onus.yaml declares for a symbol, file or component, or all of them: rules a change must keep. Navigation (experimental): - `onus_status`: what the map holds (components, files, symbols, edges, tests, and coverage with its notes when `tests.coverage` names files). Start here. - `onus_find`: symbols by name words, ranked by how many words match in the name, then in fields and parameters, then in the file path (`format phone` finds `formatPhoneNumber`; a field name finds the types that declare it). Every answer also lists matching files. - `onus_symbol`: one symbol: kind, location, contract shape, declared invariants, how many places use it. - `onus_dependents`: what depends on a symbol, file or component, up to `depth` steps (imports, calls, type references). Use it before changing something. - `onus_dependencies`: what a symbol, file or component depends on: code, external services, events, tables, config keys. - `onus_tests_for`: the test files that exercise a symbol, file or component, strongest link first. Each says how the link is known: `ran` (coverage shows this test running it), `static-ran` (the test refers to it and the measured run executed it), `static` (it refers to it; no coverage was given), `static-not-run` (it refers to it, but the measured run never executed it). When the coverage measured the target's file, `coverage` gives its executable and ran lines; no tests with lines that ran means the target ran through other code. Coverage comes from the files `tests.coverage` in onus.yaml names ([`onus help configuration`](/docs/configuration)). - `onus_owners`: owners and sensitivity labels. - `onus_component`: a component's public surface, the components it uses and that use it, its external services and events. - `onus_file`: a file's symbols, imports and importers. Handing work in (leave these out with `onus mcp --no-actions`): - `onus_lanes`: the lane policy: what merges on its own, what goes to the judge or a person, and the floors no policy can lower. It says whether held-out checks are configured, never what they run. - `onus_env_create`, `onus_env_run`, `onus_envs`, `onus_env_destroy`: an environment built from a commit, and commands run in it. Each run is recorded as evidence (output, JUnit results, traces, coverage) under a run id. See [`onus help environments`](/docs/environments). - `onus_evidence`: recorded runs, a run's manifest, or one of its artifacts. - `onus_run_test`: a test command once in a throwaway container, to reproduce a failure; nothing is recorded. - `onus_submit`: hands in a committed change with an intent, run ids as evidence and the ids of escalations granted during the task. Onus reports it, attaches the scope of the server's task token, classifies it, and keeps it in `.onus/submissions/` where people see it in `onus ui`. Returns the lane, every reason, and a submission id. - `onus_judge`: the verifying judge on a submission: evidence re-run in fresh containers, intent, contracts, tests, held-out checks, stopping at the first failure. The verdict is kept beside the submission. Held-out checks come from the suite `onus mcp --held-out` was given; the agent sees only how many ran and passed, and not which configuration from `lanes.judges` judged. - `onus_escalate`, `onus_escalation`: ask for rights the token lacks, with evidence, and check the answer (`wait` waits up to 50 seconds for it). A granted request returns the new token, the old rights plus exactly what was asked, and the server uses it from then on. The granted token names its request, so it proves the grant wherever the person's decision was logged. Where a request goes is in [`onus help scopes`](/docs/scopes). - `onus_blocked`: the safe exit. A test the agent did not write fails and cannot be fixed in scope, the spec contradicts itself or the code, or the task cannot be done as asked: the agent reports it with evidence (`kind` `broken-test`, `contradictory-spec` or `impossible-task`) and stops. It is filed like an escalation that asks for no rights, routed to the owners of the code involved, and listed even when the CLI is signed out. A person answers it; the agent reads the answer with `onus_escalation` (state `answered`). - `onus_outcomes`: track records and the held-out backlog, read-only. Onus Cloud (listed only when the CLI is signed in with `onus login`, `ONUS_CLOUD_TOKEN` or `ONUS_API_KEY`; see [`onus help cloud`](/docs/cloud)): - `onus_cloud_env_create`, `onus_cloud_env_run`, `onus_cloud_envs`, `onus_cloud_env_destroy`: environments built and run on Onus Cloud's machines from a pushed commit, each run recorded by Onus Cloud. - `onus_cloud_attach_evidence`: a recorded run to a pull request, for the judge; with `doorLog: true`, the task's slice of the door log too, cut by the server from its own log, for the change's provenance. - `onus_cloud_status`, `onus_cloud_pulls`, `onus_cloud_pull`: who the CLI is and what its plan allows; governed pull requests, their lanes and evidence. - `onus_cloud_map`: the map tools' questions (`{"query": "dependents", "target": …}`) asked of the map Onus Cloud keeps at a commit or a pull request's head, answered here and filtered to the token's read scope. - `onus_cloud_token`: a narrower Onus Cloud token for a sub-agent: only the scopes and repositories named, never more than the server's, ending within 24 hours. A server signed in with a login never sends the login with an agent's call: it mints a scoped token for the worktree's repository on first use and uses that. The flags: - `--key `: the root private key, held by the server and never shown to the agent. With it, grants decided by policy, or by a person in Onus Cloud, are minted by the server. Needs `--token`. - `--trusted-repo `: a checkout whose files the agent cannot edit, such as a `git worktree` of the default branch outside the agent's. Signed out, with `--key`, the policy decides escalations from its map and onus.yaml. Without it, a person does. Needs `--key`. - `--cloud-token ` (`ONUS_CLOUD_TOKEN`): a scoped Onus Cloud token to use as it is. - `--cloud-repo ` (`ONUS_CLOUD_REPO`): the repository in Onus Cloud, when the worktree's remote is not GitHub. Under a task token, calls that name a repository are checked against it. - `--cloud-ttl `: how long the token minted from a login lasts (default `8h`, at most `24h`). The order an agent should follow is in the server's instructions: commit, run the tests with `onus_env_create` and `onus_env_run`, `onus_submit` with an intent and those runs, then `onus_judge`. What agents cannot do through MCP: record outcomes, grant or deny escalations, mint or widen task tokens, widen an Onus Cloud token, or choose the scope a submission is checked against. With `--token`, submissions, environments (their hosts and secrets) and escalations use the server's task token, whatever the agent asks for. Every call is checked at the door and logged. With `--token`, each tool first checks the token: its signature when the server starts, its expiry on every call, then the tool's own rule. A refused call says which right is missing and how to ask for it: ```yaml denied: `net:host:evil.example` is not in the token of task `sms-alerts`. Your net rights: `net:host:sms.test.example`. To get it: onus_escalate with scopes ["net:host:evil.example"] and evidence (a failing test is strongest). ``` - Environments belong to the task that made them. Another task cannot run commands in one, destroy it, read its runs or see it in `onus_envs`; a run used as evidence must come from the task's own environments. An environment whose hosts or secrets the token no longer allows must be made again. - `onus_run_test` takes no `setup` under a token: setup runs with open network, and the token decides the network. Use `onus_env_create`. - `onus_submit` takes `escalations`: ids from `onus_escalate`. Each must be granted, for the task, with a token signed by the root key, minted for that request, that holds exactly what was asked; otherwise the submission is refused with the reason. `onus submit --escalation` and `onus ui` check the same. - A grant is minted from the first block of a token, so it drops any narrowing (`onus token attenuate`). A server whose token was narrowed for a sub-agent does not take up such a grant, and does not hand it out; the person narrows the granted token the same way and restarts the server with it. - A task plan can name the agent (`agent: {tool: claude-code, model: opus}`). The token then carries it, and submissions and log entries made under the token use it; what the agent declares (`agent_tool`, …) is ignored, and the answer says so. - Hosted environments and runs (`onus_cloud_*`) are checked the same way, from the log of what the task made. A pull request or a list of them is this worktree's repository: `onus_cloud_pulls` without `repository` lists only its pull requests. An environment made without `hosts` has no network. - A tool with no rule is closed to a token. Every call, with or without a token, is written to the audit log: the tool, a digest and a shortened copy of its arguments, the token's task and id (never the token), the agent and where its identity came from, the decision (`allowed` or `denied`, then `done` or `failed` for tools that act) and the reason. The log is `.onus/audit.jsonl` in the worktree, which `onus ui` shows; the server adds it to the repository's own `.git/info/exclude`, so `git add -A` does not commit it. `onus mcp --audit ` puts it where the agent cannot write. When the log cannot be written, the call is refused. Without a token nothing is refused, and calls are logged as `local`. See [`onus help scopes`](/docs/scopes). Targets can be a symbol id (`billing:src/payments.ts#chargeCard`), a bare symbol name, a file path, a module id or a component id. Answers are JSON, sorted, and limited (default 50 items, `limit` up to 500); `truncated` says when there is more. Each answer also says which version of the map it came from and whether the map was rebuilt for it. The same questions work from a shell, with the same answers: ```sh onus query status onus query find format phone onus query dependents UserPreferences --depth 2 onus query tests-for services/billing/src/payments.ts onus query impact UserPreferences --change add-required-member onus query invariants onus query overview onus query check --base main ``` ## Many agents, many worktrees Agents often work in parallel, each in its own git worktree of one repository. Onus is built for that: - **One server per repository.** Every `onus mcp` and `onus query` of a repository talks to one map server over a private Unix socket, and starts it if it is not running. Ten agents in a worktree share one map and one rebuild. - **One map per worktree, kept current.** The server watches each worktree's files. A change marks the map stale; the next question rebuilds it once, however many agents are asking, and questions between changes are answered from memory in about a millisecond. - **Facts shared across worktrees.** Parsing a file is most of the work, and worktrees share almost all of their files. The server keeps each file's facts by content, in memory and in the repository's git folder (`.git/onus/facts`), so a second worktree, a rebuild after an edit, or a restarted server parses only files whose content is new. - **Checks reuse base commits.** `onus_check` keeps the extracted base commit and its map, so agents checking against the same commit pay for it once. Measured on Twenty, an open-source TypeScript monorepo with 31,000 source files, 72,000 symbols and 272,000 edges (Apple M-series laptop): | | Time | |---|---| | A question to a warm map (MCP tool call) | under 1 ms; `onus_find` about 30 ms | | The same from `onus query` (a new process each time) | 20–50 ms | | First question in a repository (full build) | 4.8 s | | First question in a second worktree | 1.9 s | | First question after an agent edits a file | 2.0 s | | Eight agents asking at once after an edit | 1.7 s, one rebuild | | `onus_check` against a commit already checked | 2.4 s | The server exits after 30 minutes without questions. `onus query stats` shows its worktrees and cache use; `--no-server` builds the map in the calling process instead (the only mode on Windows). ## What it reads and runs The same as every other command: the files of the worktree and, for `onus_check`, the base commit through `git archive`. Nothing from the repository is executed, the server only listens on a socket readable by your user, and no plugins are loaded. --- ## Kinds of change URL: https://onushq.com/docs/changes Every kind and subkind of change Onus reports # Kinds of change Every row has a **kind** (one of seven), a **subkind** (the specific change) and a **level** (relationship, behavior or structure). The Kind column of the Markdown report shows a short label; the JSON has `kind` and `subkind`. | Kind | Meaning | |---|---| | security-sensitive | Data leaves the system in a new way, a secret was committed, authentication or authorization code changed, a new HTTP route or public API operation appeared, or code in a sensitive component changed how it decides or writes. Always needs a person. | | breaking | Callers or consumers outside this change may break: a contract lost something or changed in a way Onus cannot prove compatible and some user was not updated, an API operation or route was removed, or a boundary rule was broken. Always needs a person. | | dependency | Third-party code changed: a new, upgraded or removed npm package, a lockfile, or a patched package (which needs a person). | | config | Configuration changed. CI, policies, onus.yaml and CODEOWNERS are also rules of the game. Migrations, root build config and infrastructure need a person. | | additive | Something new that existing callers do not notice: an optional field, a new export, a new event consumer, a new read; also a contract change whose users were all updated in the same change. | | test | Tests were weakened (rules of the game), removed together with the code they tested, or edited to match behavior that changed (`--behavior`). | | internal | Everything else, collapsed into one row per component, plus renames, generated code and notable edits outside sensitive components. | "Sensitive component" means a component with a label of medium or high sensitivity in onus.yaml; `payments`, `auth` and `pii` are high unless onus.yaml says otherwise. ## Relationships between components, data and the outside world These are compared per component, so moving code around inside a component never looks like a new relationship. | Subkind | Kind | When | |---|---|---| | new-external-service | security-sensitive | A component starts calling an external service that is new to the repository, or that sends a kind of data (phone numbers, email addresses, payment details, ...) the component never sent before. Its novelty lists `new-vendor:` and `new-data-egress:`. A new npm package that is the service's SDK is folded into this row (`new-dependency:`, and `new-package:` when no other component used it). | | new-external-call | additive | A component starts calling a service another component already uses, sending nothing new. | | external-service-removed | internal | A component stops calling a service. | | new-event-consumer | additive | A component starts subscribing to an event. The row says whether the publishers also changed; with no publisher in the map it is low confidence. | | new-event-publisher | additive | A component starts publishing an event; the row names the components that consume it. | | event-consumer-removed | breaking | A component stops subscribing to an event. | | event-publisher-removed | breaking | A component stops publishing an event others still consume (internal when nobody consumes it). | | new-data-write | security-sensitive or additive | A component starts writing a table (Prisma). Security-sensitive in a sensitive component. | | new-data-read | additive | A component starts reading a table. | | data-write-removed, data-read-removed | internal | A component stops writing or reading a table. | | new-cross-component-dependency | additive | A component starts depending on another component's code. The row names what it uses, and says so when that is internal to the other component. | | rule-violation | breaking | A declared boundary rule is broken by a new reference (see [`onus help configuration`](/docs/configuration)). Rules of the game. Not reported when the same violation existed before. | ## Contracts Contracts are the public symbols of a component: everything reachable from its entrypoint through exports and re-exports. Shapes come from the declared types; a type that is inferred rather than written is "unverified". | Subkind | Kind | When | |---|---|---| | contract-field-added-optional | additive | An optional field or member is added (`phoneVerified?: boolean`). | | contract-member-added | additive | An enum member or a class method or property is added. | | contract-param-added-optional | additive | An optional or rest parameter is added. | | contract-param-now-optional | additive | A required parameter becomes optional. | | contract-key-added | additive | A public value without a type annotation gains a key: its initializer is an object literal, or a call such as `Schema.struct({...})` or `z.object({...})`, and the object gains a key. | | contract-type-widened | additive | A field, method or result type changes only by gaining optional parameters or optional fields (`(p: string) => R` becomes `(p: string, opts?: O) => R`); callers and implementations keep compiling. | | contract-union-widened | additive | A union type accepts more members (`'A' \| 'B'` becomes `'A' \| 'B' \| 'C'`). | | export-added | additive | A symbol becomes public. Several new exports of one component are one row. | | contract-field-added-required | breaking | A required field or method is added to an interface or type, and an implementation outside this change was not updated. | | contract-field-removed | breaking | A field, member or method is removed. | | contract-field-now-required | breaking | An optional field becomes required. | | contract-field-now-optional | breaking | A required field becomes optional: readers may now get `undefined`. | | contract-field-now-readonly | breaking | A field becomes readonly. | | contract-param-added-required | breaking | A required parameter is added. | | contract-param-removed | breaking | A parameter is removed. | | contract-param-now-required | breaking | An optional parameter becomes required. | | export-removed | breaking | A public symbol is removed or no longer exported. | | contract-key-removed | breaking | A public value without a type annotation loses a key of its initializer. | | contract-union-narrowed | breaking | A union type no longer accepts some members. | | contract-changed-unverified | breaking | A type changed in a way Onus cannot prove compatible: a parameter, return, field or key type, or type parameters. Types are compared with object members and union members sorted, so reordering (as generators do) is no change. | | contract-inferred-changed | internal | Public symbols whose types are inferred, not written, changed their bodies, and Onus has nothing to compare. One row for all of them, low confidence. | A contract change is breaking only when something outside the change can break: a file that uses the symbol and was not changed, or, for a new required member, an implementation that was not changed (code that only reads the type is unaffected). Breaking rows name those files, test files first. Implementations are found by how they are written: `implements X`, `satisfies X`, `: X = {...}`, a function declared to return `X`, and Effect's `Layer.succeed(X, ...)` and `X.of({...})`. A cast (`as X`) is not an implementation: it compiles whatever members `X` gains. When every user was updated in the same change, or nothing outside it uses the symbol, the row keeps its subkind but is additive ("Contract change, users updated" or "no users affected") and does not need a person. It stays breaking when Onus cannot see every user: the package is published (its package.json is not private and has `files` or `publishConfig`), or some imports of it could not be resolved. Renaming a parameter is not a contract change. When a contract declared in onus.yaml has invariants, the row repeats them. A contract declared in onus.yaml is a rule of the game. When its code changes (it is added or removed, or the body, shape or value of it or one of its members differs; formatting, comments and moves do not count), every row about it is marked `rulesOfTheGame` and needs a person. When no row is about it, for example a body-only edit, Onus adds one: | Subkind | Kind | When | |---|---|---| | declared-contract-edited | internal | The code of a contract declared in onus.yaml changed and no other row is about it. Rules of the game. | ## Renames and moves | Subkind | Kind | When | |---|---|---| | rename | internal | A symbol is renamed with an identical body. The row counts the call sites and files updated. | | rename-incomplete | breaking | The same, but some references still use the old name. | | moved-between-components | internal or breaking | Symbols moved from one component to another: one row per pair of components ("17 symbols moved from `orders-core` to `billing-shared`"). Exported symbols edited while moving (same name and kind, most members in common) are included, and their contract changes are listed in the row; breaking when any of those changes is breaking. The row also says when the old component no longer exports them. | | moved-incomplete | breaking | The same, but some references still point at the old place. | A symbol or file that moved inside one component without other changes, and a file whose only changes are formatting, comments or import paths, is not a row: it is listed under "Structure only". ## Dependencies | Subkind | Kind | When | |---|---|---| | new-dependency | dependency | A package.json gains a third-party package. Novelty `new-package:` when no other component used it. A package new to the repository that several components add is one row, and new packages added to the same components share a row. | | dependency-version-changed | dependency | A declared version range changed. | | dependency-removed | dependency | A package is removed. | | lockfile-changed | dependency | A lockfile changed while no manifest dependency did. The row lists the installed versions that change (read from pnpm-lock.yaml, package-lock.json and yarn.lock), or says that none do when only patch hashes, checksums or resolution changed. | | dependency-patched | dependency | A patch of a third-party package is added, changed or removed (`patches/*.patch` for pnpm and patch-package, `.yarn/patches/`). The row names the package and the files the patch edits. Needs a person: installed code changes without a version change, and the patch stops applying when the version moves. | Workspace packages (your own `@scope/...` packages) are not third-party: depending on one shows as a relationship between components. | Subkind | Kind | When | |---|---|---| | external-api-first-use | internal | Production code uses an API of a package the repository already depends on (`Effect.retry` from `effect`) that no other code in the repository uses. The row names the version the repository pins, to check the API exists in it: code written from memory of another version often calls functions that are not there. Node's built-in modules and framework virtual modules (`$app/…`, `$env/…`) are left out, and an API counts as used when the repository already reaches it another way (`RawData` imported by name, `WebSocket.RawData` through the default import). | ## Risk classes Some changes need a person whatever their size or shape, so they get their own row instead of disappearing into a component's changed lines. Onus finds them by fixed patterns in the changed files. | Subkind | Kind | When | |---|---|---| | migration-changed | config | A database migration is added, edited or removed: versioned files in a `migrations`, `migration` or `migrate` folder, `*.migration.*`, Flyway `V1__name.sql`, Drizzle `drizzle/0001_name.sql`, Alembic versions, and versioned upgrade commands (`upgrade…/…command……`). The row says what the migration does to stored data, read from its SQL and TypeORM calls (creates or drops tables, adds or drops columns, creates indexes, deletes or updates rows, grants), and mentions `ON DELETE CASCADE`. The `down` step is left out. Editing a migration that already ran is called out. Snapshots and journals the tool writes beside it (`drizzle/meta/`, Prisma's `migration_lock.toml`) are part of the migration, and other code in a migrations folder gets a row only when it touches stored data. | | public-api-changed | security-sensitive, additive or breaking | GraphQL queries, mutations or subscriptions are added, removed or change their signature, in schema files (`.graphql`, `.gql`), `gql` templates or code-first resolvers (`@Query`, `@Mutation`, `@Subscription`). Operations that skip authentication (`@skipAuth`, `@Public()` and similar) get their own security-sensitive row. Always needs a person. | | route-added | security-sensitive | New HTTP routes: SvelteKit `+server` files, Next.js `route` files and `pages/api`, and controller decorators (`@Controller('users')` with `@Get(':id')`). The row names the methods and paths, and the `cache-control` header the route sets. | | route-removed | breaking | HTTP routes removed. | | auth-code-changed | security-sensitive | Code changed in files whose names say they decide who may do what (`auth`, `permission`, `guard`, `policy`, `role`, `acl`, `jwt`, `oauth`, `password`, …, also in camelCase names). One row per component, even for a refactor. | ## Configuration and rules of the game | Subkind | Kind | Rules of the game | Files | |---|---|---|---| | ci-changed | config | yes | .github/workflows/*, .gitlab-ci.yml, .circleci/, .buildkite/, Jenkinsfile | | onus-config-changed | config | yes | onus.yaml (the report used the base version) | | lanes-model-changed | config | yes | *lanes-model.json, the learned lane model (the lane used the base version) | | onus-pack-changed | config | yes | a framework pack listed in onus.yaml (the report used the base version) | | codeowners-changed | config | yes | CODEOWNERS | | policy-changed | config | yes | *.rego, policy/, policies/ | | test-config-changed | config | yes | jest, vitest, playwright, cypress, karma and mocha configs | | repo-settings-changed | config | yes | anything else under .github/ | | root-build-config-changed | config | needs a person | at the repository root: tsconfig*.json, babel config, nx.json, turbo.json, .npmrc, .nvmrc, .node-version, .tool-versions | | infrastructure-changed | config | needs a person | Terraform (.tf, .tfvars, .hcl), Kubernetes manifests (YAML with `apiVersion` and `kind`, or YAML in k8s, helm, charts, deploy, manifests, infra folders), Chart.yaml, serverless, fly, vercel, netlify, render, wrangler, cdk, Pulumi and a root app.yaml | | env-changed | config | no | .env* | | container-changed | config | no | Dockerfile*, docker-compose*, compose.* | | data-schema-changed | config | no | *.prisma | | workspace-config-changed | config | no | pnpm-workspace.yaml, .yarnrc.yml, lerna.json | | build-config-changed | config | no | tsconfig*.json, project.json, angular.json, ESLint, Prettier, Biome and SWC configs, and bundler, framework and codegen configs (vite, webpack, rollup, tsup, esbuild, svelte, next, nuxt, astro, babel, postcss, tailwind, codegen, …) | | package-scripts-changed | config | no | package.json `scripts` | | package-manifest-changed | config | no | other package.json fields (dependencies are reported as packages) | | config-changed | config | no | other .yaml, .yml, .json, .toml, .ini and .properties files | Rows for a changed config file name what changed: the top-level keys of JSON and YAML (with `compilerOptions` broken down in tsconfig files), and the instructions of a Dockerfile (`ENV BODY_SIZE_LIMIT`, the base image). ## Tests | Subkind | Kind | When | |---|---|---| | test-weakened | test | One row per component when any of these happen: a test case is removed; a case has fewer assertions; a case gains `skip`, `only` or `todo`; expected values in assertions are edited; a test file is deleted; or source code starts comparing against a string literal that only the tests use. Rules of the game. | | tests-removed-with-code | test | Removed test cases or assertions that used code this change deletes. They covered nothing that still exists, so they do not weaken the tests and do not need a person. | A removed case that reappears among the change's new cases (the same body, or the same title in a file of the same name, as when tests move with their code) is neither. New test cases are not a row; the component's internal row counts them. With `onus report --behavior`, the tests that reach the change also run at the base and at the head ([`onus help environments`](/docs/environments)). What they showed is one row per component, at confidence `traced`: | Subkind | Kind | When | |---|---|---| | behavior-newly-failing | internal | A case passes at the base and fails at the head, and again when run once more; or a new case fails at the head. Needs a person, and blocks the change until one approves the row. | | behavior-changed | test | The base version of a test the change edited fails against the head's code, while the head's version passes: the behavior it pinned changed, and the test was edited to match. Snapshot mismatches land here. Rules of the game. | | behavior-newly-passing | internal | A case fails at the base and passes at the head. | | behavior-output-changed | internal | A case fails at both, with a different message. | ## Notable edits inside functions Found in functions and methods that exist on both sides. Security-sensitive in a sensitive component, internal otherwise. An await, guard or throw that the change adds to another function, as when a body is extracted into a helper, moved rather than disappeared, and is not reported. | Subkind | When | |---|---| | boundary-condition-changed | A comparison flips with the same operands: `a > b` becomes `a >= b`. | | condition-constant-changed | A comparison keeps its operator but compares against a different constant. | | constant-changed | A `const` or variable initialized with a literal changes value: `LOYALTY_RATE` from 0.1 to 0.15. | | guard-removed | A function has fewer early exits (`if (...) return/throw`) or fewer `throw`s. An edited guard is not a removed guard. | | await-removed | A function awaits less, and a specific await disappeared. Awaiting calls together (`await Promise.all([...])`) is not a removal. | | error-swallowed | A new empty `catch` block. | ## Secrets | Subkind | Kind | When | |---|---|---| | secret-committed | security-sensitive | An added line matches a high-confidence pattern: AWS access key ID or secret access key, GitHub token, Stripe secret key, Slack token, Google API key, SendGrid API key, npm token, or a private key block. One row per file. The value is never printed, and it is redacted from the map too. | ## Everything else | Subkind | Kind | When | |---|---|---| | internal-changes | internal | The changed lines of a component that no other row explains, with their count, labeled as code, tests or docs. In components with more than 300 files, the row also names the module folders that changed. | | generated-code-changed | internal | Files written by a tool changed: found by folder (`generated`, `__generated__`, `codegen`), name (`.generated.`, `.gen.`, `_pb.`) or a marker in the first lines (`@generated`, `DO NOT EDIT`, "This file was generated"). One row per component that lists what the other rows would have said; review the schema or generator change it mirrors. | ## Grouped rows To keep large changes readable, Onus merges rows that say the same thing: - Several config files of one kind in one component are one row per kind of edit ("Build config: 3 files in `billing` change `module`"). - Packages already used elsewhere in the repository, dropped packages and version changes are one row per component ("2 npm packages already used in this repository added in `c`"). A package new to the repository always keeps its own row. - When the same kind of config or dependency change appears in more than 5 components, it is one row for all of them, and config rows stay apart by the keys they change ("Build config changes `module` in 262 components (445 files)"). - When more than 10 components have internal changes, the 10 largest keep their rows and the rest are summarized in one ("1,490 changed lines inside 106 other components"). - Several new exports of one component are one row. Grouped rows keep every file and line in their evidence. ## Hints on every row - `labels`: sensitivity labels of the component. - `blastRadius`: files in the new version (tests included) that depend on the subject, not counting the file that defines it. - `novelty`: `new-vendor:`, `new-data-egress:`, `new-dependency:`, `new-package:`, `secret`. - `confidence`: `declared` (from onus.yaml), `compiler` (confirmed by a compiler, indexer or language server), `static` (read from the code), `inferred` (from conventions) or `low` (Onus could not resolve something). - `rulesOfTheGame`, `intentMismatch`, `needsPerson`. - `dependentComponents`: other components (not the row's own) that depend on what the row changed. For most rows that is the subject. For a component's internal row it is the functions and classes its changed lines are in, so a body-only edit of a widely used function counts its users. 0 for other rows about a whole component. `dependents` lists those components (absent when there are none). - `tested`: for rows that change code, whether tests reach the files it changes: `tested` (a test exercises every changed file), `traced` (each changed file is tested or seen in a runtime trace) or `untested`. Absent for rows that change no code. It is per file, not line coverage. - `owners`: the owners of the row's component, from onus.yaml or else CODEOWNERS; a row merged from several components has all of theirs. Absent when nobody owns it. Used to route a review ([`onus help ci`](/docs/ci)), not to rank. The lane classifier reads these hints ([`onus help lanes`](/docs/lanes)). --- ## Running Onus in CI URL: https://onushq.com/docs/ci Running Onus on every pull request, and outcome records kept by CI # Running Onus in CI Onus only reads the checked-out files, so it is safe on pull requests from forks with read-only permissions. The one thing it writes, with `records: true`, is its own `onus/records` branch. ## GitHub Actions The Onus action downloads the release binary for the runner, checks its SHA-256, writes the report to the job summary and posts it as one pull request comment that is updated on each push: ```yaml name: Onus on: pull_request permissions: contents: read pull-requests: write # for the comment; leave it out to skip it jobs: onus: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: onushq/onus/action@v0.11.0 id: onus with: fail-on: rule-violation,secrets - uses: actions/upload-artifact@v7 with: name: onus-report path: ${{ steps.onus.outputs.report }} ``` Inputs, all optional: - `version`: the release to download, such as v0.1.0. Defaults to the tag the action is used at, otherwise the latest release. - `onus-path`: use this binary instead of downloading one (for example one you built). - `base`, `head`: the commits to compare. Default: the pull request's base and head commits. - `intent`: `true` (the default) checks the change against an `onus-intent` block in the pull request body; `false` skips the check; a file path uses that file. - `fail-on`: fail the job on `rule-violation`, `secrets`, `behavior` (with `behavior: true`), or several (comma separated). Default `none`. - `comment`: `false` to only write the job summary. - `config`: an onus.yaml to use for both commits. - `lanes`: `true` to classify the change into a risk lane with the policy in onus.yaml and the hard floors, and show it in the comment ([`onus help lanes`](/docs/lanes)). Output `lane`. - `apply-lane`: `true` (with `lanes`) to label the pull request `onus/lane:` and fail the job for the `blocked` lane. Needs `issues: write`. - `wait-for-review`: `true` (the default, with `records`) fails the job while the change is in the human lane and no review counts yet. Output `review`: `waiting` or `approved`. See "Reviews in the human lane" below. - `submission`: (with `records`) the agent's own submission of the change, for its provenance. When empty, one kept on the records branch is used. See "The agent's submission" below. - `auto-merge`: `true` (with `lanes`) to enable GitHub's auto-merge (squash) for the `auto-merge` lane. Required checks still apply. Needs `contents: write`. - `plugins`: a plugins file's contents (YAML), written outside the repository and passed with `--plugins`. For Svelte components: `plugins: [{ name: svelte, kind: language, command: [onus-plugin-svelte], files: ["**/*.svelte"] }]` (the plugin ships in every release archive). - `working-directory`: the repository, if not the workspace root. - `coverage`: coverage of the head commit (lcov, Cobertura XML or Istanbul JSON) written by an earlier step, as a glob. Passed only when the checkout is the head commit (see "Which changed lines tests ran" below). - `behavior`: `true` to run the behavior diff (`onus report --behavior`): the tests that reach the change run at the base and at the head in containers, from `environment.behavior` in the base commit's onus.yaml ([`onus help environments`](/docs/environments)). GitHub's Linux runners have Docker. A binary without the flag skips it with a warning. `behavior-budget`: seconds for it (default: the config's, else 600). With `fail-on: behavior`, a test that passes at the base and fails at the head fails the job. - `cache`: `true` (the default) keeps the base commit's map in the Actions cache, so later pushes to the same pull request skip mapping the base again. `false` maps it every time. - `feedback`: `true` (the default) ends the comment with a line asking for a 👍 or 👎 reaction and for `/onus caught` replies (see below). - `github-token`: the token for the download and the comment. Default: the workflow's token. Outputs: `report` (path of the JSON report), `markdown` (path of the Markdown report), `meaning-changes` and `needs-attention` (counts from the report's summary), and `metrics` (one line of JSON: changed files and lines, rows, findings, the time the report took and, with coverage, `coverage: {changed, ran}`). With `records: true` also `lane`, `review` (`waiting` or `approved` in the human lane), `provenance`, `rollout-strategy` (`none`, `canary` or `flag`) and `rollout-plan` (the path of `rollout.json`), and with `rollout-percent`, `rollout-decision` (see "Rollouts" below). Notes: - The pull request body is passed through an environment variable, never pasted into a script, so a body cannot inject shell commands. - The action compares the base and head commits, not branch names: on `pull_request`, the checkout is a merge commit. Commits missing from a shallow checkout are fetched. - On pull requests from forks, GitHub gives the workflow a read-only token, so the action skips the comment and only writes the job summary. - The comment starts with a hidden `` marker, which is how the action finds its own comment to update. - The action runs on Linux (x86_64 and ARM64), macOS and Windows runners. ## Outcome records on every pull request With `records: true`, the action keeps the track record lanes depend on without anyone typing an outcome. The records live on a branch of the repository, `onus/records` (input `records-branch`), as three files that only grow by whole lines: `pulls.jsonl` (what Onus knew about each pull request while it was open), `outcomes.jsonl` (what happened to it) and `signals.jsonl` (what production said: incidents, rollbacks, deploys, rollout stages and traces, [`onus help production`](/docs/production)). Nothing outside the repository is needed. ```text name: Onus on: pull_request: types: [opened, synchronize, reopened, edited, ready_for_review, closed] pull_request_review: types: [submitted, dismissed] issue_comment: types: [created] push: branches: [main] permissions: contents: write # the onus/records branch pull-requests: write # the comment, and reading reviews actions: write # re-running the check after /onus approve or an approval jobs: onus: # Comments matter only when they are /onus commands on pull requests. if: >- github.event_name != 'issue_comment' || (github.event.issue.pull_request && startsWith(github.event.comment.body, '/onus')) runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: onushq/onus/action@v0.11.0 with: records: true ``` What happens on each event: - **Opened or pushed:** the change is submitted with the report as its evidence, classified with the policy, the risk inputs (including the reverts and incidents of the components it touches, from this repository's records), the hard floors and its agent setup's record, and judged when the lane is `judge` and `judge: true`. The comment shows the lane, why, the agent setup's record and an `/onus approve` command for each row that needs a person. The latest classification is appended to `pulls.jsonl`, with the components the change touched, the change's features (what the learned lane model trains on), and the judge configuration and its verdict when the judge decided. With `lanes.learned`, the model is read from the base commit, and the comment shows its risk on a **Learned risk:** line ([`onus help lanes`](/docs/lanes)). The change's provenance record is written to `provenance.json` (output `provenance`; `onus help provenance`), and its rollout plan to `rollout.json` (outputs `rollout-strategy` and `rollout-plan`), which the comment shows on a **Rollout:** line ([`onus help production`](/docs/production)). Production traces kept in the records (`signals.jsonl`) are passed to `onus report --traces`, so the report's map includes the calls production observed. - **A review is submitted or dismissed:** the same as a push. The lane, the comment and the check are worked out again with the reviews as they now stand (see "Reviews in the human lane" below). - **Merged or closed:** the pull request's latest record becomes an outcome (`merged` or `closed`), with the commit it landed as, who merged it (`mergedBy`), the components it touched and its features. Running twice records it once. The provenance record is rebuilt from the records (output `provenance`); upload it with `actions/upload-artifact` to keep it. - **A push to the default branch:** commits that revert a merged change (`This reverts commit …`) are recorded against it; a reverted change no longer counts toward its agent setup's record. - **A `/onus` comment** from someone with write access (owner, member or collaborator); anyone else's is answered with a 😕 and ignored: /onus approve new-external-service:external:acme-sms@notifications /onus audit ok /onus audit miss the opt-out was ignored /onus incident SMS sent twice involved: notifications, notifications:src/sms.ts#sendSms `approve` records a person's approval of a row and re-runs the check, so the lane updates. `audit` records a human audit of a merged change, the miss rate that says whether the automatic lanes can be trusted, and clears the change from the audit queue (`onus outcomes audits`, [`onus help lanes`](/docs/lanes)). `incident` marks a merged change as having caused one, with the components and symbols involved: an agent setup with an incident among its last 20 changes cannot auto-merge, and `onus outcomes backlog` lists where incidents happen. The action reacts with 👍 and replies with what it recorded. `/onus accuracy` is answered the same way but records nothing (see "Accuracy checks" below). `/onus caught ` from someone with write access is recorded too, as `caught: true` on the change's outcome, or on its pull record while it is open: the learned lane model counts it as a problem, like an incident ([`onus help lanes`](/docs/lanes)). ### Rollouts A deploy workflow reports each stage of a rollout, and Onus decides from the change's plan and the metric samples: ```yaml on: deployment_status jobs: rollout: if: github.event.deployment_status.state == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - run: ./scripts/canary-metrics.sh > "$RUNNER_TEMP/canary.json" - uses: onushq/onus/action@v0.11.0 with: records: true rollout-percent: ${{ github.event.deployment.payload.percent }} rollout-metrics: ${{ runner.temp }}/canary.json ``` `rollout-change` names the change (`owner/repo#123`) or a commit; by default the deployment's commit. The step runs `onus ci rollout`, records the stage and pushes the records, and sets `rollout-decision`: `promote`, `hold`, `rollback` or `complete`. A rollback fails the step, and the change's `rolled-back` outcome is already recorded. `workflow_dispatch` with the same inputs works too. Onus never moves traffic: the workflow does, on the decision. See [`onus help production`](/docs/production). ### Reviews in the human lane A change in the human lane needs a person to approve it. `onus ci pr --reviews ` decides whether it has one, from the pull request's GitHub reviews as a JSON array: ```json [{"by": "grace", "state": "APPROVED", "commit": "", "association": "MEMBER", "bot": false, "at": 1760000000}] ``` or as an object that also says who is in each owning team: ```json {"reviews": [...], "teams": {"@acme/growth": ["grace", "lin"]}} ``` `by` is the login without `@`; `state` is `APPROVED`, `CHANGES_REQUESTED`, `COMMENTED`, `DISMISSED` or `PENDING`, in any case; `commit` is the commit reviewed; `association` is the reviewer's author association; `bot` and `at` (seconds) are optional. A host gets them with the GitHub CLI: ```text gh api repos/OWNER/REPO/pulls/N/reviews --paginate | jq '[.[] | {by: (.user.login // "ghost"), state, commit: (.commit_id // ""), association: .author_association, bot: (.user.type == "Bot"), at: ((.submitted_at // "1970-01-01T00:00:00Z") | fromdate)}]' > reviews.json ``` The rules: - Each reviewer's latest review counts, ordered by `at` and then by position. `COMMENTED` and `PENDING` never replace an earlier verdict; `DISMISSED` clears it. - A review is not counted, and is listed in `ignored` with the reason, when the reviewer is a bot (`bot`, or a login ending in `[bot]`), opened the pull request (`--author`), has no write access (an association other than OWNER, MEMBER or COLLABORATOR), or approved a commit other than the head. - Changes requested by a counted reviewer stand at any commit, as on GitHub. - Whose approval counts in the human lane is `lanes.approval` in the base commit's onus.yaml, so a change cannot loosen it for itself. An explicit `--config` inside the repository is read at the base commit too; one outside it (a host's own) is taken as given. An onus.yaml that does not parse leaves the default, with a warning. - `owner`, the default when the change has owners: only an owner of what changed. That is a person named as an owner (`@grace`), or a member of an owning team (`@acme/growth`) when the reviews file lists the team's members. An approval from anyone else is listed in `ignored` as "not an owner". Changes requested by anyone with write access still stand. - `member`, the default when nobody owns what changed: any person with write access. - When `owner` cannot be checked, any person with write access counts, and `note` says why: an owning team whose members the file does not list, an owner given as an email, no owners at all, or no owner but the author who could approve (the only owner opened the pull request, or the owning teams hold nobody else). A team the file lists with no members has none: beside other owners it adds nobody. - In the human lane the review is satisfied with at least one counted approval and no outstanding changes requested. Other lanes need no approval: there every approval from someone with write access is recorded, for provenance, and `approval` is `member`. The owners are read at the base commit, as GitHub reads CODEOWNERS, so a change can name neither its own owner nor, with `owner`, its own approver. For each file of the rows that need a person (of every row when that finds nobody), they are the owners of the component holding it in the base commit's onus.yaml (or the `--config` read as above), else the owners CODEOWNERS at the base commit gives the file. The report's `hints.owners` come from the head and are not used here. They are the owners to ask, each once per pull request: the pull record keeps everyone asked so far. `onus ci owners --base --report [--config ]` prints them, `{"owners": [...]}`, so a host can look up the members of owning teams before it runs `ci pr --reviews`. The answer is the `review` key of what `onus ci pr` prints, always present: ```json {"lane": "human", …, "review": {"required": true, "satisfied": false, "approvedBy": [], "changesRequestedBy": [], "owners": ["@team-growth"], "request": ["@team-growth"], "ignored": [{"by": "@eve", "why": "no write access (CONTRIBUTOR)"}], "approval": "owner"}} ``` `required` is true in the human lane only, and `satisfied` is true outside it. `approval` is the rule that counted the approvals, `owner` or `member`, and `note` is there only when `owner` fell back to `member`. `request` lists the owners to ask now: not asked before, never the author, and empty once satisfied. `lane.md` gains a **Review:** paragraph in the human lane: who was asked, or that it was approved and by whom. Without `--reviews` there are no reviews, so a human-lane change waits. The pull record keeps `requested` (everyone asked), `reviewedBy` (the counted approvals of its head) and `approval` (the rule that counted them, once a review was needed or given), and the outcome keeps `reviewedBy` and `approval`. A human-lane outcome with no `reviewedBy` merged without a counted review: an admin merge, or a check that was not required. With `records: true`, the action passes `--reviews`. It lists the pull request's reviews with the GitHub CLI, as above, and the members of each owning team of the repository's organisation that `onus ci owners` names. It asks the owners in `review.request` to review (GitHub's requested reviewers): `@org/team` of the repository's organisation as a team, `@login` as a person, never the author; emails and other organisations' teams are skipped. A name GitHub refuses (not a collaborator, or a team without access) is a warning, and the others are still asked. onus has already recorded it as asked, so it is not tried again on every push, and the comment says GitHub would not ask it. The workflow's own token cannot read team members (that needs the organisation's members: read permission), so with it onus counts anyone with write access for those teams and says so; give `github-token` a token that can read them to count only their members. While no review counts, the job fails (`wait-for-review`, default `true`) and the output `review` is `waiting`; once one does, it is `approved`. A `pull_request_review` event runs the action again, and after an approval it re-runs the failed `pull_request` check for the head (with `actions: write`), so both pass. Make the job a required status check in branch protection or a ruleset: otherwise a failed check is shown but does not stop a merge. A listing that fails passes no reviews, so the change keeps waiting. Each run is its own check of the same name on the head commit, and GitHub goes by the newest: when an approval is dismissed, the run for the dismissal fails, and it is the newest. For the same reason every review runs the action, comments too: a run that skipped the work would leave a passing check as the newest. Reviews on a closed pull request do nothing. Only `records: true` waits; `lanes: true` alone classifies without reviews, and says so in a notice when the lane is human. A pull request runs the workflow and the action as it changes them, so it could turn the wait off for itself: protect `.github/workflows` (and the action, when it is in the repository) with CODEOWNERS and *Require review from Code Owners*, or a ruleset. Onus Cloud passes `--reviews` too, and turns `satisfied` into its check. ### Who made the change Each outcome is kept against an agent setup. Onus reads it from what git and GitHub show, in order: 1. An `Onus-Agent: tool/model/config` trailer on the head commit, such as `Onus-Agent: claude-code/opus/plan-first`. The most precise; agents can be told to add it. 2. A branch named after an agent: `claude/`, `claude-code/`, `codex/`, `cursor/`, `copilot/`, `devin/`, `jules/`, `aider/`, and `onus/` for changes pushed through the Onus gateway. 3. The bot that opened the pull request, such as `copilot-swe-agent` or `devin-ai-integration[bot]`; any other bot under its own name. 4. A `Co-authored-by` trailer naming an agent. 5. Otherwise the person who opened it, as `person/`. ### The agent's submission An agent that hands in its change with `onus submit` (or `onus_submit`) under a task token has a submission: the token's task, rights, expiry and fingerprint, verified when it was made, and the escalations granted to it. Give it to the action so the change's provenance keeps them: the `submission` input (a path written by an earlier step), or a file on the records branch at `submissions/.json` or `submissions/.json`. `onus ci records pull` copies those into `submissions/` of the records folder, and a push keeps whatever else is on the branch. The action passes it to `onus ci pr --submission` and to `onus provenance --agent-submission` when the pull request closes. Only the token and the escalations are taken from it, and only when it is for one of the pull request's commits. The record marks the token `agent-submission`, ranked below the door's word, and the `escalations` gap says the escalations it lists are the agent's word: the file is plain JSON, and anyone who can write to the records branch can put one there; one that cannot be read is a warning, never a failed job. The action cannot verify the token itself (a submission never keeps it, and no root key reaches CI), and it sees no door log and none of what Onus Cloud keeps (escalations, calls, the door log an agent attached there). Those stay gaps in the provenance record unless a host passes them (`onus help provenance`). ### Reading the records `onus ui` reads `origin/onus/records` when there is no local outcomes file: the Outcomes page shows changes by week and lane, each agent setup's record and whether it may auto-merge, and the pull requests in flight. Its *Fetch from origin* button fetches the branch. On the command line: ```sh onus ci records pull --dir records onus outcomes summary --file records/outcomes.jsonl ``` ### Notes - Pull requests from forks get a read-only token on `pull_request` events, so they are classified but not recorded. - Concurrent runs merge their records by taking every line from both sides, each as many times as the side that holds it most, and retry the push when another run pushed first. So a line one run wrote twice is kept twice, and a line two runs each wrote once is kept once. - The `onus ci` commands the action runs work in any CI system: `onus ci pr`, `onus ci owners`, `onus ci closed`, `onus ci comment`, `onus ci push`, `onus ci rollout`, `onus ci rollback` and `onus ci records pull|push` (see [`onus help commands`](/docs/commands) and `onus ci --help`). ## Which changed lines tests ran With coverage of the head commit, the report says how many changed lines tests ran and lists those no test ran ([`onus help reports`](/docs/reports)). The tests must run at the pull request's head commit, not the merge commit `pull_request` checks out, so check it out for them: ```yaml steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - run: npm ci && npx vitest run --coverage --coverage.reporter=lcov - uses: onushq/onus/action@v0.11.0 with: coverage: coverage/lcov.info ``` When the checkout is another commit, the action leaves the coverage out with a warning. Coverage that environments recorded comes in through `evidence` (with records): the action passes those runs to `onus report --evidence` too, and only runs at the head commit count ([`onus help environments`](/docs/environments)). With `lanes.inputs.coverage`, too few changed lines run sends the change to the judge, and recorded coverage counts only from the commands it names in `commands`: recorded coverage from any other run sends the change to the judge too, so name the run that records it ([`onus help lanes`](/docs/lanes)). ## Caching the base map Most of a report's time goes into mapping the two commits. A pull request is reported on every push, but its base commit rarely changes, so the action caches the base map (input `cache`). On the command line, the same cache is `--cache-dir`: ```sh onus report --base "$BASE" --head "$HEAD" --cache-dir ~/.cache/onus ``` The cached map's file name is a hash of everything that shapes it: the base commit, the Onus version, the onus.yaml and its packs, the plugins file and trusted mode, and any `--base-scip` indexes. A change to any of them builds a new map; a cached file is never updated in place, and one that cannot be read is ignored. The report is byte for byte the same with or without the cache. With a cached base map, Onus extracts only the base commit's changed files (plus `onus.yaml`, its packs and `.gitignore` files), which roughly halves the time of a report on a large repository: about 12 seconds instead of 22 for a pull request in a 31,000-file repository. ## Was the report useful? 👍 👎, /onus caught and /onus accuracy The Phase 1 question is whether reviewers prefer the report to the raw diff. Two signals answer it, both left on the pull request itself: - A 👍 or 👎 reaction on the Onus comment. - A reply that starts with `/onus caught`, followed by what the report surfaced that the diff hid: /onus caught the new SMS vendor; I would have missed it in 1,400 lines Anyone's `/onus caught` counts toward the catch rate. One from someone with write access is also recorded when the action keeps records (`records: true`), and trains the learned lane model. To acknowledge those replies with a 🚀, add a second workflow; the same action handles both events: ```yaml name: Onus caught on: issue_comment: types: [created] permissions: issues: write pull-requests: write jobs: caught: if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/onus caught') runs-on: ubuntu-latest steps: - uses: onushq/onus/action@v0.11.0 ``` ### Accuracy checks Wrong facts are the metric that matters most, so people can also say whether a report was right. After reading it, reply: ```text /onus accuracy ok /onus accuracy wrong 3 the vendor is not new /onus accuracy missed the nightly cron job now sends email too ``` `wrong` names a row by its number in the table (from 1) or by its id; `missed` says what the report left out. Only people with write access (OWNER, MEMBER, COLLABORATOR) can give an accuracy check. With `records: true`, the action answers with a 👍, or with 😕 and the reason when it cannot take the check. It records nothing: an accuracy check is not an outcome and changes no track record. Onus Cloud asks for a check on a sample of reports, keeps the answers, and shows them with the 👍 and 👎 counts, the catch rate and review times on its Metrics page. The comment also carries the report's metrics line in a hidden HTML comment. `scripts/onus-metrics.sh` in the Onus repository reads all of it back with the GitHub CLI, one JSON line per pull request, or one line of totals with `--summary` (reports, large reports, the share of 👍 on large pull requests, the catch rate, changed lines per row, time to first review): ```sh scripts/onus-metrics.sh your-org/your-repo --limit 200 --summary ``` ## Without the action Install the binary and run `onus report` yourself: ```sh curl -fsSL https://onushq.com/install.sh | sh onus report --base "$BASE" --head "$HEAD" --intent pr-body.md >> "$GITHUB_STEP_SUMMARY" onus report --base "$BASE" --head "$HEAD" --format json --fail-on secrets > onus-report.json ``` ## Other CI systems Onus needs only git, the checkout and the binary. In GitLab merge request pipelines: ```sh onus report --base "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --head "$CI_COMMIT_SHA" --fail-on secrets ``` ## Using the exit code - `0`: the report ran (it may still contain findings; read `summary` in the JSON). - `1`: Onus could not run: a missing ref (fetch more history), an invalid onus.yaml, an unreadable directory. - `2`: a `--fail-on` condition was met. To branch on other findings, read the JSON, for example with jq: ```sh jq '.summary.needsAttention' onus-report.json jq -r '.changes[] | select(.hints.needsPerson) | .title' onus-report.json ``` --- ## Onus Cloud from the CLI URL: https://onushq.com/docs/cloud Onus Cloud from the CLI: onus login, scoped tokens, hosted environments, evidence, escalations, maps and agents # Onus Cloud from the CLI Onus Cloud is the hosted Onus: it analyses every pull request of the repositories you install it on, keeps the outcome records, runs the judge, and builds environments on its own machines. Its Audits page lists the merged changes Onus sampled for a person to audit ([`onus help lanes`](/docs/lanes)); marking one ok or a miss records it with `onus outcomes audit`. The CLI signs in to it as you, so you and your agents can use it from the terminal and through `onus mcp`. ## Signing in ```sh onus login # opens the browser; approve the code it shows onus login --org acme # and act in the organisation acme onus login --no-browser # print the link instead, for a machine without one ``` `onus login` shows a one-time code and opens Onus Cloud's sign-in page, where you compare the code and approve it (and pick the organisation the CLI acts in). The CLI then holds a token of its own in `~/.config/onus/cloud.json` (`$ONUS_CONFIG_DIR` or `$XDG_CONFIG_HOME` to move it), readable by you only. The token acts as you, in every organisation you belong to, with your role there; organisations that require single sign-on need you to have approved it after signing in with it. It lasts 90 days from its last use. ```sh onus cloud status # who, which organisation, its plan and what it allows onus cloud use beta # act in another organisation onus logout # the token is revoked and forgotten ``` The account page in Onus Cloud lists every signed-in CLI, and revokes any of them. **In CI**, there is no one to approve a code: set `ONUS_API_KEY` to an organisation's API key (Settings, API keys; `write` scope to create environments and attach evidence). `ONUS_CLOUD_URL` and `ONUS_CLOUD_ORG` change the address and the organisation; `--org` does too, per command. The CLI signs in with the first of: a scoped token in `ONUS_CLOUD_TOKEN`, an API key in `ONUS_API_KEY`, the login in `cloud.json`. With `ONUS_CLOUD_TOKEN` set, neither of the others is read. ## Narrow tokens for agents and scripts A login acts as you, everywhere you can. An agent or a script needs much less. Mint it a scoped token: one organisation, some repositories, some scopes, a fixed end. ```sh onus cloud token create --repo acme/shop --scope pulls:read --scope env:run --ttl 2h --label sms-otp onus_tok_7Hc0… # stdout: the token alone onus: token for acme (acme/shop): pulls:read, env:run; ends 2026-10-12T19:40:00.000Z (2h). Revoke: onus cloud token revoke 3f2a… ONUS_CLOUD_TOKEN=onus_tok_7Hc0… claude … # the agent's onus commands use it, and nothing else ``` | Scope | Opens | |---|---| | `env:read` | hosted environments, their runs and logs | | `env:run` | create, run in and destroy hosted environments (and `env:read`) | | `evidence:write` | attach evidence to a pull request | | `pulls:read` | governed pull requests, their analyses and evidence | | `map:read` | a repository and the maps Onus Cloud keeps of it | | `escalation:write` | file, read and withdraw escalations | - **Repositories.** `--repo` is repeatable; without it the token is for this folder's GitHub repository. `--all-repos` makes it for every repository. - **How long.** `--ttl` is `90s`, `30m`, `2h` or `1d`: one hour by default, at most 24 hours. The end is fixed; using the token does not move it. - **Narrowable, never widenable.** A scoped token can mint a narrower one, for a sub-agent: `ONUS_CLOUD_TOKEN= onus cloud token create --scope pulls:read --ttl 30m`. A child never has a scope, a repository or an end its parent does not. API keys cannot mint tokens. - **Revoking.** `onus cloud token list` shows your logins and scoped tokens; `onus cloud token revoke ` revokes one and every token minted from it. `onus logout` revokes the login and everything minted from it. - **Denials teach.** A call the token does not open answers with the scope it needs: `Denied: this token does not have env:read … (mint one with onus cloud token create --scope env:read, or ask with onus_escalate)`. A token that ended says to ask for a new one. `onus cloud status` says how the CLI is signed in: `with a scoped token (sms-otp) in acme: pulls:read, env:run on acme/shop; ends in 1h52m`. A scoped token acts in its one organisation, so `onus cloud use` refuses it. Onus Cloud logs every call a token makes (Settings, Audit log, CLI and agent calls). ## Hosted environments The same environments as `onus env` ([`onus help environments`](/docs/environments)), built and run by Onus Cloud instead of your machine: no Docker needed, and every run is recorded by Onus Cloud rather than by the machine that asks. ```sh onus cloud env create # this folder's repository at its pushed HEAD onus cloud env create --repo acme/shop --ref pull/42 --name sms onus cloud env run sms -- npm test onus cloud env run sms --timeout 3600 -- npx playwright test onus cloud env show sms # its runs onus cloud env log sms # a run's whole output onus cloud env list --all onus cloud env destroy sms ``` - **What is built.** The repository's `environment:` in onus.yaml (or its devcontainer), read from the **default branch**, never from the commit under test; the commit itself must be pushed, since Onus Cloud fetches it from GitHub. `--ref` takes a branch, tag, commit or `pull/`. `create` waits until it is ready (the first build of a lockfile runs the setup; later ones start from the warm image). - **Network.** None, unless you name hosts: `--host registry.npmjs.org --host '*.example.com'`. The egress proxy allows exactly those. Setup has network, as with `onus env`. Nothing in an environment can reach Onus Cloud's own network or its cloud's metadata server. - **Files.** Environments run under gVisor, where `/tmp` is in memory: a `seed` or `setup` that writes there is not kept when the judge rebuilds the environment to re-run a test. Write seed data under the repository or `/var`. - **Secrets.** None. Hosted environments are for code and synthetic data. - **Runs.** Each run is recorded like `onus env run`: the output, JUnit results and traces the environment declares, under a manifest. `run` waits and exits 0 when the command passed, 1 when it failed. Runs of one environment go one at a time. - **Lifetime.** An environment unused for 30 minutes, or 8 hours old, is destroyed. Destroy it when you are done: its minutes count until then. - **Plans.** The Free plan has 1 environment at a time and 300 environment minutes a month; Team has 5 at a time and 3,000 minutes, then bills the minutes beyond; Enterprise has no limits. `onus cloud status` shows what is left. ## Evidence the judge can trust Attach a recorded run to a pull request: ```sh onus cloud evidence acme/shop#42 --run ``` Onus Cloud keeps it for the run's commit and, when that is the pull request's head, analyses the pull request again. The judge finds the run's manifest and artifacts beside the repository, checks the run against them (the commit, the command, the exit code), and re-runs it in a fresh container with the environment's setup and seed. Its verdict says how many runs matched their manifests. Attach the task's door log, so the change's provenance keeps every call made through the door: ```sh onus cloud evidence acme/shop#42 --door-log --task sms [--audit .onus/audit.jsonl] ``` It sends the task's slice of the log: the entries from the task's first to its last, with whatever lies between (other tasks' entries on the same log included), so the hash chain can be checked (`schemas/door-log.schema.json`). Through `onus mcp --token`, the door also sends its inspection of the token it holds, never the token, so the change's provenance knows the token even when the agent never escalated. From the command line no door holds the token, so the slice gives it only when it holds the task's `mint` entry. Attach it after committing and pushing the work: the slice goes only to the pull request whose head, as Onus Cloud knows it, is the worktree's commit, so an agent cannot hang its log, and the other tasks' entries in it, on another change (this reads the pull request, so the token needs `pulls:read` as well as `evidence:write`). A slice cut at a commit that is not one of the change's gives neither the task nor the token. An Onus Cloud that keeps door logs passes the slice, as it was sent, to `onus ci pr --audit`, which checks the chain and keeps the task's entries in the provenance record, with Onus Cloud's own log of the calls made under the task token beside them (`--calls`; `onus help provenance`). Give `--run` as well to attach both. Each goes in its own request, so an older Onus Cloud, which does not take door logs, refuses it rather than keeping the run alone. ```sh onus cloud pulls --attention # waiting for a person onus cloud pulls --repo acme/shop --lane judge onus cloud pull acme/shop#42 # its lane, analyses and evidence ``` ## The map Onus Cloud keeps Onus Cloud keeps the map of every default-branch commit it maps (after each push to the default branch) and of the newest head of each pull request it analyses. A newer head of a pull request replaces the older one; a closed pull request keeps its last. Every map is the binary's own output, kept byte for byte, and so is its overview (`onus query overview`). ```text GET /v1/orgs//repos//map # the newest default-branch map GET /v1/orgs//repos//map?commit=<40-hex sha> # the map of that commit GET /v1/orgs//repos//map/overview[?commit=…] # its overview GET /v1/orgs//repos//maps # the maps kept, newest first ``` The `x-onus-map-commit` header names the commit of the map sent. A commit Onus Cloud never mapped answers 404 `no_map`: ask a worktree at that commit with `onus query` instead. The web app's map explorer shows the overview: components, owners, labels, what each uses and what uses it. `onus cloud map` asks it the same questions as `onus query`, with the same JSON. The CLI downloads the map and answers here; Onus Cloud only serves the file: ```sh onus cloud map status --repo acme/shop # the default branch's newest map onus cloud map dependents UserPreferences --commit 3f2a1c… # a commit Onus Cloud mapped onus cloud map impact formatPhone --change change-signature --pull 42 # a pull request's head onus cloud map --download map.json --commit 3f2a1c… # the map itself onus query --map map.json dependents UserPreferences --depth 2 ``` The repository defaults to this folder's GitHub remote (else `ONUS_CLOUD_REPO`). `--commit` takes a full sha, or anything this folder's git resolves. Maps of a commit never change, so they are kept by sha in `~/.cache/onus/cloud-maps/` (`$ONUS_CACHE_DIR` or `$XDG_CACHE_HOME` to move it), and a second question makes no request. A map of another schema version is refused (update onus), and so is a map of another commit than the one asked for. `check` and `stats` need a worktree: use `onus query`. ## Escalations and the safe exit An agent that needs more than its task token allows asks with evidence ([`onus help scopes`](/docs/scopes)). An agent that is blocked (a test it did not write fails, the spec contradicts itself, the task cannot be done as asked) says so with evidence and stops. When the CLI is signed in, both go to Onus Cloud, where a person sees them, decides, and is notified: ```sh onus cloud escalate --task sms-otp --scope 'write:path:services/billing/src/**' \ --evidence failing-test:services/billing/src/a.test.ts --reason "…" \ --token "$TOKEN" --key-public root.pub --key root.key [--pull 42] [--wait 600] onus cloud blocked --kind broken-test --evidence failing-test:services/orders/src/eta.test.ts \ --reason "the test expects UTC; the fixture is local time" --task sms-otp onus cloud escalation [--wait 600] # where it stands onus cloud escalation list [--waiting] onus cloud escalation withdraw ``` `--repo ` names the worktree whose `.onus/escalations` keeps the request (default: this folder). - **The facts are the binary's.** The request is built here, with its blast radius, sensitive labels and owners from the map, and kept in `.onus/escalations/.json`. Onus Cloud then works them out again on the repository's **default branch** (`onus escalate`, `onus escalation assess`), so a branch that edits onus.yaml cannot weaken its own request. Low-risk requests with a reproduced failing test are granted by policy; the rest wait for an owner of the code involved, or an admin. - **The token stays here.** Onus Cloud is sent what the task token holds (task, rights, end), never the token. On a grant, the door that holds the root key (`--key`, or `onus mcp --key`) mints the new token itself: the old rights plus exactly the scopes of the request it kept, never anything the answer names. An answer about another request is refused, and so is a request file changed after it was filed, or a grant of other scopes than the file asks for: nothing is minted. A token kept beside a request is handed out only when it proves that grant. Without the key, the answer says who granted it and the command the operator runs (`onus escalation grant .onus/escalations/.json --id --scope '' --by 'Ada (Onus Cloud)' …`). It names the request and the scopes Onus Cloud granted, so a request file the agent swapped since mints nothing. The token is picked up once it is kept beside the request. - **The audit log.** Filing writes `sent-to-onus-cloud`; a person's grant, denial or answer is written as theirs (`Ada (Onus Cloud)`), a grant by policy as the policy's (`system:policy (Onus Cloud)`), and a withdrawal from the command line at once. `onus ui` shows which requests went to Onus Cloud. - **Exit codes.** 0 when granted (the token alone on stdout) or answered, 1 when denied, withdrawn, expired or failed, 3 while it is being checked or waits for a person. A blocked report asks for no rights. The person answers it (the agent sees `answered`, with the text) or denies it (the task should stop). ## For agents When the CLI is signed in (with `onus login`, `ONUS_CLOUD_TOKEN` or `ONUS_API_KEY`), `onus mcp` adds tools for Onus Cloud: - `onus_cloud_status`: who the CLI is, the organisation, the token's scopes, what the plan allows. - `onus_cloud_env_create`, `onus_cloud_env_run`, `onus_cloud_envs`, `onus_cloud_env_destroy`: hosted environments; the repository and commit default to the agent's worktree and its pushed HEAD. - `onus_cloud_attach_evidence`: a recorded run to a pull request, for the judge; with `doorLog: true`, the task's slice of the door log, which the server cuts from its own log (the agent names only the pull request). - `onus_cloud_pulls`, `onus_cloud_pull`: governed pull requests, their lanes and evidence, read-only. - `onus_cloud_map`: a question to the map Onus Cloud keeps, at a commit or a pull request's head, answered by the CLI; under a task token, filtered to what the task may read, as the map tools are. - `onus_cloud_token`: a narrower token for a sub-agent, never wider than the server's. `onus_escalate`, `onus_escalation` and `onus_blocked` go to Onus Cloud by themselves when the CLI is signed in (see above). **The login never reaches the agent's calls.** Signed in with `onus login`, `onus mcp` mints a scoped token from the login on the first Onus Cloud call and uses it for every call after: the organisation the CLI acts in, the worktree's repository, all six scopes, 8 hours (`--cloud-ttl`, at most 24h), labelled with the task of `--token`. It mints a new one shortly before that one ends. The worktree's repository is its GitHub remote; name it with `--cloud-repo owner/name` (or `ONUS_CLOUD_REPO`) when there is none. With `--cloud-token` (or `ONUS_CLOUD_TOKEN`) it uses that token as it is, and with `ONUS_API_KEY` the key. Signed out, the tools are not listed. Nothing an agent does through Onus Cloud approves a change. --- ## Commands URL: https://onushq.com/docs/commands Every command and flag, with examples and exit codes # Commands Every command prints to stdout; errors and warnings go to stderr. Run `onus help ` for the flags of one command. ## onus map Builds the map of a directory and prints a summary. ```sh onus map . onus map . --json > map.json onus map . --config other/onus.yaml ``` - `--json`: print the whole map (format: [schemas/codebase-map.schema.json](https://github.com/onushq/onus/blob/main/schemas/codebase-map.schema.json)). - `--config `: use this onus.yaml instead of `/onus.yaml`. - `--scip `: import a SCIP index of the directory (repeatable). Runs nothing. - `--traces `: add the calls in OpenTelemetry traces (OTLP JSON) as `traced` edges (repeatable). See [`onus help environments`](/docs/environments). - `--coverage `: add line coverage of the directory (lcov, Cobertura XML or Istanbul JSON), measured at the same version of its files (repeatable). See [`onus help environments`](/docs/environments). - `--plugins `, `--trusted`, `--allow-unsandboxed`: see "Plugins" below. ## onus diff Reports the changes in meaning between two directories. ```sh onus diff old new onus diff old new --format json > report.json onus diff old new --intent pr-body.md --fail-on rule-violation,secrets ``` - `--format md|json`: Markdown (default) or the full JSON report (format: [schemas/semantic-report.schema.json](https://github.com/onushq/onus/blob/main/schemas/semantic-report.schema.json)). - `--markdown-out `: also write the Markdown report to a file, so one run gives both formats. - `--intent `: check the change against a stated intent: a YAML file, or Markdown (such as a pull request body) containing an `onus-intent` block. See [`onus help intent`](/docs/intent). - `--config `: the onus.yaml used for both trees. By default Onus uses the base tree's onus.yaml for both, so a change cannot relax the rules it is checked against. - `--fail-on `: exit with code 2 when `rule-violation` (a new boundary-rule violation) or `secrets` (a committed secret) is found. Repeat the flag or separate values with commas. `none` never fails. - `--base-scip `, `--head-scip `: import SCIP indexes of each tree (repeatable). - `--traces `: add the calls in OpenTelemetry traces to both trees (repeatable), so they are facts of both and never changes. - `--coverage `: line coverage of the head tree (lcov, Cobertura XML or Istanbul JSON), from tests run at the head (repeatable). The report then says which changed lines tests ran; see [`onus help reports`](/docs/reports). - `--plugins `, `--trusted`, `--allow-unsandboxed`: see "Plugins" below. ## onus report --base --head The same report for two git refs. Each ref is extracted with `git archive` into a temporary directory that is removed afterwards; the repository itself is never modified. Only the paths git reports as different are compared line by line, and the base tree takes the head's copy of every other file. ```sh onus report --base main --head HEAD onus report --repo ../shop --base origin/main --head feature/sms --format json ``` - `--repo `: the repository (default: the current directory). - `--format`, `--markdown-out`, `--intent`, `--config`, `--fail-on`, `--traces`, `--coverage`, `--plugins`, `--trusted`, `--allow-unsandboxed`: as for `onus diff`. SCIP indexes for `report` come from `scip` plugins, which index each ref in trusted mode. - `--evidence `: a test run from `onus env run` (repeatable). The coverage it recorded is used when its manifest is intact and it ran at the head commit; otherwise a note says why not. - `--cache-dir `: keep the base ref's map in this folder and reuse it on the next report against the same base commit. The report is the same either way; see [`onus help ci`](/docs/ci). - `--head-map-out `: also write the head ref's map ([schemas/codebase-map.schema.json](https://github.com/onushq/onus/blob/main/schemas/codebase-map.schema.json)) to a file. It is the map the report was computed against, so `onus query --map ` answers questions about the head with no second build. It holds the traces and coverage the report was given, so two reports of one commit with different evidence write different maps. Any ref git understands works: branches, tags, `HEAD~3`, commit hashes. In JSON, `base` and `head` read like `main (abc1234)`. ## Plugins - `--plugins `: a plugins file listing language, framework, SCIP and LSP plugins (default: the file named by `ONUS_PLUGINS`). It is never read from the analyzed repository. - `--trusted`: allow plugins that may run code from the repository (indexers, language servers). They run in a sandbox with no network. - `--allow-unsandboxed`: with `--trusted`, allow them even where no sandbox is available. See [`onus help plugins`](/docs/plugins). ## onus init [dir] Writes a starter onus.yaml inferred from workspaces and CODEOWNERS. Sensitivity labels are only suggested, as comments, until you confirm them. ```sh onus init onus init path/to/repo --stdout # print instead of writing onus init --force # overwrite an existing onus.yaml ``` ## onus schema Writes the JSON Schemas of the map, the report, onus.yaml, the plugins file and protocol, the provenance record (`onus provenance`) and what it reads (escalation decisions, Onus Cloud's calls, a door log slice), production signals and rollout metrics, held-out suites, and the map overview (`onus query overview`). ```sh onus schema --out schemas onus schema # print them all ``` ## onus mcp Serves the map of the current worktree to a coding agent as MCP tools, on stdin and stdout. See [`onus help agents`](/docs/agents) for the tools and how many agents share one map server. ```text claude mcp add onus -- onus mcp ``` - `--repo `: the worktree (default: the one containing the current directory). - `--no-server`: build the map in this process instead of the repository's shared map server. - `--token ` and `--key-public `: the task token every call is checked against; `--key `: the root private key, with which the server mints grants itself; `--trusted-repo `: a checkout whose files the agent cannot edit, from which the policy decides escalations when signed out; `--audit `: the log; `--no-actions`: only the map tools. - `--cloud-token`, `--cloud-repo`, `--cloud-ttl`: how the server signs in to Onus Cloud ([`onus help cloud`](/docs/cloud)). ## onus query The same questions as the MCP tools, from a shell, printed as JSON: `status`, `find `, `symbol `, `dependents `, `dependencies `, `tests-for `, `impact [--change ]`, `invariants [target]`, `owners `, `component `, `file `, `overview`, `check [--base ]` and `stats`. ```sh onus query find format phone onus query dependents UserPreferences --depth 2 --limit 20 onus query overview onus query --map map.json dependents money ``` - `--repo ` and `--no-server`: as for `onus mcp`. - `--depth ` (1 to 8) and `--limit ` (default 50, at most 500) where they apply. - `--map `: answer from a map JSON file instead of a worktree: one from `onus map --json` or `onus report --head-map-out`, or a map downloaded from Onus Cloud. `-` reads stdin, so `curl … | onus query --map - dependents money` works in scripts. A map of another schema version is refused. No map server is started, `check` and `stats` need a worktree, and `impact --change add-required-member` lists every file that names the type, since there is no source to read. - `overview`: every component with its kind, roots, owners, labels, package, files, lines and first 50 public symbols, the components it uses and that use it (with edge counts), its external services and events; then the links between components, the events, the declared contracts and the rules ([schemas/map-overview.schema.json](https://github.com/onushq/onus/blob/main/schemas/map-overview.schema.json)). It is what `onus ui` draws and what Onus Cloud shows for a map it keeps. ## onus ui Opens the web interface for the repository: everything below and above, in a browser, served on 127.0.0.1. See [`onus help web-interface`](/docs/web-interface). ```sh onus ui onus ui --repo ../shop --port 0 --no-open ``` - `--repo `, `--port ` (default 4387; 0 picks one), `--no-open`. - `--outcomes `, `--audit `, `--escalations `, `--keys `: where outcome records, the audit log, escalation requests and the root key pair are kept. ## Scopes, lanes and environments - `onus token`, `onus scope`, `onus gateway`, `onus escalate`, `onus escalation`, `onus audit`, `onus run-test`: task tokens, the git gateway, escalation and the audit log. See [`onus help scopes`](/docs/scopes). - `onus submit`, `onus classify`, `onus judge`, `onus outcomes`: risk lanes, the judge and outcome records. `onus outcomes audits` lists the merged changes sampled for a person to audit and the audit log; `onus outcomes audit` records an audit. `onus lanes train` fits the learned lane model to outcome records; it may only raise lanes. See [`onus help lanes`](/docs/lanes). - `onus env`, `onus evidence`: environments and the evidence store. See [`onus help environments`](/docs/environments). - `onus signals ingest|traces|areas`: production signals kept with the records, production traces for the map, and each component's history. See [`onus help production`](/docs/production). ## onus ci What the GitHub Action runs to keep outcome records on a branch ([`onus help ci`](/docs/ci)); usable from any CI system. ```sh onus ci records pull --dir records # the onus/records branch into a folder onus ci owners --base "$BASE" --report report.json # who owns it, as the base commit says onus ci pr --base "$BASE" --head "$HEAD" --change acme/shop#7 --branch codex/sms --author mike \ --body-file body.md --report report.json --records records --out out onus ci comment --change acme/shop#7 --body-file comment.md --author lead --association MEMBER --records records onus ci closed --change acme/shop#7 --merged --commit "$SHA" --merged-by mike --records records onus ci push --records records --since "$BEFORE" onus ci rollout --commit "$SHA" --percent 25 --metrics canary.json --records records onus ci rollback --change acme/shop#7 --reason "checkout 500s" --records records onus ci records push --dir records --message "acme/shop#7 closed" ``` - `ci pr` submits, classifies and (with `--judge`) judges the change, appends to `pulls.jsonl`, writes `submission.json`, `classification.json`, `judgment.json`, `provenance.json`, `rollout.json` and the comment section `lane.md` to `--out`, and prints `{lane, verdict, agent, record, review, rollout, risk}` (`record.recentRollbacks` counts its agent setup's recent rollbacks; `rollout` is the plan's strategy, steps, bake time and whether it rolls back automatically; `risk` is the learned model's, or null). `--evidence ` adds test runs; `--reviews ` gives the pull request's GitHub reviews, for the human lane's review ([`onus help ci`](/docs/ci)); `--model ` uses that learned lane model instead of the one at the base commit (it still needs `lanes.learned`); `--escalations ` and `--token-inspection ` give the escalations and the door's inspection of the task token that Onus Cloud keeps, for the provenance record (`onus help provenance`); `--calls ` Onus Cloud's log of the calls made under the task token; `--audit ` the task's door log, or the slice of it the agent attached in Onus Cloud, whose hash chain is checked; `--submission ` the agent's own submission, whose token and escalations the record keeps when it is for one of the pull request's commits; `--task ` the task whose door log entries and calls to keep. They never stop the run: what cannot be read is a warning and a gap in the record. Only a token given instead of its inspection is refused. An escalation with no pull request is joined by `--branch` and the change's commits. - `ci owners` prints `{owners}`: the owners `ci pr` asks to review and, with `lanes.approval: owner`, the people whose approval counts, read at the base commit (onus.yaml, then CODEOWNERS), so a host can look up owning teams' members for `--reviews`. - `ci comment` acts on `/onus approve `, `/onus audit ok|miss [note]` and `/onus incident [involved: a, b]` from OWNER, MEMBER or COLLABORATOR, and prints `{command, ok, reply, rerun}`. It also answers `/onus caught ` (from anyone) and `/onus accuracy ok|wrong |missed ` (write access) with what was said, `verdict`, `row`, `note` and `by`. Accuracy is never recorded. `caught` from OWNER, MEMBER or COLLABORATOR is recorded as `caught: true` on the change's outcome, or on its pull record while it is open, and its answer says so with `recorded`. `--report ` (the report's JSON) checks `wrong ` against the report's rows and turns a row number into its id. - `ci closed` turns the pull request's latest record into an outcome, and samples a change approved without a person for an audit ([`onus help lanes`](/docs/lanes)). `--merged-by ` keeps who merged it on the outcome, as `@login`, for its provenance. `ci push` records reverts. - `ci rollout` (`--change` or `--commit`) prints the change's rollout plan (for a change recorded before plans, from `lanes.rollout` in `--repo` or `--config`); with `--percent` (and `--metrics`, the stage's samples) it decides `promote`, `hold`, `rollback` or `complete`, records the stage, records a `rolled-back` outcome on `rollback`, and exits 0 for promote or complete, 2 for rollback and 3 for hold. `ci rollback` records a rollback decided elsewhere; `--planned` marks one done on purpose, which counts against no one. See [`onus help production`](/docs/production). - `ci records` takes `--remote` (default `origin`) and `--branch` (default `onus/records`). ## onus provenance One record of why a change was allowed through: the intent, the agent and model, the token it held, the evidence, any escalations, the lane and the judge's steps, who or what approved it, and how it rolled out (`onus help provenance`, [schemas/provenance.schema.json](https://github.com/onushq/onus/blob/main/schemas/provenance.schema.json)). ```sh onus provenance --change acme/shop#7 --records records --from out/provenance.json --out p.json onus provenance --submission-id 4fa91c onus provenance --verify p.json ``` - `--change `: the change, as the records name it. Default: the change of `--from`, else the submission's head commit. - `--records ` (or `--outcomes `): the pull and outcome records and production signals, for the stage, approver and rollout. `--signals ` reads the signals from another file. - `--from `: an earlier record of the change; its facts from while the change was open are kept. - `--submission ` or `--submission-id `, `--classification `, `--judgment `: what was handed in and decided. `--submission-id` reads `.onus/submissions` of `--repo`, with the judgment, audit log, escalations and outcomes beside it. - `--token ` with `--key-public `: a task token, verified (an error when it does not verify) and never kept; `--audit `, `--escalations ` and `--task `: the door log, escalation requests, and whose door decisions to include. `--audit` also takes the task's slice of the door log that the agent attached in Onus Cloud ([schemas/door-log.schema.json](https://github.com/onushq/onus/blob/main/schemas/door-log.schema.json)); its chain is checked from its first entry. - `--calls `: Onus Cloud's log of the calls made under the task token ([schemas/cloud-calls.schema.json](https://github.com/onushq/onus/blob/main/schemas/cloud-calls.schema.json)), summed up beside the door log as `doors.calls`, on Onus Cloud's word. - `--agent-submission `: the agent's own submission of a pull request, as `onus ci pr --submission` takes it: only its token and escalations, only when it is for one of the change's commits, and a warning when it cannot be read. The action passes it when the pull request closes. - `--escalations `: escalations as Onus Cloud keeps them, with their decisions ([schemas/escalation-decisions.schema.json](https://github.com/onushq/onus/blob/main/schemas/escalation-decisions.schema.json)); those for another repository or pull request, and those that neither name the change (its pull request, a commit in `--repo`, the submission or `--from`) nor share its task, are left out. One that cannot be read is left out with a warning and a gap. With `--submission-id`, the kept requests are read too. A path that does not exist is an error. `--token-inspection `: the door's inspection of the task token (`{task, rights, expires}`), for `--task` when it is given. Never a token itself. - `--out `: write it instead of printing it. `--verify `: check a record's digest and schema version; exit 1 when they do not match. ## onus login, onus logout, onus cloud Onus Cloud from the terminal ([`onus help cloud`](/docs/cloud)). `onus login` signs the CLI in as you (approve the code it shows in the browser); in CI, `ONUS_API_KEY` stands in for it, and a scoped token in `ONUS_CLOUD_TOKEN` stands in for both. ```sh onus login [--org acme] [--no-browser] onus cloud status onus cloud env create [--repo acme/shop] [--ref pull/42] [--name sms] [--host registry.npmjs.org] onus cloud env run sms -- npm test onus cloud evidence acme/shop#42 --run onus cloud evidence acme/shop#42 --door-log --task sms onus cloud pulls --attention onus cloud token create --scope pulls:read [--repo acme/shop | --all-repos] [--ttl 2h] [--label sms] onus cloud map dependents UserPreferences [--repo acme/shop] [--commit | --pull 42] onus cloud escalate --task sms --scope 'write:path:services/billing/**' --evidence … --reason … onus cloud blocked --kind broken-test --evidence failing-test:src/a.test.ts --reason … --task sms onus cloud escalation [--wait 600] onus logout ``` - `cloud env create|run|list|show|log|destroy`: hosted environments; `create` and `run` wait unless `--no-wait`, and `run` exits 1 when the command failed. `--json` prints the API's answer. - `cloud pulls`, `cloud pull `: governed pull requests, their lanes, analyses and evidence. - `cloud evidence --run `: a recorded run to a pull request, for the judge. `--door-log --task ` attaches the task's slice of the door log (`--audit `, default `.onus/audit.jsonl` in the worktree), for the change's provenance, only when the pull request's head in Onus Cloud is the worktree's commit. - `cloud token create|list|revoke`: scoped tokens; `create` prints the token alone on stdout. - `cloud map | --download `: `onus query`'s questions asked of the map Onus Cloud keeps, answered here. - `cloud escalate`, `cloud blocked`, `cloud escalation |list|withdraw`: escalations and blocked reports in Onus Cloud. `--token`, `--key-public` and `--key` are the door's; with `--key` a grant is minted here. Exit 0 when granted or answered, 1 when denied, withdrawn, expired or failed, 3 while waiting. - `--org ` on any `cloud` command, or `ONUS_CLOUD_ORG`, picks the organisation; `ONUS_CLOUD_URL` the address. ## onus help [command | topic] ```sh onus help # commands and guide topics onus help diff # the flags of one command onus help configuration # a guide topic ``` ## Exit codes | Code | Meaning | |---|---| | 0 | Success, including reports that contain findings | | 1 | A usage error (unknown flag, missing argument) or a runtime error (unreadable directory, unknown git ref, invalid onus.yaml) | | 2 | A `--fail-on` condition was met | Exit code 2 is never used for errors, so CI can tell "Onus found something" from "Onus could not run". --- ## Configuration URL: https://onushq.com/docs/configuration The onus.yaml reference: components, rules, labels, extractors # Configuration: onus.yaml Onus works without configuration. An `onus.yaml` at the repository root adds what Onus cannot infer: which components are sensitive, which boundaries must hold, how your events are published, and SDKs it does not know. `onus init` writes a starter file; the format is in [schemas/onus-config.schema.json](https://github.com/onushq/onus/blob/main/schemas/onus-config.schema.json). `onus ui` edits it with a form: every edit is checked before it is saved, a save never overwrites a change made to the file meanwhile, and the map is rebuilt with the new config at once (as it is for any edit to the file: the map server follows onus.yaml like any other file). ## A complete example ```yaml version: 1 components: orders: { path: "services/orders/**", owners: ["@team-orders"] } notifications: { path: "services/notifications/**", owners: ["@team-growth"] } user-preferences: { path: "services/user-preferences/**", labels: [pii] } billing: { path: "services/billing/**", labels: [payments] } contracts: UserPreferences: symbol: "user-preferences:src/types.ts#UserPreferences" invariants: ["phone numbers are stored in E.164"] rules: - deny: { from: billing, to: notifications.internal } labels: payments: { sensitivity: high } pii: { sensitivity: high } extractors: events: publish: ["bus.publish($EVENT, ...)"] subscribe: ["bus.subscribe($EVENT, ...)"] externals: "@acme/sms": { vendor: "Acme SMS", category: sms, egress: [phone] } prisma: clients: [prisma, db] tests: globs: ["**/*.test.ts", "e2e/**"] command: "npx vitest run {test}" coverage: ["coverage/lcov.info"] testData: ["fixtures/**"] ``` Unknown keys are an error, so a typo never silently does nothing. ## Which onus.yaml a report uses `onus diff` and `onus report` build both versions with the **base** version's onus.yaml (or `--config`). A pull request therefore cannot relax the rules it is checked against; if it changes onus.yaml, that change is itself a row needing a person. ## version Must be `1`. ## components A map from component id to its settings. Ids may not be empty or contain `:`, `#`, `/` or spaces. - `path` (required): a glob, or a list of globs, of the component's files, such as `"services/orders/**"`. When several components match a file, the most specific one wins. - `kind`: `service`, `package` or `module`. Default: `service` under services/ or apps/, `package` under packages/ or libs/, otherwise `module`. - `owners`: default: the last matching rule in CODEOWNERS (looked up in .github/, the root, then docs/). - `labels`: sensitivity labels, such as `payments`, `auth`, `pii` or your own. - `entrypoints`: source files that define the public surface, relative to the component folder. Default: from package.json `exports`, `types`, `module` or `main`, mapped from build output back to source (`dist/index.js` → `src/index.ts`), else `src/index.*` or `index.*`. Without a `components` section, Onus finds components itself: - Cargo packages (`Cargo.toml` with a `[package]`) and Python projects (`pyproject.toml` with a project name) are components named after the package. - In an Nx repository (an `nx.json` at the root), every folder with a `project.json` is a component, named after the project. `projectType: application` is a service and `library` a package. - npm, pnpm or yarn workspace packages are components too; where a workspace package contains Nx projects, each file belongs to the most specific one. - Failing both, every folder under services/, packages/ and apps/. Files outside every component belong to `root`. Only symbols reachable from an entrypoint (through exports and re-exports) are public contracts. A component with no entrypoint has no public surface; for libraries Onus notes it in the map, while applications are expected to have none. ## contracts Names important contracts and the invariants people rely on. `symbol` is a map id, `:#`; list all ids with `onus map . --json` and look under `symbols`. The invariants are repeated on any row that changes the contract. An id that matches nothing is noted in the map. ## rules Boundary rules, evaluated on the new version of the code. Only new violations are reported. ```yaml rules: - deny: { from: billing, to: notifications } # billing may not use notifications at all - deny: { from: billing, to: notifications.internal } # only its public surface ``` A reference is an import, a call or a type reference. When onus.yaml declares components, both sides must be among them, and `from` and `to` must differ. Use `--fail-on rule-violation` to fail CI on a new violation. ## labels The sensitivity of each label: `low`, `medium` or `high`. Labels of medium or high sensitivity make a component sensitive: new data writes and notable edits there become security-sensitive. `payments`, `auth` and `pii` are high by default; set one to `low` to turn that off. ## extractors.events Call patterns that publish and subscribe to events. `$EVENT` marks the argument holding the event name; `...` stands for the rest. ```yaml events: publish: ["bus.publish($EVENT, ...)", "emitter.emit($EVENT, ...)"] subscribe: ["bus.subscribe($EVENT, ...)", "@OnEvent($EVENT)"] ``` A pattern matches the call itself and any call that ends with it, so `bus.publish` also matches `this.bus.publish` and `app.bus.publish`. Patterns starting with `@` match decorators. Declaring patterns replaces the defaults: ```text publish: bus.publish($EVENT, ...), eventBus.publish($EVENT, ...), events.publish($EVENT, ...) subscribe: bus.subscribe($EVENT, ...), eventBus.subscribe($EVENT, ...), events.subscribe($EVENT, ...), @OnEvent($EVENT) ``` Event names are read from string literals, constants (also imported ones) and enum members. A name computed at runtime is recorded with low confidence and noted in the map. ## extractors.externals External services, keyed by the npm package of their SDK. They extend, and for the same package override, the built-in registry. ```yaml externals: "@acme/sms": { vendor: "Acme SMS", category: sms, egress: [phone], hosts: [api.acme-sms.example] } ``` - `vendor`: the name shown in reports. - `category`: `sms`, `email`, `payments`, `ai`, `analytics`, `monitoring`, `storage`, `chat`, `http` or your own. - `egress`: kinds of data that leave the system through it: `phone`, `email`, `payment`, `bank-account`, `pii`, `text`, `files`, `errors` or your own. A component sending a new kind is security-sensitive. - `hosts`: hostnames, so `fetch("https://api.acme-sms.example/...")` and `axios` calls count as calls to this vendor. Calls to other literal hosts are reported under the host name. Built-in registry: | Package | Vendor | Category | Egress | |---|---|---|---| | twilio | Twilio | sms | phone | | stripe | Stripe | payments | payment | | @sendgrid/mail, @sendgrid/client | SendGrid | email | email | | nodemailer | SMTP email | email | email | | mailgun.js | Mailgun | email | email | | postmark | Postmark | email | email | | @aws-sdk/client-ses | AWS SES | email | email | | @aws-sdk/client-sns | AWS SNS | sms | phone | | @aws-sdk/client-s3 | AWS S3 | storage | files | | @slack/web-api | Slack | chat | text | | openai | OpenAI | ai | text | | @anthropic-ai/sdk | Anthropic | ai | text | | @segment/analytics-node | Segment | analytics | pii | | posthog-node | PostHog | analytics | pii | | @sentry/node | Sentry | monitoring | errors | | plaid | Plaid | payments | bank-account | | braintree | Braintree | payments | payment | An SDK counts as called when a function calls something imported from it, including through a class field declared with the SDK's type (`private readonly client: AcmeSmsClient` and then `this.client.send(...)`). ## extractors.packs Framework packs: YAML files of tree-sitter queries that say what a pattern in the code means (routes, events, data access, other relationships), relative to onus.yaml. See [`onus help plugins`](/docs/plugins). ```yaml extractors: packs: [onus/packs/nestjs.yaml] ``` Packs are read from the base version of a change, like this file. ## extractors.prisma `clients`: identifiers that hold a Prisma client (default: `prisma`). Calls such as `prisma.payment.update(...)` become writes to `payment`; `findMany`, `count` and the other read operations become reads. Raw queries (`$queryRaw`, `$executeRaw`) are noted in the map, since their tables are unknown. ## tests `globs`: which files are tests. Declaring globs replaces the defaults: ```text **/*.test.* **/*.spec.* **/__tests__/** **/test/** **/tests/** **/e2e/** ``` `command`: how to run one test file, with `{test}` replaced by its path, such as `npx vitest run {test}`. `onus escalation assess` uses it to reproduce the failing test an escalation names, in a container built from `environment:` ([`onus help scopes`](/docs/scopes)). Without it, failing tests are not reproduced, so no escalation is granted by policy. A command without `{test}` would run every test, so it reproduces nothing either. `coverage`: coverage files of the working tree (lcov, Cobertura XML or Istanbul JSON), relative to the repository root, for the map agents query (`onus mcp`, `onus query`). Each rebuild reads the ones that exist, and leaves out any source file edited after the coverage file was written, with a note. With them, `onus_tests_for` says which tests ran a target, and finds tests that only reach it through other code ([`onus help agents`](/docs/agents)). Reports read coverage only from `--coverage` and `--evidence` ([`onus help environments`](/docs/environments)). ## testData Globs of files that are test data rather than code: fixture projects, sample apps, recorded responses. ```yaml testData: - fixtures/** - e2e/recordings/** ``` Test data is left out of the map, so a fixture app's imports, services and contracts do not show up as your code. Changes to it are summarized in one "changed lines of test data" row, and config files inside it (a fixture's package.json or tsconfig.json) are not reported as config changes. Secret-shaped values added in test data are not counted as committed secrets: `fail-on: secrets` does not trip on them, and they appear as one row, "... added in test data", that names the files without the values. Declare only folders that hold fake credentials on purpose. As with every setting, the report uses the base commit's onus.yaml, so a pull request cannot declare its own new secret as test data. ## Files Onus never reads Folders named .git, .onus (where Onus keeps its records), node_modules, dist, build, out, .next, .turbo, .cache, coverage, target or vendor; anything ignored by a .gitignore inside the tree; files over 2 MB; and *.min.js files. ## lanes What happens to a change after it is reported: the default lane, rules that set or raise it, the built-in risk inputs, a learned model that may raise it, the shares of automatic merges a person audits before and after merging, the record an agent setup needs before it may auto-merge, whose review lets a human-lane change through, an optional taste reviewer and the configurations the judge rotates between, and how each lane's changes meet production traffic. See [`onus help lanes`](/docs/lanes) and [`onus help production`](/docs/production). ```yaml lanes: default: judge # auto-merge, judge, human or blocked auditRate: 0.05 # would-be auto-merges sent to a person before merging auditAfterMerge: 0.05 # changes approved without a person, audited after they merge minRecord: 10 approval: owner # whose review counts in the human lane: owner (default with owners) or member rules: - { lane: auto-merge, match: every, kinds: [internal], components: [docs] } - { lane: human, subkinds: [migration-changed] } judges: # one judges each change; optional - name: default # uses lanes.taste - name: strict taste: { command: [./scripts/review-strict.sh] } heldOutLimit: 20 # at most this many held-out checks inputs: # each key optional; these are the defaults blastRadius: { judge: 3 } # other components that depend on a row tests: { untested: judge } # code no test or trace reaches confidence: { low: judge } # rows on parts of the map Onus could not resolve history: { days: 90, reverts: { judge: 1 }, incidents: { human: 1 } } coverage: # off unless written minChanged: 0.8 required: false commands: ["npm test -- --coverage"] # whose recorded coverage counts learned: # off unless written model: onus.lanes-model.json # from onus lanes train, read at the base commit judge: 0.6 # raise to the judge at or above this risk human: 0.85 # raise to a person at or above this risk rollout: # each lane optional; built-in plans otherwise judge: strategy: canary # none, canary or flag steps: [5, 25, 100] # % of traffic bakeMinutes: 30 guards: - { metric: error_rate, maxRatio: 1.5 } - { metric: checkout_conversion, minRatio: 0.97 } ``` Inputs only raise a lane. A threshold of N means N or more, and the highest lane whose threshold is met applies (`judge`, `human`, `blocked`). Writing a key replaces its default as a whole: `blastRadius: { human: 10 }` has no judge threshold, and `blastRadius: {}` turns the input off. A lane of `auto-merge` turns `tests` or `confidence` off. Thresholds are at least 1 and must not go down as the lane goes up; `history.days` is 1 to 3650. `coverage` reads the report's coverage of the head ([`onus help reports`](/docs/reports)): when tests ran less than `minChanged` (a share from 0 to 1) of the changed executable lines, the change goes to the judge. So does a change to a file with runtime code that the coverage leaves out (`notMeasured`): coverage of other files cannot let it through. With `required: true`, a change without coverage goes to the judge too. A change with no changed executable lines, and no changed file the coverage leaves out, passes. Coverage given with `--coverage` counts. Coverage an environment recorded counts only when its run's command is one of `commands`, written exactly as `onus env run` ran it: an agent picks the commands it runs, and one could write a coverage file. When the report holds recorded coverage from another command, the change goes to the judge, whatever `required` says: an extra run must not hide an honest one. The input runs only while the change would auto-merge. Held-out checks live outside the repository, in a suite given with `--held-out` ([`onus help lanes`](/docs/lanes)). `heldOut: { command, image, setup }` still works as one check, but agents can read onus.yaml. `judges` names must be unique, without `@` or spaces; the judge picks one per change, and an empty list turns rotation off. The learned model also only raises a lane, and never to `blocked`. Its thresholds need `0 < judge <= human <= 1`, and the model file's name must end in `lanes-model.json`, so a change to it is a rule of the game and goes to a person. `rollout` sets the plan of `auto-merge`, `judge` and `human` changes; a lane left out keeps its built-in plan ([`onus help production`](/docs/production) has the table), and `blocked` has none. Steps go up strictly from 1 to 100 and end at 100, and `none` takes no steps. A guard names a `metric` and at least one of `max`, `min` (on the value), `maxRatio` and `minRatio` (on the value over its baseline). ## environment The container `onus env create` builds from a commit: its image (default: the devcontainer's), a setup command run once per lockfile and kept as a warm image, a seed command, and the files a run writes that are kept as evidence. See [`onus help environments`](/docs/environments). ```yaml environment: image: node:22-alpine setup: npm ci seed: node scripts/seed-test-data.mjs evidence: ["reports/**/*.xml"] # JUnit XML traces: ["otel/*.json"] # OTLP JSON coverage: ["coverage/lcov.info"] # lcov, Cobertura XML or Istanbul JSON lockfiles: [package-lock.json] # default: the usual lockfiles egressImage: ubuntu/squid # the proxy for hosts a token names behavior: # onus report --behavior command: "npx vitest run --reporter=junit --outputFile=reports/onus-behavior.xml {files}" junit: ["reports/onus-behavior*.xml"] budget: 600 maxFiles: 40 depth: 1 ``` `behavior` is the behavior diff (`onus report --behavior`): `command` runs the selected test files (`{files}`, required) and writes JUnit XML to a file `junit` matches; `budget` is seconds for the whole diff, `maxFiles` the most test files run, and `depth` the code-dependency hops (0 to 2) from changed code to the tests chosen. It is read from the base commit like the rest of the config. See [`onus help environments`](/docs/environments). --- ## Environments and evidence URL: https://onushq.com/docs/environments Environments built from a commit, the evidence store, traces and coverage into the map, and the behavior diff # Environments and evidence An environment is a container built from your repository at a commit. Commands run in it leave evidence (their output, test results, traces and coverage) in a content-addressed store, so a reviewer, or the judge, relies on what an environment recorded instead of what an agent says it ran. Environments need Docker or Podman. ## Declaring the environment In onus.yaml: ```yaml environment: image: node:22-alpine setup: npm ci # with network, once per lockfile seed: node scripts/seed-test-data.mjs # synthetic data, in each new environment evidence: ["reports/**/*.xml"] # JUnit XML the tests write traces: ["otel/*.json"] # OTLP JSON trace files coverage: ["coverage/lcov.info"] # lcov, Cobertura XML or Istanbul JSON ``` Without an `image`, Onus uses `.devcontainer/devcontainer.json`: its `image`, and its `onCreateCommand`, `updateContentCommand` and `postCreateCommand` as the setup. A devcontainer that builds from a Dockerfile needs the built image named in `environment.image`. The declaration comes from your working tree (or `--config`), never from the commit under test: a change cannot give itself a different environment. ## Creating one ```sh onus env create --ref feature/sms --name sms onus env create --ref HEAD --token "$TOKEN" --key-public root.pub # named after the token's task ``` 1. **Warm image.** `setup` runs once per image, setup command and lockfile contents, and the result is kept as `onus-warm:`. The next environment with the same lockfiles starts from it in about a second. 2. **The commit's files** are copied in. Files of the commit the warm image was built from are removed first, so deletions take effect. 3. **Seed.** `seed` runs, so tests get synthetic data, never production data. The container has no capabilities, 4 GB of memory, 2 CPUs and at most 1024 processes. **Network:** none. If the task's token names hosts (`net:host:api.example.com`, or `*.example.com` for subdomains), the environment sits on an internal network whose only way out is an egress proxy that allows exactly those hosts (`ubuntu/squid` by default; `environment.egressImage` to change it). `HTTP_PROXY` and `HTTPS_PROXY` point at it. **Secrets:** each `secret:NAME` in the token is set as `NAME` from your `ONUS_SECRET_NAME` variable; creating fails if it is not set. No other secret reaches the container, and secret values are replaced by `[secret NAME]` in everything the store keeps. ## Running commands ```sh onus env run sms -- npm test onus env run sms -- npx vitest run --reporter=junit --outputFile=reports/unit.xml ``` Each run prints a test run naming its manifest, and exits 1 when the command failed. The output is ready for `onus submit --evidence`: ```sh onus env run sms -- npm test > run.json onus submit --base main --head feature/sms --evidence run.json --agent-tool claude-code --out submission.json ``` The judge checks every test run that names a manifest against the store: the manifest must be there, unaltered, from the head commit, with the same command and exit code. It still re-runs the command in a fresh container with the environment's setup and seed. ```sh onus env list # name, commit, state, image, hosts onus env destroy sms # the container, its proxy and its network ``` ## The evidence store Runs are kept in the repository's git directory (`.git/onus/evidence`, shared by its worktrees; `--store` for another place): ```sh onus evidence list # id, commit, environment, exit code, command, test counts onus evidence show 3fa2c1d9 # a manifest onus evidence show 3fa2c1d9 --artifact - # its output log onus evidence show 3fa2c1d9 --artifact otel/trace.json > trace.json ``` A manifest records the commit, the command, the exit code, start and finish times, the environment (image, warm image, setup, seed, hosts, secret names) and its artifacts with their sha256: the log, the JUnit files the run wrote that match `evidence` (with test counts summed), the trace files that match `traces`, and the coverage files that match `coverage`. A manifest is named by the sha256 of its own contents, and artifacts by theirs, so an edited file no longer matches and is refused. ## Traces into the map Traces show calls static analysis cannot see, such as an event bus dispatching to a handler, or a call into another service: ```sh onus map . --traces trace.json onus report --base main --head HEAD --traces trace.json ``` Spans with `code.filepath` and `code.function` (OpenTelemetry's code attributes) resolve to symbols, and each span's parent is its caller: the pair becomes a `calls` edge with confidence `traced`. A span of a service named like a component ties the caller to that component. Traced edges are only added; `diff` and `report` add them to both trees, so they never show up as changes. Files may hold one OTLP JSON document or one per line, as the OpenTelemetry Collector's file exporter writes them. Production traces are kept with the records: `onus signals traces` turns an export into one `trace` signal in `signals.jsonl`, and `--traces` reads that file too, taking the calls of its `trace` lines and skipping the rest ([`onus help production`](/docs/production)). A call the map had only guessed (confidence `inferred` or `low`) that a trace observed is upgraded to `traced`; `onus map` says how many (`traces: 3 recorded calls, 2 traced calls added, 1 confirmed, 1 upgraded`). Static, compiler and declared edges stay as they are. With `records: true`, the action passes the records' `signals.jsonl` to `onus report --traces`. ## Coverage into the map Coverage says which lines tests ran. Onus reads lcov (Jest, Vitest, c8, nyc, cargo-llvm-cov, `coverage lcov`), Cobertura XML (`coverage xml`, JaCoCo and .NET converters) and Istanbul JSON (`coverage-final.json`), and tells them apart by their content: ```sh onus map . --coverage coverage/lcov.info --json onus diff base/ head/ --coverage head-coverage.xml onus report --base main --head HEAD --coverage coverage/lcov.info --coverage py/coverage.xml ``` Coverage applies to the head only: its line numbers belong to the commit the tests ran at. Check out that commit before running the tests. Coverage that names a line past the end of a file was measured at another commit, and that file is left out with a note. Paths may be absolute (`/home/runner/work/shop/shop/src/a.ts`): the longest map path they end with counts. Coverage of test files is dropped. The map keeps each measured file's covered and uncovered lines. An lcov `TN:` record that names a test file (`TN:services/billing/src/discount.test.ts`) also links that test to the symbols it ran (a bare file name links only when one test file has it), so `onus query tests-for noDiscount` finds a test that only imports `applyDiscount`. `onus map` says what it read (`coverage: 1 sources (lcov 1), 41 files matched, 3 not in the map; 1,204 of 1,530 lines ran; 18 test links`). A run in an environment keeps the files that match `environment.coverage` as `coverage` artifacts. `onus report --evidence run.json` reads them, as long as the manifest is intact and the run was at the head commit; a run at another commit is skipped with a note, and so is a kept file that is not coverage, such as an HTML report the glob caught. They count as `recorded`; a file given with `--coverage` counts as `supplied`. Each recorded source keeps its run's command. A submission made without a report (`onus ci pr`, `onus_submit`) reads the recorded coverage of its evidence the same way: ```sh onus env run ci -- npm test -- --coverage onus report --base main --head HEAD --evidence run.json ``` An intact manifest proves what a run recorded, not who picked the command. An agent picks its own commands (`onus_env_run`), and one of them could write a coverage file. So the coverage lane policy counts recorded coverage only from the commands it names in `lanes.inputs.coverage.commands`, such as `npm test -- --coverage`, and sends a change with recorded coverage from any other command to the judge ([`onus help lanes`](/docs/lanes)). The command still runs the change's own code, so coverage is evidence that tests ran the lines, not proof. For agents, `tests.coverage` in onus.yaml lists coverage files of the worktree ([`onus help configuration`](/docs/configuration)). The map server reads them on each rebuild and leaves out any source file edited after the coverage was written, with a note in `onus_status`. ## Behavior diff `onus report --behavior` runs the tests that reach a change at the base and at the head, and reports which cases now behave differently (ADR 0019). It is off unless asked for, and needs a container engine. Declare the command in onus.yaml: ```yaml environment: image: node:22-alpine setup: npm ci behavior: command: "mkdir -p reports && npx vitest run --reporter=junit --outputFile=reports/onus-behavior.xml {files}" junit: ["reports/onus-behavior*.xml"] # the default budget: 600 # seconds for everything, environments included (default 600) maxFiles: 40 # most test files run (default 40) depth: 1 # code-dependency hops from changed code to tests, 0 to 2 (default 1) ``` `{files}` is replaced with the selected test files, shell-quoted and relative to the repository root. The command must write JUnit XML to a file `junit` matches. Like the rest of the report's config, it comes from the base commit (or `--config`), so a pull request cannot change the command that checks it. ```sh onus report --base main --head HEAD --behavior onus report --base main --head HEAD --behavior --behavior-budget 300 --fail-on behavior ``` 1. **Selection.** From the maps: changed test files first, then the tests that exercise a changed file, then those that exercise code depending on it, up to `depth` hops. Both maps are asked, so code deleted at the head still selects its base tests. At most `maxFiles` run; the rest are counted as `truncated`. When nothing is selected, the report says "no tests reach the changed code", which is a fact worth knowing too. 2. **Base and head.** One environment per commit, made in parallel, each running the selected files that exist on its side. 3. **Retry.** Files with cases that fail at the head, and passed or did not exist at the base, run once more at the head. A case that passes then is flaky: no row, and it is listed in `behavior.flaky`. 4. **Cross.** The base versions of selected test files the change edited (and their changed `__snapshots__` files) are written over the head's tree and run against the head's code. Its manifest records this as `overlay: {fromCommit, paths}`, and the judge never accepts such a run as evidence of the head. 5. Both environments are destroyed, also after an error or a timeout. Each case is compared, and becomes one of four rows, grouped per component ([`onus help reports`](/docs/reports)): `behavior-newly-failing`, `behavior-changed`, `behavior-newly-passing` and `behavior-output-changed`. A case whose name and file are unknown to the maps counts under `root`. Cases failing at both sides with the same message are listed in `behavior.alreadyFailing`. Every run is an ordinary evidence manifest, so `onus evidence show ` shows its log and JUnit. Each step gets what is left of the budget; one that would start after it is skipped, a run still going when it ends is stopped, and the status becomes `partial` with the reason. A newly failing case whose retry did not run still gives a row, at confidence `inferred`. Creating an environment is not stopped part way, so a slow setup can take the time past the budget. A problem never fails the report: without `environment.behavior`, `{files}` in the command, a container engine or an image, the status is `skipped` with the reason, and one `onus: behavior: …` line goes to stderr. Limits: only runners that write JUnit and take test files as arguments; the cross run uses the head's versions of helpers and fixtures; a case renamed at the head pairs only by name, so its base and head runs are not compared (its cross run still counts); flakiness is ruled out with one re-run; and traces from these runs are not added to the map. --- ## Getting started URL: https://onushq.com/docs/getting-started Install Onus, build a map and read your first report # Getting started Onus turns a change to a codebase into a short report of changes in meaning: new external services, contract changes, new relationships between components, weakened tests, broken rules. It reads TypeScript and JavaScript today. Onus only reads files. It never installs dependencies or runs the code it analyzes, so it is safe to run on pull requests from forks. ## Install With Homebrew, on macOS or Linux: ```sh brew install onushq/tap/onus ``` Or, without Homebrew, the installer downloads the binary for your system from the latest GitHub release, checks its SHA-256 and puts it in ~/.local/bin: ```sh curl -fsSL https://onushq.com/install.sh | sh ``` Set ONUS_VERSION=v0.1.0 to pin a version and ONUS_INSTALL_DIR to install elsewhere. Archives for every platform, including Windows, are at https://github.com/onushq/onus/releases. Or build from source with Rust (https://rustup.rs): ```sh git clone https://github.com/onushq/onus && cd onus cargo build --release # the binary is target/release/onus ``` Or install the binary into ~/.cargo/bin: ```sh cargo install --git https://github.com/onushq/onus onus-cli --locked ``` Check it works: ```sh onus --version ``` ## Your first map The map is Onus's picture of a repository: components, the contracts they expose, how they depend on each other, events, data access, external services and tests. ```sh onus map path/to/repo # a summary onus map path/to/repo --json # the whole map ``` The summary lists each component with its kind, public symbols, labels and owners, the external services found, and anything Onus could not resolve. ## Your first report Compare two git refs of a repository: ```sh onus report --repo path/to/repo --base main --head my-branch ``` Or two directories: ```sh onus diff old-checkout new-checkout ``` Try it on the fixture that ships with Onus, the manifesto's "text customers when their order ships" change: ```sh cp -R fixtures/shop/base /tmp/shop-head cp -R fixtures/shop/scenarios/s1-sms-alerts/services /tmp/shop-head/ onus diff fixtures/shop/base /tmp/shop-head ``` You get four rows instead of 1,408 changed lines. [`onus help reports`](/docs/reports) explains how to read them. ## Tell Onus about your repository Onus works without configuration: it finds components from npm, pnpm or yarn workspaces (or folders under services/, packages/ and apps/) and owners from CODEOWNERS. A small `onus.yaml` makes reports sharper: sensitivity labels, boundary rules, event patterns and SDKs Onus does not know. ```sh onus init # writes a starter onus.yaml ``` Review it, uncomment the labels it suggests, and commit it. See [`onus help configuration`](/docs/configuration). ## Next - [`onus help reports`](/docs/reports): reading a report - [`onus help changes`](/docs/changes): every kind of change Onus reports - [`onus help intent`](/docs/intent): checking a change against what it says it does - [`onus help ci`](/docs/ci): running Onus on every pull request --- ## How Onus works URL: https://onushq.com/docs/how-it-works How maps are built and compared # How Onus works ```text base tree ──► build map ──┐ ├──► semantic diff ──► ranked rows ──► Markdown / JSON head tree ──► build map ──┘ ▲ └── line diff of the two trees ``` Onus builds a map of each version of the code and compares the maps, not the text. Every fact in a report comes from that analysis and a fixed sentence template; no language model writes any of it. Onus only reads files. ## 1. Building a map Map building is a pipeline of providers: discovery providers find components, language providers analyze files, and fact providers add relationships. The ones below are built in; plugins add more, including SCIP indexes and language servers ([`onus help plugins`](/docs/plugins)). **Files.** Onus walks the tree, skipping dependencies, build output and anything gitignored (see [`onus help configuration`](/docs/configuration)). **Components** come from onus.yaml, else from Nx projects and npm, pnpm or yarn workspaces, else from the folders under services/, packages/ and apps/. Owners come from onus.yaml or CODEOWNERS. Each component's entrypoint comes from its package.json, mapped from build output back to source. **Parsing.** Every .ts, .tsx, .js, .jsx, .mts and .cts file is parsed in parallel with tree-sitter. From each file Onus records: - imports and exports, including aliases and re-exports; - declarations: functions, interfaces, type aliases, classes, enums, constants and variables; - each declaration's **shape**: parameters and which are optional, return types, fields and which are optional, all as normalized type text. A type that is inferred rather than written is marked unverified; - each declaration's **fingerprint**: a hash of its code with comments, formatting, quote style, its own name and its local variable names removed. Two functions with the same logic under different names share a fingerprint; - **body facts**: comparisons, guards, throws, awaits and empty catch blocks; - **references**: calls and type uses, and which declaration they occur in; - **patterns**: event publish and subscribe calls, Prisma reads and writes, `process.env` reads, calls into known SDKs, and `fetch`/`axios` calls to literal hosts; - **tests**: cases with their full names, assertions, skip/only/todo markers and expected values. **Linking.** Onus resolves every import itself, with no Node and no TypeScript compiler: relative paths, index files, tsconfig `paths` and `baseUrl`, and workspace package names mapped to their source. It follows re-export chains and respects local scope, so a call resolves to the declaration it really reaches. Imports of anything else become third-party packages. **The map** gives every node a stable id: `billing:src/discount.ts#applyDiscount`, `event:OrderShipped`, `db-table:payment`, `config-key:SENDGRID_API_KEY`, `external:acme-sms`, `npm:date-fns`. Edges (`imports`, `calls`, `references-type`, `reads`, `writes`, `publishes`, `consumes`, `calls-external`, `reads-config`) connect them, each with the files and lines where it happens. Symbols reachable from an entrypoint are public; the rest are internal. The map also holds package dependencies, tests, boundary rules and diagnostics for anything Onus could not resolve. `onus map . --json` prints it. ## 2. The semantic diff The passes run in this order: 1. **Matching.** Symbols with the same id are the same symbol. Of the rest, a removed and an added symbol with the same fingerprint are the same code: a move (same name, possibly into another component) or a rename (new name). A symbol with the same name and kind in another file whose members mostly overlap was moved and edited at once. Files that moved with identical content, or with only formatting and import-path changes, are matched too. 2. **Renames** become one row with the number of call sites updated; moves between components become one row per pair of components. 3. **Contracts.** Shapes of public symbols are compared, ignoring the order of object and union members: optional additions are additive; removals, new required members and unprovable type changes are breaking when a user or implementation outside the change was not updated. 4. **Boundary rules** from onus.yaml are checked on the new map; violations that already existed are dropped. 5. **Relationships** are compared per component: events published and consumed, tables read and written, external services called, and dependencies between components. A new vendor or a new kind of data leaving the system is security-sensitive. 6. **Packages.** New, changed and removed npm dependencies, and patched packages. An SDK of a newly reported external service is folded into that row. 7. **Risk classes.** Migrations, GraphQL operations, HTTP routes and authentication or authorization code get their own rows. 8. **Configuration** files are classified and the keys that changed are named; CI, policies, onus.yaml and CODEOWNERS are flagged as rules of the game, root build config and infrastructure as needing a person. 9. **Tests** are compared case by case for weakening; tests that moved, or went with the code they tested, are told apart. 10. **Notable edits** inside functions on both sides: flipped comparisons, changed constants, removed guards, throws and awaits (unless they moved into another function), swallowed errors. 11. **Secrets** in added lines. 12. **Generated code.** Rows about files a tool wrote become one row per component. 13. **Internal rows.** Every row lists the lines it explains; changed lines no row explains collapse into one internal row per component. Then rows that say the same thing are grouped ([`onus help changes`](/docs/changes)), hints are filled in (labels, blast radius, confidence), the intent check marks mismatches, and the rows are ranked ([`onus help reports`](/docs/reports)). ## Why it is built this way - **Facts are computed, never generated.** A wrong fact costs more trust than a missing one, so every row comes from deterministic analysis. - **Conservative under uncertainty.** What Onus cannot resolve is marked low confidence and ranked as higher risk, never dropped. - **Deterministic.** Everything is sorted; there are no timestamps or absolute paths. The same inputs give byte-identical output. - **Safe.** Nothing from the analyzed code is installed or run, so Onus can run on any pull request. - **Fast.** A generated workspace of about 230,000 lines maps in under a second on a laptop. The design and its decisions are in PLAN.md and docs/adr in the repository. --- ## The intent check URL: https://onushq.com/docs/intent Checking a change against its stated intent # The intent check The gap between what a change says it does and what it actually does is the most important signal in a review. Onus checks it deterministically: you (or the agent that wrote the change) state the intent in a small block, and every row outside it is an intent mismatch, ranked at the very top and marked as needing a person. ## Stating an intent In a pull request body, a fenced block: ````md ```onus-intent summary: Add SMS alerts on OrderShipped touches: [notifications] contracts: [UserPreferences] externals: [Acme SMS] ``` ```` Or a YAML file with the same keys. Pass either to `--intent`: ```sh onus report --base main --head HEAD --intent pr-body.md onus diff old new --intent intent.yaml ``` All keys are optional; unknown keys are an error. A file ending in `.md`, `.markdown` or `.txt` is read as Markdown: only its `onus-intent` block counts, and without one the check is skipped with a warning on stderr. Other files are read as YAML, unless they look like Markdown and have no block. ## What each key covers - `summary`: free text, shown in the report. - `touches`: the components the change may touch, by id (`notifications`), or path globs (`services/notifications/**`, any entry with `/` or `*`). A row is inside `touches` when its component is listed, or when every file it points at matches one of the globs. - `contracts`: the contracts the change may alter, by name (`UserPreferences`) or map id. Applies to contract rows (subkinds `contract-*` and `export-*`). - `externals`: the external services the change may start calling, by vendor (`Acme SMS`), id (`acme-sms`) or SDK package (`@acme/sms`), case-insensitive. Applies to `new-external-service` and `new-external-call` rows. Every row must be inside `touches`. Contract rows must also be listed in `contracts`, and external-service rows in `externals`. Everything else is a mismatch, including the internal row of a component outside `touches`. ## What the report shows ```md **Intent check:** **2 changes outside stated intent** · **Boundary rules:** no new violations Stated intent: "Add structured fields to log lines" - 10 lines of new code inside `billing`: `billing` is not in `touches` (logger) - `billing` now writes to the `payment` table: `billing` is not in `touches` (logger) ``` The mismatching rows move to the top of the table with "Outside stated intent" in their Kind column. In JSON, they have `hints.intentMismatch: true`, and `intentCheck.mismatches` lists `{ changeId, reason }` for each. ## Tips - Ask coding agents to end every pull request description with an `onus-intent` block. It costs them nothing and makes scope creep visible. - Keep `touches` tight. A change that legitimately touches more should say so; that is the point. - An intent never hides a row: rows inside it are still reported and ranked as usual. --- ## Risk lanes and the judge URL: https://onushq.com/docs/lanes Risk lanes, change submissions, the learned model, the verifying judge, outcome records and provenance # Risk lanes and the judge Once Onus has described a change, lanes decide what happens to it: merge it automatically, have the judge verify it, send it to a person, or block it. Lanes come from rules you write and from hard floors no rule can lower. A learned model you train and commit can raise a lane, never lower it. | Lane | What happens | |---|---| | `auto-merge` | It merges when required checks pass (the action can enable GitHub's auto-merge). | | `judge` | `onus judge` verifies it; it merges on `approve`. | | `human` | An owner of what changed (`lanes.approval`) who did not open it approves its head commit. Its owners are asked to review. | | `blocked` | It may not merge as it is. | ## Submitting a change An agent (or a person) bundles a change with what a reviewer needs and nothing of its reasoning: ```sh onus submit --base main --head HEAD --intent intent.yaml --evidence run.json \ --token "$TOKEN" --key-public root.pub \ --agent-tool claude-code --agent-model sonnet --agent-config default --out submission.json ``` The submission holds the stated intent, Onus's report, every changed path, the test runs from `onus run-test` (to be re-run), the token's verified scope, granted escalations (`--escalation `, each verified with `--key-public`: granted, for the token's task, unchanged since it was filed; requests are read from `.onus/escalations`, or `--escalations-dir`), approvals (`--approve =`), and the agent setup. When the token names an agent, that is the agent setup, whatever the flags say. ## Classifying ```sh onus classify --submission submission.json --outcomes outcomes.jsonl onus classify --report report.json # a change without a submission (CI) onus classify --submission submission.json --outcomes outcomes.jsonl --change acme/shop#42 ``` The answer is the lane and every rule that applied, in order. Each entry has a `source` (`default`, `policy`, `input`, `floor`, `model`, `record` or `audit`), a reason, and the ids of the report rows behind it (`rows`): 1. The policy's `default` lane, or a lower one when a `match: every` rule covers every row of the change (for example, internal changes in a docs component). 2. Rules that raise the lane when any row matches. 3. Risk inputs (`lanes.inputs`, below). They can only raise the lane. 4. Hard floors: - **Blocked:** a committed secret, a write outside the token's scope, weakened tests without a person's approval, or tests that pass at the base and fail at the head (`behavior-newly-failing`, from `onus report --behavior`) without a person's approval of that row. - **Human:** rows in sensitive code (`auth`, `payments`, `pii`, or any label of medium or high sensitivity), security-sensitive rows, rules of the game (tests, CI, policies, `onus.yaml`, and edits to the code of a declared contract), rows outside the stated intent, and third-party code new to the repository (an npm package no component used before). - **Judge:** a new dependency never auto-merges: a package new to a component, or a new dependency between components. 5. The learned model (`lanes.learned`, below), read from the base commit: at or above its thresholds it raises a lower lane to `judge` or `human`. It never lowers a lane and never blocks. 6. An agent setup with fewer than `minRecord` merged changes on record, or with an incident or a rollback among its recent changes, cannot auto-merge. A share (`auditRate`) of the remaining auto-merges is sent to a person before merging, chosen by a hash of the head commit. Every classification also carries the change's `rollout` plan for its lane: how it should meet production traffic, and when it rolls back ([`onus help production`](/docs/production)). ## Risk inputs Four built-in inputs read facts Onus computes for every change. They are on by default, each can be tuned or turned off, and none can lower a lane. A fifth, `coverage`, is off unless written: | Input | Fires when | Default | |---|---|---| | `blastRadius` | A row changes something other components depend on (`dependentComponents` in the report), including a body-only edit of a function they call. The highest lane whose threshold the count meets applies. | 3 or more components → `judge` | | `tests` | A row changes code no test exercises and no runtime trace has observed (`tested: untested`). | `judge` | | `confidence` | A row rests on parts of the map Onus could not resolve (`confidence: low`). | `judge` | | `history` | A component the change touches had reverts, rollbacks or incidents in the last `days` days, read from `--outcomes` and the production signals beside it (`signals.jsonl`). A planned rollback does not count. | a revert → `judge`, an incident → `human`, over 90 days | | `coverage` | Tests ran less than `minChanged` (0 to 1) of the change's changed executable lines, from coverage of the head ([`onus help reports`](/docs/reports)), or the coverage leaves out a changed file with runtime code; with `required: true`, also a change without coverage. | off | ```yaml lanes: inputs: blastRadius: { judge: 3, human: 10 } # `{}` turns it off tests: { untested: judge } # `auto-merge` turns it off confidence: { low: judge } history: days: 90 reverts: { judge: 1 } incidents: { human: 1 } coverage: { minChanged: 0.8, commands: ["npm test -- --coverage"] } # off unless written ``` Every classification also carries `features`: what the change looked like when it was classified, which the learned model trains on, and `learned` when `lanes.learned` is set. Test reach is per file: a file counts as tested when a test in the map exercises anything in it, and as traced when a runtime trace saw it ([`onus help environments`](/docs/environments)). It is not line coverage; `coverage` is. Coverage comes from `onus report --coverage` or `--evidence`, or, for a submission made without a report (`onus ci pr`, `onus_submit`), from the coverage its evidence runs recorded at the head commit ([`onus help environments`](/docs/environments)). The `coverage` input runs only while a change would auto-merge. It counts recorded coverage only from the commands listed in `commands`, since an agent picks the commands it runs and one could write a coverage file; when the report holds recorded coverage from any other command, the change goes to the judge, required or not. Coverage given with `--coverage` counts. History counts each change once per component, from every record in the window, so a revert and an incident count at the time they were recorded. Code outside every component counts as `root`. A backfill (`onus ci pr --at`) sees only what was recorded by then. Records shared by several repositories are scoped with `--change owner/repo#N`: only that repository's changes count, and the change itself is left out. `onus ci pr` does this for you. ## A learned model that only raises Once outcome records show which changes went wrong, a model can learn from them. It may move a change up a lane, never below the lane the rules give it, and never to `blocked`. ```sh onus ci records pull --dir rec onus lanes train --outcomes rec/outcomes.jsonl # writes onus.lanes-model.json git add onus.lanes-model.json && git commit -m "Lanes model" lanes: learned: model: onus.lanes-model.json # the default judge: 0.6 # raise to the judge at or above this risk human: 0.85 # raise to a person at or above this risk ``` **What it learns from.** Every classification computes a fixed set of features from the report (rows of each kind, blast radius, dependent components, lines and files, novelty, sensitive code, rows that need a person, map confidence, test reach, unverified contracts, rule violations, a stated intent), the submission (passing and failing test runs) and the records (earlier changes and problems in the same components, the agent setup's record). `onus ci pr` keeps them on the pull record, `onus ci closed` copies them onto the outcome, and reverts, incidents and audits keep them. `onus outcomes record --classification ` copies them from a classification, for outcomes recorded by hand. **What counts as a problem.** A change that had an incident, was reverted or rolled back (a planned rollback is not a problem: it counts as a clean merge), whose audit found a miss, or about which a person with write access said `/onus caught` ([`onus help ci`](/docs/ci)). A merged change counts as clean once it is `--settle-days` old (default 14) at the latest record's time, so reverts and incidents have had time to show. Open changes, changes closed without a catch, unsettled merges and records without features are left out, and the output says how many. **Training.** A logistic regression on standardized features, class-balanced and L2-regularised, fitted by gradient descent from zero with a fixed number of steps. There is no randomness: the same records in any line order give the same file, byte for byte. A fifth of the changes, chosen by a hash of their ids, is held out: a first fit without them is measured on them, then the model is fitted on everything. It refuses with "too little data" (exit 1) below `--min-examples` (default 50) labelled changes or `--min-positives` (10) problems, or when the part left after holding out the fifth lacks problems or clean merges. With `--config` and `--out`, nothing is read from `--repo`, so it runs outside a repository. `--dry-run` measures and writes nothing. It prints: ```json { "out": "onus.lanes-model.json", "written": true, "model": "3f2a9c1e0b7d", "examples": 412, "positives": 23, "skipped": { "noFeatures": 80, "staleFeatures": 0, "unsettled": 12, "undecided": 35 }, "holdout": { "examples": 83, "positives": 5, "auc": 0.82, "atJudge": { "raised": 0.21, "recall": 0.8 }, "atHuman": { "raised": 0.04, "recall": 0.4 } }, "weights": [ { "feature": "kind.breaking", "weight": 1.21 }, … ] } ``` `raised` is the share of held-out changes at or above a threshold, and `recall` the share of held-out problems at or above it. Tune `judge` and `human` with them: lower thresholds raise more changes and catch more problems. The risk is a relative score, not a calibrated probability, because the classes are balanced. **Applying it.** `onus classify`, `onus judge`, `onus ci pr`, `onus ui` and the MCP `onus_submit` tool read the model from the base commit (the submission's base; the working tree for `onus classify --report`), so a pull request cannot delete or retrain the model to avoid a raise. `--model ` hands in another one. A raise is listed with `source: "model"`: ```text model → human: the learned model puts this change's risk at 0.91 (a person from 0.85, the judge from 0.60): 2 breaking rows (+1.42), blast radius 38 (+0.97), 3 earlier problems in billing (+0.61); model 3f2a9c1e0b7d, trained on 412 outcomes ``` When it does not raise, `learned` still holds the risk and its top factors, and the CI comment shows a **Learned risk:** line. A model file that is missing at the commit, was trained on another feature set (`stale`), or does not parse (`invalid`) is never an error: `learned.status` and `learned.note` say why, and the lane comes from rules only. **Rules of the game.** The model file and `onus.yaml` decide lanes, so a pull request that changes either goes to a person (`lanes-model-changed`, `onus-config-changed`). Retrain after a release that adds features: an older model reports `stale` until then. **What it is not.** Outcomes are shaped by the lanes changes got: a change a person reviewed was less likely to go wrong. The model learns what went wrong despite the lanes, which is the right target for raising a lane, but not a causal estimate of risk. ## The policy ```yaml lanes: default: judge auditRate: 0.05 auditAfterMerge: 0.05 minRecord: 10 rules: - { lane: auto-merge, match: every, kinds: [internal], components: [docs] } - { lane: human, subkinds: [migration-changed, route-added] } taste: command: [./scripts/review.sh] judges: - name: claude-reviewer # uses taste above - name: gpt-reviewer taste: { command: [./scripts/review-gpt.sh] } - name: strict heldOutLimit: 20 ``` A rule matches a row by `kinds`, `subkinds`, `components` and `labels` (an empty list matches anything). [`onus help changes`](/docs/changes) lists the kinds and subkinds. Held-out checks are not in the policy: they live outside the repository (below). ## The judge ```sh onus judge --submission submission.json # exit 0 approve, 2 reject, 3 escalate onus judge --submission submission.json --held-out ~/acme-held-out/shop ``` It verifies first and judges taste last, and stops at the first failure: 1. **Evidence:** each test run in the submission is run again at the head commit in a fresh container; a run that passed for the author must pass again. A run made in an environment (`onus env run`) names its manifest, and must match it: recorded at the head commit, with the same command and exit code ([`onus help environments`](/docs/environments)). The first run that fails stops the step; the rest are not re-run. 2. **Intent:** an intent must be stated, and no row may fall outside it. 3. **Contracts:** no breaking contract changes and no boundary-rule violations. 4. **Tests:** no weakened tests without a person's approval. With a report made with `--behavior`, a test edited to match changed behavior (`behavior-changed`) is a rule of the game, so it reaches a person instead. 5. **Held-out checks:** checks the author never saw, from a suite outside the repository, chosen from the map for this change (below). They run in order, most relevant first, up to the first failure. The author sees only counts and `a held-out check failed (exit N)`: never a check's id, command or output. 6. **Taste (optional):** `taste.command` gets the submission as JSON on stdin and prints `concern: …` lines. Concerns send the change to a person; taste can never approve, and it runs only when everything else passed. Onus itself calls no model. Once a step fails, every later step is `skipped` with `not reached: failed`, and no more containers start. So `reasons` holds one failing step's reasons: the first thing to fix. Run `onus_check` (or `onus query check`) before submitting for the whole checklist. The judge's evidence step and the behavior diff are separate. The first re-checks what the author claims to have run; the second is Onus's own observation of the tests that reach the change, at both commits ([`onus help environments`](/docs/environments)). A run whose manifest records files written over the head's tree (the behavior diff's cross run) is never accepted as evidence. The verdict is `approve`, `reject` with reasons the agent can act on, or `escalate` to a person. A change in the `human` lane is escalated even when every check passes; a `blocked` change is rejected. ### Held-out suites A suite is a directory holding `held-out.yaml` and the files its checks need. Keep it outside the repository: in a private repository, a CI secret or Onus Cloud. The judge refuses a suite inside the repository's worktree, where the author could read it. ```yaml version: 1 image: node:22-alpine # default image for checks (default node:22) setup: npm ci # default setup, run with network and without the suite limit: 6 # at most this many checks per change, most relevant first checks: - id: sms-respects-opt-out command: node --test /held-out/sms-opt-out.test.mjs covers: [notifications, user-preferences] - id: send-sms-unit command: node --test /held-out/send-sms.test.mjs covers: ["notifications:src/sms.ts#sendSms"] - id: smoke command: npm run build # no covers: runs for every change ``` Each check runs in a container at the head commit with no network, the repository at `/work` (the working directory) and the suite at `/held-out`. The suite is copied into the test container only, never into the setup container, which has network. Ids are unique slugs; unknown fields are refused. Give it to the judge with `--held-out DIR` or `ONUS_HELD_OUT`: on `onus judge`, `onus ci pr`, `onus ui` and `onus mcp` (for `onus_judge`). Two commands help people write one, without a container engine: ```sh onus held-out check ~/acme-held-out/shop --repo . # valid? its hash, ids and covers onus held-out select --held-out ~/acme-held-out/shop --submission sub.json # what would run, and why ``` **Chosen from the map.** The judge works out what the change reaches in the head map: the components of its rows and of the files git says changed (`touched`), its rows' subjects, and the pairs of components that interact with at least one side touched: a code dependency either way (a file import counts, so a change that breaks the symbols between two components still pairs them), or one publishes an event the other consumes. A `covers` entry is a component id, or a map id standing for its component; `event:` and `db-table:` ids count only as subjects. A check is chosen for its best reason: | reason | when | |---|---| | `combination` | two components it covers form a pair: the cross-feature test an author's own suite rarely has | | `subject` | a map id it covers is a row's subject, or the module of one | | `touched` | it covers a touched component | | `always` | it covers nothing (and `lanes.heldOut`) | | `neighbour` | it covers a component that interacts with a touched one | Within a reason checks keep their order in the file. The configuration's `heldOutLimit`, else the suite's `limit`, cuts the list. The head map is built only when some check has `covers`, with the same onus.yaml as the lanes; if it cannot be built, the rows alone decide, and the report says `"map": false`. **Who sees what.** The judgment carries `heldOut: {suite, selected, ran, passed}`, safe for the author. What each check did (`why`, `with`, status, exit code and output) is for people: `onus judge --show-held-out`, `held-out.json` beside `judgment.json` from `onus ci pr`, and the lanes page of `onus ui`. No agent tool returns it, and `onus_lanes` shows only that held-out checks are configured. When a check cannot run at all (an image that will not pull, say), the author reads only that the held-out checks could not run; the error is in the report. **The honest limit.** On a machine where an agent has a shell, a suite is only as hidden as that machine's file permissions. So is the report: `onus ui` and `onus mcp` keep it as `.onus/submissions/.held-out.json` in the worktree, where such an agent can read it. Real secrecy comes from where the suite lives and where the judge runs: a CI secret, a private repository cloned with its own token, or Onus Cloud. `lanes.heldOut` in onus.yaml still works, as one check with id `lanes.heldOut` that runs for every change. Agents can read onus.yaml, so `onus judge` notes it on stderr; prefer a suite. ### Judge configurations With `lanes.judges` set, one configuration judges each change. Each has a `name`, and optionally its own `taste` reviewer (default `lanes.taste`) and `heldOutLimit`. The rotation picks one from a hash of the head commit and the suite's full hash, so an author who cannot read the suite cannot predict which configuration will judge a commit. Without a suite the choice is predictable from the head commit. The judgment records `rotation: {config, index, of, forced}`, and its judge id is `@`, so outcome records and `onus outcomes summary`'s `byJudge` track each configuration by name. `onus judge --judge-config ` (and the lanes page of `onus ui`) forces one, for example to judge an audited change again under another configuration; that is recorded as `forced: true`. Agents cannot choose. Without `judges` nothing changes, and the judge id stays the policy's hash. Agents do not see which configuration judged. `onus_judge` leaves out `rotation`, and its judge id is the policy's hash, the same for every configuration; the judgment kept beside the submission has both, for people. Otherwise an agent could amend its commit, judge it again, and repeat until a lenient configuration came up. That is the limit: an author who sees the configuration can still shop by amending. `lane.md` from `onus ci pr` names it, so in CI an author who reads the pull-request comment can push again until it likes the one it gets, and on a machine where an agent has a shell, it can read the kept judgment. What still holds there is the record: `byJudge` shows what each configuration let through, so a lenient one shows up in its reverts, incidents and audit misses. ## Outcomes The GitHub Action keeps these records for you with `records: true`: outcomes when pull requests close, reverts when the default branch moves, and approvals, audits and incidents from `/onus` comments ([`onus help ci`](/docs/ci)). By hand, or from another system: ```sh onus outcomes record --file outcomes.jsonl --change acme/shop#42 --agent claude-code/sonnet/default \ --lane auto-merge --verdict approve --judge 1a2b3c4d --result merged --audited onus outcomes summary --file outcomes.jsonl ``` Pass `--component ` (repeatable) to name the components the change touched, for the history input, and `--classification ` to keep the change's features for the learned model. `onus ci pr` records them from the report, and `onus ci closed`, reverts and incidents keep them. The summary gives each agent setup its record, and each judge configuration its record of the changes it decided (a verdict on record): merged, reverted, incidents, audited, missed. It also gives the share of changes in the human lane (the review budget), and the miss rate of human audits of automatic approvals, the number that says whether the automatic lanes can be trusted. A judge configuration id is a hash of the lane policy and the Onus version, so changing either starts a new record; with `lanes.judges`, each configuration keeps its own record as `@`. ## Audits after merge Some changes merge with no person looking: the `auto-merge` lane, and the `judge` lane when the judge approves. A share of them (`auditAfterMerge`, default 0.05) is sampled for a person to audit after they merge. `onus ci closed` makes the choice from a hash of the merge commit, which does not exist until the merge, so an agent cannot know beforehand whether its change will be audited. The choice is written into the outcome as `"sampled": true` and nowhere else: not in the comment, the check or the CI log. `auditRate` is a different draw: it sends would-be auto-merges to a person before they merge. Set either to 0 to turn it off. ```sh onus outcomes audits --file outcomes.jsonl # what waits for a person, and the audit log onus outcomes audit --file outcomes.jsonl --change acme/shop#42 --by @alice \ --result miss --note "the opt-out was ignored" ``` `audits` prints `{awaiting, audited}`: the sampled changes no one audited yet, oldest first, and every audited change, most recent first. A sampled change that was later reverted or caused an incident still waits; leaving out the ones that went badly would bias the sample. `audit` records who audited, when, ok or a miss, and a note. `/onus audit ok|miss` on a merged pull request writes the same record ([`onus help ci`](/docs/ci)). The summary's `sampledMissRate` is the number to watch: misses among the sampled changes a person audited. Because the sample is random, it estimates how many risky changes slip through without a person. `auditMissRate` counts every audit, including the ones people chose to make, and `awaitingAudit` counts what is still waiting. ## The production loop What happens after merging changes the record: ```sh onus outcomes ingest-reverts --file outcomes.jsonl --since v1.4.0 # reverts in git history onus outcomes incident --file outcomes.jsonl --change acme/shop#42 \ --involved notifications --involved "notifications:src/sms.ts#sendSms" --note "SMS sent twice" onus outcomes backlog --file outcomes.jsonl ``` Reverts are found from commit messages (`This reverts commit …`) and recorded against the change they undo; a reverted change no longer counts toward its agent setup's record. A change turned off by a feature flag or a rolled-back rollout is recorded with `onus ci rollback`, `onus ci rollout` or `onus outcomes record --result rolled-back` and counts the same way, unless it was `--planned`: a planned rollback still counts as a merged change. An agent setup with an incident or a rollback among its last 20 changes cannot auto-merge. Incidents and rollbacks also come straight from production: `onus signals ingest` records signals from monitoring and deploy tools and turns those tied to a change into its outcomes ([`onus help production`](/docs/production)). `backlog` lists the components and symbols incidents involved, most often first: where the next held-out tests should go. Onus does not write tests. ## Provenance Every change carries one record of why it was allowed through: the intent, the agent and model, the token it held, the evidence, any escalations, the lane and the judge's steps, who or what approved it, and how it rolled out. `onus ci pr` writes it as `provenance.json` beside the submission while the pull request is open. `onus provenance` rebuilds it from the records as outcomes arrive: ```sh onus provenance --change acme/shop#7 --records rec --from out/provenance.json --out p.json onus provenance --submission-id 4fa91c # a submission kept by onus ui or onus mcp onus provenance --submission sub.json --audit audit.jsonl --escalations escalations --task sms onus provenance --change acme/shop#7 --records rec --escalations cloud.json --token-inspection token.json onus provenance --change acme/shop#7 --records rec --audit door-log.json --calls calls.json onus provenance --change acme/shop#7 --records rec --agent-submission submissions/7.json onus provenance --verify p.json ``` `--from` is an earlier record of the change. Its facts from while the change was open (intent, report, token, evidence, escalations, lane reasons, judge, doors) are kept unless fresh ones are given. The stage, merge commit, approver and rollout are worked out again from the records every time. `--submission-id` reads `.onus/submissions/.json`, its judgment, and the repository's `.onus` audit log, escalations and outcomes. A change made through Onus Cloud has no submission or door log in CI. Onus Cloud keeps its escalations instead, each with the door's inspection of the task token. `--escalations FILE` (also on `onus ci pr`) takes them as a JSON array (`schemas/escalation-decisions.schema.json`): `id`, `kind`, `scopes`, `state`, `decidedBy`, `decidedAt` (Unix seconds or an RFC 3339 date in UTC), `reason` or `answer`, `repository`, `pull`, `ref`, `commit`, and `token: {task, rights, expires}`. The API's `{"escalations": [...]}` with a nested `decision` is read too, and so is a stored row (`status`, `pullNumber`, `input`). Pass the API's views where you can: a stored row still `waiting` past its `expiresAt` is recorded as `expired`, as the view shows it. Which escalations are this change's: - For a pull request (`owner/repo#n`), those for another repository or pull request are left out. The repository is matched ignoring case. - An escalation that names the change is kept whatever its task. It names the change when it is on the pull request, the submission lists it, an earlier record (`--from`) kept it, or its `ref` or `commit` is one of the change's commits, its branch (`ci pr --branch`) or `refs/pull//head`. A `commit` elsewhere outweighs the branch's name, since a branch name may be used again. `ci pr` lists the change's commits with git; `onus provenance` does so in `--repo` when it holds them. - One that does not name the change is kept only for the change's task. With no task, it is left out. - The change's task is `--task`, the token's, the submission's, the inspection's or the earlier record's. Else it is the one task the escalations that name the change name. When they name several, none is picked. An escalation that does not name the change never says its task, so it never brings its token into the record. - For a local change (a head commit), the repository and pull request are not checked. Those of the change's task are kept, or all of them when the task is not known. An escalation that cannot be read (an unknown `state`, a value of the wrong type, a time that is not one) is left out with a warning, and `escalations` stays a gap that names it. The note names the field and what it should be, never the value given, so a token pasted into the wrong field never reaches the record. A list whose escalations are all for other changes is a gap too. An `expires` that is not a positive whole number is no expiry. `onus ci pr` never stops for this input: a file it cannot read is a warning and a gap. Only a token given instead of its inspection is refused, before anything is written. An inspection `onus ci pr` cannot read is named in the `token` gap. `onus provenance` fails on a file it cannot read, a missing one included. `--token-inspection FILE` takes the door's inspection of the token on its own (`{task, rights, expires}`, as `onus token inspect` prints it; an `expires` of 0 is no expiry). With `--task`, it must be for that task. Without the file, the first escalation of the task that kept an inspection gives it. Either way the rights are widened by the scopes Onus Cloud granted to the task. Those are what Onus Cloud granted, not what the door minted again: the agent may never have taken up a grant. `onus provenance --escalations` given a folder reads the escalation requests kept there, as before. Given a file with `--submission-id`, it also reads the requests kept in `.onus/escalations`, so what Onus Cloud's list leaves out (kind, scopes, grade) comes from them. On a rebuild with `--from` where Onus Cloud's list is the only fresh input, it updates the escalations kept earlier, so those only the door knew stay. The door log can travel with the work too. An agent attaches its task's slice of the log to the pull request in Onus Cloud with `onus_cloud_attach_evidence` (`doorLog: true`) or `onus cloud evidence --door-log --task `. The slice is the run of entries from the task's first entry to its last, with whatever lies between, so its hash chain can be checked where it is read (`schemas/door-log.schema.json`). Through `onus mcp`, the server cuts it from its own log: the agent names nothing but the pull request. `--audit FILE` (also on `onus ci pr`) takes the slice, or a whole audit log as before. A slice's chain is checked from its first entry: whether any entry in it was changed or removed, not what came before it. The record keeps the task's entries with `doors.source: attached`, and whether the chain verified. A slice that does not verify is still recorded, as not intact, with the entries of the task it names, and the `doors` gap says why. A slice of another task than the change's keeps none of its entries, and the gap names both tasks. When nothing else names the change's task, the slice's task does, if the slice verifies. Through `onus mcp --token`, the door also sends its inspection of the token it holds with the slice (`inspection`: `{task, rights, attenuations, expires}`, never the token). So a change made through Onus Cloud by an agent that never escalated still has its token on record, as `token.source: attached`. Without an inspection, the slice's `mint` entry gives it, when the slice holds one. Either is taken only when the slice verifies and was cut at one of the change's commits (its `head`), and ranks below every other source. A slice cut at another commit may be another change's: it gives neither the task, the token nor any decision, keeps none of its entries, and the `doors` gap names the commit. So does a slice that does not say where it was cut, when the change's commits are known. Attach it after committing and pushing the work: onus attaches a slice only to the pull request whose head in Onus Cloud is the worktree's commit. An attached slice is the agent's word. The chain is plain sha256 with no key, so it shows that nothing in the slice was changed after it was cut, not who cut it: anyone can build a slice that verifies. So the `doors` gap stays even when it verifies, narrowed to say so. Its decisions on escalations are read only when it verifies, is of the change's task and was cut at one of its commits, and Onus Cloud's decision on the same escalation is recorded over it whatever its state; the `escalations` gap names any disagreement, and any decision only the slice gives. Its entries between the task's first and last include other tasks' entries on the same log, which travel with it. Each hash is over the entry's JSON as the door wrote it, so a host keeps the slice as it was sent (its text): a number written again another way (`1.0` as `1`, an integer past 2^53) no longer verifies. A door log that verifies but holds nothing of the change still leaves the `doors` gap: when the change's task is not known (give `--task`, also on `onus ci pr`), or the log holds no entry of the task. Whatever the source, a door log that does not verify never gives the token or the task. `--calls FILE` (also on `onus ci pr`) takes Onus Cloud's log of the calls made under the task token (`schemas/cloud-calls.schema.json`): a JSON array of `{at, method, route, scope, decision, tokenId, task, reason}`, where `decision` is `allowed`, `denied` or `failed`. The API's `{"calls": [...]}` with `createdAt` and `credentialId` is read too. The record sums them up in `doors.calls`: the totals, the first and last call, the token ids, the counts for each route and scope by decision, and every denial (at most 100) with its reason. Calls of another task are left out when the change's task is known, and calls never say the task. Calls that name no task are counted as the change's, on Onus Cloud's word, and `untasked` says how many. Onus Cloud's API rows name no task, so a host gives only the calls made under the change's task token. When the change's task is not known, every call given is counted and the `doors` gap says so. They are marked `source: onus-cloud`, apart from the door log: they are Onus Cloud's word and have no hash chain. With calls and no door log, `doors.source` is `onus-cloud`, `intact` is false, and the `doors` gap stays: the agent's own door log, with its MCP and gateway calls, did not reach the run. A call that cannot be read is left out with a warning, and the gap says the counts may be short. An empty list is on record: no call was made there. Only those fields are read: the API's rows carry more (`path`, `userAgent`, `credentialName`), some of it set by whoever made the call, and none of it is read or kept. A call with a `token`, or a field read that looks like one (an Onus Cloud token or API key, a PASETO, a run of base64url as long as a Biscuit), is left out like one that cannot be read; the warning and the gap name the field, never the value. A door log or a calls file that cannot be read at all is a warning and a `doors` gap, in `onus ci pr` and `onus provenance` alike, so a host refreshing a record keeps what came after the close. | Field | What it holds | |---|---| | `stage` | `open`, `merged` or `closed`. | | `intent` | Where the intent came from (`pull-request` or `submission`), its sha256, and the report's intent check. | | `agent` | The agent setup, how it was recognised (`trailer`, `branch`, `bot`, `co-author`, `person`, `submission`), and its track record (`merged`, `recentIncidents`, `recentRollbacks`). | | `token` | The task, rights, attenuations, expiry and a 16-digit fingerprint. Never the token itself. `source` says where it came from: `token` (verified by this run), `submission` (verified when the submission was made), `agent-submission` (the agent's own submission handed in beside the run, with `onus ci pr --submission` or `onus provenance --agent-submission`, such as one kept on the records branch: on its word, since the run cannot tell it was verified when it was made), `audit` (the door log's `mint` entry, widened by grants, when the log verifies), `inspection` (the door's inspection, from `--token-inspection` or kept by Onus Cloud with an escalation, widened by the grants there; not verified by the run) or `attached` (the door log the agent attached in Onus Cloud: the inspection sent with it, or its `mint` entry, when it verifies). The first that is given wins, in this order: `token`, `submission`, `audit`, `inspection`, `agent-submission`, `attached`. The agent's own submission ranks below the door's word. On a rebuild with `--from`, a kept token verified by a run or a submission, or taken from a door log this run read, comes before an inspection. | | `evidence` | Each test run: command, commit, image, exit code, manifest, and whether it ran at the head commit. | | `escalations` | Each request: kind, scopes, best evidence grade, and the last decision in the door log, by whom (`policy` for an automatic grant). A decision Onus Cloud kept, with Onus Cloud's id as `cloudId`: `granted`, `denied`, `needs-person` (waiting), `answered` (a blocked report a person answered), `open` (being checked), `withdrawn`, `expired` or `failed`. It replaces an open or waiting one, and a final decision from the submission or the door log stands against one Onus Cloud has not made yet. When both are final and differ, Onus Cloud's is recorded, since it decided, and the `escalations` gap names the disagreement. A decision only the agent gives (the escalations its own submission lists, or the door log it attached) stands against nothing: Onus Cloud's is recorded whatever its state. Kept on the agent's word alone, the `escalations` gap names it. `reason` is the decider's (the door log's reason, or Onus Cloud's `answer`), never why the agent asked. | | `lane`, `judge` | The final lane, every rule that applied, the lane policy's hash, and the judge's verdict, steps and reasons. | | `approval` | Who or what approved it (below), the people, the rows they approved, and who merged it. | | `rollout` | The result now, the merge commit and time, whether a person audited it, and every outcome in order. From production: `status` (`pending`, `not-shipped`, `awaiting-deploy`, `in-progress`, `holding`, `complete`, `rolled-back` or `reverted`), the `percent` of traffic last reported, the `plan`, every deploy and rollout stage (`stages`) and the other signals tied to it (`signals`). [`onus help production`](/docs/production) has the rules. | | `doors` | The door log's decisions for the task, and whether its hash chain is intact. `source` says where they came from: absent for the door log this run read, `attached` for the slice the agent attached in Onus Cloud, or `onus-cloud` when only Onus Cloud's calls are known. `calls` sums up Onus Cloud's log of the calls made under the task token (`source: onus-cloud`). | | `gaps` | What the record cannot vouch for, and how to fill it. | Who or what approved it: | Stage | Final lane | Judge verdict | `approval.by` | |---|---|---|---| | open | any | any | `pending` | | closed | any | any | `none` | | merged | `auto-merge` | any | `policy` | | merged | `judge` | `approve` | `judge` | | merged | `judge` | `escalate`, `reject` or none | `person` | | merged | `human` | any | `person` | | merged | `blocked` | any | `override` (it merged although Onus blocked it) | `approval.people` lists the people who approved rows, the approving reviewers, then who merged it (`onus ci closed --merged-by`), each once. When a person approved and none is known, `gaps` says so. A gap is listed for each of `intent`, `token`, `evidence`, `escalations`, `judge` and `doors` that is empty. In CI no task token reaches the run, so `token` is a gap there unless Onus Cloud passes the door's inspection or the agent's submission comes with the change. An escalations list from Onus Cloud is on record even when it is empty, so it is not a gap. `doors` stays a gap in a hosted run: the door log the agent attaches, and Onus Cloud's calls, narrow it, never close it, since each is on its sender's word. What a run in CI can know depends on what reaches it: - **The agent's submission** (`onus ci pr --submission FILE`, the action's `submission` input, or one kept on the records branch at `submissions/.json` or `submissions/.json`). It brings the task token's facts (`token.source: agent-submission`) and the escalations it lists, on its word: the file is plain JSON, and the record cannot tell it was verified when it was made. Its token ranks below the door's word, and the escalations it lists are recorded as granted only until Onus Cloud says otherwise; while nothing else confirms them, the `escalations` gap says they are the agent's word. It must be for one of the pull request's commits; one for another commit is left out with a warning, and so is one that cannot be read. Only its token and escalations are taken: the intent, the agent and the evidence are this run's. When the pull request closes, the action rebuilds the record with `onus provenance --agent-submission FILE`, which takes it the same way: for one of the commits in the pull records (or in `--repo` between their base and head), and only its token and escalations. - **What the action cannot know.** It cannot verify the token itself: a submission keeps the token's task, rights, expiry and fingerprint, never the token, and no root key reaches CI, so it vouches only for what was verified when the submission was made. Whoever can write to the records branch can put a submission there. The action does not see the door log, the escalations or the calls Onus Cloud keeps; a host that has them passes `--audit`, `--escalations` and `--calls`. Without a submission or Onus Cloud, `token`, `escalations` and `doors` are gaps. - **Onus Cloud** passes the escalations with the door's inspection of the token. Its call log (`--calls`) and the door log the agent attached (`--audit`) reach the record only from an Onus Cloud that keeps them and passes them on; an older one refuses a door log on its evidence route. `digest` is the sha256 of the record's compact JSON with the digest left empty. `--verify` checks it and the schema version, and exits 1 when either does not match. The format is `schemas/provenance.schema.json`; new optional fields may be added under `schema: 1`. ## In CI In the human lane, `onus ci pr --reviews ` decides whether the change has the review it needs: an approval of the head commit from someone with write access who is not a bot and did not open the pull request, and no outstanding request for changes. It also names the owners of the rows that need a person (onus.yaml, else CODEOWNERS) for the host to ask, each once per pull request. `lanes.approval` says whose approval counts. With `owner`, the default when the change has owners, only an owner counts: a person named as an owner, or a member of an owning team when the host says who is in it. With `member`, the default when nobody owns what changed, anyone with write access counts. When Onus cannot check an owner (a team whose members it was not given, or an email), or no owner but the author could approve, anyone with write access counts and the review says why. The rule and the owners are read at the base commit (onus.yaml, then CODEOWNERS), so a change can neither loosen the rule for itself nor name its own approver. The rule applies in the human lane only, and it is kept on the pull record, the outcome and the provenance record (`approval.review`). The action (with `records: true`) and Onus Cloud both pass the reviews, ask the owners to review, and keep their check failing or waiting until a review counts; see [`onus help ci`](/docs/ci). The action classifies every pull request with `lanes: true` and shows the lane in its comment. `apply-lane: true` labels the pull request `onus/lane:` and fails the job for `blocked`; `auto-merge: true` enables GitHub's auto-merge for the `auto-merge` lane. See [`onus help ci`](/docs/ci). With `records: true`, the action also keeps the outcome records on an `onus/records` branch: outcomes when pull requests close, reverts when the default branch moves, and approvals, audits and incidents from `/onus` comments. --- ## Introduction URL: https://onushq.com/docs What Onus does, how to install it, and where to find each topic. # Introduction Onus turns a pull request into a short report of changes in meaning: new external services, contract changes, new relationships between components, weakened tests, broken rules. Every row is computed from the code by deterministic analysis, never generated, and links to the files and lines behind it. Onus only reads files: it never installs or runs the code it analyzes. ## Install With Homebrew, on macOS or Linux: ```sh brew install onushq/tap/onus ``` Or with the installer, which checks the download's SHA-256 and puts `onus` in `~/.local/bin`: ```sh curl -fsSL https://onushq.com/install.sh | sh ``` Then, in a git repository: ```sh onus report --base main --head HEAD ``` Archives for macOS, Linux and Windows are on the [releases page](https://github.com/onushq/onus/releases), and the GitHub Action reports on every pull request (see [CI](/docs/ci)). ## The guide These pages are the guide that ships inside the `onus` binary: `onus help ` prints any of them in the terminal, offline. | Topic | What it covers | | --- | --- | | [getting-started](/docs/getting-started) | Install Onus, build a map and read your first report | | [commands](/docs/commands) | Every command and flag, with examples and exit codes | | [reports](/docs/reports) | Reading a report: rows, ranking, evidence, the changed lines tests ran and the JSON | | [changes](/docs/changes) | Every kind and subkind of change Onus reports | | [configuration](/docs/configuration) | The onus.yaml reference: components, rules, labels, extractors | | [intent](/docs/intent) | Checking a change against its stated intent | | [ci](/docs/ci) | Running Onus on every pull request, and outcome records kept by CI | | [agents](/docs/agents) | The map for coding agents: onus mcp, its tools, many agents and worktrees | | [scopes](/docs/scopes) | Task tokens, the git gateway, escalation with evidence and the audit log | | [lanes](/docs/lanes) | Risk lanes, change submissions, the learned model, the verifying judge, outcome records and provenance | | [production](/docs/production) | Production signals, rollout plans per lane and automatic rollback | | [web-interface](/docs/web-interface) | The web interface: onus ui opens all of it in your browser | | [environments](/docs/environments) | Environments built from a commit, the evidence store, traces and coverage into the map, and the behavior diff | | [cloud](/docs/cloud) | Onus Cloud from the CLI: onus login, scoped tokens, hosted environments, evidence, escalations, maps and agents | | [plugins](/docs/plugins) | New languages and frameworks, SCIP indexes, language servers, trusted mode | | [how-it-works](/docs/how-it-works) | How maps are built and compared | | [troubleshooting](/docs/troubleshooting) | Map confidence notes, common questions and current limits | The source is in the [onus repository](https://github.com/onushq/onus/tree/main/crates/onus-cli/guide). --- ## Plugins URL: https://onushq.com/docs/plugins New languages and frameworks, SCIP indexes, language servers, trusted mode # Plugins: new languages, frameworks and compiler-backed facts Onus builds a map with *providers*: - **discovery** providers find components (built in: `onus.yaml`, Nx projects, npm/pnpm/yarn workspaces, folders); - **language** providers turn source files into symbols, shapes, references and tests (built in: TypeScript and JavaScript on tree-sitter); - **fact** providers add facts to the map built so far (SCIP indexes, framework plugins). Plugins add providers without changing Onus. They are listed in a *plugins file* that you pass with `--plugins ` (or `ONUS_PLUGINS`). The plugins file belongs to whoever runs Onus: Onus never reads plugins from the analyzed repository, so a pull request cannot make CI run new code. ## Framework packs: no code at all Most framework knowledge is a pattern in the code: a decorator, a method on a known client, a call with a literal argument. A *pack* is a YAML file of tree-sitter queries and what a match means. Packs never run code, so they may be listed in the analyzed repository's `onus.yaml` (`extractors.packs`) as well as in the plugins file (`packs`). Like the rest of `onus.yaml`, packs are read from the base version of a change: a pull request cannot edit a pack to hide what it adds, and editing one is reported as a rules-of-the-game change. ```yaml pack: nestjs rules: - id: route query: | ((decorator (call_expression function: (identifier) @method arguments: (arguments . (string) @path))) (#match? @method "^(Get|Post|Put|Patch|Delete)$")) emit: { route: "{method|upper} {path}" } - id: on-event query: | ((decorator (call_expression function: (identifier) @fn arguments: (arguments . (_) @name))) (#eq? @fn "OnEvent")) emit: { event: consumes, name: "@name" } - id: cache-write query: | ((call_expression function: (member_expression object: (identifier) @c property: (property_identifier) @op)) (#eq? @c "redis") (#eq? @op "set")) emit: { edge: writes, to: "db-table:redis-cache" } ``` Each rule has an `id`, a tree-sitter `query` over the TypeScript grammar (the same query runs on TSX and JavaScript), and one `emit`: | Emit | Means | |---|---| | `event: publishes` or `consumes`, `name: "@capture"` | An event, named by the capture; literals, constants and enum members are resolved | | `data: reads` or `writes`, `table: "