prr
// Note: prr is a whimsical name reflecting the fun, yet serious nature of this resolver tool.
prr (PR Resolver)
/\_____/\
/ o o \
( == ^ == )
) (
( )
( ( ) ( ) )
(__(__)___(__)__)
sits on your PR and won't get up until it's ready
CLI tool to automatically resolve PR review comments using LLM-powered fixing and verification. Works with comments from humans, bots, or any reviewer.
Philosophy
There are plenty of AI tools that autonomously create PRs, write code, and push changes without human involvement. prr takes a different approach.
Human-driven, AI-assisted: You stay in control. You decide which PR to work on, when to run prr, and when it's done. The AI handles the tedious back-and-forth with reviewers (human or bot), but you're the driver.
The right tool for the job: Sometimes you want to manually address feedback. Sometimes you want AI to grind through 50 nitpicks from CodeRabbit. prr gives you the option without taking over your workflow.
AI talking to AI, supervised by humans: Modern PRs often involve bot reviewers (CodeRabbit, Copilot, etc.) that leave dozens of comments. Instead of manually addressing each one, let prr's AI negotiate with the reviewer AI while you focus on what matters. You can always interrupt, inspect, and override.
Safe over sorry verification: When PRR is unsure whether a fix really covers a lifecycle, cache, cleanup, or multi-path issue, it should keep the issue open instead of optimistically marking it fixed.
What real logs actually showed (and how PRR responds now): Audits found genuine problems — not hypotheticals — including verified ∩ dismissed overlap (misleading “done”), tracked file not found on paths that existed under a different extension or diff prefix, bare .d.ts / fragment paths misclassified, 0%-success models burning rotation, and summaries that could look greener than the threads. Today: all per-comment verified/dismissed/unverified transitions go through transitionIssue (tools/prr/state/state-transitions.ts) so verifiedThisSession, commentStatuses, and legacy verifiedFixed / verifiedComments stay aligned — markVerified / dismissIssue are thin wrappers; state load still repairs legacy overlap and logs repair; tryResolvePathWithExtensionVariants + stripGitDiffPathPrefix (shared/path-utils.ts) address common tsconfig.js / .tsx / etc. cases; open allowed-path policy (isPathAllowedForFix) avoids silently dropping valid repo files under non-standard top-level dirs and lets reviews target adjacent paths; set PRR_STRICT_ALLOWED_PATHS=1 to restore the legacy first-segment filter; fragments use path-unresolved via isReviewPathFragment / pathDismissCategoryForNotFound; ElizaCloud uses ELIZACLOUD_SKIP_MODEL_IDS, PRR_SESSION_MODEL_SKIP_FAILURES, optional PRR_SESSION_MODEL_SKIP_RESET_AFTER_FIX_ITERATIONS, and startup warnings when the post-skip rotation is very thin; RESULTS SUMMARY excludes dismissed IDs from the verified “fixed” count and warns if overlap still appears at exit. Residual risk: LLMs and heuristics can still be wrong on edge paths or weak verifiers — use RESULTS SUMMARY, After Action Report, PRR_STRICT_FINAL_AUDIT / PRR_STRICT_FINAL_AUDIT_UNCERTAIN, and GitHub’s threads together; after rebases, delete .pr-resolver-state.json in the clone workdir if numbers disagree with the PR (see Troubleshooting).
WHY: False negatives cost another pass. False positives hide real bugs, create misleading "all fixed" states, and make PR threads look cleaner than the code really is.
Visible decisions over hidden confidence: In verbose runs, PRR should show the actual per-comment decisions it is using internally, not just aggregate counts or a final "done" message.
WHY: When the PR still shows open comments, operators need to compare PRR's internal state with the GitHub conversation directly. A readable issue table makes classification mistakes obvious and auditable.
Philosophy in practice:
- Run prr on a specific PR (you choose)
- Watch it work, interrupt with Ctrl+C anytime
- Inspect the PR clone (workdir), modify files, continue — this is not necessarily the directory where you ran
prr(process.cwd()); see AGENTS.md (“Clone workdir”). - Push when you decide it's ready
Features
Core Loop
- Fetches review comments from PRs (humans, bots, or any reviewer)
- Parses all bot comments: Reads every comment from known review bots (not just the latest), with noise filtering and path-less issue recovery. Why: Bots may post multiple comments across re-reviews; parsing only the latest missed issues from earlier reviews.
- Uses LLM to detect which issues still exist in the code
- Conservative issue detection for distributed bugs: Lifecycle/cache/leak comments and ordering/history comments now get broader analysis context before PRR decides they are already fixed. Why: Some bugs live across declaration, usage, cleanup, and trimming sites; a narrow anchor snippet can make a real issue look resolved.
- Path-resolution categories instead of blanket stale dismissals: PRR now distinguishes
missing-filefrompath-unresolved, and carries canonical resolved paths forward when a review cites a basename or truncated path. Why: "File no longer exists" was previously hiding very different root causes such as ambiguous basenames, summary-table leakage, and path fragments that only needed repo-path expansion. - PR-scoped basename disambiguation: When a bare filename matches multiple tracked files, PRR can resolve it to the single path that also appears in the PR’s changed-file list (diff vs base). Why: Issue comments on
foo.tsshould target the copy the PR actually edits, not another package’s same-named file; without this, the fix loop could skip the real path as “not in clone” (see DEVELOPMENT.md — path accounting). - Canonical path for disk and git: Where PRR reads files, checks
git ls-tree, or records dismissals for a thread, it prefersresolvedPath/getIssuePrimaryPathover raw GitHubcomment.pathwhen the clone resolved a basename or extension variant. Logs and fragment gates may still show the API path so operators match GitHub. Why: Same rationale as basename disambiguation — wrong string → wrong file or “unreadable” snippets. - Dedup cluster + no-change
ALREADY_FIXED: If the fixer returnsRESULT: ALREADY_FIXEDwith no disk edits, PRR dismisses the whole LLM dedup group (canonical + merged duplicates), not only the one row left in the queue. Why: Otherwise sibling thread IDs stay “open” in GitHub terms while the queue is empty → confusing BUG DETECTED repopulate and a false “remaining” handoff. - After Action Report (fixed this session): Boilerplate bodies (e.g. leading
### What this adds) and threads verified only as duplicates of a canonical fix are collapsed into a short count line instead of full previews. Why: Keeps the AAR readable without hiding how many threads were satisfied this run. - Catalog-backed dismissal + auto-heal for bogus model-id advice: Bots with stale training sometimes flag a valid OpenAI/Anthropic API id as a “typo” and suggest another valid id. When both ids appear in the committed
generated/model-provider-catalog.json, PRR dismisses the comment in solvability and (by default) restores the catalog id inside quoted literals near the review line, then can commit when the run would otherwise skip the fix loop. Why: Avoids burning the fixer on bad vendor advice and prevents silent adoption of the wrong model string in code. See DEVELOPMENT.md (“Commit gate and catalog model auto-heal”) and docs/MODELS.md. - Shared test-path inference: Prompt building, create-file solvability, and retries now reuse the same test-target inference helper instead of maintaining slightly different regex copies. Why: When those phases drift, PRR can decide one test file should be created while another phase allows or explains a different one.
- Generates fix prompts and runs Cursor CLI, Claude Code, or opencode to fix issues
- Config-driven concurrency: Optional
PRR_MAX_CONCURRENT_LLM(default 1) caps in-flight LLM requests; analysis batches, verification, and (with llm-api) parallel fix groups share this cap. On 429, concurrency is halved for 60s. Why: Default keeps behavior unchanged; raising (e.g. to 3) can cut wall-clock when the backend supports it. See Configuration. - Verifies fixes with LLM to prevent false positives
- Debug issue table: Verbose mode prints a human-readable per-comment table after analysis and again at exit. Why: This exposes the exact
open/dismissed/<category>/verifieddecision PRR is using so you can compare it with the PR thread list. - Final audit: Adversarial re-verification of all issues before declaring success — asks what is still wrong (not “is it fixed?”) so weak verifiers do not rubber-stamp. Rare audit overrides (UNFIXED vs previously verified) are opt-in strict exit via
PRR_STRICT_FINAL_AUDIT; passes via UNCERTAIN / truncation guard can fail CI withPRR_STRICT_FINAL_AUDIT_UNCERTAIN. See DEVELOPMENT.md andPRR_FINAL_AUDIT_MODELin Configuration.
Smart Retry Strategies
- Lessons learned: Tracks what didn't work to prevent flip-flopping between solutions
- LLM-powered failure analysis: Learns from rejected fixes to generate actionable guidance
- Smart model rotation: Interleaves model families (Claude → GPT → Gemini) for better coverage
- Rotation order & skip list (llm-api/elizacloud): Default lists ordered by observed fix success (Claude first); models that 500/timeout or have 0% fix rate are in a skip list and never selected. Why: Audit showed 0%-success and error-prone models wasting rotation slots; leading with best performers improves throughput.
- Duplicate prompt skip: When the same set of issue IDs and lesson count is sent to the same model again, we skip the fixer and rotate to the next model. Why: Full-prompt hashes rarely matched; hashing issue IDs + lesson count detects same-issues-same-context and avoids redundant LLM calls.
- Proportional batch reduce: When the fix prompt exceeds the context cap, batch size is reduced proportionally (cap/promptLength) instead of halving. Why: Halving wasted iterations when only slightly over cap; proportional reduction converges in 1–2 steps.
- Rotation reset per push iteration: At the start of each push cycle (after the first), the model index resets to the first model so each cycle gets a "best model first" attempt instead of continuing from where the previous cycle left off. Why: Later push iterations were reusing the last model from the previous cycle (often one that had just 500'd or timed out), wasting time.
- ALREADY_FIXED multi-model dismissal: When 3+ consecutive models return ALREADY_FIXED for the same issue (any explanation), dismiss as already-fixed. Counter resets when the fixer makes changes or the issue is verified. Why: The existing same-explanation counter only fired when explanation text matched; a separate any-explanation counter catches the broader pattern where multiple models independently agree the issue is resolved.
- couldNotInject in-loop dismissal: At the start of each fix iteration, issues that have hit the could-not-inject threshold (file not in repo + no-change cycles) are dismissed and removed from the queue; if the queue is empty afterward, the run exits as all fixed. Create-file issues use a lower threshold (1) so we dismiss after one couldNotInject when the path was clearly "create this file". Why: The threshold was only checked at push-iteration start; inside the fix loop the same issues were retried 10+ times (output.log audit). Applying the check every iteration stops the loop; lower create-file threshold avoids retrying when the file is never created.
- Apply-failure and no-changes handling: When the fixer's search/replace fails to match (no files written), we treat it as no meaningful changes, skip verification, and persist a short "Previous attempt: …" for the next fix prompt. Per-file S/R failure count and consecutive no-changes use tuned thresholds (3 and 2) so we dismiss as remaining earlier instead of burning iterations. Why: output.log audit — the model wasn't getting explicit apply-failure feedback; passing it into the next attempt and bailing sooner reduces wasted runs. See DEVELOPMENT.md "Fix loop audits (output.log)".
- Solvability for new comments: When new bot comments arrive mid-fix-loop, they are run through the same solvability check (e.g. (PR comment), lockfiles) before being added to the queue; unsolvable ones are dismissed and not sent to the fixer. Why: New comments were previously added without solvability, so (PR comment) and other unfixable paths entered the queue and burned iterations (prompts.log audit).
- Missing test files stay actionable: If a review asks for a test/spec file that does not exist yet, PRR keeps that issue open as a create-file target instead of dismissing it as stale. Why: For missing-test comments, non-existence is often the thing the fixer is supposed to change.
- Coverage-only wording on explicit test files stays in create-file flow: If the review path already points at a missing
*.test.tsfile, PRR preserves that path even when the body says "coverage is missing here" instead of repeating "add tests". Why: The path itself is already strong evidence that the requested fix is to create or fill in that test file. - ALREADY_FIXED batch filter: Issues the fixer has already said ALREADY_FIXED 2+ times are excluded from the next batch fix prompt (they are still dismissed at 3× by the existing counter). Why: Batch prompts were re-including those issues and the fixer would again return ALREADY_FIXED, wasting large prompts (prompts.log audit).
- STALE→YES override (snippet not visible): When the batch verifier returns STALE but the explanation says "can't evaluate", "doesn't show", "only shows the beginning", or "incomplete", we override to YES so the issue is not falsely dismissed. Why: Judge instructions say use YES when code is not visible; the verifier often used different phrasings, causing 48 false STALE verdicts in one audit.
- Hedged visibility (truncated snippet/excerpt suggests): Explanations that hedge on truncated context (e.g. "the truncated snippet suggests…", "the truncated excerpt suggests…") are treated as missing-code visibility, so we keep the issue open instead of accepting low-confidence STALE/NO. Why: "Suggests" or "appears to" with truncated context is uncertainty, not evidence the issue is fixed or obsolete.
- Weak-identifier stale retargeting: When a comment's line is out of range and the only backtick-extracted tokens are built-in/type names (e.g.
BigInt,symbol,Map), PRR keeps the issue solvable with a context hint instead of dismissing as stale. Why: Those tokens are poor anchors; using "identifier not found" for them produced incorrect stale dismissals. - Single-issue focus mode: When batch fixes fail, tries one issue at a time with randomization
- Dynamic model discovery: Auto-detects available models for each fixer tool
- Stalemate detection & bail-out: Detects when agents disagree, bails out after N cycles with zero progress
- Large-prompt batch reduce: When a fix prompt exceeds ~200k chars and fails (error or no-changes), the next fix iteration immediately uses a smaller batch size. Why: Oversized prompts cause gateway 500s and timeouts; reducing batch size on the next attempt keeps prompts within limits without burning rotation slots.
Git Integration
- Auto-stashing: Handles interrupted runs gracefully by stashing/restoring local changes
- Auto-rebase on push rejection: If remote has new commits, automatically rebases and retries
- Rebase vs merge detection: When finishing after conflict resolution, we detect rebase (
.git/rebase-merge) using the repo’s absolute path so the right command runs (rebase --continuevscommit). Why: PRR runs in a workdir that’s often not the process cwd; a relative.gitpath would missrebase-mergeand wrongly runcommitduring a rebase, leaving a stuck state. - Push retry cleanup: If the post-rejection rebase fails (e.g. conflicts or "rebase-merge directory already exists"), we try
rebase --abortfirst, then fall back to full git cleanup only if abort fails. Why: Abort restores commits; full cleanup is for stuck/corrupt state so the next run isn’t blocked. - Non-interactive rebase continue: All
rebase --continuepaths usecontinueRebase(git), which setsGIT_EDITOR=trueso git never opens an editor. Why: In headless/workdir runs there’s no TTY; the configured editor would fail with "Standard input is not a terminal" or "problem with the editor 'editor'". One helper keeps behavior consistent. - Base branch merge (explicit refspec): When merging the PR's base branch (e.g.
v2.0.0) into the PR branch, PRR fetches the base with an explicit refspec (+refs/heads/<branch>:refs/remotes/origin/<branch>) so the tracking ref is always updated. Why: On--single-branchclones the default fetch config only includes the PR branch; a plaingit fetch origin v2.0.0would not updateorigin/v2.0.0, leaving a stale ref and the merge-base check incorrectly reporting "already up-to-date", so the PR would stay "dirty" on GitHub. Explicit refspec forces the ref to match the remote tip every run. - Auto-conflict resolution: Uses LLM tools to resolve merge conflicts automatically. Resolution is 3-way (base + ours + theirs), with sub-chunking at AST boundaries when a conflict region exceeds the model’s segment char cap or exceeds
CONFLICT_OVERSIZED_LINE_THRESHOLDlines (TOP_TAILS_FALLBACK_MAX_CHUNK_LINES + 20— WHY: dense short-line regions can stay under the char cap but still blow one-shot merges; eliza-style route files). If AST boundaries collapse to a single giant segment, fallback splits (blank lines / line cap) before one-shotting. Validation (parse TS/JS, size-regression) before write/stage. Attempt 2 (direct API) sorts by largest conflict first, prints(i of n)per file, and a heartbeat with active path; a yellow preflight warns when any region is too large for top+tails if the main merge fails. When the main path fails, top+tails runs where safe (cap 280 lines per region). Parse validation failures trigger up to two retries with the error (and location) in the prompt. Why: Two-way merge forces the model to guess; proper merge needs the common ancestor. See tools/prr/CONFLICT-RESOLUTION.md. - Conflict prompt injection skip: File-content injection is skipped for conflict-resolution prompts. Why: The conflict prompt already embeds each file; re-injecting would duplicate content (e.g. CHANGELOG twice), blow prompt size, and cause 504s.
- Large conflicted files (chunked embed): For files over ~30k chars with conflicts, only the conflict sections (with context) are embedded in the prompt, not the full file. Why: Full-file embed doubles prompt size and causes 504s; sections are enough for correct
<search>/<replace>output. - Token auto-injection: Ensures GitHub token is in remote URL for push authentication; fetch and pull also use the token when the remote has no credentials (one-shot auth URL), so "Checking for conflicts" and pull never hang on a password prompt. Why: Repos cloned without token in the URL would otherwise block during fetch with no visible output; timeout + token fix it (see CHANGELOG).
- CodeRabbit auto-trigger: Detects manual mode and triggers review on startup if needed
- Batched commits with LLM-generated messages (not "fix review comments")
- Thread replies (GitHub feedback): With
--reply-to-threads, PRR posts a short reply on each review thread when it fixes or dismisses an issue (e.g. "Fixed in `abc1234`." or "No changes needed — already addressed before this run."). Resolving threads (collapse with checkmark) is on by default with replies; use--no-resolve-threadsorPRR_RESOLVE_THREADS=0to leave conversations open. WHY: Reviewers see visible feedback in the PR conversation instead of only in PRR's exit summary; one reply per thread keeps noise low and leaves room for human follow-up. See docs/THREAD-REPLIES.md. - Thread working reactions (👀): While working each inline review comment, PRR can post an
eyesreaction on that comment (REST), on by default, independent of--reply-to-threads. WHY: Gives the same lightweight “someone is looking at this” signal many review bots use, during long fix runs, without opting into full thread replies. Throttled + deduped + disables on sustained rate-limit or first hard API error so REST issues never stall the fix loop. Opt out:--no-thread-working-reactionsorPRR_THREAD_WORKING_REACTIONS=0. See docs/THREAD-REPLIES.md (section Thread working reactions).
Token & cost optimizations
- Fix iterations default:
--max-fix-iterationsdefaults to0meaning unlimited — the fix loop runs until all issues are resolved or another exit (e.g. stalemate). Why: Previously 0 was used literally so the loop ran zero times; we now map 0 to "no cap" so the default behaves as documented. - Think-tag stripping: Models like Qwen emit
<think>reasoning blocks; we strip them from responses and ask Qwen not to emit them. Why: Saves ~30% output tokens and avoids breaking parsers that expect responses to start with "YES"/"NO". - Verifier rejection cap: After the verifier rejects an issue twice (fix or ALREADY_FIXED claim), we dismiss it as "exhausted" and stop retrying. Why: Fixer/verifier stalemates otherwise loop indefinitely.
- No-verified-progress exit: After two consecutive push iterations with zero new verified fixes, we exit cleanly. Why: Same issues keep failing; re-run after manual edits or new bot comments.
- Dismissal-comment pre-check: Before calling the LLM to generate a "Note:" comment, we check a ±7 line window for an existing Note:/Review: comment, and also skip when the reason already describes a code change (already-fixed). Why: Avoids redundant LLM calls when a comment was already added or the fix is self-documenting.
- Skip dismissal LLM for already-fixed: We no longer call the LLM to generate a Note for issues dismissed as already-fixed; code/diff is self-documenting. Why: Audit showed 62% of dismissal LLM responses were EXISTING; skipping saves tokens.
- Relax file constraint on retry: When the fixer returns CANNOT_FIX/WRONG_LOCATION and mentions another file, we persist that path and allow it on the next attempt so the fixer can edit the correct file. Why: Prompts.log audit showed 7 identical 33k-char prompts for one cross-file issue; persisting the other file avoids burning all models and can resolve on retry.
- Persisted dedup cache: LLM dedup results (including cross-file grouping) are stored in state keyed by comment ID set with
schema: 'dedup-v2'; repeat runs with the same comments skip the dedup LLM steps. Why: In-memory cache reset each run; persisting saves tokens and latency. Older state withoutschemarecomputes so grouping matches the current pipeline. - Heuristic dedup same-caller: Comments on the same file that share the same primary symbol (e.g. method name) and the same caller file (e.g. "runner.py:146") are merged even when authors differ. Why: Prompts.log audit showed duplicate issues from cursor vs claude describing the same async/caller mismatch; merging them avoids duplicate fix attempts.
- Dedup GROUP validation + prompt guard: Dedup now rejects malformed
GROUP:lines when any index is out of range or the canonical index is not in the group, and the prompt explicitly says valid indices are only1..N. Why: A prompts.log audit showed the model returningGROUP: 2,5,7for only 3 comments; rejecting invalid groups avoids wrong merges and the prompt reduces those hallucinations up front. - Verifier strength for API/signature fixes: Fixes whose comment mentions async/await, caller, signature, or TypeError are verified with a stronger model when available. Why: Weak default verifier approved a fix that missed the call-site update (e.g. print_results still calling generate_report() without await/args); stronger model catches call-site bugs.
- Verifier expanded context for type/signature issues: When a review comment mentions async/await, signatures, TypeErrors, or callers, the verifier sees up to 500 lines of the file (vs default 200). Why: Type/signature fixes often involve function bodies and their call sites; the default window was too narrow, causing false "never assigned" rejections.
- Lifecycle-aware verification context: Comments about leaks, stale cache entries, missing cleanup, or unbounded maps/sets now get lifecycle-aware snippets that include declaration, usage, and cleanup sites across the file, and they bypass the risky "pattern absent" auto-verify shortcut. Why: These issues are about control flow over time, not a single line. A narrow local snippet can make a broken cleanup path look fixed.
- Ordering-aware analysis context: Comments about newest-vs-oldest retention,
fromEnd, or history trimming now use either full-file context or multi-range ordering excerpts during analysis. Why: These bugs usually depend on the interaction between data ordering and a later selection call; a single local excerpt often misses half the bug. - Dismissal-comment skips: We skip the dismissal-comment LLM when the reason says "file no longer exists" or "file not found", and we post-filter generated comments that only restate the surrounding code. Why: Avoids sending a prompt for a missing file and avoids inserting generic "extracts metrics"-style noise.
- Multi-file nudge: When TARGET FILE(S) lists multiple files and the review mentions callers (await, file:line), the fix prompt adds a line urging updates to implementation and every call site. Why: Reduces fixer updating only one file and leaving call sites broken.
- Wider batch snippets: When context headroom ≥100k chars, batch verification uses 2500/3000 char limits per comment/code snippet (vs 2000/2000). Why: Reduces false positives from truncation.
- Rotation by success rate: Legacy model rotation orders models by persisted success rate (best first). Why: Low-success models no longer get tried before proven performers.
- CodeRabbit analysis chain stripping: Comment bodies are sanitized to remove CodeRabbit "Analysis chain" and "Script executed" blocks before analysis/fix prompts. Why: CodeRabbit embeds 5–15 shell runs per comment (~200–1500 chars each); the analyzer only needs the actual finding, not script output—saves ~30% on affected prompts.
- No-op change skip: Identical search/replace blocks from the fixer are skipped. Why: Prevents wasted verification and keeps file-change counts accurate.
- All-no-op skip verification: When every fixer change block was a no-op (search === replace), we treat the iteration as "no changes" and skip the verification LLM call, going straight to rotation. Why: Avoids running the verifier on unchanged code; saves latency and keeps behavior consistent with git state.
- Skip model recommendation for 1–2 issues: The separate model-recommendation LLM call runs only when there are 3+ unresolved issues; otherwise we use default rotation. Why: Saves ~29s and tokens on simple runs.
- Skip predict-bots when --no-wait-bot: When
--no-wait-botis set, we skip the LLM "likely new bot feedback" prediction after commit. Why: Prediction is display-only; skipping saves ~26s when the user isn't waiting for bot reviews. - Predict-bots changed-files guard: The display-only predictor skips tiny meta-only diffs (e.g. small
.gitignorechanges), tells the model to output only files present in the commit diff, and filters predictions tochangedFiles. Why: A prompts.log audit showed the predictor hallucinatingscripts/build-skills-docs.jsfrom a.gitignore-only diff; filtering to actual changed files saves tokens and removes noisy UX output. - Lesson caps for large batches: When fixing 10+ issues at once, we cap global and per-file lessons so the prompt stays under ~100k chars. Why: Prevents gateway timeouts and prompt poisoning from oversized prompts.
- LLM dedup (per file): Runs when a file has 3+ survivors after heuristic dedup, or exactly 2 when authors differ and
primarySymbolFromBodymatches (after severity/framing strip). Why: Heuristics cover same-author pairs; cross-bot pairs on the same bug (e.g. Claude + Cursor on_multiplier) need one cheap-model merge. Why not always 2: Same-author pairs of two are usually already merged heuristically; extra LLM calls only when the signal is strong. - Issue-comment ingestion: Non-review bodies (README/setup dumps, very short non-actionable text) are filtered before markdown parsing; parsed
ic-*rows from the same author/path are collapsed by similarity before snippet fetch. Why: Re-posted bot reviews and pasted docs inflated the queue and file reads (elizaOS/cloud#417 class PRs). - Cross-file dedup (Phase 3): When ≥5 issues remain after per-file dedup, one batched cheap-model pass may merge items on different files that share the same root-cause fix; canonical gets a
contextHintsline listing sibling paths. Why: One mistake repeated across services (e.g. CoT + temperature) should not require four separate fix cycles. - maxFixIterations 0 = unlimited:
--max-fix-iterations 0is treated as unlimited (not zero). Why: Without this, 0 meant zero iterations and the run did analysis-only with no fix attempts. - File injection by issue count & dynamic budget: Injected file contents are chosen by how many issues reference each file (most first); total injection budget is tied to the model’s context cap. Why: Puts the injection cap toward files most likely to need search/replace; avoids overshooting small-context or underusing large-context models.
- Shared prompt budget for injected code text: Snippet windows, full-file audit excerpts (when too large for one prompt), and per-fix “current code” in batch verification share
shared/prompt-budget.ts—computeBudget(model ceiling minus reserved wrapper chars, optional per-slot divisor) andfitToBudget(line-centered excerpts). Why: Previously each path used its own char/line caps; they drifted and caused either too little context (weak verifier / false STALE) or prompts that blew small model windows. One module tracks WHY each knob exists; see DEVELOPMENT.md. - Batch injection filter (rounds 2+): In later fix rounds, file injection is limited to files that still have at least one unfixed issue via
allowedPathsForInjection. Why: Already-fixed files waste context budget; filtering keeps the prompt focused and leaves room for files that need changes. - Single-issue full file context: Single-issue fix prompts send the full file (up to 600 lines) instead of a short snippet. Why: Models responded INCOMPLETE_FILE/UNCLEAR when given only 15-30 lines; full file gives enough context for correct fixes.
- Rewrite escalation for non-injected files: Files mentioned in the prompt but not injected (or with repeated S/R failures) are escalated to full-file rewrite. Why: When the model never saw file content, search/replace usually fails; asking for the full file avoids matching failures.
- Delay full-file escalation for simple issues: For files where all targeting issues have importance ≤ 3 and ease ≤ 2, we only escalate to full-file rewrite when the file was not injected (not when over S/R failure threshold). Why: Full-file rewrites are expensive and time out more; for simple issues we rely on S/R first.
Robustness
- Hash-based work directories for efficient re-runs
- State persistence: Resumes from where it left off, including tool/model rotation position and dedup cache (comment set → duplicate grouping). Why: Dedup is deterministic for the same comment set; persisting avoids re-running the dedup LLM on every run.
- Model performance tracking: Records which models fix issues vs fail, displayed at end of run; used to sort rotation order (best first). Why: Tries proven models before chronic low performers.
- 5-layer empty issue guards: Prevents wasted fixer runs when nothing to fix
- Graceful shutdown: Ctrl+C saves state immediately; double Ctrl+C force exits
- Session vs overall stats: Distinguishes "this run" from "total across all runs"
- Prompt size and injection caps: Base prompt + injected file content are capped (e.g. 200k total) with a minimum injection allowance so the model still sees key files when the base prompt is large. Why: Prevents gateway 500s from oversized requests while avoiding "zero injection" when the base is already big.
- No-changes parsing: When the fixer reports "no changes", the explanation is parsed from prose only; content inside
<change>,<newfile>, and<file>blocks is ignored. Why: Prevents false positives from code or test fixtures that happen to contain phrases like "already fixed" or "no changes". - Empty snippet handling: When the judge or fix-verifier has no code snippet for an issue, we show an explicit placeholder instead of an empty block (e.g. "snippet unavailable — do NOT respond STALE"). Why: Empty blocks forced the model to guess; the placeholder steers toward YES-with-explanation when code isn't visible and avoids false STALE.
- Grouping rule (same method, different fix): Comment dedup does not group comments that target the same method but require different fixes (e.g. "add method" vs "change call site"). Why: Wrong merges cause one fix to address both or drop nuance; the rule reduces false groupings observed in audits.
- Recap/meta bare-file filtering: Summary/status recap blocks are filtered before path inference, and bare filenames are only treated as actionable when the comment wording clearly points to a file-specific fix. Why: Review recaps like "| Location | Suggestion |" were leaking
banner.ts,logger.ts, andreply.tsinto the queue as fake file issues. - Incomplete-snippet verdict recovery: Explanations that admit the model could not actually see enough code now keep the issue open even if the model answered
NO, not justSTALE. Why: "Fixed" is not trustworthy when the verifier explicitly says the relevant code was truncated or not visible. - 504/gateway timeout: two retries with backoff: On 504 or request timeout, we retry up to twice with 10s then 20s delay. Why: Single retry was often insufficient for transient gateways; two retries give the gateway time to recover.
- AAR exhausted list: The After-Action Report lists every exhausted issue (path:line) so operators know which need human follow-up. Why: Exhausted issues were only summarized by count; listing them makes follow-up actionable.
- Judge rule (NO when code already implements): Batch verification prompt instructs the judge to respond NO and cite code when the current code already addresses the review. Why: Reduces unnecessary ALREADY_FIXED fix attempts when the judge would otherwise say YES.
- Model recommendation wording: We ask "explain why these models in this order" instead of "brief reasoning". Why: Models echoed "brief reasoning" literally; the new wording yields actionable explanation.
- Credential redaction in push logs: All push and rebase error/debug output is sanitized so
https://token@hostis never logged. Why: Git errors can contain remote URLs with tokens; redacting prevents credential leakage. - Worktree-safe rebase detection: Rebase-vs-merge and "rebase still in progress?" use a shared helper that resolves the real git dir when
.gitis a file (worktrees). Why: In worktrees the check would otherwise fail; one implementation keeps completeMerge and pull conflict loop correct.
Installation
This repo contains prr (PR Resolver), pill (Program Improvement Log Looker), split-plan (PR decomposition planner), split-exec (execute split plan), and story (PR narrative & changelog). All use a shared library under shared/; tool code lives under tools/prr/, tools/pill/, tools/split-plan/, tools/split-exec/, and tools/story/.
Repository layout (contributors & agents)
WHY document this: PRR clones the target PR’s repo into a workdir; this monorepo is the tool source. Confusing the two causes wrong paths in patches and audits.
| Layer | Path | Role |
|---|---|---|
| Shared | shared/ |
Logger (plus timing/token modules), config, git helpers, shared/constants/ (barrel via shared/constants.ts shim), runners, path-utils — no imports of tools/pill/ from here (WHY: lower layer stays tool-agnostic). |
| PRR | tools/prr/ |
CLI, resolver.ts, resolver-proc.ts (re-export facade only), workflow/, llm/, github/, state/. |
| Other tools | tools/pill/, tools/split-plan/, … |
Each tool has its own entry point; pill after logs runs from those CLIs via after-close-logs.ts, not from shared/logger.ts (WHY: layering). |
For architecture diagrams, key file tables, and fix-loop internals, see DEVELOPMENT.md. For clone workdir rules, constants file names, and conventions (e.g. option bags XOptions), see AGENTS.md. For optional future structural ideas (e.g. moving GitHubAPI / LLMClient into shared/), see docs/ROADMAP.md. Completed refactors are recorded in CHANGELOG.md.
npm install
npm run typecheck
# Run prr directly
node dist/tools/prr/index.js <pr-url>
Split-plan: PR decomposition planner
The split-plan tool analyzes a large PR (diffs, commits, dependencies), discovers open PRs on the same base branch as “buckets,” and writes a human-editable .split-plan.md with a dependency analysis and a proposed split into smaller, reviewable PRs. Why: LLM agents often produce PRs that mix refactors, features, and fixes; splitting by concern keeps reviews human-sized. split-exec reads that plan and iteratively cherry-picks commits into existing or new PR branches and opens new PRs. See tools/split-plan/README.md and tools/split-exec/README.md for full documentation and WHYs.
Pill: Program Improvement Log Looker
pill audits a project using its output.log and prompts.log (from prr, story, split-exec, or a previous pill run) and appends an improvement plan to pill-output.md and pill-summary.md. Those files are generated artifacts — do not commit them (see .gitignore). In this repo, triaged follow-ups live in pill-inventory/INDEX.md (DEVELOPMENT.md — Pill output triage). Pill is analysis-only: no fixers, verification, or commits. Why: Logs are evidence of behavior (failures, retries, model rotations); turning that into an actionable plan helps improve the project without duplicating prr’s fix loop. Pill runs on close only when you pass --pill (prr, story, split-exec, split-plan). While assembling context (especially story-read on huge logs), the spinner shows stages and chapter progress (i/n); --verbose prints the same as gray [pill] … lines. See tools/pill/README.md for full documentation and WHYs.
# Or link globally (prr, pill, split-plan, split-exec, and story available)
npm link
prr --version # See the cat!
pill --help # Pill CLI
split-plan --help # PR decomposition planner
split-exec --help # Execute split plan (cherry-pick, push, create PRs)
story --help # PR narrative & changelog
Story: PR or branch narrative & changelog
The story tool builds a narrative, feature catalog, and changelog (Added/Changed/Fixed/Removed) from a PR or branch. Three modes: PR (title/body + commits + files), single branch (commit history only, no comparison), two branches (--compare <branch>; order auto-detected, story is about the branch you passed first). See tools/story/README.md for full documentation and WHYs.
# PR
story https://github.com/owner/repo/pull/123
story owner/repo#456
# Single branch (commit history only)
story owner/repo@feature/siwe
story https://github.com/owner/repo/tree/feature/siwe
# Two branches (story from older → newer; primary branch = first arg)
story https://github.com/owner/repo/tree/v2-develop --compare v1-develop
# Write to file; verbose; tune context size
story owner/repo#456 --output CHANGELOG.md
story owner/repo@branch -v --max-commits 200 --max-files 500
story --help # PR narrative & changelog
Requires the same config as prr: GITHUB_TOKEN and an LLM provider (e.g. ELIZACLOUD_API_KEY or ANTHROPIC_API_KEY). Logs: story-output.log, story-prompts.log.
Configuration
Same as prr: GITHUB_TOKEN plus one of ELIZACLOUD_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY. Optional: PRR_LLM_PROVIDER, PRR_LLM_MODEL. See root README and .env.example.
| Variable | Purpose |
|---|---|
GITHUB_TOKEN |
GitHub API access |
PRR_GIT_SHA / PRR_SOURCE_COMMIT |
Optional — stamp startup/output.log when the prr tree has no .git (vendored install). If .git exists in the prr package root, revision comes from git rev-parse instead. Not GITHUB_SHA (host repo). |
ELIZACLOUD_API_KEY / provider keys |
LLM gateway or direct API: ANTHROPIC_API_KEY, OPENAI_API_KEY, NVIDIA_API_KEY or NVIDIA_CLOUD_API_KEY (NVIDIA NIM / integrate API), OPENROUTER_API_KEY (OpenRouter). First-class providers: nvidiacloud, openrouter, ollama, lmstudio (see below). |
NVIDIA_BASE_URL |
Optional — OpenAI-compatible API root for nvidiacloud (default https://integrate.api.nvidia.com/v1). |
OPENROUTER_BASE_URL |
Optional — API root for openrouter (default https://openrouter.ai/api/v1). |
OPENROUTER_HTTP_REFERER / OPENROUTER_APP_TITLE |
Optional — OpenRouter ranking headers (HTTP-Referer, X-Title) when using openrouter. |
OPENAI_BASE_URL |
Optional — when using PRR_LLM_PROVIDER=openai or the llm-api fixer, the OpenAI client / HTTP layer uses this as the API root. For local OpenAI-compatible servers it must end with /v1 (e.g. Ollama http://host:11434/v1, LM Studio http://localhost:1234/v1). This is not Ollama’s native /api/... URL (that tree is a different protocol). See Local OpenAI-compatible backends below. You can still point openai at NVIDIA or OpenRouter URLs if you prefer one key layout; first-class nvidiacloud / openrouter IDs give clearer defaults and diagnostics. |
PRR_LLM_MODEL |
Pin the primary fixer/verifier model |
PRR_VERIFIER_MODEL |
Stronger model for batch verification (when default is weak) |
PRR_FINAL_AUDIT_MODEL |
Model for adversarial final-audit pass only |
PRR_STRICT_FINAL_AUDIT |
1 / true — exit 2 when final audit re-queues were overridden (audit said UNFIXED for previously verified issues) |
PRR_STRICT_FINAL_AUDIT_UNCERTAIN |
1 / true — exit 2 when the run succeeds but final audit passed any issue via UNCERTAIN or truncation guard |
PRR_THREAD_REPLY_INCLUDE_CHRONIC_FAILURE |
1 / true — with --reply-to-threads, also post a short reply for chronic-failure dismissals (default: skip) |
PRR_MAX_CONCURRENT_LLM |
In-flight LLM cap (default 1) |
PRR_ELIZACLOUD_SERVER_ERROR_RETRIES |
ElizaCloud only: inner 500/502/504 retry index per complete() call (0–15). Unset: 3 HTTP attempts locally, 5 when CI=true. Override in CI for flaky gateways (e.g. empty 500 bodies on batch check). |
PRR_ELIZACLOUD_GATEWAY_FALLBACK_MODELS |
ElizaCloud only: comma-separated model ids tried after 2 consecutive gateway/server-class errors on the current model in one complete() (e.g. DeepInfra 502 on Qwen). off / none / 0 disables. Default chain skips ids in the built-in skip list unless PRR_ELIZACLOUD_INCLUDE_MODELS re-enables them. getElizacloudGatewayFallbackModels in shared/constants.ts. |
PRR_ELIZACLOUD_EXTRA_SKIP_MODELS |
Comma-separated ids added to the built-in ElizaCloud skip list (shared/constants.ts ELIZACLOUD_SKIP_MODEL_IDS) |
PRR_ELIZACLOUD_INCLUDE_MODELS |
Comma-separated ids to remove from the built-in skip list (re-enable after transient timeouts) |
PRR_SESSION_MODEL_SKIP_FAILURES |
Skip a model for the rest of the run after N zero-fix verification failures (0 = off) |
PRR_SESSION_MODEL_SKIP_RESET_AFTER_FIX_ITERATIONS |
After N fix iterations since each model was session-skipped, drop that key so rotation can retry it (0 / unset = off) |
PRR_DIMINISHING_RETURNS_ITERATIONS |
Warn after N consecutive iterations with no new verified fixes (0 = off) |
PRR_EXIT_ON_STALE_BOT_REVIEW |
1 / true — exit setup before clone if bot review SHA ≠ PR HEAD (stale inline comments) |
PRR_EXIT_ON_UNMERGEABLE |
1 / true — exit when GitHub (REST) reports mergeable: false or mergeableState: dirty and --no-merge-base is in effect: before clone (setup) and at the start of each push iteration after a fresh pulls.get (so mergeable: null at first fetch does not skip the check). With default base merge, PRR still runs but logs visible merge-noise warnings (see DEVELOPMENT.md / Cycle 80). |
PRR_CLEAR_ALL_DISMISSED_ON_HEAD |
1 / true — on PR HEAD change, clear all dismissals (default: clear already-fixed, chronic-failure, and stale; keep other categories) |
PRR_STRICT_ALLOWED_PATHS |
1 / true — restore legacy first-segment allowlist for fixer paths (static REPO_TOP_LEVEL + PR changedFiles roots). Default (unset): any repo-relative path passes except absolute, node_modules, dist/, .cursor / .prr / root. WHY default open: audits showed unknown roots like agent/ were stripped from allowedPaths, blocking injection and wasting iterations; adjacent files in reviews need to be editable without maintaining a global dir list. |
PRR_MID_LOOP_NEW_COMMENT_CAP |
Max new bot threads to enqueue per mid–fix-loop batch (default 45). 0 = unlimited. Defers overflow until the next full comment analysis. |
PRR_DISABLE_BLAST_RADIUS |
1 / true — skip blast-radius graph (no deprioritization or injection subset from radius) |
PRR_BLAST_RADIUS_DEPTH |
Max graph hops from changed files over imports and reverse edges (default 2) |
PRR_BLAST_RADIUS_DISMISS |
1 / true — dismiss issues whose primary path is outside the radius as out-of-scope (default: deprioritize only) |
PRR_BLAST_RADIUS_MAX_FILES |
Max tracked source files scanned for the graph (default 5000) |
PRR_BLAST_RADIUS_TIMEOUT_MS |
Abort graph build after this many ms (default 30000) |
PRR_BLAST_RADIUS_MAX_DIR_NEIGHBORS |
Skip same-directory proximity when a directory has more than this many tracked files (default 30) |
PRR_DISABLE_LATENT_MERGE_PROBE |
1 / true — skip git merge-tree dry-merge vs origin/<prBranch> during sync (default: probe on) |
PRR_DISABLE_LATENT_MERGE_PROBE_BASE |
1 / true — skip the second dry-merge vs origin/<prBase> (GitHub mergeable/dirty); default runs when base ≠ PR branch |
PRR_MATERIALIZE_LATENT_MERGE |
1 / true — when the PR-tip probe predicts conflicts, run git merge origin/<branch> --no-commit --no-ff before pull so LLM conflict resolution can run early |
PRR_MATERIALIZE_LATENT_MERGE_BASE |
1 / true — when the PR-vs-base probe predicts conflicts, run git merge origin/<prBase> --no-commit --no-ff for early LLM resolution |
PRR_BOT_LOGIN |
Optional override for thread-reply idempotency; if unset, PRR uses GET /user with your token |
PRR_REPLY_TO_THREADS |
true / 1 — opt in to posting thread replies (same as CLI --reply-to-threads) |
PRR_RESOLVE_THREADS |
0 / false / off — when replies are on, do not resolve review threads (default is to resolve; same as --no-resolve-threads) |
PRR_THREAD_WORKING_REACTIONS |
0 / false / off — disable 👀 on review comments while working issues (default: on; same as --no-thread-working-reactions) |
PRR_THREAD_WORKING_REACTION_MIN_MS |
Minimum ms between reaction POSTs in one run (default 1000; invalid values fall back to default) |
PRR_THINKING_BUDGET |
Extended thinking token budget for Claude-class models; values above 500,000 clamp with a warning (shared/config.ts) |
PRR_LLM_MIN_DELAY_MS |
Override min ms between ElizaCloud request starts per slot (default 6,000 — see shared/constants/models.ts) |
PRR_LLM_TASK_TIMEOUT_MS |
Optional cap (ms) on concurrent pool tasks (0 = none) |
PRR_LLM_API_REQUEST_TIMEOUT_MS |
llm-api only: fixed per-request timeout (ms) for non-full-file fix calls; unset = auto 90s → 180s by prompt size (full-file rewrite stays 180s). Merge-conflict batches (MERGE CONFLICT RESOLUTION) use lower char thresholds for the same caps (18k+ → 120s, 28k+ → 150s, 45k+ → 180s) so ~30–40k batches are less likely to hit client timeouts |
PRR_CLONE_TIMEOUT_MS / PRR_FETCH_TIMEOUT_MS |
Clone / fetch timeouts for large remotes (AGENTS.md / Troubleshooting) |
PRR_DISABLE_CONFLICT_SEPARATOR_REPAIR |
1 — disable automatic insertion of missing ======= between conflict markers |
PRR_DISABLE_MODEL_CATALOG_SOLVABILITY / PRR_DISABLE_MODEL_CATALOG_AUTOHEAL |
Disable catalog 0a6 dismissal and/or quoted-literal auto-heal (AGENTS.md) |
PRR_MODEL_CATALOG_PATH |
Override path to model-provider-catalog.json (malformed → empty catalog + warn) |
PILL_OUTPUT_LOG_PATH / PILL_PROMPTS_LOG_PATH |
Standalone pill rerun on explicit log paths (.env.example pill section) |
CLI (related): pass --merge-base when GitHub reports the PR as not mergeable / dirty and you want PRR to merge the PR base before the fix loop.
Create a .env file (see .env.example):
# Required
GITHUB_TOKEN=ghp_xxxx
# LLM for verification (anthropic or openai)
PRR_LLM_PROVIDER=anthropic
PRR_LLM_MODEL=claude-sonnet-4-5-20250929
ANTHROPIC_API_KEY=sk-ant-xxxx
# Or use OpenAI
# PRR_LLM_PROVIDER=openai
# PRR_LLM_MODEL=gpt-4o
# OPENAI_API_KEY=sk-xxxx
# Or NVIDIA NIM / integrate (OpenAI-compatible)
# PRR_LLM_PROVIDER=nvidiacloud
# NVIDIA_API_KEY=nvapi-...
# PRR_LLM_MODEL=meta/llama-3.1-405b-instruct
# Or OpenRouter
# PRR_LLM_PROVIDER=openrouter
# OPENROUTER_API_KEY=sk-or-...
# PRR_LLM_MODEL=google/gemini-2.0-flash-001
# Default fixer tool (rotates automatically when stuck)
# If not set, prr will auto-detect which tool is installed
# PRR_TOOL=cursor
# Optional: max concurrent LLM requests (default 1). Raise to reduce wall-clock time when the backend allows.
# PRR_MAX_CONCURRENT_LLM=3
# Optional: min delay in ms between starting successive requests per slot (default 6000). Override if tuning rate limits.
# PRR_LLM_MIN_DELAY_MS=6000
# Optional: comma-separated ElizaCloud model IDs to include even if on the skip list (e.g. if timeouts were gateway-specific).
# PRR_ELIZACLOUD_INCLUDE_MODELS=openai/gpt-4o-mini,anthropic/claude-3.7-sonnet
# Optional: legacy strict first-segment allowlist for fixer paths (see README “Fixer allowed paths”).
# PRR_STRICT_ALLOWED_PATHS=1
Local OpenAI-compatible backends (Ollama, LM Studio)
Both servers expose an OpenAI-compatible /v1 HTTP API. PRR supports them two ways:
-
Preferred — first-class providers:
PRR_LLM_PROVIDER=ollamaorlmstudio(see bullets below). WHY: Dedicated defaults (OLLAMA_BASE_URL/LMSTUDIO_BASE_URL),max_tokens-style request fields, startup reachability checks, andllm-apiselection followPRR_LLM_PROVIDERso the fixer subprocess matches PRR’s analysis backend without hand-syncingOPENAI_BASE_URL. -
Legacy — generic OpenAI client:
PRR_LLM_PROVIDER=openaiplusOPENAI_BASE_URLending in/v1,OPENAI_API_KEY(often any non-empty string locally), andPRR_LLM_MODEL. WHY: One code path for any OpenAI-compatible root (including OpenRouter/NVIDIA URLs) when you already use that layout; same/v1rule — the SDK andllm-apicallmodels.listand/chat/completionsunder that root.
Ollama (first-class): Default OLLAMA_BASE_URL is http://127.0.0.1:11434/v1. If you use OLLAMA_HOST / a reverse proxy on another port, set OLLAMA_BASE_URL to …/v1, not …/api (WHY: /api/* is Ollama’s native JSON API, not OpenAI-compatible).
LM Studio (first-class): Start the local server (default LMSTUDIO_BASE_URL http://127.0.0.1:1234/v1 — see developer OpenAI-compat docs). PRR_LLM_MODEL is required — use the model id from the LM Studio server UI. WHY: There is no single universal default id across installs.
Smoke-test (from any machine that can reach the server):
curl -sS "${OPENAI_BASE_URL%/}/models" | head
curl -sS -X POST "${OPENAI_BASE_URL%/}/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${OPENAI_API_KEY:-ollama}" \
-d "{\"model\":\"${PRR_LLM_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK\"}],\"max_tokens\":128}"
Limitations: Model rotation / skip lists are tuned for cloud gateways; local runs are best with PRR_TOOL=llm-api or another fixer that uses your keys, and a single pinned PRR_LLM_MODEL. Some reasoning-heavy models may fill completion_tokens in a server-specific reasoning field until max_completion_tokens is high enough — if PRR logs empty LLM bodies for PRR_LLM_PROVIDER=openai, try a higher completion budget on the server or a model that returns the answer in message.content.
llm-api, explicit providers, and model-list validation (WHY)
-
PRR_LLM_PROVIDER=openrouterornvidiacloud:llm-apiuses that backend only when the matching key is set (OPENROUTER_API_KEY, orNVIDIA_API_KEY/NVIDIA_CLOUD_API_KEY). If you set the provider explicitly but omit the key,checkStatusreports not ready instead of silently falling through to ElizaCloud whenELIZACLOUD_API_KEYis also present. WHY: Avoids the subprocess fixer hitting a different gateway than PRR’s main config (audit: key-order vs explicit intent). -
isAvailable()sets both internal and publicprovideron success so logs between availability checks andgetClient()show the correct backend. WHY: Operators and debug paths readrunner.providerimmediately after detection. -
Startup rotation (
validateAndFilterModelsintools/prr/models/rotation.ts): OpenRouter/NVIDIA model lists use keys fromloadConfig()or, if those fields were not passed through,OPENROUTER_API_KEY/getNvidiaApiKeyFromEnv()from the environment.llm-api’s resolvedprovideralso gates whether OpenRouter/NVIDIA/v1/modelsis queried. WHY: Keeps list-fetch aligned withcheckStatuswhen config objects and env differ (split-brain). -
Empty
GET /v1/models(or equivalent) — network failure, local server down, or empty response: Forllm-apion OpenAI-compatible backends (OpenRouter, NVIDIA, Ollama, LM Studio, native OpenAI/Anthropic when lists are used), rotation entries built from fallbacks or LM Studio’s pinnedPRR_LLM_MODELare kept when the fetched set is empty; PRR only removes a model when the provider returned a non-empty list and the id is missing. WHY: Otherwise a failed list call stripped every fallback and the fixer had nothing to rotate to (audit: local providers). -
Reachability (
validateOllamaReachable/validateLmStudioReachable): Connection errors are detected from the fullErrorchain includingcause(OpenAI SDK often wrapsECONNREFUSEDthere). WHY: Fail fast with a clear message instead of misclassifying as an unknown API error.
NVIDIA Cloud and OpenRouter (first-class providers)
Both use the same OpenAI-compatible transport as openai / ElizaCloud (chat + models.list), with provider-specific env vars and defaults in shared/config.ts and shared/constants/models.ts:
PRR_LLM_PROVIDER=nvidiacloud— setNVIDIA_API_KEYorNVIDIA_CLOUD_API_KEY. OptionalNVIDIA_BASE_URL(defaulthttps://integrate.api.nvidia.com/v1). DefaultPRR_LLM_MODELwhen unset:meta/llama-3.1-405b-instruct(override if your account exposes different NIM ids).llm-api, pill, and split-plan use the same provider id.PRR_LLM_PROVIDER=openrouter— setOPENROUTER_API_KEY. OptionalOPENROUTER_BASE_URL(defaulthttps://openrouter.ai/api/v1),OPENROUTER_HTTP_REFERER,OPENROUTER_APP_TITLE. Default model when unset:google/gemini-2.0-flash-001(OpenRouter-stylevendor/modelids — pinPRR_LLM_MODELto what your key can access).PRR_LLM_PROVIDER=ollama— local OpenAI-compatible bridge (defaultOLLAMA_BASE_URLhttp://127.0.0.1:11434/v1). OptionalOLLAMA_API_KEY(defaultollamafor the SDK). DefaultPRR_LLM_MODELwhen unset:llama3.2— override to a model you have pulled.llm-apihonorsPRR_LLM_PROVIDERfirst so the fixer matches PRR’s backend.PRR_LLM_PROVIDER=lmstudio— LM Studio local server (defaultLMSTUDIO_BASE_URLhttp://127.0.0.1:1234/v1). OptionalLMSTUDIO_API_KEY(defaultlm-studio).PRR_LLM_MODELis required (no universal default id — use the model id from the LM Studio server UI). SamePRR_LLM_PROVIDERpriority forllm-api.
Compatibility: You can still use PRR_LLM_PROVIDER=openai with OPENAI_BASE_URL set to Ollama, LM Studio, OpenRouter, or NVIDIA …/v1 and a matching OPENAI_API_KEY; first-class nvidiacloud / openrouter / ollama / lmstudio are preferred for defaults, max_tokens vs max_completion_tokens, key discovery in llm-api, and startup validation messages. generated/model-provider-catalog.json is not extended for these gateways in the first pass — runtime /v1/models discovery is used for rotation where applicable.
OpenAI-compatible chat: max_tokens vs max_completion_tokens (WHY)
Official OpenAI and ElizaCloud chat completions accept max_completion_tokens (and newer OpenAI models reject legacy max_tokens). Many third-party /v1/chat/completions stacks (notably NVIDIA, OpenRouter, Ollama, and LM Studio) still expect max_tokens only; sending max_completion_tokens alone can return 400. PRR, llm-api, and pill therefore branch on provider when building the request body (shared/llm/openai-compat-chat-params.ts — single place so transport, subprocess fixer, and pill stay aligned).
pill (--pill) and OpenAI-compat providers (WHY)
Standalone pill and prr --pill use the same env keys as PRR. The pill CLI’s default --audit-model is still an Anthropic id for historical reasons; when the detected provider is openai, nvidiacloud, openrouter, ollama, or lmstudio and you did not set PILL_AUDIT_MODEL, pill substitutes that provider’s default audit/story models so requests are not sent to the wrong API (tools/pill/config.ts). PILL_LLM_PROVIDER=lmstudio requires PILL_LLM_MODEL (same as PRR). Override anytime with PILL_AUDIT_MODEL, PILL_LLM_MODEL, or PILL_LLM_PROVIDER. Details: tools/pill/README.md (LLM provider and default models).
Concurrency (optional)
PRR_MAX_CONCURRENT_LLM(integer 1–32, default unset ⇒ 1): Maximum number of LLM requests in flight at once. Analysis batches, verification, and (when using llm-api) parallel fix groups all share this cap. WHY: Default 1 keeps behavior unchanged and avoids 429s; raising it (e.g. to 3) lets analysis and fix run in parallel and can cut wall-clock time significantly when the backend (e.g. ElizaCloud) supports it.PRR_LLM_MIN_DELAY_MS(integer ≥ 0, default unset ⇒ 6000): Minimum milliseconds between starting successive requests per slot. WHY: Spacing requests reduces burst 429s; override only when tuning for a specific gateway.
ElizaCloud skip-list override (optional)
PRR_ELIZACLOUD_INCLUDE_MODELS(comma-separated model IDs): Models to include in rotation even if they are on the default skip list (e.g.openai/gpt-4o,openai/gpt-4o-mini,anthropic/claude-3.7-sonnet). WHY: Those models are skipped by default because audits showed timeouts or 0% fix rate on some gateways; if your environment is different, set this to re-enable them (e.g.PRR_ELIZACLOUD_INCLUDE_MODELS=openai/gpt-4o-mini). Full IDs or short names (e.g.gpt-4o-mini) both work.
Fixer allowed paths (optional)
PRR_STRICT_ALLOWED_PATHS(1/true/yes): Enables the strict first-segment heuristic inshared/path-utils.tsisPathAllowedForFix. WHY off by default: Real PRs use top-level dirs outside the old static list; stripping those paths emptiedallowedPathsandallowedPathsForInjection, so batch fixes could not see the target file (Cycle 72). WHY on sometimes: If comment bodies often mention dependency-style paths you do not want in the allowlist, strict mode rejects segments that look like package names unless they appear inREPO_TOP_LEVELor in the PR’sgit diff --name-onlyfile list (setDynamicRepoTopLevelDirsintools/prr/workflow/main-loop-setup.ts). Hard denies (absolute paths,node_modules,dist/,.cursor/.prr) always apply. See DEVELOPMENT.md (State invariants — fixer allowed paths).
Blast radius (optional)
After the PR’s changed files are known, PRR can build a best-effort dependency “bubble” (whole-file regex for imports/includes across common languages, plus same-directory and filename-pattern neighbors), then:
- Deprioritize threads whose primary file is outside that set (
sortByPriority— out-of-scope last). - Narrow llm-api prompt injection to paths inside the bubble (
allowedPathsForInjection); the fixer’s batch allowlist stays full so edits to legitimately related files are not blocked — WHY: Save context on huge repos without the silent “empty injection” failure mode strict path filters caused. - Opt-in dismiss with
PRR_BLAST_RADIUS_DISMISS=1(out-of-scope+ thread reply). Default is deprioritize only.
WHY the feature: Focus order and tokens on files likely related to the PR diff without requiring tsc, go list, language servers, or parsers in the clone. WHY graceful degradation: If the graph hits PRR_BLAST_RADIUS_TIMEOUT_MS, PRR_BLAST_RADIUS_MAX_FILES, or any error, PRR treats all issues as in-scope (same as PRR_DISABLE_BLAST_RADIUS=1) — no silent “fix nothing” path. Implementation uses async filesystem probes in shared/dependency-graph/specifier-resolver.ts so large scans do not block the event loop. See DEVELOPMENT.md (Blast radius), .env.example, docs/ROADMAP.md (optional follow-ups).
Fix-loop hygiene (optional)
Document preview limited. Open the file to continue