The Repowise PR bot is a GitHub App that runs the same intelligence layers as the CLI and dashboard against every pull request. It posts one short comment, with a checks section when coverage, security or docs have something to say, optional pass/fail checks a branch rule can require, and review threads on the lines that need them. In Signals, the default mode, the comment is deterministic and posts only when there is something worth saying: no model, no per-PR tokens, and free forever for public / OSS repos. AI triage and Full AI review add a model and are part of Pro, billed from credits. Private repos need the Pro plan too. See review modes.
Install it from the GitHub App page: github.com/apps/repowise-bot. Pick individual repos or a whole org. There is also a marketing overview and you can manage installs from the dashboard.
Install
Install the GitHub App. Open github.com/apps/repowise-bot, choose the repos (or the whole org), and approve.
Connect the install to your account. GitHub sends you back to
repowise.dev/bot/installed, which shows a "Finishing setup" state while it
waits for GitHub's webhook to register the installation. It polls for that on
its own, up to eight times at 1.5-second intervals, and tells you if
registration is taking longer than expected. Two cases from there:
- Personal account install. Claimed automatically as soon as you are signed in. Nothing to click. If the claim fails the page says so and offers a retry button.
- Organization install. An organization admin has to confirm it. The page shows a "Confirm organization access" button, which needs a GitHub sign-in fresh enough to carry a provider token; without one the button is disabled and the page asks you to sign in with GitHub again.
If you are not signed in at all, comments still appear on pull requests. You just cannot manage the install or claim the indexed repos until you sign in.
Indexing starts. Once the install is connected, the default branch of each
covered repo is indexed through the same pipeline the hosted dashboard uses,
typically 3 to 10 minutes per repo. Until a repo's first snapshot exists the
page lists it as indexing.
Open a pull request. On opened / synchronize / reopened, the bot
re-parses the changed files, computes the health delta between base and head,
and decides whether to comment.
Read the report. Each comment links to the pull request's page on
repowise.dev (/pr/{owner}/{repo}/{number}), with the full analysis,
findings and tests to run.
Permissions requested: Contents: Read · Pull requests: Write · Metadata: Read · Issues: Write (GitHub treats PR comments as issue comments). The optional merge gate, pass/fail checks and annotations on lines additionally need Checks: Write; existing installs re-consent when you turn either on. The one-click coverage setup needs Contents: Read and write and Workflows: Read and write.
Review modes
The org policy picks the mode for every repo
in an installation, and a repo can override it (Settings → PR bot). The mode is
set only on the dashboard: the bot ignores review_mode in
.repowise/bot.yaml, so a pull request from a fork cannot turn on a paid pass.
| Mode | What it does | Plan | Cost |
|---|---|---|---|
| Signals | Comments when a contract breaks, a finding lands, or tests or companion files are missing. No model runs. | Free on public repos | None |
| AI triage | When a signal fires, a model picks what matters from it and writes the comment. | Pro | 1 to 2 cents a pull request |
| Full AI review | A model reads the whole diff and the code around it on every PR, and a second pass checks each point against the lines it cites before it is posted. Each point is a review thread on its line, and the PR comment links to it. A clean PR gets one line saying so. | Pro | About 20 to 35 cents a pull request, re-reviews included |
AI review draws on the same credit balance as chat, docs and MCP, with no per-seat review fee. Pro includes $5 of credits a month, about 15 to 25 fully reviewed pull requests, and 100 pull requests cost about $20 to $35. Each repository also has a monthly AI spend limit ($5 by default). When the plan does not include AI review, the balance is empty, or the limit is reached, the bot posts only what Signals would and notes the pause on its next comment.
The silence rule
This applies in Signals and AI triage. Most PR bots are noise. This one stays silent unless there is a measurable signal worth interrupting a reviewer for. A green PR with no findings gets no comment at all. Full AI review is the exception: it reviews every PR and posts one line on a clean one. When the bot does comment, improvements are shown alongside problems so cleanup gets credit. It posts when any of these fire:
- net repo health degrades, or any modified file's score drops;
- a hotspot file (high churn + many dependents) is touched;
- a top co-change partner of a modified file is missing from the PR;
- new dead code is introduced (or removed, which is a win);
- a modified file is on a declining-health trajectory;
- a changed file carries substantial recent bug-fix history;
- the diff's change-risk score lands in this repo's high band.
The bot edits its existing comment when new commits land instead of posting a wall of duplicates; if the silence rule fires after an earlier comment, the stale comment is deleted.
What's in the comment
The comment is short and leads with what the reviewer should do. Each part
renders only when it has something to say, and the full analysis is one link
away on the pull request's page (/pr/{owner}/{repo}/{number}):
| Part | What it shows |
|---|---|
| Verdict and review actions | The evidence that made the bot post, as actions: a broken contract, a finding this PR introduced, a missing companion file, the tests to run. At most three. |
| Health line | The repo score and how this PR moved it (show_health). |
| Gate line | The merge gate's verdict, when the gate is on. |
| Coverage | One verdict line: patch coverage of the changed lines, the project delta, and the threshold when one is set. The uncovered changed lines are folded under it, per file, each range linked to its lines. Measured, and only when CI uploads a report (see coverage setup). Nothing renders while CI is still running. |
| Security | New findings on the lines the pull request adds, at or above security_shows_at. Secrets are masked. |
| Tests | Only when there is no coverage report and none is coming: the changed files no test reaches through the call or import graph. Inferred, so it never shows a percentage. |
| Doc drift | Opt-in (doc_drift). Refuted claims in docs the pull request edits. Documents that only name a changed file stay on the PR page and in the Repowise / doc drift check run. |
| Evidence | A folded table of what is known and unknown about the change: owners, components, provenance with its coverage. Standard detail only. |
| Reviewers | Suggested reviewers (show_ownership). Standard detail only. |
Coverage, security, tests and doc drift form the comment's checks section (see checks). In Signals nothing in the comment is written by a model, so every comment is reproducible. AI triage and Full AI review write their part with a model, and the pull request page names the model that wrote it.
Findings on lines
on_lines decides what lands on the changed lines themselves:
| Value | What it posts |
|---|---|
off | Nothing on the lines; the findings stay in the comment. |
annotations | Check Run annotations in Files changed: high and critical health findings on lines this PR added, and changed signatures that code outside the PR still calls. With the merge gate off they ride on a neutral check posted only when there is something to mark. |
threads | The default. The strongest findings as review threads you can reply to: leaked keys, breaking signature changes with outside callers, and Full AI review points. |
both | Annotations and threads. |
In Full AI review its points on code this PR wrote are capped by the review sensitivity.
The merge gate
Beyond commenting, the bot can post a GitHub Check Run named
Repowise / code health that a branch protection rule can require, turning
code health into a merge gate. It is off by default (health_gate.mode):
off: no check is created.advisory: always concludesneutral. The verdict is visible on the PR but never blocks. The safe default to start with.blocking: concludesfailurewhen a rule trips, which a required check will block the merge on.
The rules (evaluated only in blocking mode):
| Rule | Blocks when |
|---|---|
repo_drop_block | The repo health score drops by more than this many points (default 0.3). |
min_new_file_score | A changed file scores below this floor, out of 10. |
block_on_introduced | The PR introduces a finding in its added lines. |
block_at | With block_on_introduced, the lowest severity that fails: critical, high, medium or low. Unset means any severity. |
Or start from a named profile and nudge single knobs: health_gate.profile
accepts warn-only, block-new-file-floor or block-any-decline. The
profile seeds the rules; any explicit key set alongside it wins.
not_evaluated decides what every Repowise check run concludes when it
could not evaluate, for example no health score yet or no coverage report:
neutral (the default) passes a required check, failure holds the merge
until it can evaluate.
Because the bot already re-scores the repo at both the base and head commits, the gate needs no extra index; it reads the same per-PR analysis the comment is built from.
Checks
The comment's checks section carries what the tests, the security scan and the
docs say about the change (see what's in the comment).
checks_comment decides when it renders:
on_signal(the default): only when a check has something to say. Once shown it is always updated, to an all-clear line when the findings are gone, so a stale finding never lingers.always: on every pull request.off: never.
Pass/fail checks
Each check can also post its own GitHub check run, which a branch protection
rule can require. check_runs lists the ones that run; all are off by default
and work with or without the checks section:
check_runs entry | Check run | Fails when |
|---|---|---|
coverage | Repowise / coverage | Patch coverage is below coverage_fail_under (a percent, 0 to 100). With no minimum it reports the figure and concludes neutral. |
security | Repowise / security | The pull request adds a finding at or above security_fail_on (high by default). |
doc_drift | Repowise / doc drift | A doc the pull request edits carries a claim the code refutes, at high confidence. |
A check that could not evaluate concludes as not_evaluated says.
Coverage setup
Measured coverage needs a coverage report from your CI. In the dashboard
(Settings → PR bot → a repo's settings → Checks), Set up coverage has
the bot open a draft pull request that adds
.github/workflows/repowise-coverage.yml: it runs your tests with coverage and
uploads the report with the step below. The bot detects the stack (Python,
Node, Go, Java with Maven or Gradle) and the pull request says how to adjust
the test command. If your
tests already run in another workflow, it explains how to move the upload step
there so they run once. Review it, mark it ready and merge it; the next pull
request carries a coverage line.
The one-click setup needs two extra repository permissions on the PR bot: Contents: Read and write and Workflows: Read and write. Existing installs see "Approve the new permission" in the dashboard, which links to the installation's settings on GitHub. Everything else (the checks section, check runs, uploads) works without them.
Or add it yourself. The dashboard shows the generated workflow, and the upload
step alone, to copy. Add the step to the job that runs your tests, after the
step that writes the coverage report, and give the job the id-token: write
permission:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # lets the upload prove it comes from this repository
steps:
# ... check out, install, and run your tests with coverage ...
- name: Upload coverage to Repowise
if: always()
continue-on-error: true
env:
REPORTS: "coverage.xml" # space-separated paths or globs
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
shopt -s globstar nullglob
files=()
for pattern in $REPORTS; do for f in $pattern; do [ -f "$f" ] && files+=(-F "report=@$f"); done; done
if [ ${#files[@]} -eq 0 ]; then echo "::warning::Repowise: no coverage report found at: $REPORTS"; exit 0; fi
auth=()
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
token=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=repowise" | jq -r '.value // empty' || true)
[ -n "$token" ] && auth=(-H "Authorization: Bearer $token")
fi
curl -sS --fail-with-body --max-time 60 "${auth[@]}" "${files[@]}" \
-F "repository=$GITHUB_REPOSITORY" -F "head_sha=$HEAD_SHA" -F "pr_number=$PR_NUMBER" \
-F "event=$GITHUB_EVENT_NAME" -F "run_url=$RUN_URL" -F "workspace=$GITHUB_WORKSPACE" \
https://api.repowise.dev/ci/coverage \
|| echo "::warning::Repowise: coverage upload failed; see the message above."It is plain curl and jq, so it needs no extra action. It never fails the
job: a missing report or a failed upload is a warning in the log, and the
server's answer (including how many report paths did not match a file in the
repository) is printed there too.
REPORTS is a space-separated list of report paths or globs (** works),
relative to the repository root. Supported formats: LCOV, Cobertura XML,
JaCoCo XML, Clover XML, Go coverprofile, and the Repowise coverage JSON. The
format is detected from each file, and absolute paths in a report are made
relative to the checkout. Typical values:
| Stack | Test command (example) | REPORTS |
|---|---|---|
| Python | pytest --cov --cov-report=xml | coverage.xml |
| Node | npx vitest run --coverage --coverage.reporter=lcov | coverage/lcov.info |
| Go | go test ./... -coverprofile=coverage.out | coverage.out |
| Java (Maven) | mvn -B verify with the jacoco-maven-plugin report goal | target/site/jacoco/jacoco.xml |
| Java (Gradle) | ./gradlew test jacocoTestReport | build/reports/jacoco/test/jacocoTestReport.xml |
| Several or unsure | "coverage.xml coverage/lcov.info lcov.info coverage.out **/jacoco.xml **/cobertura*.xml **/clover.xml" |
Runners. GitHub-hosted runners have curl and jq installed.
Self-hosted runners need curl and jq; without jq the upload has no identity
and is refused.
Fork pull requests. The upload proves which repository it comes from with a short-lived identity token from the workflow, so no secret is needed. A pull request from a fork gets no such token. Its upload is accepted only when the repository is public and the upload is for the open pull request's current head commit; the comment then labels it "uploaded from a fork pull request without verified identity". Such an upload never replaces a verified one for the same commit. On a private repository, fork uploads are refused.
Getting coverage into hosted
Measured coverage reaches a repo's Tests page and the hosted MCP tools by one of two routes. Both read the same report formats your test runner already writes.
| Route | Where | Gives |
|---|---|---|
| CI upload | The upload step above, from a GitHub Actions job | File and line totals, plus the per-test map when a default-branch push run sends a report that carries it |
| Manual upload | The upload panel on the repo's Tests page | File and line totals, plus the per-test map when the report carries it |
Formats. The manual upload takes LCOV, Cobertura XML, Clover XML, the
Repowise coverage JSON, and a coverage.py .coverage file. The CI upload takes
the formats listed under REPORTS above, plus a .coverage file.
Default-branch pushes fill the Tests page. Only an upload from a push to
the default branch reaches the Tests page and the MCP tools. Uploads from pull
request runs feed the pull request coverage check and nothing else, so run the
coverage job on both events:
on:
push:
branches: [main] # your default branch
pull_request:The job needs permissions: id-token: write (and contents: read) so the
upload can prove which repository it comes from.
Per-test map. Knowing which tests to run for a change needs a report that
records which test covered each line. Produce it with
pytest --cov=<package> --cov-context=test (needs pytest-cov), which writes a
.coverage file, or as an LCOV report where each block carries a distinct
TN: test name. See
Building a per-test map.
Send it either way:
- Manual upload on the repo's Tests page.
- CI upload from a
pushrun on the default branch, with.coverageinREPORTS(the step sends it as-F report=@.coverage). The workflow the bot generates for a Python project sends it on push.
Pull request runs give totals for the pull request check only, even when they
send .coverage.
Where it shows. The Tests page shows per-file and per-module coverage,
untested hotspots, and the risk-versus-coverage view. Through the hosted MCP,
get_risk returns tests_to_run and
get_change_risk lists impacted tests, both from
the per-test map.
Checks settings
| Key | Default | Meaning |
|---|---|---|
checks_comment | on_signal | When the checks section renders: off, on_signal or always. |
coverage | show | Coverage in the comment: off or show. check_run shows it and also runs Repowise / coverage. |
tests_reach | true | Name the changed files no test reaches when there is no coverage report. |
show_security_check | true | Security in the comment. |
security_shows_at | med | Lowest severity shown: high, med or low. |
doc_drift | off | Doc drift in the comment: off, show, or a confidence (high, medium, low) at which a refuted claim alone posts the comment. |
check_runs | [] | Pass/fail check runs: any of coverage, security, doc_drift. |
coverage_fail_under | none | Patch coverage percent below which Repowise / coverage fails. Empty reports only. |
security_fail_on | high | Lowest severity that fails Repowise / security: high, med or low. |
not_evaluated | neutral | What a check run concludes when it could not evaluate: neutral or failure. |
Configuration
Settings come from layers, lowest first; a higher layer wins for each key it sets:
- the bot's defaults;
- the org policy of the installation;
.repowise/bot.yamlin the repo;- the repo's own settings on the dashboard;
- the keys the org policy locks.
Policy keys decide a gate, so a pull request cannot loosen them from its own
branch: health_gate, check_runs, coverage_fail_under, security_fail_on,
not_evaluated, ignore_paths and apply_labels are read from
.repowise/bot.yaml on the base branch only. Paid keys (review_mode,
ai_monthly_cap_cents) are read from the dashboard only. ignore_paths is a
union across layers, so a layer can add scope but never un-ignore a path
another layer skips. A malformed value is skipped and the layer below stands.
Org policy and repo overrides
Settings → PR bot → an installation opens its org policy: the settings every repo it covers starts from. It offers three presets:
| Preset | Sets |
|---|---|
| Quiet | Posts only for critical findings. No pass/fail checks, no merge gate. |
| Standard | The bot's defaults: posts for high-severity findings, no pass/fail checks, no merge gate. |
| Strict gate | Posts from medium severity, check_runs: [coverage, security], a blocking merge gate, and not_evaluated: failure. |
No preset changes the review mode, which is a paid choice of its own. The page shows how many repos follow the preset (override none of its keys), and can make selected repos follow it: after a confirmation naming the repos and the settings, it clears their overrides of the preset's keys and leaves the rest.
Each policy row has a lock. A locked key wins over the repo's dashboard
settings and its .repowise/bot.yaml, so a repo cannot loosen it; the repo's
page shows the row read only. Locking a key the policy does not set yet pins
the value the repos get at that moment. The merge gate locks as one key.
A repo's page (Customise on the policy, or Settings on a repo row)
shows every setting with its value and where it comes from: Default,
Org default, Overridden or Locked by org. A change there overrides
the policy for that repo; Reset on a row, or Reset all to org default,
removes the override. The dashboard cannot see .repowise/bot.yaml, so a value
marked Default or Org default may still be changed by the repo's YAML.
.repowise/bot.yaml
Committed and versioned with the code:
bot:
enabled: true
comment_mode: on_signal # or "always" to post a clean bill of health
verbosity: standard # compact | standard
introduced: high # off | show | critical | high | medium | low
recent_decline: show # same values; "show" never posts on its own
change_risk: signal # off | signal (PR page) | comment
on_lines: threads # off | annotations | threads | both
apply_labels: false # opt in to repowise:* PR labels
ignore_paths:
- "vendor/**"
- "**/*.generated.*"
health_gate:
mode: advisory # off | advisory | blocking
repo_drop_block: 0.3
min_new_file_score: 7.0
block_on_introduced: true
block_at: high
checks_comment: on_signal # off | on_signal | always
check_runs: [coverage] # coverage | security | doc_drift
coverage_fail_under: 80 # percent of changed lines; omit to report only
not_evaluated: neutral # neutral | failureThe comment's parts each have a switch: show_health, show_checklist,
show_coupling, show_ownership, show_evidence and show_declining_alert.
Pre-v2 key names (show_introduced with introduced_posts_at,
inline_threads, show_findings_on_lines, coverage_check_run,
checks_comment_mode and the rest) still read into the key they became.
PR labels (opt-in)
With apply_labels: true the bot also applies repowise:* labels, created on
demand and reconciled on every run: a label is removed once its signal clears,
and labels outside the repowise: namespace are never touched.
| Label | Applied when |
|---|---|
repowise:gate-failed | A blocking gate you configured actually tripped |
repowise:risk-high | Top tercile of this repository's own commit-risk distribution |
repowise:missing-tests | A file with recent fix history changed, and no test that imports it was touched |
repowise:size-xl | Too large for granular analysis |
They are off by default, and the list is short on purpose. A label is permanent, binary, and public in the pull-request list, so it reads as a verdict on the author in a way an editable comment does not, which means it has to be reserved for things that are actually rare. Signals that fire on ordinary work (a repo score that dipped, a hotspot touched, a moderately-sized diff) stay in the comment, where they carry their context with them.
Pricing
Signals is free forever for public / OSS repos: no model, no tokens, no PR cap. AI triage and Full AI review are part of Pro and billed from credits (see review modes). Private repos require the Pro plan as well. Private installs still index on connect, so the snapshot is ready when you upgrade. See the pricing page.
How it relates to the rest of Repowise
Same engine, different surface. The bot reads the code-health,
git-intelligence, and
dependency-graph layers, and every
comment links to the pull request's page on the dashboard, where the same data is queryable
by humans (the web UI) and by AI agents (the MCP tools,
including get_health for a pre-PR self-check).