Serve daemon & GitHub Action
Two ways to run miu-cr’s PR review automatically. Both reuse the exact same
in-process review path as miucr review --pr (GitHub PR review):
there is no second engine and no shelling out.
miucr serve: a long-running daemon you host. The default mode handles webhooks or lightweight polling from CLI flags. Host mode (--host) loads a YAML file, uses Postgres, and watches multiple repositories from one process.- The composite GitHub Action: no daemon to host. A workflow in the target
repo installs the released binary and runs
miucr review --pr --post.
By default both are PAT + webhook-secret only. serve can also opt into a GitHub App installation-auth backend and an authenticated JSON REST API for queuing/reading reviews; see REST API & GitHub App auth. Those are opt-in; this page documents the default webhook + Action paths.
miucr serve
Section titled “miucr serve”WEBHOOK_SECRET=… GITHUB_TOKEN=… ANTHROPIC_API_KEY=… \ miucr serve --addr :8080 --repos owner/repo,owner/other --gate highEndpoints
Section titled “Endpoints”| Method | Path | Behaviour |
|---|---|---|
POST | /webhook | HMAC-verified GitHub pull_request receiver. |
GET | /healthz | 200 {"status":"ok"} liveness probe (no auth). |
Configuration
Section titled “Configuration”| Source | Name | Required | Notes |
|---|---|---|---|
| env | WEBHOOK_SECRET | yes | Shared HMAC secret. Empty → fail-fast serve.secret_required (exit 2): an empty secret would accept forged webhooks. |
| env | GITHUB_TOKEN / GH_TOKEN | yes | PAT with repo scope to clone + post. Empty → serve.token_required. |
| env | ANTHROPIC_API_KEY (or compatible) | yes | LLM credential; see Credentials. |
| flag | --addr | no | Listen address (default :8080). |
| flag | --repos | yes | Owner/repo allowlist, comma-separated. Empty allowlist reviews nothing → serve.repos_required. |
| flag | --gate | no | Publish-severity gate only; controls which findings are posted. It never affects daemon liveness or exit code. |
Secrets are resolved from the environment only. The webhook secret, GitHub token,
and API key are never logged, never put in the JSON envelope, and never
persisted. Every serve-side error is routed through RedactString before it
reaches a log line, because the clone URL embeds the PAT.
Request flow & semantics
Section titled “Request flow & semantics”- Body cap: the request body is wrapped in a 5 MB
MaxBytesReaderbefore HMAC validation; an oversized body is rejected413. - Event guard: non-
pull_requestdeliveries get a cheap200ignore before parsing (so unknown event types can’t crash the parser). - HMAC: a bad or missing
X-Hub-Signature-256is rejected401; nothing is dispatched. - Filter: only
opened,synchronize,reopened,ready_for_revieware reviewed; draft/closed PRs are ignored, and host polling cancels unfinished jobs once the PR leaves the open set. An event for a repo outside--reposis200-ignored and logged. - Respond first: serve returns
200before dispatching, so GitHub’s ~10 s delivery budget is never spent on the LLM review. - Bounded async worker: the review runs on a worker pool, never on the HTTP goroutine. A panic in one review is recovered and can’t kill a worker.
- Per-PR coalesce: two rapid events for the same
{owner, repo, number}collapse to a single in-flight review. (Re-runs are also safe at the publish layer: the summary issue comment is upserted in place, and inline-comment fingerprints prevent duplicates across commits.) - No silent drop: if the queue is genuinely full, the drop is loud-logged and counted; it is never swallowed silently.
GitHub webhook setup
Section titled “GitHub webhook setup”In the target repo: Settings → Webhooks → Add webhook.
- Payload URL:
https://your-host/webhook - Content type:
application/json - Secret: the same value as
WEBHOOK_SECRET - Events: Let me select individual events → Pull requests
Shutdown
Section titled “Shutdown”SIGINT / SIGTERM triggers a graceful HTTP shutdown followed by a pool drain,
so in-flight reviews finish before the process exits.
Poll mode (opt-in)
Section titled “Poll mode (opt-in)”The webhook is the default. For environments that can’t receive an inbound
webhook (a laptop behind NAT, a private runner, a bot PAT), --poll turns serve
into a trigger that periodically asks GitHub which PRs need review and
dispatches each one onto the same review path. It is a trigger only; there
is no second review engine, no change to fork handling or publish.
# Poll-only (no webhook secret needed):GITHUB_TOKEN=… ANTHROPIC_API_KEY=… \ miucr serve --poll --repos owner/repo,owner/other --poll-interval 60s
# Webhook AND poll together (both share one ctx; a secret is still required for# the webhook half):WEBHOOK_SECRET=… GITHUB_TOKEN=… ANTHROPIC_API_KEY=… \ miucr serve --poll --repos owner/repo --addr :8080| Flag | Default | Notes |
|---|---|---|
--poll | off | Opt-in. Without it serve is webhook-only (the default). |
--poll-interval | 60s | Poll floor. Effective interval = max(this, X-Poll-Interval). |
--poll-source | notifications | Candidate source: notifications (default) or pulls. |
--repos | required | The same owner/repo allowlist; bounds the PAT + LLM blast radius. |
Cost model: each new head SHA is one full LLM review. Poll mode reviews a PR once per distinct head commit. The poller keeps a local dedup cursor so the same head is never reviewed twice, but a re-pushed head is a new SHA and gets a fresh review. The allowlist and the per-head dedup are the only spend guards; there is no budget cap, so keep
--repostight and the interval sane.
Candidate sources
Section titled “Candidate sources”notifications(default, lighter): reads your GitHub notifications with aSincecursor and mapsreview_requested/ mention notifications to the PR. Only sees PRs the PAT is subscribed to / requested on, and misses PRs opened before the poller started (cold start). Best for a bot that is added as a reviewer.pulls(full coverage): lists open PRs per allowlisted repo (Pulls.List(state=open)). The only source that works for a PAT not subscribed to a repo and the only cold-start-complete one. Costs one list call per repo per tick; use it when you need every open PR reviewed regardless of subscription.
How a tick works
Section titled “How a tick works”- Enumerate candidates (per the source above), filtered to
--repos; non-PullRequestnotification subjects are dropped. - Pre-
GetPRdedup (notifications only): if a notification’supdated_atis unchanged since last tick, the candidate is skipped with noGetPR(a cheap cost guard before spending any API call to resolve the head). - Resolve the head SHA: one
GetPRfor the notifications source; thepullssource already carriespr.Head.SHA(no extra call). - Per-head dedup: if the cursor already saw this
owner/repo#Nat this head SHA, skip (no review). Otherwise dispatch onto the serve pool. - Record on success only: the head SHA is recorded as reviewed after the
review succeeds (via an
OnDonecallback). A failed/dropped review stays retryable next tick. TheSincecursor advances to the tick start only after every candidate that tick is handled, so no candidate is lost.
Rate limits & interval floor
Section titled “Rate limits & interval floor”The poller never polls faster than the server’s X-Poll-Interval header (read off
each response): the effective wait is max(--poll-interval, X-Poll-Interval). On
a *RateLimitError it sleeps until the rate Reset; on an *AbuseRateLimitError
it honors Retry-After; other transient errors back off exponentially with jitter
(cap ~15 min). On any error the cursor is never advanced and no review is
re-run: there is never a tight retry loop.
The cursor (restart-safe, never holds the token)
Section titled “The cursor (restart-safe, never holds the token)”Dedup state is a small JSON file under ~/.config/miu/cr/poll-cursor.json:
{ "since": "…", "seen": { "owner/repo#N": "<head-sha>" }, "notif_seen": { "owner/repo#N": "<updated_at>" } }- Written atomically (
MkdirAll(0700)+ temp file + rename, file mode0600). - The GitHub token is never a field: it is resolved per tick in memory only, never persisted, never logged.
- A missing or corrupt file is tolerated as an empty cursor (warn, never fatal) so the poller always starts.
- Entries are pruned by staleness (untouched ~14 days), not by absence from a tick, so an open PR that drops out of one tick’s candidate set keeps its reviewed-head and is never re-reviewed.
Poll-mode shutdown
Section titled “Poll-mode shutdown”SIGINT / SIGTERM cancels the shared context: the ticker stops and the worker
pool is drained exactly once (poll-only drains in RunPoll; in webhook+poll
the HTTP server is the sole drainer and the poller never drains), so in-flight
reviews finish and there is no goroutine leak.
Host mode (Postgres multi-repo)
Section titled “Host mode (Postgres multi-repo)”miucr serve --host is the long-running, multi-repository form of serve. It is
designed for a local server, VM, or small container deployment where one daemon
watches several repositories, keeps durable state in Postgres, and dispatches
each PR head as a separate review session.
MIUCR_CONFIG=/etc/miu/cr/host.yaml \MIUCR_STORE_BACKEND=postgres \MIUCR_PG_DSN='postgres://miucr:miucr@localhost:5432/miucr?sslmode=disable' \ miucr serve --host
# Validate YAML and print a redacted summary only.MIUCR_CONFIG=examples/review-host/config.example.yaml \ miucr serve --host --dry-run-config -o jsonHost mode currently focuses on poll_source: pulls for full open-PR coverage.
The classic miucr serve webhook path above remains available for webhook-only
deployments; the host YAML reserves webhook fields for the multi-repo host path
without changing the existing webhook contract.
Host YAML shape
Section titled “Host YAML shape”The host config lives at ~/.config/miu/cr/host.yaml by default, or at
MIUCR_CONFIG when set. It is YAML, not TOML, so shared blocks can use anchors
and each repo can stay short:
version: 1default_provider: anthropic
providers: anthropic: kind: anthropic auth_env: ANTHROPIC_API_KEY
store: backend: postgres
github: default_account: personal-pat accounts: personal-pat: mode: pat auth_env: GITHUB_TOKEN app-installation: mode: app app_id_env: MIUCR_GITHUB_APP_ID installation_id_env: MIUCR_GITHUB_APP_INSTALLATION_ID private_key_path_env: MIUCR_GITHUB_APP_PRIVATE_KEY_PATH
x: review_defaults: &review_defaults gate: high filter_mode: diff_context min_severity: low format: full # comment presentation: full (default) | minimal (drops the summary section + all badges) timeout: 900s stalled_timeout: 5m provider_retry: max_retries: 10 initial_backoff: 5s max_backoff: 2m max_elapsed: 10m expand: 20 token_budget: 0 deep_context: true conversation: true tools: max_retries: 2 max_turns: 24 retry_backoff: 250ms
review: <<: *review_defaults
agent: system_prompt_file: prompts/default-reviewer.md
host: workers: 4 poll: true poll_source: pulls poll_interval: 60s workspace_dir: /var/lib/miucr/host/workspaces retention: janitor_interval: 15m workspace_ttl: 168h closed_workspace_ttl: 24h max_workspace_bytes: 50GiB max_workspaces: 100 min_free_space: 10GiB db_ttl: 2160h review: post: true force: false suggest: true patch_repair: false thread_resolution_sync: mode: off interval: 5m approval: mode: off
repos: - name: service-api slug: example-org/service-api git_url: https://github.com/example-org/service-api.git github_account: personal-pat agent: system_prompt_file: prompts/service-reviewer.md rules: - rules/service.md review: min_severity: medium timeout: 1800s stalled_timeout: 8mLayering is intentionally simple: built-in defaults -> top-level review and
agent -> host.review -> repos[].review / repos[].agent. Repo-level
settings are where risky write behavior belongs (post, force, suggest,
patch_repair, thread_resolution_sync, approval) because they decide what
the host may do for that repository. review.tools follows the same
layering, but it is read-only and only controls scan/output limits plus bounded
tool retry behavior.
timeout follows the same layering. The review default is 900s; raise
repos[].review.timeout for large or critical repos instead of making every
host review wait longer. The host uses the resolved timeout for both the review
context and the job lease, so a timed-out review fails cleanly and is retried by
the normal durable job policy.
stalled_timeout is the no-progress watchdog. The default is 5m: every
review progress event resets it, and a run that stays silent longer than this
fails as review.stalled instead of consuming the full total timeout. Use
0s only to disable the watchdog for debugging; otherwise tune it per large
repo alongside timeout.
debounce (default off / 0s) delays a review until the PR head has been
stable for that long, so a burst of pushes coalesces into one review on the
settled head instead of each push superseding the prior in-flight review. It
follows the same review/repos[].review layering — raise it for repos whose
PRs are pushed rapidly. It changes only when a review runs, never the review
itself, so tuning it does not re-review already-reviewed heads. When a debounced
host job is first queued, miucr upserts the summary with a queued-until timestamp;
once the review starts, the same comment switches to a reviewing status while the
previous completed result remains visible.
provider_retry is the provider API retry policy. The default retries 429,
5xx/529, and temporary transport failures 10 times with jittered exponential
backoff (5s initial, 2m max, 10m elapsed) inside the same review timeout.
Tune it per critical or flaky-provider repos; set max_retries: 0 only when you
need fail-fast debugging.
review.subagents follows the same layering. Configure broad defaults at the
top level, then override the scoped agents per repo when a project benefits from
different prompts or globs. A required subagent failure marks the run degraded,
so host approval will not approve and checks-mode will not report success.
Approval is a nested policy, not a boolean. Keep it off at the host level unless all repos should share the same merge behavior, then enable it per repo:
repos: - name: low-risk-service slug: example-org/low-risk-service review: approval: mode: threshold max_priority: P3 note: on_findings
- name: strict-infra slug: example-org/strict-infra review: approval: mode: cleanSymbol context bounds in host mode:
review: provider_retry: max_retries: 10 initial_backoff: 5s max_backoff: 2m max_elapsed: 10m tools: max_retries: 2 max_turns: 24 retry_backoff: 250ms symbol_context: max_bytes: 16000 max_files: 2000 max_parallel: 8max_retries is capped at 5 and applies only to transient tool execution
failures. max_turns caps model tool-use turns before miucr withdraws tools and
forces the final JSON answer; 0 or unset keeps the default 24, and the value
is capped at 64. Bad tool arguments, unknown tools, missing files, and canceled
contexts are returned to the model immediately.
mode: clean approves only zero-finding reviews. mode: threshold approves
when the worst active finding is at or below max_priority. For example,
max_priority: P3 allows P3/P4 findings and blocks approval for P0-P2. By
default, a first approval body is short LGTM-style copy with a link to the code
review summary. Clean re-approvals after a later push can stay bodyless. Set
note: none to suppress approval text, or note: on_findings to keep clean
approvals bodyless and add text only when findings remain. The old
clean-approval key is removed; use review.approval in host and repo configs.
thread_resolution_sync.mode is off by default. Set mode: poll for repos
where the host should periodically mirror GitHub review-thread state into the
summary table. If a miucr inline conversation is manually resolved, the existing
summary row moves to Resolved with conversation resolved; if that same
conversation is later unresolved, only that conversation-resolved row reopens.
interval controls the per-PR polling cadence and defaults to 5m. This never
starts an LLM review and never feeds approval decisions.
GitHub also exposes
pull_request_review_thread
webhooks for resolved and unresolved conversations. The reserved future mode for
event-driven sync is mode: webhook, but current host mode supports only off
and poll.
review.pr_filter also layers top-level -> host.review -> repos[].review.
Draft PRs are skipped unless include_drafts: true. default_action defaults
to include, so normal configs review every ready PR except ones matched by an
exclude rule. Set default_action: exclude for allowlist repos. Rules are
evaluated in order and the last matching rule wins, so repo-level include
rules can override global excludes:
review: pr_filter: default_action: include include_drafts: false comment_trigger_regexes: - '(^|\s)(/miucr review\b|@vanducng\b)' rules: - action: exclude title_regexes: ['^chore\(deps\):']
repos: - name: example-infra slug: example-org/infra review: pr_filter: rules: - action: exclude title_regexes: - '^chore\(deps\):' - '^chore\(fluxcd\):'
- name: example-dbt-project slug: example-org/example-dbt-project review: pr_filter: default_action: exclude rules: - action: include authors: ["vanducng"] - action: include requested_reviewers: ["vanducng"]Matchers inside one rule are ANDed. Use title-only rules for generated PRs that
may be opened by a human account, such as Renovate branches pushed by a
maintainer token. Because rules append across layers, keep broad include rules
out of the global block when a repo should be a strict allowlist.
Supported matchers are authors (exact login), author_types (Bot, User,
Organization), author_associations (OWNER, MEMBER, etc.),
title_regexes, labels, requested_reviewers, base_branches, and
head_branches. comment_trigger_regexes are used by the GitHub Action
comment-trigger workflow, not by serve --host poll_source: pulls, because PR
list polling does not include issue comment bodies. A PR excluded by the filter
is treated as no longer tracked by the host poller, so any prior open session or
pending job for that PR is closed/canceled on the next reconcile.
At MIUCR_LOG_LEVEL=debug, filtered PRs log host: PR ignored by filter with
structured fields for later debugging: repo, pr, head_sha, branches,
author metadata, labels, requested reviewers, reason_code, a short reason,
and matched rule fields such as rule_index, rule_action, and
rule_title_regexes.
Accounts, prompts, and rules
Section titled “Accounts, prompts, and rules”github.accountscan mix PATs and GitHub App installations. PATs supportauth_env,auth_file, orauth_command. App private keys supportprivate_key_path,private_key_path_env, orprivate_key_command.providers.*supportsauth_envandauth_commandtoo. Literalauth_tokenexists for compatibility, but env or command indirection is preferred.agent.system_promptandagent.system_prompt_fileare mutually exclusive. A repo prompt overrides the global prompt; absent repo prompts inherit it.repos[].rulescan reference Markdown files or a non-recursive directory of*.mdfiles. Host rules are trusted operator context, appended after the system prompt. The built-in finding schema and severity contract remain in the protected system prompt.
Storage and retention
Section titled “Storage and retention”Host mode stores repo/session/job/cursor state in Postgres. Set
MIUCR_STORE_BACKEND=postgres as well so review history and trace records use
the same Postgres database instead of the default local SQLite store. The runner
claims jobs with row locks so multiple workers or instances do not review the
same PR head concurrently. Completed jobs, old attempts, closed sessions,
inactive workspace records, and stale poll cursors are pruned by
host.retention. Startup applies versioned Postgres migrations under an
advisory lock and records them in schema_migrations, so multiple host
instances can boot without racing schema changes.
The workspace-size fields are validated and reserved for the managed-workspace
phase. V1 PR reviews still use the existing temp-clone path, so host mode does
not delete arbitrary filesystem children under workspace_dir.
Host logging and trace payloads
Section titled “Host logging and trace payloads”MIUCR_LOG_LEVEL controls process logs (debug, info, warn, error;
default info). The review-host compose example defaults it to debug so local
dogfood shows poll activity, review progress, tool turns, and lifecycle events.
Live trace payload logging is separate. Set MIUCR_TRACE_LOG=true to stream
captured review trace steps into debug logs: system prompt, user prompt,
selected files, injected rules, resolved model, tool calls, tool results, and
final response. Tool results are bounded before they enter the trace. Each
payload is redacted with the same free-text redactor used elsewhere and then
truncated to MIUCR_TRACE_LOG_MAX_BYTES (minimum 256). The review-host
.env.example enables it for local dogfood with a 20000 byte cap per step,
while the compose fallback remains off for deployments without an env file.
Treat it as a local/debug switch because payloads can include prompt and diff
context.
Set MIUCR_TRACE_REASONING=true to add a reasoning trace step capturing what
the model thought before answering: Claude and z.ai GLM 4.5+ return their
thinking verbatim; OpenAI returns only a reasoning-token count plus a
[hidden by provider] marker. It is off by default, redacted in storage like
the other trace steps, and only produces output when [review].thinking is on
(it reads the existing thinking, it does not add a separate reasoning budget).
View it with miucr trace <review-id>.
A complete runnable example is in
examples/review-host/.
The Docker publish workflow builds the same runtime image and pushes it as
ghcr.io/<owner>/miu-cr for VPS or Kubernetes deploys.
GitHub Action
Section titled “GitHub Action”A reusable composite action (the static binary makes a Docker action pure overhead). Drop it into any workflow in the repo you want self-reviewed:
name: PR Reviewon: pull_request: types: [opened, synchronize, reopened, ready_for_review, closed, converted_to_draft]permissions: pull-requests: write contents: readconcurrency: group: miucr-review-${{ github.event.pull_request.number }} cancel-in-progress: truejobs: review: runs-on: ubuntu-latest # Never run on fork code or release automation PRs. if: >- ${{ github.event.pull_request.head.repo.fork != true && github.event.action != 'closed' && github.event.pull_request.draft != true && !startsWith(github.head_ref, 'release-please--') && !startsWith(github.event.pull_request.title, 'chore(main): release ') }} steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: ~/.config/miu/cr key: miucr-${{ runner.os }}-${{ github.repository_id }}-${{ github.event.pull_request.number }} - uses: vanducng/miu-cr@vX.Y.Z # pin a released tag; see github.com/vanducng/miu-cr/releases with: api-key: ${{ secrets.ANTHROPIC_API_KEY }} gate: highThe cache step is optional. It preserves the local state.db review history and
trace records between workflow runs for the same PR. Same-head --post rerun
skipping does not depend on the cache: miucr also writes a hidden completed-publish
marker into the PR summary comment, and only reuses it when the head SHA and
review-shape hash match.
For on-demand reviews, use
examples/workflows/miucr-review.yml.
It adds an issue_comment trigger. By default, a write collaborator can comment
/miucr review ..., @vanducng review ..., or just @vanducng to run a
review. Configure include regexes in [review.pr_filter].comment_trigger_regexes
or review.pr_filter.comment_trigger_regexes; the workflow reads trusted
base-branch config before matching. Repo variable MIUCR_COMMENT_TRIGGER_PATTERN
is only an override.
Inputs
Section titled “Inputs”| Input | Required | Default | Notes |
|---|---|---|---|
api-key | yes | (none) | Review-provider key (sent as ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN). |
github-token | no | ${{ github.token }} | Token to read the PR and post comments. |
gate | no | high | Fail the run if a finding reaches this severity (none…critical). Use none to never block CI. |
version | no | latest | miucr release tag to install. |
base-url | no | "" | Optional Anthropic-compatible gateway base URL. |
model | no | "" | Optional model override. |
sarif-file | no | "" | When set, also write a SARIF 2.1.0 report to this path (see below). Unset keeps inline-review-only behavior. |
filter-mode | no | diff_context | Inline-eligibility filter: added|diff_context|file|nofilter. |
suggest | no | true | Emit native GitHub one-click suggestions when a fix is proven. |
patch-repair | no | false | Repair near-miss suggestions with a focused second pass; requires suggest. |
build-from-source | no | false | Build the checked-out source instead of installing a release. Used for miu-cr dogfood. |
pr-number | no | event PR number | Override the PR number to review. |
instruction | no | "" | Extra free-text steer passed to miucr review --instruction. |
All credentials are passed to the binary via environment variables, never on the command line, so they don’t appear in process listings or step logs.
SARIF / code scanning
Section titled “SARIF / code scanning”Set sarif-file to also publish findings to the GitHub code-scanning Security
tab (and as PR annotations on changed lines), alongside the inline review. The
SAME single review run writes the report (the action passes --sarif-out), so
there is no second LLM pass. miucr writes the file only on a successful
review: a failed review leaves no file, so the hashFiles-guarded upload below
is simply skipped. Upload the file yourself with
github/codeql-action/upload-sarif, which needs the security-events: write
permission:
permissions: contents: read pull-requests: write security-events: write # required to upload SARIFsteps: - uses: vanducng/miu-cr@vX.Y.Z with: api-key: ${{ secrets.ANTHROPIC_API_KEY }} gate: high sarif-file: miucr.sarif - if: ${{ always() && hashFiles('miucr.sarif') != '' }} uses: github/codeql-action/upload-sarif@v3 with: sarif_file: miucr.sarif category: miucrA full copy-paste workflow is in
examples/github-action/code-review-sarif.yml.
Locally, miucr review --pr <ref> -o sarif > out.sarif produces the same document.
Required check (--mode checks)
Section titled “Required check (--mode checks)”The composite action runs the review reporter (inline comments + summary). For a required status check (one that works on fork PRs, survives force-push, and can block merge via branch protection), run the Check Run reporter by invoking miucr directly in a workflow step:
permissions: contents: read checks: write # required to create the Check Runsteps: - run: curl -fsSL https://cr.miu.sh/install.sh | sh - run: | miucr review --pr "${GITHUB_REPOSITORY}#${PR_NUMBER}" --post --mode checks --gate high env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} PR_NUMBER: ${{ github.event.pull_request.number }}Then mark the resulting miu-cr check required in Settings → Branches →
Branch protection. See the Check Run reporter
for the full reporter semantics.
permissions
Section titled “permissions”The workflow must grant pull-requests: write so the action can post inline
comments and the summary. contents: read is enough for the checkout.
Fork limitation
Section titled “Fork limitation”The action uses the pull_request trigger and is guarded by
head.repo.fork != true, so it does not run on pull requests from forks.
This is deliberate: pull_request_target would carry repo secrets while checking
out untrusted PR code, which is a well-known token-exfiltration vector. Fork-safe
automated review is the job of the serve path (which never runs PR code), not
the action. Same-repo PRs (including from branches in the repo) are reviewed
normally.
One review path
Section titled “One review path”Both modes funnel into the same cli.PRReviewer.ReviewPR pipeline that backs
miucr review --pr - same diff fetch, same engine, same upserted summary issue
comment + head-SHA-anchored inline review comments. serve adds only the HTTP front,
security guards, and the async worker; the action adds only install + invocation.
Neither duplicates review logic.