@in-the-loop-labs/pair-review 3.7.2 → 3.9.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.
- package/README.md +188 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/local.js +28 -8
- package/public/js/pr.js +92 -7
- package/public/js/utils/provider-model.js +9 -2
- package/public/setup.html +8 -0
- package/src/ai/claude-provider.js +31 -30
- package/src/ai/codex-provider.js +4 -3
- package/src/ai/copilot-provider.js +15 -1
- package/src/ai/cursor-agent-provider.js +4 -3
- package/src/ai/executable-provider.js +12 -4
- package/src/ai/gemini-provider.js +15 -1
- package/src/ai/index.js +14 -0
- package/src/ai/opencode-provider.js +15 -1
- package/src/ai/pi-provider.js +4 -3
- package/src/ai/provider.js +294 -38
- package/src/chat/chat-providers.js +30 -9
- package/src/councils/headless-council.js +128 -0
- package/src/councils/resolve-council.js +167 -0
- package/src/database.js +52 -0
- package/src/git/worktree.js +7 -1
- package/src/interactive-analysis-config.js +152 -0
- package/src/local-review.js +54 -23
- package/src/main.js +1125 -25
- package/src/mcp-stdio.js +40 -5
- package/src/review-config.js +164 -0
- package/src/routes/bulk-analysis-configs.js +45 -15
- package/src/routes/config.js +9 -4
- package/src/routes/executable-analysis.js +10 -1
- package/src/routes/local.js +170 -109
- package/src/routes/mcp.js +28 -31
- package/src/routes/pr.js +118 -56
- package/src/single-port.js +131 -5
- package/src/utils/logger.js +25 -4
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
|
+
- [Headless analysis mode](#headless-analysis-mode)
|
|
23
24
|
- [Configuration](#configuration)
|
|
24
25
|
- [Environment Variables](#environment-variables)
|
|
25
26
|
- [GitHub Token](#github-token)
|
|
@@ -192,6 +193,12 @@ pair-review --local [path]
|
|
|
192
193
|
| `<PR-URL>` | Full GitHub PR URL (e.g., `https://github.com/owner/repo/pull/123`) |
|
|
193
194
|
| `--ai` | Automatically run AI analysis when the review loads |
|
|
194
195
|
| `--ai-draft` | Run AI analysis and save suggestions as a draft review on GitHub |
|
|
196
|
+
| `--headless` | Run AI analysis, store it in the local database, report the results, then exit. No server, no browser, no GitHub post. Implies analysis (does not require `--ai`). Works with a `<PR-number-or-URL>` or with `--local`. See [Headless analysis mode](#headless-analysis-mode). |
|
|
197
|
+
| `--json` | With `--headless`, emit the completed run plus its consolidated suggestions as a single JSON document on a clean stdout. For any outcome of the headless run (success, zero findings, or an error raised while running it), stdout carries a JSON document with an `ok` field; failures emit an `{ "ok": false, "error": … }` envelope on stdout with a non-zero exit. (Pre-flight config/startup failures are the exception: they exit non-zero with a stderr message and no JSON envelope — branch on the exit code first.) stderr is quiet by default (only warnings/errors) — add `--debug` for verbose progress logs. Only valid together with `--headless`. |
|
|
198
|
+
| `--instructions <text>` | Per-run custom instructions for this analysis. Applies to any mode that runs analysis — `--headless`, `--ai-draft`, `--ai-review` (consumed directly), and `--ai`/`--council` (carried into the browser-triggered analysis). Rejected with a clear error if no analysis mode is selected, so it is never silently dropped. Default: none. |
|
|
199
|
+
| `--instructions-file <path>` | Read per-run custom instructions from a file (5000-character cap). Mutually exclusive with `--instructions`. |
|
|
200
|
+
| `--council <handle>` | Run analysis with a saved multi-voice council. Implies analysis. The handle resolves by council name, name-slug, id (prefix), or a partial name fragment (resolving when it matches a single council, otherwise listing the candidates). When set, `--model` is ignored (council voices use their own per-voice models). Works in headless PR (`--ai-draft`/`--ai-review`/`--headless`), interactive PR (`--ai`), and local (`--local --ai`) modes. |
|
|
201
|
+
| `--list-councils` | List saved councils with their handles, names, types, and last-used repo, then exit. Use a printed handle with `--council`. |
|
|
195
202
|
| `--configure` | Show setup instructions and configuration options |
|
|
196
203
|
| `-d`, `--debug` | Enable verbose debug logging for troubleshooting |
|
|
197
204
|
| `-h`, `--help` | Show help message with full CLI documentation |
|
|
@@ -209,10 +216,166 @@ pair-review 123 # Review PR #123 in current repo
|
|
|
209
216
|
pair-review https://github.com/owner/repo/pull/456
|
|
210
217
|
pair-review --local # Review uncommitted local changes
|
|
211
218
|
pair-review 123 --ai # Auto-run AI analysis
|
|
219
|
+
pair-review --list-councils # List saved councils and their handles
|
|
220
|
+
pair-review 123 --ai-draft --council security-review # Headless draft with a council
|
|
221
|
+
pair-review --local --ai --council security-review # Local review with a council
|
|
222
|
+
pair-review --local --headless # Analyze local changes, print a summary, exit
|
|
223
|
+
pair-review --local --headless --json # Analyze local changes, emit JSON, exit
|
|
224
|
+
pair-review 123 --headless # Analyze a PR, print a summary, exit
|
|
212
225
|
pair-review --register # Register pair-review:// URL scheme (macOS)
|
|
213
226
|
pair-review --register --command "node bin/pair-review.js" # Custom command
|
|
214
227
|
```
|
|
215
228
|
|
|
229
|
+
> **Councils** are saved multi-voice review configurations created and managed
|
|
230
|
+
> in the web UI (under Analysis settings). `--council <handle>` selects one from
|
|
231
|
+
> the CLI; the handle can be the council's name, its name-slug, an id prefix, or
|
|
232
|
+
> a partial name fragment (which resolves when it uniquely identifies a council,
|
|
233
|
+
> otherwise lists the candidates). Run `pair-review --list-councils` to discover
|
|
234
|
+
> available handles.
|
|
235
|
+
|
|
236
|
+
> **Repo default review config:** when neither `--council` nor `--model` is
|
|
237
|
+
> given, pair-review uses the repository's configured default — its default
|
|
238
|
+
> council if one is set, otherwise its default provider/model, otherwise the
|
|
239
|
+
> global config default. This applies to `--headless`, the submit modes, and the
|
|
240
|
+
> web UI's default **Analyze** action, so `--council`/`--model` are optional when
|
|
241
|
+
> a repo default is configured.
|
|
242
|
+
|
|
243
|
+
### Headless analysis mode
|
|
244
|
+
|
|
245
|
+
`--headless` runs an AI analysis (single provider or council), stores the results
|
|
246
|
+
in the local SQLite database exactly as the web UI does, reports them, and exits.
|
|
247
|
+
It never starts the server, opens a browser, or posts to GitHub — it is the
|
|
248
|
+
analysis core without the interactive or submit steps.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Analyze uncommitted local changes, print a human-readable summary
|
|
252
|
+
pair-review --local --headless
|
|
253
|
+
|
|
254
|
+
# Analyze a PR (no review is created on GitHub)
|
|
255
|
+
pair-review 123 --headless
|
|
256
|
+
|
|
257
|
+
# Use a council and per-run instructions
|
|
258
|
+
pair-review --local --headless --council security-review \
|
|
259
|
+
--instructions "Focus on auth and input validation."
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Exit codes:** a successful analysis exits `0` regardless of how many findings it
|
|
263
|
+
surfaces (zero findings is still success, with an empty `suggestions` array).
|
|
264
|
+
Non-zero exit is reserved for operational errors — invalid flags, an unreadable
|
|
265
|
+
`--instructions-file`, or an analysis failure.
|
|
266
|
+
|
|
267
|
+
**Machine-readable output (`--json`):** add `--json` to emit the completed run
|
|
268
|
+
plus its consolidated final suggestions as a single JSON document on stdout, so
|
|
269
|
+
stdout is a clean, parseable document suitable for scripting and AI coding
|
|
270
|
+
agents:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
pair-review --local --headless --json | jq '.suggestions | length'
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Because this mode is consumed mostly by coding agents — whose shell tools capture
|
|
277
|
+
stderr alongside stdout — **stderr is quiet by default**: only genuine warnings
|
|
278
|
+
and errors are emitted, not progress narration. Add `--debug` to restore the full
|
|
279
|
+
verbose log stream on stderr when diagnosing a run.
|
|
280
|
+
|
|
281
|
+
The JSON document has this shape (fields reflect the stored run and the
|
|
282
|
+
consolidated suggestion layer the app shows by default):
|
|
283
|
+
|
|
284
|
+
```jsonc
|
|
285
|
+
{
|
|
286
|
+
"ok": true, // false on failure (see below)
|
|
287
|
+
"mode": "local", // "pr" or "local"
|
|
288
|
+
"run": {
|
|
289
|
+
"id": "…", // analysis run id
|
|
290
|
+
"review_id": 1,
|
|
291
|
+
"provider": "claude", // for a council run: "council"
|
|
292
|
+
"model": "opus", // for a council run: the council id
|
|
293
|
+
"tier": "balanced",
|
|
294
|
+
"config_type": "standard", // or "council" / "advanced"
|
|
295
|
+
"status": "completed",
|
|
296
|
+
"summary": "…",
|
|
297
|
+
"total_suggestions": 3,
|
|
298
|
+
"files_analyzed": 5,
|
|
299
|
+
"started_at": "…",
|
|
300
|
+
"completed_at": "…",
|
|
301
|
+
"head_sha": "…",
|
|
302
|
+
"parent_run_id": null,
|
|
303
|
+
"custom_instructions": null,
|
|
304
|
+
"global_instructions": null,
|
|
305
|
+
"repo_instructions": null,
|
|
306
|
+
"request_instructions": "Focus on auth…", // from --instructions[-file]
|
|
307
|
+
"levels_config": { /* parsed */ },
|
|
308
|
+
"level_outcomes": { /* parsed */ }
|
|
309
|
+
},
|
|
310
|
+
"suggestions": [
|
|
311
|
+
{
|
|
312
|
+
"id": 42,
|
|
313
|
+
"ai_run_id": "…",
|
|
314
|
+
"ai_level": null, // consolidated layer (per-level rows excluded)
|
|
315
|
+
"ai_confidence": 0.9,
|
|
316
|
+
"file": "src/app.js",
|
|
317
|
+
"line_start": 120,
|
|
318
|
+
"line_end": 124,
|
|
319
|
+
"type": "bug",
|
|
320
|
+
"title": "…",
|
|
321
|
+
"body": "…",
|
|
322
|
+
"severity": "high",
|
|
323
|
+
"status": "active", // active or adopted
|
|
324
|
+
"is_file_level": 0,
|
|
325
|
+
"reasoning": { /* parsed */ },
|
|
326
|
+
"created_at": "…"
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
"count": 1 // suggestions.length
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**On failure** (invalid flags, an unreadable `--instructions-file`, or an
|
|
334
|
+
analysis error raised while running the headless flow) `--json` emits a JSON
|
|
335
|
+
document on stdout — a compact failure envelope — and the exit code is non-zero,
|
|
336
|
+
so a consumer parses one stream and branches on `ok`:
|
|
337
|
+
|
|
338
|
+
```jsonc
|
|
339
|
+
{
|
|
340
|
+
"ok": false,
|
|
341
|
+
"mode": "local", // "pr" or "local"
|
|
342
|
+
"error": { "message": "…" }
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The envelope covers errors raised once the headless run is under way. A fatal
|
|
347
|
+
*pre-flight* failure — an unreadable or malformed `~/.pair-review/config.json`, an
|
|
348
|
+
unwritable config directory, or an invalid port — exits non-zero with a message
|
|
349
|
+
on **stderr** and does **not** emit a JSON envelope on stdout. Branch on the exit
|
|
350
|
+
code first (as the example below does), then parse stdout.
|
|
351
|
+
|
|
352
|
+
**Agent workflow.** A coding agent can run a headless analysis against its own
|
|
353
|
+
uncommitted work, parse the JSON, and act on the suggestions:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
result=$(pair-review --local --headless --json \
|
|
357
|
+
--council security-review \
|
|
358
|
+
--instructions "Review the changes I just made for security issues.")
|
|
359
|
+
|
|
360
|
+
# Bail out on failure: non-zero exit, and `ok` is false in the JSON envelope.
|
|
361
|
+
if [ $? -ne 0 ] || [ "$(echo "$result" | jq -r '.ok')" != "true" ]; then
|
|
362
|
+
echo "analysis failed: $(echo "$result" | jq -r '.error.message')" >&2
|
|
363
|
+
exit 1
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
# Act on high-severity findings
|
|
367
|
+
echo "$result" | jq -r '.suggestions[]
|
|
368
|
+
| select(.severity == "high")
|
|
369
|
+
| "\(.file):\(.line_start) [\(.type)] \(.title)"'
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Once the run starts, `--json` produces a JSON document on stdout for any outcome
|
|
373
|
+
— `{ "ok": true, … }` on success (any number of findings, including zero) and
|
|
374
|
+
`{ "ok": false, … }` with a non-zero exit on an error raised while running it —
|
|
375
|
+
so the agent reads exactly one stream and branches on `ok`. Because a pre-flight
|
|
376
|
+
config/startup failure is the one case that exits without a JSON envelope,
|
|
377
|
+
checking the exit code first (as above) covers every outcome.
|
|
378
|
+
|
|
216
379
|
## Configuration
|
|
217
380
|
|
|
218
381
|
On first run, pair-review will prompt you to configure the application.
|
|
@@ -360,11 +523,11 @@ You can override provider settings and define custom models in your config file.
|
|
|
360
523
|
"command": "opencode",
|
|
361
524
|
"extra_args": ["--verbose"],
|
|
362
525
|
"env": { "OPENCODE_TELEMETRY": "off" },
|
|
526
|
+
"default_model": "anthropic/claude-sonnet-4",
|
|
363
527
|
"models": [
|
|
364
528
|
{
|
|
365
529
|
"id": "anthropic/claude-sonnet-4",
|
|
366
530
|
"tier": "balanced",
|
|
367
|
-
"default": true,
|
|
368
531
|
"name": "Claude Sonnet 4",
|
|
369
532
|
"description": "Fast and capable for most reviews",
|
|
370
533
|
"tagline": "Best balance of speed and quality"
|
|
@@ -395,7 +558,11 @@ You can override provider settings and define custom models in your config file.
|
|
|
395
558
|
| `extra_args` | Additional arguments to pass to the CLI |
|
|
396
559
|
| `env` | Environment variables to set when running the CLI |
|
|
397
560
|
| `installInstructions` | Custom installation instructions shown in UI |
|
|
561
|
+
| `availability_timeout_seconds` | Seconds to allow for the startup availability probe before the provider is reported unavailable (default `10`). Raise it for providers whose check runs a slow build/compile step. Also supported per chat provider under `chat_providers.<id>`. |
|
|
562
|
+
| `availability_command` | Command run to decide availability. Executable providers default to always-available when omitted; chat providers fall back to `<command> --version` (or, for the built-in Pi, the cached AI-provider status). Pair with `availability_timeout_seconds` when the probe runs a slow build. |
|
|
398
563
|
| `models` | Array of model definitions (see below) |
|
|
564
|
+
| `default_model` | Model `id` to use as the provider's default (in the picker and when no model is specified). Preferred over the per-model `default: true` flag. If it names an unknown or disabled model, the provider falls back to automatic selection. |
|
|
565
|
+
| `disabled_models` | Array of model selectors to hide, matched by model `id` or alias. Disabled models disappear from the model picker for that provider. Works on built-in models too, so you can remove a built-in option without redefining the rest. A list that would hide *every* model is ignored. |
|
|
399
566
|
|
|
400
567
|
#### Model Configuration Fields
|
|
401
568
|
|
|
@@ -408,10 +575,29 @@ You can override provider settings and define custom models in your config file.
|
|
|
408
575
|
| `tagline` | No | Short description shown in model picker |
|
|
409
576
|
| `badge` | No | Badge text (e.g., "NEW", "BETA") |
|
|
410
577
|
| `badgeClass` | No | CSS class for badge styling |
|
|
411
|
-
| `default` | No | Set to `true` to make this the default model for the provider |
|
|
578
|
+
| `default` | No | **Deprecated** — use the provider-level `default_model` field instead. Set to `true` to make this the default model for the provider. Still honored for backward compatibility, but `default_model` takes precedence when both are present. |
|
|
412
579
|
| `extra_args` | No | Model-specific CLI arguments |
|
|
413
580
|
| `env` | No | Model-specific environment variables |
|
|
414
581
|
|
|
582
|
+
#### Choosing and Hiding Models
|
|
583
|
+
|
|
584
|
+
Use the provider-level `default_model` and `disabled_models` fields to tailor which models a provider exposes — these work on **built-in** models too, so you can adjust a provider without redefining its entire model list.
|
|
585
|
+
|
|
586
|
+
```json
|
|
587
|
+
{
|
|
588
|
+
"providers": {
|
|
589
|
+
"claude": {
|
|
590
|
+
"default_model": "sonnet-4.6",
|
|
591
|
+
"disabled_models": ["haiku", "fable"]
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
- `disabled_models` removes those models from the picker. It matches by model id or alias, so either a canonical id (`opus-4.8-xhigh`) or a convenience alias (`opus`) works. To find the built-in IDs for a provider, see the comments in `config.example.json` (e.g. Claude ships `opus-4.8-xhigh`, `sonnet-4.6`, `haiku`, …).
|
|
598
|
+
- `default_model` selects which of the *remaining* models is the default. If it points at a model that is unknown or also disabled, the provider falls back to automatic selection (the model marked `default: true`, then the first `balanced`-tier model, then the first model).
|
|
599
|
+
- Prefer `default_model` over the per-model `default: true` flag, which is deprecated. Setting `default_model` suppresses the deprecation warning.
|
|
600
|
+
|
|
415
601
|
#### Model Tiers
|
|
416
602
|
|
|
417
603
|
Models are grouped by tier to help users choose appropriately:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
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.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
package/public/js/local.js
CHANGED
|
@@ -72,20 +72,40 @@ class LocalManager {
|
|
|
72
72
|
// Auto-trigger analysis if ?analyze=true is present
|
|
73
73
|
const autoAnalyze = new URLSearchParams(window.location.search).get('analyze');
|
|
74
74
|
if (autoAnalyze === 'true' && !window.prManager.isAnalyzing) {
|
|
75
|
+
const manager = window.prManager;
|
|
75
76
|
try {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// Prefer a stashed bulk-analysis config (threaded via analysisConfigId).
|
|
78
|
+
// The CLI uses it to carry --instructions plus the resolved
|
|
79
|
+
// provider/model or council snapshot into the browser auto-analyze;
|
|
80
|
+
// mirrors PRManager._maybeAutoAnalyze.
|
|
81
|
+
const storedConfig = await manager._fetchAutoAnalysisConfigFromUrl();
|
|
82
|
+
let config;
|
|
83
|
+
if (storedConfig.requested) {
|
|
84
|
+
if (!storedConfig.config) {
|
|
85
|
+
// The stored config expired (TTL/eviction/restart). The diff has
|
|
86
|
+
// already rendered, so leave the review usable for manual analysis
|
|
87
|
+
// rather than failing; warn and bail.
|
|
88
|
+
const message = 'Could not load the selected analysis settings. Start analysis manually to choose new settings.';
|
|
89
|
+
if (window.toast) window.toast.showWarning(message);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
config = storedConfig.config;
|
|
93
|
+
} else {
|
|
94
|
+
// Fetch repo settings so we honour the repository's default provider/council
|
|
95
|
+
const [repoSettings, reviewSettings, appConfig] = await Promise.all([
|
|
96
|
+
manager.fetchRepoSettings().catch(() => null),
|
|
97
|
+
manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
|
|
98
|
+
manager._getAppConfig()
|
|
99
|
+
]);
|
|
100
|
+
config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig);
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
await this.startLocalAnalysis(null, config);
|
|
86
104
|
} finally {
|
|
87
105
|
const cleanUrl = new URL(window.location);
|
|
88
106
|
cleanUrl.searchParams.delete('analyze');
|
|
107
|
+
cleanUrl.searchParams.delete('analysisConfigId');
|
|
108
|
+
cleanUrl.searchParams.delete('council');
|
|
89
109
|
history.replaceState(null, '', cleanUrl);
|
|
90
110
|
}
|
|
91
111
|
}
|
package/public/js/pr.js
CHANGED
|
@@ -638,6 +638,44 @@ class PRManager {
|
|
|
638
638
|
const defaultTab = repoSettings?.default_tab || 'single';
|
|
639
639
|
const councilId = repoSettings?.default_council_id || reviewSettings?.last_council_id || null;
|
|
640
640
|
|
|
641
|
+
// A `?council=<id>` URL param (set by the CLI when opening the browser) takes
|
|
642
|
+
// highest priority for council selection. When present we force the council
|
|
643
|
+
// branch regardless of default_tab/settings, and derive configType from the
|
|
644
|
+
// council's own type ('council' or 'advanced') rather than the repo default.
|
|
645
|
+
const urlSearch = (typeof window !== 'undefined' && window.location && window.location.search) || '';
|
|
646
|
+
const urlCouncilId = new URLSearchParams(urlSearch).get('council');
|
|
647
|
+
if (urlCouncilId) {
|
|
648
|
+
let councilConfig = null;
|
|
649
|
+
let councilName = null;
|
|
650
|
+
let councilType = null;
|
|
651
|
+
try {
|
|
652
|
+
const resp = await fetch(`/api/councils/${urlCouncilId}`);
|
|
653
|
+
if (resp.ok) {
|
|
654
|
+
const data = await resp.json();
|
|
655
|
+
councilConfig = data.council?.config || null;
|
|
656
|
+
councilName = data.council?.name || null;
|
|
657
|
+
councilType = data.council?.type || null;
|
|
658
|
+
} else {
|
|
659
|
+
console.warn(`Failed to fetch council "${urlCouncilId}" from URL param (status ${resp.status}); falling back to default analysis config`);
|
|
660
|
+
}
|
|
661
|
+
} catch (e) {
|
|
662
|
+
console.warn('Failed to fetch council config for URL council param:', e);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Only honor the URL council if we successfully fetched its config.
|
|
666
|
+
// Otherwise fall through to the existing default-selection logic.
|
|
667
|
+
if (councilConfig) {
|
|
668
|
+
return {
|
|
669
|
+
isCouncil: true,
|
|
670
|
+
councilId: urlCouncilId,
|
|
671
|
+
councilConfig,
|
|
672
|
+
councilName,
|
|
673
|
+
configType: councilType || 'advanced',
|
|
674
|
+
customInstructions: null
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
641
679
|
if ((defaultTab === 'council' || defaultTab === 'advanced') && councilId) {
|
|
642
680
|
// Fetch the full council config so the progress modal can render correctly
|
|
643
681
|
let councilConfig = null;
|
|
@@ -763,6 +801,7 @@ class PRManager {
|
|
|
763
801
|
const cleanUrl = new URL(window.location);
|
|
764
802
|
cleanUrl.searchParams.delete('analyze');
|
|
765
803
|
cleanUrl.searchParams.delete('analysisConfigId');
|
|
804
|
+
cleanUrl.searchParams.delete('council');
|
|
766
805
|
history.replaceState(null, '', cleanUrl);
|
|
767
806
|
}
|
|
768
807
|
}
|
|
@@ -3706,7 +3745,13 @@ class PRManager {
|
|
|
3706
3745
|
// /file-content fetches) entirely when this body has none. The selector
|
|
3707
3746
|
// mirrors the one validatePendingEofGaps scans for.
|
|
3708
3747
|
if (entry.fileBody.querySelector('tr.context-expand-row[data-pending-eof-validation="true"]')) {
|
|
3709
|
-
|
|
3748
|
+
// Keep the in-flight promise on the entry. Until it resolves, the
|
|
3749
|
+
// trailing EOF gap still carries EOF_SENTINEL coords, which makes
|
|
3750
|
+
// findMatchingGap()'s overlap test unmatchable for a real target line.
|
|
3751
|
+
// Line-anchoring callers (expandForSuggestion) await this so a
|
|
3752
|
+
// suggestion/comment on a trailing unchanged line doesn't silently fail
|
|
3753
|
+
// to expand. Fire-and-forget for everyone else.
|
|
3754
|
+
entry.eofValidationPromise = this.validatePendingEofGaps(entry.fileBody);
|
|
3710
3755
|
}
|
|
3711
3756
|
}
|
|
3712
3757
|
|
|
@@ -4626,6 +4671,26 @@ class PRManager {
|
|
|
4626
4671
|
// lazy body has rendered. Without this the gap query below returns nothing.
|
|
4627
4672
|
await this.ensureFileBodyRendered(file);
|
|
4628
4673
|
|
|
4674
|
+
// The trailing end-of-file gap is created with EOF_SENTINEL (-1) coords and
|
|
4675
|
+
// resolved to real line numbers asynchronously by validatePendingEofGaps(),
|
|
4676
|
+
// which _renderFileBodyNow fires fire-and-forget as the body renders. Until
|
|
4677
|
+
// it settles the gap's NEW/OLD end is negative, so findMatchingGap()'s
|
|
4678
|
+
// overlap test can never match a real target line — a suggestion (or
|
|
4679
|
+
// comment, via ensureLinesVisible) on a trailing unchanged line silently
|
|
4680
|
+
// fails to expand and never anchors. Pre-lazy-render this validation ran at
|
|
4681
|
+
// renderDiff time, long before any suggestion was placed; lazy rendering
|
|
4682
|
+
// collapsed that head start to nothing, which is the regression. Await the
|
|
4683
|
+
// same in-flight promise (not a second /file-content fetch) so the EOF gap
|
|
4684
|
+
// carries real coordinates before we match below.
|
|
4685
|
+
const lazyEntry = this._lazyFileBodies?.get(fileElement.dataset?.fileName || file);
|
|
4686
|
+
if (lazyEntry?.eofValidationPromise) {
|
|
4687
|
+
try {
|
|
4688
|
+
await lazyEntry.eofValidationPromise;
|
|
4689
|
+
} catch {
|
|
4690
|
+
// Validation removes the gap on fetch failure; matching simply misses.
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4629
4694
|
// Check if file is collapsed (generated files)
|
|
4630
4695
|
if (fileElement.classList.contains('collapsed')) {
|
|
4631
4696
|
debugLog?.('expandForSuggestion', 'File is collapsed, expanding first');
|
|
@@ -7069,22 +7134,42 @@ class PRManager {
|
|
|
7069
7134
|
}
|
|
7070
7135
|
|
|
7071
7136
|
/**
|
|
7072
|
-
*
|
|
7073
|
-
*
|
|
7074
|
-
*
|
|
7075
|
-
*
|
|
7137
|
+
* Build the worktree-not-found recovery URL. When the user arrived via
|
|
7138
|
+
* auto-analyze (?analyze=true), the reload link preserves the auto-analyze
|
|
7139
|
+
* state so analysis re-triggers after worktree setup. Both the
|
|
7140
|
+
* `analysisConfigId` and `council` params are carried through, since
|
|
7141
|
+
* `_buildDefaultAnalysisConfig()` treats `?council=<id>` as the
|
|
7142
|
+
* highest-priority analysis source — dropping it would silently fall back to
|
|
7143
|
+
* the repo/default analysis configuration on retry.
|
|
7076
7144
|
* @param {string} owner - Repository owner
|
|
7077
7145
|
* @param {string} repo - Repository name
|
|
7078
7146
|
* @param {number} number - PR number
|
|
7147
|
+
* @returns {string} The recovery URL (unescaped)
|
|
7079
7148
|
*/
|
|
7080
|
-
|
|
7149
|
+
_buildWorktreeRecoveryUrl(owner, repo, number) {
|
|
7081
7150
|
let setupUrl = `/pr/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(number)}`;
|
|
7082
7151
|
if (this._autoAnalyzeRequested) {
|
|
7152
|
+
const currentParams = new URLSearchParams(window.location.search);
|
|
7083
7153
|
const params = new URLSearchParams({ analyze: 'true' });
|
|
7084
|
-
const analysisConfigId =
|
|
7154
|
+
const analysisConfigId = currentParams.get('analysisConfigId');
|
|
7155
|
+
const councilId = currentParams.get('council');
|
|
7085
7156
|
if (analysisConfigId) params.set('analysisConfigId', analysisConfigId);
|
|
7157
|
+
if (councilId) params.set('council', councilId);
|
|
7086
7158
|
setupUrl += `?${params.toString()}`;
|
|
7087
7159
|
}
|
|
7160
|
+
return setupUrl;
|
|
7161
|
+
}
|
|
7162
|
+
|
|
7163
|
+
/**
|
|
7164
|
+
* Show an error when the worktree is not found during analysis.
|
|
7165
|
+
* Displays a helpful message with a reload link that preserves any
|
|
7166
|
+
* auto-analyze state (see _buildWorktreeRecoveryUrl).
|
|
7167
|
+
* @param {string} owner - Repository owner
|
|
7168
|
+
* @param {string} repo - Repository name
|
|
7169
|
+
* @param {number} number - PR number
|
|
7170
|
+
*/
|
|
7171
|
+
showWorktreeNotFoundError(owner, repo, number) {
|
|
7172
|
+
const setupUrl = this._buildWorktreeRecoveryUrl(owner, repo, number);
|
|
7088
7173
|
const container = document.getElementById('pr-container');
|
|
7089
7174
|
if (container) {
|
|
7090
7175
|
container.innerHTML = `
|
|
@@ -19,13 +19,20 @@
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {Object} providerInfo - One entry from /api/providers
|
|
21
21
|
* @param {string} modelId
|
|
22
|
-
* @returns {boolean} Whether modelId is one of the provider's models
|
|
22
|
+
* @returns {boolean} Whether modelId is one of the provider's models, matched by
|
|
23
|
+
* canonical id OR any of its aliases. A persisted/configured value may name an
|
|
24
|
+
* alias (e.g. `opus`) rather than the canonical id; matching aliases keeps the
|
|
25
|
+
* user's choice instead of silently falling back to the provider default. The
|
|
26
|
+
* model cards carry their `aliases` in the /api/providers payload. Mirrors the
|
|
27
|
+
* backend `modelMatches()` helper in src/ai/provider.js.
|
|
23
28
|
*/
|
|
24
29
|
function _modelBelongsToProvider(providerInfo, modelId) {
|
|
25
30
|
return !!(
|
|
26
31
|
providerInfo &&
|
|
27
32
|
Array.isArray(providerInfo.models) &&
|
|
28
|
-
|
|
33
|
+
// Optional chaining: a model with no `aliases` short-circuits to undefined
|
|
34
|
+
// (falsy) rather than throwing, so no Array.isArray guard is needed.
|
|
35
|
+
providerInfo.models.some(m => m && (m.id === modelId || m.aliases?.includes(modelId)))
|
|
29
36
|
);
|
|
30
37
|
}
|
|
31
38
|
|
package/public/setup.html
CHANGED
|
@@ -770,6 +770,10 @@
|
|
|
770
770
|
var qs = new URLSearchParams(window.location.search);
|
|
771
771
|
if (qs.get('analyze') === 'true') targetUrl.searchParams.set('analyze', qs.get('analyze'));
|
|
772
772
|
if (qs.get('analysisConfigId')) targetUrl.searchParams.set('analysisConfigId', qs.get('analysisConfigId'));
|
|
773
|
+
// Forward the CLI-supplied council selection (PR & local cold-start
|
|
774
|
+
// and delegated paths route through here); the review page keys
|
|
775
|
+
// council auto-analysis on this param.
|
|
776
|
+
if (qs.get('council')) targetUrl.searchParams.set('council', qs.get('council'));
|
|
773
777
|
window.location.href = targetUrl.toString();
|
|
774
778
|
return;
|
|
775
779
|
}
|
|
@@ -831,6 +835,10 @@
|
|
|
831
835
|
var qs = new URLSearchParams(window.location.search);
|
|
832
836
|
if (qs.get('analyze') === 'true') targetUrl.searchParams.set('analyze', qs.get('analyze'));
|
|
833
837
|
if (qs.get('analysisConfigId')) targetUrl.searchParams.set('analysisConfigId', qs.get('analysisConfigId'));
|
|
838
|
+
// Forward the CLI-supplied council selection (PR & local cold-start
|
|
839
|
+
// and delegated paths route through here); the review page keys
|
|
840
|
+
// council auto-analysis on this param.
|
|
841
|
+
if (qs.get('council')) targetUrl.searchParams.set('council', qs.get('council'));
|
|
834
842
|
window.location.href = targetUrl.toString();
|
|
835
843
|
}
|
|
836
844
|
}, 400);
|
|
@@ -33,8 +33,8 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
33
33
|
const CLAUDE_MODELS = [
|
|
34
34
|
// ── Thorough tier ───────────────────────────────────────────────────────
|
|
35
35
|
{
|
|
36
|
-
id: 'fable',
|
|
37
|
-
aliases: ['fable
|
|
36
|
+
id: 'fable-5-xhigh',
|
|
37
|
+
aliases: ['fable'],
|
|
38
38
|
cli_model: 'claude-fable-5',
|
|
39
39
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'xhigh' },
|
|
40
40
|
name: 'Fable 5 XHigh',
|
|
@@ -61,49 +61,49 @@ const CLAUDE_MODELS = [
|
|
|
61
61
|
extra_args: ['--thinking', 'adaptive']
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
|
-
id: 'opus',
|
|
65
|
-
aliases: ['opus
|
|
66
|
-
cli_model: 'claude-opus-4-
|
|
64
|
+
id: 'opus-4.8-xhigh',
|
|
65
|
+
aliases: ['opus'],
|
|
66
|
+
cli_model: 'claude-opus-4-8',
|
|
67
67
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'xhigh' },
|
|
68
|
-
name: 'Opus 4.
|
|
68
|
+
name: 'Opus 4.8 XHigh',
|
|
69
69
|
tier: 'thorough',
|
|
70
70
|
tagline: 'Maximum Depth',
|
|
71
|
-
description: 'Opus 4.
|
|
71
|
+
description: 'Opus 4.8 (newest) with extra-high effort — deepest analysis',
|
|
72
72
|
badge: 'Most Thorough',
|
|
73
73
|
badgeClass: 'badge-power',
|
|
74
74
|
default: true
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
|
-
id: 'opus-4.
|
|
78
|
-
cli_model: 'claude-opus-4-
|
|
77
|
+
id: 'opus-4.8-high',
|
|
78
|
+
cli_model: 'claude-opus-4-8',
|
|
79
79
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
|
|
80
|
-
name: 'Opus 4.
|
|
80
|
+
name: 'Opus 4.8 High',
|
|
81
81
|
tier: 'thorough',
|
|
82
|
-
tagline: '
|
|
83
|
-
description: 'Opus 4.
|
|
84
|
-
badge: '
|
|
82
|
+
tagline: 'Newest',
|
|
83
|
+
description: 'Opus 4.8 (newest) with high effort',
|
|
84
|
+
badge: 'Latest',
|
|
85
85
|
badgeClass: 'badge-power'
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
|
-
id: 'opus-4.
|
|
89
|
-
cli_model: 'claude-opus-4-
|
|
88
|
+
id: 'opus-4.7-xhigh',
|
|
89
|
+
cli_model: 'claude-opus-4-7',
|
|
90
90
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'xhigh' },
|
|
91
|
-
name: 'Opus 4.
|
|
91
|
+
name: 'Opus 4.7 XHigh',
|
|
92
92
|
tier: 'thorough',
|
|
93
|
-
tagline: '
|
|
94
|
-
description: 'Opus 4.
|
|
95
|
-
badge: '
|
|
93
|
+
tagline: 'Previous Gen',
|
|
94
|
+
description: 'Opus 4.7 with extra-high effort',
|
|
95
|
+
badge: 'Previous Gen',
|
|
96
96
|
badgeClass: 'badge-power'
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
|
-
id: 'opus-4.
|
|
100
|
-
cli_model: 'claude-opus-4-
|
|
99
|
+
id: 'opus-4.7-high',
|
|
100
|
+
cli_model: 'claude-opus-4-7',
|
|
101
101
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
|
|
102
|
-
name: 'Opus 4.
|
|
102
|
+
name: 'Opus 4.7 High',
|
|
103
103
|
tier: 'thorough',
|
|
104
|
-
tagline: '
|
|
105
|
-
description: 'Opus 4.
|
|
106
|
-
badge: '
|
|
104
|
+
tagline: 'High Effort',
|
|
105
|
+
description: 'Opus 4.7 with high effort — thorough, quicker than XHigh',
|
|
106
|
+
badge: 'Thorough',
|
|
107
107
|
badgeClass: 'badge-power'
|
|
108
108
|
},
|
|
109
109
|
{
|
|
@@ -162,7 +162,7 @@ class ClaudeProvider extends AIProvider {
|
|
|
162
162
|
* @param {Object} configOverrides.env - Additional environment variables
|
|
163
163
|
* @param {Object[]} configOverrides.models - Custom model definitions
|
|
164
164
|
*/
|
|
165
|
-
constructor(model =
|
|
165
|
+
constructor(model = ClaudeProvider.getDefaultModel(), configOverrides = {}) {
|
|
166
166
|
super(model);
|
|
167
167
|
|
|
168
168
|
// Command precedence: ENV > config > default
|
|
@@ -858,9 +858,10 @@ class ClaudeProvider extends AIProvider {
|
|
|
858
858
|
* Test if Claude CLI is available
|
|
859
859
|
* Uses fast `--version` check instead of running a prompt.
|
|
860
860
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
861
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
|
|
861
862
|
* @returns {Promise<boolean>}
|
|
862
863
|
*/
|
|
863
|
-
async testAvailability() {
|
|
864
|
+
async testAvailability(timeoutMs = 10000) {
|
|
864
865
|
return new Promise((resolve) => {
|
|
865
866
|
// For availability test, we just need to check --version
|
|
866
867
|
// Use the already-resolved command from the constructor (this.claudeCmd)
|
|
@@ -889,10 +890,10 @@ class ClaudeProvider extends AIProvider {
|
|
|
889
890
|
const availabilityTimeout = setTimeout(() => {
|
|
890
891
|
if (settled) return;
|
|
891
892
|
settled = true;
|
|
892
|
-
logger.warn(
|
|
893
|
+
logger.warn(`Claude CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
893
894
|
try { claude.kill(); } catch { /* ignore */ }
|
|
894
895
|
resolve(false);
|
|
895
|
-
},
|
|
896
|
+
}, timeoutMs);
|
|
896
897
|
|
|
897
898
|
claude.stdout.on('data', (data) => {
|
|
898
899
|
stdout += data.toString();
|
|
@@ -939,7 +940,7 @@ class ClaudeProvider extends AIProvider {
|
|
|
939
940
|
}
|
|
940
941
|
|
|
941
942
|
static getDefaultModel() {
|
|
942
|
-
return 'opus';
|
|
943
|
+
return 'opus-4.8-xhigh';
|
|
943
944
|
}
|
|
944
945
|
|
|
945
946
|
static getInstallInstructions() {
|