cora

a convention-oriented review agent

pre-release · self-hostable agentic PR review · source

cora reads a pull request the way a careful colleague would - diff, history, surrounding code, your conventions - and produces a verdict your merge gate can require. Built for GitOps and IaC teams running their own LLM, extracted from a cluster where it reviews every PR in production. No vector store, no SaaS, no agent platform required.

§1 A review, end to end

Every review starts from a plain pull_request event on a GitHub-hosted runner and ends in three artifacts: a sticky comment, a check run, and (optionally) proposed patches. In between, cora picks one of two modes.

                 pull_request event
                        │
                 TriggerPolicy ──── deny ──▶  skip · check run fails closed
                        │ allow
        context: diff + PR metadata + retrieved docs
                        │
          ┌─────────────┴─────────────┐
        quick                       deep
   one LLM call,             multi-turn agent loop:
   no tools                  grep_repo · git_show · MCP,
                             token + tool-call budgets
          └─────────────┬─────────────┘
                  wall-clock hit?
                        │──── yes ──▶  escalate a tier, continue
                        │
              verdict:  🟢 · 🟡 · 🔴
                        │
        ┌───────────────┼───────────────┐
  sticky comment    check run «cora»   propose_patch
  edited in place,  required by your   hybrid patch
  <!-- cora:v1 -->  branch protection  dispatch
Fig. 1 - the review flow. Soft failures skip loudly; they never fake a verdict.
quick
A single LLM call over the assembled context. Cheap, fast, no tools - the mode untrusted PRs are forced down.
deep
A multi-turn agent that greps the checkout, reads blame and history, and calls your MCP tools - under explicit token and tool-call budgets. When a deep review hits its wall-clock limit, an escalation ladder can hand the transcript to a bigger model and keep going instead of giving up. A single-model deployment is simply a one-rung ladder.

Review prompts are conventions, not magic: cora is told to verify before it flags, to reserve its strongest severity for breakage it has personally confirmed, and to say nothing about style your linters already enforce. A false-positive blocker on a teammate's PR is treated as the worst failure mode the system has.

§2 The verdict contract

The whole point of an automated reviewer is a signal you can gate on. cora's output is a fixed, parseable vocabulary:

verdictmeaningcheck run
🟢 looks goodnothing worth holding the merge forsuccess
🟡 minorsuggestions worth reading, none blockingsuccess
🔴 needs changesat least one verified blockerfailure
- no verdictreview skipped, errored, or deniedfails closed

Findings inside the comment use two severities: 🚨 Blocker - only for things the agent has verified will break, with file:line and what to fix - and ⚠️ Concern for suggestion-grade issues. The verdict lands as a check run named cora; make it a required status check and the verdict is your merge gate. A review-override label is the deliberate, audit-visible escape hatch when a human decides the robot is wrong.

cora commented on this pull request

<!-- cora:v1 -->

cora review - 🟡 minor

  • ⚠️ Concern: charts/ingress/values.yaml:41 - the new timeout is set on the Service but the upstream annotation at templates/ingress.yaml:23 still pins 30s; the lower one wins.
  • ⚠️ Concern: scripts/rotate.sh:12 - set -e without pipefail; the curl | jq on line 19 can fail silently.

deep review · 2 tiers · 41 tool calls · edited in place on the next push

Fig. 2 - the sticky comment. One comment per PR, found by its marker and edited on every re-review, so the thread never silts up.

Re-reviews are CI-failure-aware: when your pipeline fails after a verdict, cora re-reads the PR with the failure in hand. And because the engine targets self-hosted reasoning models, it strips chain-of-thought leaks from output before anything reaches GitHub.

§3 Zero infrastructure by default

Everything cluster-shaped sits behind a provider seam with a working default, so pip install plus an OpenAI-compatible endpoint is a complete deployment - and each seam upgrades independently when you have the infrastructure to back it.

seamdefaultupgrades to
RetrievalProvider none - review runs on the diff and your conventions file BM25 over your local docs (Glob, zero infra), or hybrid dense+sparse Qdrant with TEI reranking
GitProvider the local checkout - grep_repo, git show your own SCM backend
Reporter GitHub comment + check run NullReporter for dry-runs and evals - a review returns a plain ReviewResult you can inspect
LLM any OpenAI-compatible endpoint - built for self-hosted models, with an escalation ladder across however many tiers you run

CI for cora itself runs on GitHub-hosted runners with no private infrastructure - the same constraint an adopter starts from.

§4 Reviewing hostile input

A PR reviewer executes a tool-using agent against attacker-controlled text. cora's posture is diff-as-data: PR content is evidence to evaluate, never instructions to follow - and an injection attempt in a diff is itself flagged as a 🚨 Blocker.

Above the prompt sits TriggerPolicy, a default-deny gate on who may fire a review and at what capability: author trust rules (association, allowlist, or an approval label), a comment-only ceiling for untrusted and fork PRs - forced quick mode, no patch dispatch - and rate caps per author and per repo. Trust lookups fail closed. The shipped workflow template runs on plain pull_request, never pull_request_target.

The full threat model, policy reference, and canonical workflow wiring live in SECURITY.md. Read it before pointing cora at a repo where strangers can open PRs.

§5 One config object

A review is driven by a single dataclass, ReviewerConfig. Every engine tunable is a field on it; from_env() maps the same fields onto workflow environment variables, so a deployment is just env vars in a job. No YAML dialect, no second config system.

from cora import ReviewerConfig, run_review

cfg = ReviewerConfig.from_env()          # or construct it directly
result = run_review(cfg)                 # ReviewResult: verdict, findings,
                                         # tiers run, budgets spent
Fig. 3 - the whole public API surface, more or less.

Default review prompts ship in the package; point a config field at your own markdown to replace them. Telemetry (OpenTelemetry traces of every tier, tool call, and budget) is an opt-in extra, never a hard dependency.