Code review agent for typical private cloud corporate environments. The name is German for "Nörgler" (grumbler/complainer).
Built for the realities of enterprise setups: self-hosted Bitbucket Server, on-prem Jira, GitHub Copilot Business seats, and corporate CA certificates. Receives PR webhooks, sends diffs to a Copilot-backed LLM, and posts findings back as inline comments plus a summary comment on the PR.
- Automatic AI-powered code review on PR open/modify
- Incremental reviews — only reviews new changes on push, not the entire PR
- Cross-file context analysis — detects when changed symbols are referenced in other PR files
- Mention-based interaction — ask questions or trigger re-reviews via
@noerglerin PR comments - Smart context enrichment — fetches full file content, not just diffs, for better AI understanding
- Asymmetric and dynamic diff context expansion with language-aware scope detection
- Token-aware chunking and compression for large PRs
- Prompt-cache-optimised template layout — stable rules and examples first, per-PR variables (ticket context, diff) last, so repeated reviews maximise LLM prompt-cache reuse and the diff lands where model recall is strongest
- Jira ticket compliance checking against acceptance criteria
- Project-specific review guidelines via
AGENTS.md - Comment deduplication against existing review comments
- Feedback collection and usefulness tracking
- HMAC-SHA256 webhook signature validation
- Corporate CA certificate support
For a detailed description of the review pipeline, see HOW_IT_WORKS.md.
- Webhook — Bitbucket Server fires a
pr:openedorpr:from_ref_updatedevent to the/webhookendpoint. The request is validated via HMAC-SHA256. - Diff fetch — On new PRs, the full diff is fetched. On updates, noergler performs an incremental review covering only changes since the last review (falling back to full review after force-pushes).
- Context enrichment — Full file content is fetched for each reviewable file. Diff hunks are expanded with asymmetric context and language-aware scope detection. Cross-file analysis maps changed symbols to their references in other PR files.
- AI review — Files are grouped into token-aware chunks and sent to the configured LLM API (any OpenAI-compatible endpoint). The prompt includes file content, diffs, cross-file relationships, repo guidelines (
AGENTS.md), and Jira ticket context. - Post results — Findings are deduplicated against existing comments, sorted by severity, capped at the configured limit, and posted as inline comments. A summary comment tracks the reviewed commit for incremental reviews.
Besides automatic reviews on PR open/modify, you can mention noergler in any PR comment:
- Ask a question —
@noergler Why was this endpoint changed?— noergler replies to your comment with an answer based on the PR diff. - Trigger a full review —
@noergler review— Runs a full review as if the PR was just opened. Also triggered by@noerglerwith no text,re-review, orrereview.
The mention trigger is @<BITBUCKET_USERNAME> — whichever Bitbucket service account noergler runs as. Configure it via the BITBUCKET_USERNAME env var.
Alongside the inline findings, noergler posts (or updates) a single summary comment on each PR with at-a-glance health info:
- Top findings — severity-sorted excerpt of the review comments, capped to a few lines.
- Scope — whether an
AGENTS.mdwas used (with token count againstREVIEW_AGENTS_MD_WARN_TOKENSso you can spot context bloat), plus Jira ticket status or a tip if none was found. - Ticket compliance — per-acceptance-criterion verdict (✅ / ❌) when
REVIEW_TICKET_COMPLIANCE_CHECKis on and a Jira ticket is linked. - Reviewed / skipped files — counts, added/removed line totals, and which files were skipped (lock files, binaries, config).
- Token usage — prompt / completion token counts for the run.
- Last reviewed commit — so incremental reviews on
pr:from_ref_updatedknow where to start.
On every pr:from_ref_updated the existing comment is updated in place rather than duplicated, so the PR activity stream stays clean.
-
Copy
.env.exampleto.envand fill in the required values:cp .env.example .env
-
Build and run with Podman Compose (no pre-built image is published):
podman compose up -d
Or build and run manually:
podman build -t noergler -f Containerfile . podman run -p 8080:8080 --env-file .env \ -v ./prompts:/app/prompts:ro \ noergler
All configuration is driven by environment variables. The required variables are:
| Variable | Description |
|---|---|
BITBUCKET_URL |
Bitbucket Server base URL |
BITBUCKET_TOKEN |
Bitbucket Server API token |
BITBUCKET_WEBHOOK_SECRET |
Webhook HMAC secret for signature validation |
BITBUCKET_USERNAME |
Bitbucket service account username (used to identify bot comments) |
COPILOT_OAUTH_TOKEN |
Long-lived GitHub OAuth token for a Copilot Business seat (provision via hack/copilot-provision-token.sh) |
JIRA_URL |
Jira Server/Cloud base URL |
JIRA_TOKEN |
Jira API token |
DATABASE_URL |
PostgreSQL connection string (see Database below) |
See .env.example for all optional settings and their defaults.
If your org runs riptide as a delivery-metrics collector, noergler can forward two event types so that LLM finops (model, tokens, cost) and reviewer-precision (disagree feedback) live alongside your DORA metrics instead of in a parallel API. Set both:
| Variable | Description |
|---|---|
RIPTIDE_URL |
base URL, e.g. https://riptide-collector.example.com |
RIPTIDE_TOKEN |
your team's raw bearer (issued by the riptide platform team) |
Leave either unset to disable forwarding entirely — noergler runs standalone.
When set, noergler verifies reachability and the bearer at startup via
GET /auth/ping:
- 200 → continue normally.
- 401 → noergler fails to start with a clear error.
- Connection error / timeout → noergler starts with a warning; runtime emissions are best-effort and never block PR webhooks.
PR lifecycle (open/merge/decline) is not forwarded — riptide already captures that from Bitbucket directly.
noergler requires PostgreSQL for review state, deduplication, and statistics. The database connection is validated on startup — the app will not start without it.
Create a PostgreSQL database and user, then set DATABASE_URL:
CREATE USER noergler WITH PASSWORD 'changeme';
CREATE DATABASE noergler OWNER noergler;DATABASE_URL=postgresql://noergler:changeme@localhost:5432/noerglerBoth postgresql:// and postgres:// URI schemes are accepted.
Tip: In production, inject
DATABASE_URLvia container or orchestrator secrets (e.g. Kubernetes Secrets,--env-file) rather than storing credentials in plaintext.
What gets stored:
| Table | Purpose |
|---|---|
pr_reviews |
Tracks reviewed PRs, lifecycle timestamps (opened_at / merged_at / deleted_at), summary comment IDs, and per-PR cost totals. Rows are retained across merge and delete — never hard-deleted. |
review_findings |
Individual code findings with file, line, severity, and Bitbucket comment ID. Used for inline-comment dedup on incremental reviews. |
feedback_events |
Disagree reactions on review comments. Used to skip duplicate reactions. |
Metrics (cost-by-model, reviewer-precision, etc.) live in riptide, not in noergler. Set RIPTIDE_URL + RIPTIDE_TOKEN to forward them.
Running migrations:
Schema is managed with Alembic. Run migrations before first use:
alembic upgrade head-
Generate a webhook secret:
openssl rand -hex 32
-
Set the generated value as
BITBUCKET_WEBHOOK_SECRETin your.envfile. This same value must be configured on Bitbucket's side — the service and Bitbucket share the HMAC secret.
Use scripts/onboard_repo.py to create/update the webhook on one or more repos in a single, idempotent command. It verifies token permissions, reconciles the webhook configuration, and triggers Bitbucket's built-in connectivity test.
python -m scripts.onboard_repo config.json [--name noergler] [--dry-run] [--env-file PATH]Example config.json — repos are grouped under their Bitbucket project:
{
"bitbucket_url": "https://bitbucket.example.com",
"webhook_url": "https://noergler.internal/webhook",
"projects": [
{
"project": "PROJ",
"repos": ["payments-service", "ledger-api"]
},
{
"project": "PLATFORM",
"repos": ["auth-gateway"]
}
]
}Credentials. Secrets are never read from the JSON. BITBUCKET_TOKEN (bearer, needs repo read + write on every listed repo) and BITBUCKET_WEBHOOK_SECRET are resolved from, in order: process environment → .env in the current directory → --env-file <path>. Process env wins on conflict. BITBUCKET_WEBHOOK_SECRET must match the value the running noergler service is using — this script only programs Bitbucket's side; it does not generate a new secret.
Flags.
--name— webhook name (defaultnoergler). Used to find an existing hook to update instead of creating a duplicate.--dry-run— print the create/update diff and the body that would be sent, without mutating Bitbucket.--env-file PATH— additional env file for CI (/run/secrets/noergler.envetc.).
Behaviour. Each repo runs through three steps: verify read access, create-or-update the webhook (idempotent — re-running prints already up to date), then trigger Bitbucket's test endpoint and report the downstream status. A failure on one repo logs and continues to the next; the script exits non-zero iff any repo failed, and prints a final summary table.
Known limitation — secret-only drift. Bitbucket's webhook API does not return the stored secret, so the script cannot detect when only the secret has changed on one side. If you rotate BITBUCKET_WEBHOOK_SECRET on the service side, delete the webhook in Bitbucket (or rename it so this script recreates it) before re-running — otherwise the script will report already up to date while Bitbucket continues signing with the old secret.
In Bitbucket Server, go to Repository settings > Webhooks > Create webhook:
- URL:
https://<host>:8080/webhook - Secret: the value of
BITBUCKET_WEBHOOK_SECRET - Events:
pr:opened,pr:from_ref_updated,pr:comment:added,pr:merged,pr:deleted
All webhook requests must include a valid X-Hub-Signature header (HMAC-SHA256). Requests with missing or invalid signatures are rejected.
Edit prompts/review.txt to change the review focus or output format. The prompt template uses {files} and {repo_instructions} as placeholders. The prompts directory is mounted as a volume, so changes take effect on the next review without rebuilding.
Both prompts/review.txt and prompts/mention.txt are ordered deliberately:
- Stable prefix — role, rules, output format, examples, and prompt-injection guardrails. No placeholders, identical on every call.
- Per-repo block —
{repo_instructions}(theAGENTS.mdcontent). Stable across all PRs in the same repo. - Per-PR block —
{ticket_context}and finally{files}(or{diff}+{question}in the mention template).
Three reasons this matters, and they all push the same layout:
- Prompt cache reuse — OpenAI-compatible prompt caching matches on the longest stable prefix. Keeping all variable content at the tail means the entire stable portion is served from cache on every subsequent call.
- "Lost in the middle" — long-context LLMs recall the start and end of a prompt better than the middle. Putting the diff (the thing the model must reason about) at the very end of the context gives it the strongest recall.
- KV-cache eviction on very long contexts — some long-context implementations evict middle tokens first under pressure. Stable rules at the top are cheap to lose; the diff at the bottom stays intact.
If you edit the templates, preserve this ordering: keep new stable rules above the guardrail section, and any new per-PR placeholders below it, right before {files} / {diff}.
Drop an AGENTS.md file in the repository root to provide project-specific review guidelines. noergler automatically picks it up from the PR source branch (falling back to the target branch) and injects the content into the review prompt. Use it to tell the reviewer about project conventions, forbidden patterns, or areas to focus on.
By default, reviews are gated on the presence of AGENTS.md — without one, noergler skips the review and posts a short summary comment explaining why. To review PRs without an AGENTS.md, set REVIEW_REQUIRE_AGENTS_MD=false.
The review summary reports how many tokens AGENTS.md consumes against a configurable soft limit (REVIEW_AGENTS_MD_WARN_TOKENS, default 4000). When the file exceeds that limit, the summary flags it as a context bloat risk so you know to trim it. Reviews still run either way — this is a warning, not a hard cap.
python -m pytest tests/ -vTests use pytest + pytest-asyncio with respx for HTTP mocking. No external services needed. CI runs via GitHub Actions on push and PR.
Kubernetes/OpenShift manifests are provided in the openshift/ directory. See openshift/README.md for step-by-step instructions.
In OpenShift, mount the cluster's trusted CA bundle (e.g. via a ConfigMap labeled config.openshift.io/inject-trusted-cabundle: "true") into the pod and point SSL_CERT_FILE at the resulting ca-bundle.crt. See openshift/README.md.
GET /health → {"status": "ok"}
Noergler does not expose metrics directly. Set RIPTIDE_URL + RIPTIDE_TOKEN
(see Optional: forward review-cost + reviewer-precision events to riptide)
to forward LLM finops (model, tokens, cost) and reviewer-precision (disagree feedback)
to a riptide collector — all dashboards,
SQL queries, and DORA/SPACE rollups live there alongside delivery metrics from
Bitbucket / ArgoCD / CI.
app/
main.py # FastAPI app, /webhook, /health endpoints
riptide_client.py # Optional outbound emitter to riptide-collector
reviewer.py # Review orchestrator (diff → AI → comments)
llm_client.py # OpenAI SDK client for the configured LLM API, token-aware chunking
context_expansion.py # Asymmetric & dynamic diff context expansion
cross_file_context.py # Cross-file symbol reference analysis
diff_compression.py # Large PR compression and file prioritization
bitbucket.py # Bitbucket Server REST API client
jira.py # Jira ticket fetching and compliance checking
models.py # Pydantic models (webhook payloads, findings)
config.py # Environment-based configuration
feedback.py # Feedback classification
db/
pool.py # asyncpg connection pool management
repository.py # Database operations (upsert, query, insert)
prompts/
review.txt # Review prompt template
mention.txt # Mention Q&A prompt template
openshift/ # OpenShift/K8s deployment manifests
tests/ # pytest test suite

