Troubleshooting

Each of these is a real message from the code, with what actually causes it.

“No findings survived the quality gate”#

Often correct — a clean diff should produce silence. To tell a clean diff from a misconfiguration, read the rest of that line:

No findings survived the quality gate — 4 dropped (3 low confidence, 1 hallucination).
Stage namedWhat happenedWhat to do
(no drops at all)The reviewer proposed nothing. Not a gate problem.Check the context was assembled: re-run with --dry-run --format text. If the changed functions are missing, see language support.
low confidenceProposed, then scored below min_confidence once the skeptic's confidence was multiplied in.Lower min_confidence toward 0.6 if you want more through.
hallucinationThe finding quoted code that is not in the diff or the context. It was dropped before any paid call.Nothing — this is the gate doing its job. Frequent hits suggest a weaker model on stage 4.
bad locationThe quote was real, the line number was not inside a changed hunk.Nothing. The comment could not have been placed anyway.
dedupeAlready posted on this pull request in an earlier run.Nothing. This is what stops repeat comments on every push.
“no changes to review”#

The diff came back empty. Either the base ref is wrong, or the branch really has no changes against it. On CI, the usual cause is a shallow clone: the job has no base commit, so the diff is empty rather than an error.

“git diff ... failed”#

A GitError carrying git's own message. Almost always a shallow clone. Set fetch-depth: 0 on GitHub, GIT_DEPTH: 0 on GitLab, or clone: depth: full on Bitbucket.

“detected dubious ownership”#

Git refusing to operate on a checkout owned by a different user, which happens when a container runs as a non-root user against a mounted workspace. The published image already marks mounted paths as safe; if you built your own, add git config --system --add safe.directory '*' to it.

“looks like it contains a live API key”#

A ConfigError, naming the field. Something key-shaped is in .groundtruth.yml. Move it to an environment variable — that check is what makes the config file safe to commit, so it fails loudly rather than skipping quietly.

“would set where your API key is sent”#

A ConfigError: .groundtruth.yml names an endpoint (llm_base_url, base_url, api_base, or a per-stage one). Endpoints are refused in that file because the pull request under review can edit it. Move the value to GROUNDTRUTH_BASE_URL in the environment or --base-url on the command line. See configuration for why.

Review calls fail with a reasoning model#

The run reports failed calls, but the key and endpoint are right. Reasoning models spend part of max_tokens thinking, so the default 4096 can run out before the JSON is finished — and a reply cut off mid-object cannot be parsed. Raise it in model_params, and set temperature: 1 if the model's own documentation asks for it. See sampling settings.

“is sent to NVIDIA's API catalog but is not prefixed 'nvidia_nim/'”#

The model name is missing its provider prefix. NVIDIA's ids include their vendor, so openai/gpt-oss-20b is read as provider openai, model gpt-oss-20b — the wrong provider, the wrong key, and a model NVIDIA does not serve. Put nvidia_nim/ in front of the whole id: nvidia_nim/openai/gpt-oss-20b. The error names the corrected spelling.

Authentication fails against NVIDIA's endpoint#

Two usual causes. The key variable is NVIDIA_NIM_API_KEY, not OPENAI_API_KEY, even though the endpoint speaks the OpenAI protocol. And the base URL must be exactly https://integrate.api.nvidia.com/v1 — LiteLLM recognizes it by exact string. Groundtruth trims a trailing slash for you, but a different path will fall back to generic OpenAI handling and read the wrong key.

“estimated cost ... exceeds max_cost_per_run”#

The run stopped before spending past your ceiling. The message names which phase it stopped at, because the ceiling is re-checked before the verification pass and again before the summary — the number of those calls is not knowable until the review returns. Raise the ceiling, shrink the diff, or inspect with --dry-run.

A finding posted, but no inline comment appeared#

The adapter logs a warning and continues. Usually a force-push race: the head commit moved between the review and the post, so the platform no longer considers that line part of the diff. The summary comment still lists the finding. Re-running on the new head resolves it.

GitLab runs clean and posts nothing#

Almost always the token. CI_JOB_TOKEN cannot post notes on a merge request. Use a project access token with the api scope as GROUNDTRUTH_GITLAB_TOKEN.

The same comment appeared twice#

The review's memory lives in a hidden marker inside its own summary comment. If that comment is deleted, the memory goes with it and the next run has no way to know what it already said. Edit the summary comment freely — just do not delete it.

No caller context, ever#

Check your file extension against the 18 the caller search covers. Outside that list you get enclosing functions but no cross-file callers.

It costs more than expected on big pull requests#

Stage 4 makes one call per changed file, so cost scales with files touched, not with repository size. A file whose diff exceeds max_diff_tokens_per_call is split into hunk groups, adding calls. Set max_cost_per_run to make that a hard stop rather than a surprise, and split the tiers so only stage 4 uses the expensive model.