@in-the-loop-labs/pair-review 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +82 -7
  2. package/package.json +2 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/.claude-plugin/plugin.json +3 -4
  5. package/plugin-pair-loop/.claude-plugin/plugin.json +19 -0
  6. package/plugin-pair-loop/skills/loop/SKILL.md +233 -0
  7. package/public/js/index.js +87 -10
  8. package/public/js/local.js +19 -9
  9. package/public/js/pr.js +64 -36
  10. package/public/js/repo-links.js +11 -3
  11. package/public/js/utils/analyze-params.js +69 -0
  12. package/public/js/utils/provider-model.js +66 -1
  13. package/public/js/vendor/pierre-diffs-worker.js +16158 -0
  14. package/public/js/vendor/pierre-diffs.js +1880 -0
  15. package/public/local.html +1 -0
  16. package/public/pr.html +1 -0
  17. package/public/setup.html +35 -16
  18. package/src/config.js +150 -28
  19. package/src/database.js +127 -3
  20. package/src/external/github-adapter.js +18 -3
  21. package/src/github/client.js +37 -0
  22. package/src/github/parser.js +41 -7
  23. package/src/interactive-analysis-config.js +2 -2
  24. package/src/links/repo-links.js +66 -28
  25. package/src/local-review.js +134 -5
  26. package/src/local-scope.js +38 -0
  27. package/src/main.js +199 -33
  28. package/src/routes/config.js +47 -12
  29. package/src/routes/external-comments.js +13 -1
  30. package/src/routes/github-collections.js +175 -13
  31. package/src/routes/local.js +11 -20
  32. package/src/routes/pr.js +63 -36
  33. package/src/routes/setup.js +63 -8
  34. package/src/routes/shared.js +85 -0
  35. package/src/routes/stack-analysis.js +39 -3
  36. package/src/server.js +74 -3
  37. package/src/setup/local-setup.js +23 -7
  38. package/src/setup/pr-setup.js +237 -39
  39. package/src/setup/stack-setup.js +7 -2
  40. package/src/single-port.js +73 -18
  41. package/src/utils/host-resolution.js +157 -0
  42. package/plugin-code-critic/skills/loop/SKILL.md +0 -373
package/README.md CHANGED
@@ -20,6 +20,7 @@
20
20
  - [AI-Guided Review](#3-ai-guided-review-when-youre-accountable)
21
21
  - [Quick Start](#quick-start)
22
22
  - [Command Line Interface](#command-line-interface)
23
+ - [Local review scope](#local-review-scope)
23
24
  - [Headless analysis mode](#headless-analysis-mode)
24
25
  - [Configuration](#configuration)
25
26
  - [Environment Variables](#environment-variables)
@@ -89,7 +90,8 @@ Compared to giving feedback in chat, this feels like moving from a machete to a
89
90
 
90
91
  **Tips:**
91
92
  - Stage previous changes in git, then only review new modifications in the next round
92
- - Local mode only shows unstaged changes and untracked files (opinionated by design)
93
+ - By default local mode reviews unstaged changes and untracked files; adjust the range with `--scope` (or the web UI scope selector) — e.g. `branch..untracked` to cover the whole branch (see [Local review scope](#local-review-scope))
94
+
93
95
  ### 2. Meta-Review: Judging AI Suggestions
94
96
 
95
97
  **When to use:** You're not going to read every line of code. Let AI be your reader.
@@ -203,7 +205,10 @@ pair-review --local [path]
203
205
  | `-d`, `--debug` | Enable verbose debug logging for troubleshooting |
204
206
  | `-h`, `--help` | Show help message with full CLI documentation |
205
207
  | `-l`, `--local [path]` | Review local uncommitted changes. Optional path defaults to current directory |
208
+ | `--scope <start>..<end>` | **Local mode only.** Set the diff range a local review covers. Stops (in order): `branch`, `staged`, `unstaged`, `untracked`. Six valid ranges (contiguous, must include `unstaged`); default `unstaged..untracked`. `branch..*` diffs from the merge-base with the base branch. See [Local review scope](#local-review-scope). |
209
+ | `--base <branch>` | **Local mode only.** With a `branch..*` scope, override base-branch auto-detection. Errors if used without a branch-start scope. |
206
210
  | `--model <name>` | Override the AI model for any provider. Model availability depends on provider configuration. |
211
+ | `--provider <name>` | Override the AI provider. Applies to headless modes (`--ai-draft` / `--ai-review`) **and** to browser-driven auto-analysis (`--ai`), where it overrides the repo/app default the browser would otherwise use — including across single-port delegation to an already-running server. Defaults to the repo/app default provider (`claude`). Pair with `--model` when the model belongs to a non-default provider (e.g. `--provider codex --model gpt-5.5`). |
207
212
  | `--register` | Register `pair-review://` URL scheme handler (macOS only) |
208
213
  | `--unregister` | Unregister `pair-review://` URL scheme handler (macOS only) |
209
214
  | `--command <cmd>` | Custom CLI command for `--register` (default: `npx @in-the-loop-labs/pair-review`) |
@@ -216,8 +221,10 @@ pair-review 123 # Review PR #123 in current repo
216
221
  pair-review https://github.com/owner/repo/pull/456
217
222
  pair-review --local # Review uncommitted local changes
218
223
  pair-review 123 --ai # Auto-run AI analysis
224
+ pair-review 123 --ai --provider codex # Auto-run analysis in the browser with a specific provider
219
225
  pair-review --list-councils # List saved councils and their handles
220
226
  pair-review 123 --ai-draft --council security-review # Headless draft with a council
227
+ pair-review 123 --ai-draft --provider codex --model gpt-5.5 # Headless draft with a specific provider + model
221
228
  pair-review --local --ai --council security-review # Local review with a council
222
229
  pair-review --local --headless # Analyze local changes, print a summary, exit
223
230
  pair-review --local --headless --json # Analyze local changes, emit JSON, exit
@@ -240,6 +247,44 @@ pair-review --register --command "node bin/pair-review.js" # Custom command
240
247
  > web UI's default **Analyze** action, so `--council`/`--model` are optional when
241
248
  > a repo default is configured.
242
249
 
250
+ ### Local review scope
251
+
252
+ `--scope <start>..<end>` sets which changes a **local** review covers (it has
253
+ no effect on PR reviews). The scope walks four ordered stops — `branch` →
254
+ `staged` → `unstaged` → `untracked` — and a range must be contiguous and always
255
+ include `unstaged`, since the AI models read files from the working tree and the
256
+ diff must cover that state. That leaves six valid ranges:
257
+
258
+ | Scope | Covers |
259
+ |-------|--------|
260
+ | `branch..unstaged` | All tracked changes since the base branch (committed + staged + unstaged); no new files |
261
+ | `branch..untracked` | Everything since the base branch, including new files |
262
+ | `staged..unstaged` | Staged changes plus working-tree edits vs `HEAD`; no new files |
263
+ | `staged..untracked` | Staged changes, working-tree edits, and new files |
264
+ | `unstaged..unstaged` | Only unstaged working-tree edits (staged changes treated as already reviewed) |
265
+ | `unstaged..untracked` | Unstaged working-tree edits plus new files (**default**) |
266
+
267
+ A scope ending at `untracked` includes new (untracked) files automatically — no
268
+ `git add -N` needed. `branch..*` diffs from the merge-base with the base branch.
269
+
270
+ `--base <branch>` overrides base-branch auto-detection for a `branch..*` scope.
271
+ Detection order when `--base` is omitted: Graphite stack state (only when
272
+ `enable_graphite: true` is set in `~/.pair-review/config.json`) → GitHub PR base →
273
+ `origin`'s default branch → `main`/`master`. `--base` errors if used without a
274
+ branch-start scope.
275
+
276
+ Both flags are **local-mode only**: they error when given a PR argument and do
277
+ **not** imply `--local`. An explicit `--scope` is persisted on the review
278
+ session, so opening the web UI for that review later shows the same scope.
279
+
280
+ ```bash
281
+ # Review everything on this branch since it diverged from the trunk, plus new files
282
+ pair-review --local --scope branch..untracked
283
+
284
+ # Same, but against an explicit base branch (stacked branches / non-default trunk)
285
+ pair-review --local --scope branch..untracked --base develop
286
+ ```
287
+
243
288
  ### Headless analysis mode
244
289
 
245
290
  `--headless` runs an AI analysis (single provider or council), stores the results
@@ -427,7 +472,7 @@ Nested objects (like `chat`, `providers`, `chat_providers`, `monorepos`) are dee
427
472
 
428
473
  ### Alternate Git Hosts
429
474
 
430
- pair-review can review pull requests on self-hosted Git platforms that expose a GitHub-compatible REST API, configured per-repository via `repos["owner/repo"].api_host` and related keys. See [docs/alt-host.md](docs/alt-host.md) for the full configuration guide.
475
+ pair-review can review pull requests on self-hosted Git platforms that expose a GitHub-compatible REST API, configured per-repository via `repos["owner/repo"].api_host` and related keys. By default an `api_host` repo is treated as living exclusively on the alt host; set `exclusive: false` on the repo entry to mark it as **dual-host**, where some PRs live on `github.com` and others on the alt host and pair-review resolves the host per PR. See [docs/alt-host.md](docs/alt-host.md) for the full configuration guide.
431
476
 
432
477
  ### Environment Variables
433
478
 
@@ -444,10 +489,13 @@ pair-review supports several environment variables for customizing behavior:
444
489
  | `PAIR_REVIEW_CURSOR_AGENT_CMD` | Custom command to invoke Cursor Agent CLI | `agent` |
445
490
  | `PAIR_REVIEW_PI_CMD` | Custom command to invoke Pi CLI | `pi` |
446
491
  | `PAIR_REVIEW_MODEL` | Override the AI model to use (same as `--model` flag) | Provider default |
492
+ | `PAIR_REVIEW_PROVIDER` | Override the AI provider to use (same as `--provider` flag) | `claude` |
447
493
 
448
494
  **Note:** `GITHUB_TOKEN` is the standard environment variable used by many GitHub tools (gh CLI, GitHub Actions, etc.). When set, it takes precedence over the `github_token` field in the config file.
449
495
 
450
- **Note:** The `--model` CLI flag is shorthand for setting `PAIR_REVIEW_MODEL`. If both are specified, the CLI flag takes precedence.
496
+ **Note:** The `--model` CLI flag is shorthand for setting `PAIR_REVIEW_MODEL`, and `--provider` is shorthand for `PAIR_REVIEW_PROVIDER`. If both the flag and env var are specified, the CLI flag takes precedence. The `--provider` flag selects which provider runs the analysis; `--model` selects the model within that provider, so set both when the model belongs to a non-default provider (e.g. `--provider codex --model gpt-5.5`). The override outranks saved repository settings (`CLI/env > repo settings`); if the repo's default is a Review Council, an active `--provider`/`--model` override forces the single-provider path instead.
497
+
498
+ **Delegation caveat:** With single-port mode (the default `single_port: true`), a new invocation delegates to an already-running server. On that path the `--provider`/`--model` override is carried to the running server only alongside browser auto-analysis (`--ai`). Delegating without `--ai` (just opening the review) does not seed the override into the other process's manual analysis dialog — use `--ai`, or a headless mode, to pin the provider on the delegated path.
451
499
 
452
500
  These variables are useful when:
453
501
  - Your CLI tools are installed in a non-standard location
@@ -472,6 +520,9 @@ PAIR_REVIEW_ANTIGRAVITY_CMD="/usr/local/bin/agy" pair-review --local
472
520
  # Force a specific model for this review
473
521
  PAIR_REVIEW_MODEL="opus" pair-review 123
474
522
 
523
+ # Force a specific provider + model for a headless review
524
+ PAIR_REVIEW_PROVIDER="codex" PAIR_REVIEW_MODEL="gpt-5.5" pair-review 123 --ai-draft
525
+
475
526
  # Combine multiple settings
476
527
  PAIR_REVIEW_CLAUDE_CMD="/opt/claude/bin/claude" PAIR_REVIEW_MODEL="haiku" pair-review 123
477
528
  ```
@@ -791,7 +842,10 @@ Templates typically include `{description}` to render the suggestion body.
791
842
 
792
843
  ### Local Mode
793
844
 
794
- Review **unstaged**, uncommitted changes before creating a PR:
845
+ Review your uncommitted changes before creating a PR — by default the unstaged
846
+ working-tree edits plus untracked files, with the range adjustable via `--scope`
847
+ (or the web UI scope selector), e.g. `branch..untracked` to cover the whole
848
+ branch (see [Local review scope](#local-review-scope)):
795
849
 
796
850
  ```bash
797
851
  pair-review --local
@@ -806,11 +860,28 @@ Perfect for:
806
860
 
807
861
  ## Claude Code Plugins
808
862
 
809
- pair-review provides two [Claude Code plugins](https://code.claude.com/docs/en/plugins) that bring AI-powered code review directly into Claude Code.
863
+ pair-review provides three [Claude Code plugins](https://code.claude.com/docs/en/plugins) that bring AI-powered code review directly into Claude Code.
864
+
865
+ ### pair-loop — Agent-Orchestrated Review Loop
866
+
867
+ An implement→review→fix loop with pair-review as the review oracle. The agent runs multi-model council reviews through the headless CLI, triages the findings (fixing, dismissing with reasons, or asking you), applies fixes, and repeats with narrowing instructions until a final review returns no blockers. Every round persists to pair-review, and when the server is running the agent writes its triage back — so you can open the web UI, inspect every round, and see exactly what was fixed or dismissed and why. No MCP required.
868
+
869
+ **Install via Marketplace:**
870
+
871
+ ```
872
+ /plugin marketplace add in-the-loop-labs/pair-review
873
+ /plugin install pair-review@pair-loop
874
+ ```
875
+
876
+ **Available Skills:**
877
+
878
+ | Skill | Description |
879
+ |-------|-------------|
880
+ | `/pair-loop:loop` | Implement, review with pair-review councils, triage, fix, and repeat until clean |
810
881
 
811
882
  ### code-critic — Standalone Analysis
812
883
 
813
- AI-powered code review analysis that works without any server or MCP dependency. Install this plugin for three-level AI analysis and implement-review-fix loops directly in your coding agent.
884
+ AI-powered code review analysis that works without any server or MCP dependency. Install this plugin for three-level AI analysis directly in your coding agent.
814
885
 
815
886
  **Install via Marketplace:**
816
887
 
@@ -824,7 +895,8 @@ AI-powered code review analysis that works without any server or MCP dependency.
824
895
  | Skill | Description |
825
896
  |-------|-------------|
826
897
  | `/code-critic:analyze` | Run three-level AI analysis using Task agents directly (standalone, no server needed) |
827
- | `/code-critic:loop` | Implement code, review with AI, fix issues, and repeat until clean |
898
+
899
+ Looking for an implement-review-fix loop? That's the [pair-loop plugin](#pair-loop--agent-orchestrated-review-loop) above, which replaced the former `/code-critic:loop` skill.
828
900
 
829
901
  These skills work standalone. If the pair-review MCP server happens to be available (from the pair-review plugin), `code-critic:analyze` will use it for prompts and push results to the web UI — but it's entirely optional.
830
902
 
@@ -857,10 +929,12 @@ If you prefer not to use the marketplace, load plugins directly from an npm-inst
857
929
 
858
930
  ```bash
859
931
  # From a local clone
932
+ claude --plugin-dir ./path/to/pair-review/plugin-pair-loop
860
933
  claude --plugin-dir ./path/to/pair-review/plugin-code-critic
861
934
  claude --plugin-dir ./path/to/pair-review/plugin
862
935
 
863
936
  # From a globally installed npm package
937
+ claude --plugin-dir "$(npm root -g)/@in-the-loop-labs/pair-review/plugin-pair-loop"
864
938
  claude --plugin-dir "$(npm root -g)/@in-the-loop-labs/pair-review/plugin-code-critic"
865
939
  claude --plugin-dir "$(npm root -g)/@in-the-loop-labs/pair-review/plugin"
866
940
  ```
@@ -880,6 +954,7 @@ To pre-configure plugins for all contributors on a repository, add this to your
880
954
  }
881
955
  },
882
956
  "enabledPlugins": {
957
+ "pair-review@pair-loop": true,
883
958
  "pair-review@code-critic": true,
884
959
  "pair-review@pair-review": true
885
960
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "public/",
15
15
  "plugin/",
16
16
  "plugin-code-critic/",
17
+ "plugin-pair-loop/",
17
18
  "config.managed.json",
18
19
  "README.md",
19
20
  "LICENSE"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "code-critic",
3
- "version": "4.0.0",
4
- "description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
3
+ "version": "4.1.0",
4
+ "description": "AI-powered code review analysis — Run three-level AI analysis directly in your coding agent. Works standalone, no server required.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
7
7
  "email": "tjwp@inthelooplabs.com"
@@ -13,7 +13,6 @@
13
13
  "code-review",
14
14
  "ai",
15
15
  "analysis",
16
- "code-critic",
17
- "critic-loop"
16
+ "code-critic"
18
17
  ]
19
18
  }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "pair-loop",
3
+ "version": "4.1.0",
4
+ "description": "Agent-orchestrated review loop — implement, review with pair-review multi-model councils via the headless CLI, triage, fix, and repeat until a final review returns no blockers. MCP-independent.",
5
+ "author": {
6
+ "name": "in-the-loop-labs",
7
+ "email": "tjwp@inthelooplabs.com"
8
+ },
9
+ "homepage": "https://github.com/in-the-loop-labs/pair-review",
10
+ "repository": "https://github.com/in-the-loop-labs/pair-review",
11
+ "license": "Apache-2.0",
12
+ "keywords": [
13
+ "code-review",
14
+ "ai",
15
+ "review-loop",
16
+ "council",
17
+ "feedback-loop"
18
+ ]
19
+ }
@@ -0,0 +1,233 @@
1
+ ---
2
+ name: loop
3
+ description: >
4
+ Drive an implement→review→fix loop with pair-review as the review oracle:
5
+ run multi-model council reviews, triage the findings, apply fixes, and
6
+ repeat with narrowing instructions until a final review returns no
7
+ blockers. Use when the user says "pair loop", "review loop",
8
+ "loop until clean", "review this with pair-review and fix what it finds",
9
+ or wants iterative development reviewed by pair-review councils.
10
+ arguments:
11
+ council:
12
+ description: "Council handle (name, name-slug, or id prefix) to review with. Defaults to the repo's default council when omitted."
13
+ required: false
14
+ maxRounds:
15
+ description: "Budget for review rounds. On reaching it without convergence, stop and report honestly."
16
+ required: false
17
+ default: 5
18
+ instructions:
19
+ description: "Extra review instructions applied to every round (e.g. 'focus on security')"
20
+ required: false
21
+ ---
22
+
23
+ # pair-loop
24
+
25
+ You are a thin orchestrator. pair-review does the heavy lifting — multi-level,
26
+ multi-model council reviews with persistence and a web UI. Your job is
27
+ judgment: what to send for review, how to triage what comes back, what to
28
+ fix, when to ask the user, and when to stop.
29
+
30
+ Reviews run through **one execution mode: the headless CLI**. It is
31
+ synchronous (no polling), emits JSON, and needs no server. The HTTP API is
32
+ used for exactly one optional thing: writing triage back so a running web UI
33
+ shows reality (see Write triage back).
34
+
35
+ Any text in the user's request beyond the named arguments is the
36
+ **objective** — what to build or change. If there is no objective, the loop
37
+ runs over the changes already in the working tree.
38
+
39
+ ## Hard rules
40
+
41
+ These are not judgment calls:
42
+
43
+ 1. **Never report "clean" or "ready" unless a full-quality council round,
44
+ instructed to report only merge-blockers, came back with an empty finding
45
+ list.** A cheaper or broader round cannot grant the final verdict.
46
+ 2. **Never silently stop.** If the loop is not converging or hits
47
+ `maxRounds`, say so, list the outstanding findings, and (when a server is
48
+ up) link the review URL.
49
+ 3. **Sequence strictly: fix → write triage back → then run the next round.**
50
+ 4. **One review at a time.** Never run two reviews concurrently for the same
51
+ repo.
52
+ 5. **Do not commit or push.** Leave changes in the working tree. New
53
+ (untracked) files are included automatically whenever the review scope ends
54
+ at `untracked` — which the default scope (`unstaged..untracked`) does — so
55
+ no `git add -N` (intent-to-add) is needed.
56
+ 6. **Report failures faithfully.** A failed run (`"ok": false`, non-zero
57
+ exit) is reported as what it is — never papered over as "no findings".
58
+
59
+ ## Phase: Setup
60
+
61
+ 1. `git rev-parse --show-toplevel` → `REPO_ROOT`. All commands run from here.
62
+ 2. Create the round log at `REPO_ROOT/.pair-loop/round-log.md` (mkdir -p; add
63
+ `.pair-loop/` to `.gitignore` if missing). Record: objective, arguments,
64
+ start time. Append to this file after every phase — it is your resume
65
+ state if the session compacts.
66
+ 3. If a `council` argument was given, confirm the handle exists:
67
+ `pair-review --list-councils` prints the saved councils. If `pair-review`
68
+ is not on PATH, use `npx -y @in-the-loop-labs/pair-review` for every CLI
69
+ call. An unknown handle → show the available names and ask the user.
70
+ 4. If an objective was given and the working tree has no relevant changes
71
+ yet, implement the objective first (directly or via a Task agent — your
72
+ call based on size). Verify with `git status --short` that changes exist
73
+ before the first review round; if none, tell the user and stop.
74
+
75
+ ## Running a review round
76
+
77
+ ```bash
78
+ pair-review --local --headless --json --council <handle> [--scope <range>] --instructions "<round instructions>"
79
+ ```
80
+
81
+ - Run from `REPO_ROOT`. `--local` with no path uses the current directory.
82
+ - Omit `--council` to use the repo's default council.
83
+ - **Scope (`--scope`).** By default a local review covers only uncommitted
84
+ working-tree changes plus new files (`unstaged..untracked`). That is right
85
+ only while all the work under review stays uncommitted. If the work under
86
+ review already spans commits on a branch, a clean working tree would review as
87
+ **empty** — pass
88
+ `--scope branch..untracked` so every round covers the whole branch
89
+ (everything since the base branch) plus the working tree and new files. Add
90
+ `--base <branch>` when the base branch is not auto-detected (a non-default
91
+ trunk, or a stacked branch). `--scope`/`--base` are local-mode only.
92
+ - **Be patient — council reviews routinely take 15–40 minutes.** Individual
93
+ council voices have their own internal timeouts; trust the CLI to finish.
94
+ Run the command in the background (or with a timeout of at least 45
95
+ minutes) and wait for it to exit. Do not abandon or kill it early; do not
96
+ start a second review while one is running (hard rule 4).
97
+ - stdout is exactly one JSON document (logs go to stderr):
98
+ - success: `{"ok": true, "mode": "local", "run": {...}, "suggestions":
99
+ [...], "count": N}` — exit code 0. **Zero suggestions is still
100
+ success.** Record `run.id` (the runId) and `run.review_id` (the
101
+ reviewId, used for write-back and the review URL).
102
+ - failure: `{"ok": false, "error": {"message": ...}}`, exit code 1.
103
+
104
+ ### Round instructions
105
+
106
+ Build `--instructions` per round from, in order: this standing line —
107
+ "Do not report praise findings; report only issues." — then the user's
108
+ `instructions` argument, the objective, and this round's directive (see
109
+ Convergence judgment). On rounds after the first, add: "Earlier review
110
+ rounds already reported issues that have since been fixed or dismissed;
111
+ report only issues present in the current code."
112
+
113
+ ### Suggestion fields
114
+
115
+ Each entry in `suggestions`: `id`, `file`, `line_start`, `line_end`, `type`
116
+ (`bug|improvement|suggestion|design|performance|security|code-style` —
117
+ praise is instructed away; ignore it if it appears anyway), `severity`
118
+ (`critical|medium|minor`), `title`, `body`, `reasoning`, `ai_confidence`
119
+ (0–1).
120
+
121
+ ## Triage
122
+
123
+ For each finding, decide one of:
124
+
125
+ - **Fix** — it is real and in scope. Goes on this round's fix list.
126
+ - **Dismiss** — false positive, out of scope, or explicitly contrary to the
127
+ user's stated intent. Requires a stated reason in the round log.
128
+ - **Ask the user** — mandatory stop, not a preference, when any of:
129
+ (a) you would dismiss a **critical**-severity finding;
130
+ (b) the fix would change behavior beyond the stated objective;
131
+ (c) the same finding has survived two fix rounds;
132
+ (d) findings conflict with the user's explicit instructions.
133
+ Batch the questions, ask once per round at most, record the answers in the
134
+ round log so later rounds do not re-ask.
135
+
136
+ Dedup against your round log: a finding you already dismissed in an earlier
137
+ round, reappearing unchanged, keeps its dismissal (re-dismiss it in
138
+ write-back) — unless a fix touched that code since, in which case judge it
139
+ fresh.
140
+
141
+ ## Fix
142
+
143
+ Apply the fixes (directly or via a Task agent for large batches — your
144
+ call). Run the project's relevant tests for the changed code. A fix that
145
+ breaks tests is not a fix.
146
+
147
+ ## Write triage back (optional, needs the server)
148
+
149
+ If the pair-review server is running, keep its UI truthful. Probe once per
150
+ loop: `curl -s http://localhost:7247/health` → it is pair-review iff the
151
+ response has `"service": "pair-review"`. (7247 is the default and is right
152
+ most of the time; if it isn't pair-review, check `port` in
153
+ `REPO_ROOT/.pair-review/config.json` then `~/.pair-review/config.json`, then
154
+ give up on write-back for this loop.)
155
+
156
+ When the server is up, after fixing/dismissing, for each handled finding:
157
+
158
+ ```bash
159
+ curl -s -X POST http://localhost:<port>/api/reviews/<run.review_id>/suggestions/<id>/status \
160
+ -H 'Content-Type: application/json' -d '{"status": "dismissed"}'
161
+ ```
162
+
163
+ - Only `"dismissed"` and `"active"` are accepted; `"adopted"` is reserved
164
+ for the human's adopt flow — never send it.
165
+ - Newer servers accept a `reason` field on this endpoint (check
166
+ `http://localhost:<port>/api.md?reviewId=<reviewId>` if unsure); when
167
+ supported, include it: `"Fixed in loop round <n>: ..."` or
168
+ `"Dismissed: <why>"`. Otherwise the fixed-vs-rejected distinction lives in
169
+ your round log and final report.
170
+ - The human may be triaging in the same UI: before fixing, you may re-check
171
+ `GET /api/reviews/<reviewId>/suggestions` and skip findings whose status
172
+ is no longer `active` — the human got there first. Do not resurrect
173
+ anything the human dismissed.
174
+ - The review URL for your report: `http://localhost:<port>/local/<reviewId>`.
175
+ Note: the UI does not live-update during CLI runs; results appear on
176
+ refresh.
177
+
178
+ No server → skip this entirely; triage lives in the round log and final
179
+ report, and the persisted run history is visible whenever the user next
180
+ starts the server.
181
+
182
+ ## Convergence judgment
183
+
184
+ You decide each round's directive and whether to continue. Principles:
185
+
186
+ - **Round 1**: broad. Directive like: "Review these changes thoroughly.
187
+ Objective: <objective>."
188
+ - **Escalate** back to a broad directive whenever the previous round
189
+ produced any `critical` finding.
190
+ - **Narrow** as findings shrink. The gate directive:
191
+ "Final review. Only report blockers — issues that must be fixed before
192
+ merging. Is this ready to merge?"
193
+ - **The gate uses the same council.** The round that says "merge it" must be
194
+ just as rigorous as the rounds that found problems. Do not substitute a
195
+ cheaper configuration for gate rounds.
196
+ - **Continue vs stop**: another round is worth it when the last one caused
197
+ substantial fixes. When rounds churn the same ground without progress,
198
+ stop and ask the user instead of grinding the budget.
199
+ - The loop **ends successfully** when a gate round returns **no blockers** —
200
+ an empty finding list (per hard rule 1). Read the run's `summary` for the
201
+ reviewer's own answer to the ready-to-merge question and quote it in your
202
+ report.
203
+
204
+ ## PR mode
205
+
206
+ The loop also works against a GitHub PR:
207
+
208
+ ```bash
209
+ pair-review <pr-number-or-url> --headless --json [--council <handle>] [--instructions "..."]
210
+ ```
211
+
212
+ This self-bootstraps the PR (fetches metadata, prepares a review worktree)
213
+ and requires a GitHub token in `~/.pair-review/config.json`. The review's
214
+ checkout lives in a pair-review worktree — your fixes belong in whatever
215
+ checkout the user asked you to work in; do not edit the pool worktree.
216
+ Write-back and the review URL work the same way via `run.review_id`.
217
+ `--scope`/`--base` do not apply here — they are local-mode only; a PR review
218
+ always covers the PR's full diff.
219
+
220
+ ## Reporting
221
+
222
+ Every loop ends with a report containing:
223
+
224
+ 1. **Verdict** — converged (quote the final run's summary) or not converged
225
+ (why, and what remains).
226
+ 2. **Rounds table** — per round: directive, findings found, fixed, dismissed,
227
+ asked.
228
+ 3. **Triage detail** — every dismissed finding with its reason; every fix in
229
+ one line each.
230
+ 4. **The review URL** when a server is up, so the user can inspect
231
+ everything in the web UI.
232
+ 5. Working-tree status reminder: changes are uncommitted, tests run and
233
+ their result.
@@ -445,8 +445,14 @@
445
445
  ? ''
446
446
  : '<td class="col-author">' + authorDisplay + '</td>';
447
447
 
448
+ // `data-host` carries the alt-host api_host string for PRs that live on an
449
+ // alternate host; github.com rows have no host and render an empty
450
+ // attribute. The click handler threads it into the setup call so a dual
451
+ // repo's alt-host PR opens against the right system without re-probing.
452
+ var hostAttr = pr.host ? ' data-host="' + escapeHtml(pr.host) + '"' : '';
453
+
448
454
  return '' +
449
- '<tr class="collection-pr-row" data-pr-url="' + escapeHtml(prUrl) + '" data-owner="' + escapeHtml(pr.owner) + '" data-repo="' + escapeHtml(pr.repo) + '" data-number="' + pr.number + '">' +
455
+ '<tr class="collection-pr-row" data-pr-url="' + escapeHtml(prUrl) + '" data-owner="' + escapeHtml(pr.owner) + '" data-repo="' + escapeHtml(pr.repo) + '" data-number="' + pr.number + '"' + hostAttr + '>' +
450
456
  '<td class="col-repo">' + escapeHtml(repoFull) + '</td>' +
451
457
  '<td class="col-pr"><span class="collection-pr-number">#' + pr.number + '</span></td>' +
452
458
  '<td class="col-title" title="' + escapeHtml(pr.title || '') + '">' + escapeHtml(pr.title || '') + '</td>' +
@@ -1293,7 +1299,14 @@
1293
1299
  return {
1294
1300
  owner: data.owner,
1295
1301
  repo: data.repo,
1296
- prNumber: data.prNumber
1302
+ prNumber: data.prNumber,
1303
+ // `host` tells setup which system the PR lives on: an api_host URL
1304
+ // string = alt host, null = github.com, undefined = unknown.
1305
+ host: data.host,
1306
+ bindingRepository: data.bindingRepository,
1307
+ // Whether the repo is dual (github + alt-host). Only dual repos probe,
1308
+ // so only they need an explicit github pick when the URL is a github URL.
1309
+ isDualHost: data.isDualHost
1297
1310
  };
1298
1311
  }
1299
1312
 
@@ -1343,7 +1356,22 @@
1343
1356
  // Navigate to the PR route which serves setup.html (with step-by-step progress)
1344
1357
  // for new PRs, or pr.html directly for PRs already in the database
1345
1358
  let href = '/pr/' + encodeURIComponent(parsed.owner) + '/' + encodeURIComponent(parsed.repo) + '/' + encodeURIComponent(parsed.prNumber);
1346
- if (analyze) href += '?analyze=true';
1359
+ var params = new URLSearchParams();
1360
+ if (analyze) params.set('analyze', 'true');
1361
+ // Forward the parsed host so setup binds directly instead of probing:
1362
+ // - alt-host URL string → the api_host (setup binds that alt host)
1363
+ // - github URL (host === null) on a DUAL repo → the "github" sentinel, so
1364
+ // setup binds github.com instead of probing alt-first (which would fail
1365
+ // loudly if the alt host is down for a PR we KNOW is on github)
1366
+ // - anything else (plain/exclusive repo, unknown host) → omit; no probe
1367
+ // happens for those and omitting avoids the exclusive-null throw.
1368
+ if (typeof parsed.host === 'string' && parsed.host) {
1369
+ params.set('host', parsed.host);
1370
+ } else if (parsed.host === null && parsed.isDualHost) {
1371
+ params.set('host', 'github');
1372
+ }
1373
+ var qs = params.toString();
1374
+ if (qs) href += '?' + qs;
1347
1375
  window.location.href = href;
1348
1376
  }
1349
1377
 
@@ -1385,6 +1413,11 @@
1385
1413
  window.__pairReview.chatProviders = chatProviders;
1386
1414
  window.__pairReview.defaultProvider = config.default_provider || 'claude';
1387
1415
  window.__pairReview.defaultModel = config.default_model || 'opus';
1416
+ // CLI/env override (PAIR_REVIEW_PROVIDER / PAIR_REVIEW_MODEL) surfaced
1417
+ // by /api/config. The bulk-analysis seed prepends these ahead of repo
1418
+ // settings so a `--provider` override outranks a repo's saved default.
1419
+ window.__pairReview.providerOverride = config.provider_override || null;
1420
+ window.__pairReview.modelOverride = config.model_override || null;
1388
1421
  window.__pairReview.hasGithubToken = Boolean(config.has_github_token);
1389
1422
  window.__pairReview.enableGraphite = config.enable_graphite === true;
1390
1423
  window.__pairReview.chatSpinner = config.chat_spinner || 'dots';
@@ -1891,7 +1924,7 @@
1891
1924
  * Read selected collection rows as PR descriptors.
1892
1925
  * @param {Set} selectedIds - PR URLs (data-pr-url values)
1893
1926
  * @param {string} tbodyId - tbody element ID
1894
- * @returns {Array<{owner: string, repo: string, number: string, prUrl: string}>}
1927
+ * @returns {Array<{owner: string, repo: string, number: string, prUrl: string, host: string|undefined}>}
1895
1928
  */
1896
1929
  function getSelectedCollectionRows(selectedIds, tbodyId) {
1897
1930
  var tbody = document.getElementById(tbodyId);
@@ -1908,7 +1941,11 @@
1908
1941
  owner: tr.dataset.owner,
1909
1942
  repo: tr.dataset.repo,
1910
1943
  number: tr.dataset.number,
1911
- prUrl: prUrl
1944
+ prUrl: prUrl,
1945
+ // Alt-host rows carry a host; github.com rows leave it undefined. The
1946
+ // single-row click path threads this the same way (see the collection
1947
+ // row click handler) so bulk-open binds to the right system too.
1948
+ host: tr.dataset.host
1912
1949
  });
1913
1950
  }
1914
1951
  }
@@ -1917,7 +1954,14 @@
1917
1954
 
1918
1955
  function buildReviewUrlsFromRows(rows, query) {
1919
1956
  return rows.map(function (row) {
1920
- return '/pr/' + encodeURIComponent(row.owner) + '/' + encodeURIComponent(row.repo) + '/' + row.number + (query || '');
1957
+ // Preserve any existing params (analyze / analysisConfigId) and append the
1958
+ // alt host so setup opens the PR against the system it lives on instead of
1959
+ // re-probing. github.com rows (no host) keep the URL unchanged.
1960
+ var qs = query || '';
1961
+ if (row.host) {
1962
+ qs += (qs ? '&' : '?') + 'host=' + encodeURIComponent(row.host);
1963
+ }
1964
+ return '/pr/' + encodeURIComponent(row.owner) + '/' + encodeURIComponent(row.repo) + '/' + row.number + qs;
1921
1965
  });
1922
1966
  }
1923
1967
 
@@ -2000,10 +2044,18 @@
2000
2044
  };
2001
2045
 
2002
2046
  var providersInfo = await getBulkProvidersInfo();
2003
- var resolvedPair = window.resolveProviderModelPair([
2004
- { provider: repoSettings?.default_provider, model: repoSettings?.default_model },
2005
- { provider: window.__pairReview?.defaultProvider, model: window.__pairReview?.defaultModel }
2006
- ], providersInfo);
2047
+ // Shape __pairReview into the appConfig fields buildProviderModelScopes reads
2048
+ // so a CLI/env `--provider` override is prepended ahead of repo settings.
2049
+ var bulkAppConfig = {
2050
+ default_provider: window.__pairReview?.defaultProvider,
2051
+ default_model: window.__pairReview?.defaultModel,
2052
+ provider_override: window.__pairReview?.providerOverride || null,
2053
+ model_override: window.__pairReview?.modelOverride || null
2054
+ };
2055
+ var resolvedPair = window.resolveProviderModelPair(
2056
+ window.buildProviderModelScopes(repoSettings, bulkAppConfig),
2057
+ providersInfo
2058
+ );
2007
2059
 
2008
2060
  var config = await bulkAnalysisConfigModal.show({
2009
2061
  currentModel: resolvedPair.model,
@@ -2163,6 +2215,21 @@
2163
2215
  return;
2164
2216
  }
2165
2217
 
2218
+ // Alt-host PR: we already know owner/repo/number and the host it lives
2219
+ // on, so navigate straight to the PR route with the host as a query
2220
+ // param (setup.html forwards it into the setup POST body). This bypasses
2221
+ // the URL-parse round trip, which can't reliably recover the host from a
2222
+ // pasted alt-host html_url. github.com rows (no host) keep the existing
2223
+ // parse-and-submit flow below, byte-identical.
2224
+ var rowHost = collectionRow.dataset.host;
2225
+ if (rowHost) {
2226
+ var o = encodeURIComponent(collectionRow.dataset.owner);
2227
+ var r = encodeURIComponent(collectionRow.dataset.repo);
2228
+ var n = encodeURIComponent(collectionRow.dataset.number);
2229
+ window.location.href = '/pr/' + o + '/' + r + '/' + n + '?host=' + encodeURIComponent(rowHost);
2230
+ return;
2231
+ }
2232
+
2166
2233
  var prUrl = collectionRow.dataset.prUrl;
2167
2234
  if (prUrl) {
2168
2235
  // Switch to PR tab to show loading state (do NOT persist to
@@ -2542,4 +2609,14 @@
2542
2609
  window.__pairReview = window.__pairReview || {};
2543
2610
  window.__pairReview.refreshAnalysisSpinners = fetchAndApplyActiveAnalyses;
2544
2611
 
2612
+ // Test-only exports. In the browser `module` is undefined, so this is a
2613
+ // no-op; under Vitest (CommonJS) it exposes the internal bulk-open helpers
2614
+ // for direct unit testing rather than duplicating their logic in a test.
2615
+ if (typeof module !== 'undefined' && module.exports) {
2616
+ module.exports = {
2617
+ buildReviewUrlsFromRows: buildReviewUrlsFromRows,
2618
+ getSelectedCollectionRows: getSelectedCollectionRows
2619
+ };
2620
+ }
2621
+
2545
2622
  })();