@in-the-loop-labs/pair-review 4.0.0 → 4.1.1
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 +82 -7
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +3 -4
- package/plugin-pair-loop/.claude-plugin/plugin.json +19 -0
- package/plugin-pair-loop/skills/loop/SKILL.md +233 -0
- package/public/js/index.js +87 -10
- package/public/js/local.js +19 -9
- package/public/js/pr.js +64 -36
- package/public/js/repo-links.js +11 -3
- package/public/js/utils/analyze-params.js +69 -0
- package/public/js/utils/provider-model.js +66 -1
- package/public/js/vendor/pierre-diffs-worker.js +16158 -0
- package/public/js/vendor/pierre-diffs.js +1880 -0
- package/public/local.html +1 -0
- package/public/pr.html +1 -0
- package/public/setup.html +35 -16
- package/src/ai/codex-provider.js +59 -11
- package/src/config.js +150 -28
- package/src/database.js +127 -3
- package/src/external/github-adapter.js +18 -3
- package/src/github/client.js +37 -0
- package/src/github/parser.js +41 -7
- package/src/interactive-analysis-config.js +2 -2
- package/src/links/repo-links.js +66 -28
- package/src/local-review.js +134 -5
- package/src/local-scope.js +38 -0
- package/src/main.js +199 -33
- package/src/routes/config.js +47 -12
- package/src/routes/external-comments.js +13 -1
- package/src/routes/github-collections.js +175 -13
- package/src/routes/local.js +11 -20
- package/src/routes/pr.js +63 -36
- package/src/routes/setup.js +63 -8
- package/src/routes/shared.js +85 -0
- package/src/routes/stack-analysis.js +39 -3
- package/src/server.js +74 -3
- package/src/setup/local-setup.js +23 -7
- package/src/setup/pr-setup.js +237 -39
- package/src/setup/stack-setup.js +7 -2
- package/src/single-port.js +73 -18
- package/src/utils/host-resolution.js +157 -0
- package/plugin-code-critic/skills/loop/SKILL.md +0 -373
package/public/local.html
CHANGED
|
@@ -591,6 +591,7 @@
|
|
|
591
591
|
<!-- Shared storage-key and provider/model helpers -->
|
|
592
592
|
<script src="/js/utils/storage-keys.js"></script>
|
|
593
593
|
<script src="/js/utils/provider-model.js"></script>
|
|
594
|
+
<script src="/js/utils/analyze-params.js"></script>
|
|
594
595
|
|
|
595
596
|
<!-- Category emoji mapping -->
|
|
596
597
|
<script src="/js/utils/category-emoji.js"></script>
|
package/public/pr.html
CHANGED
|
@@ -394,6 +394,7 @@
|
|
|
394
394
|
<!-- Shared storage-key and provider/model helpers -->
|
|
395
395
|
<script src="/js/utils/storage-keys.js"></script>
|
|
396
396
|
<script src="/js/utils/provider-model.js"></script>
|
|
397
|
+
<script src="/js/utils/analyze-params.js"></script>
|
|
397
398
|
|
|
398
399
|
<!-- Category emoji mapping -->
|
|
399
400
|
<script src="/js/utils/category-emoji.js"></script>
|
package/public/setup.html
CHANGED
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
<script src="/js/ws-client.js"></script>
|
|
531
531
|
<script src="/js/components/UpdateBanner.js"></script>
|
|
532
532
|
<script src="/js/utils/notification-sounds.js"></script>
|
|
533
|
+
<script src="/js/utils/analyze-params.js"></script>
|
|
533
534
|
|
|
534
535
|
<script>
|
|
535
536
|
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
@@ -611,6 +612,14 @@
|
|
|
611
612
|
context.path = params.get('path');
|
|
612
613
|
}
|
|
613
614
|
}
|
|
615
|
+
// Forward a delegated --scope/--base selection (the CLI carries them
|
|
616
|
+
// on the /local?path=... URL when delegating to a running server).
|
|
617
|
+
// The server re-validates; this only relays the request.
|
|
618
|
+
if (mode === 'local') {
|
|
619
|
+
const scopeParams = new URLSearchParams(window.location.search);
|
|
620
|
+
if (scopeParams.get('scope')) context.scope = scopeParams.get('scope');
|
|
621
|
+
if (scopeParams.get('base')) context.base = scopeParams.get('base');
|
|
622
|
+
}
|
|
614
623
|
|
|
615
624
|
const steps = mode === 'local' ? LOCAL_STEPS : PR_STEPS;
|
|
616
625
|
const stepStates = {};
|
|
@@ -751,7 +760,23 @@
|
|
|
751
760
|
var fetchOptions = { method: 'POST' };
|
|
752
761
|
if (mode === 'local') {
|
|
753
762
|
fetchOptions.headers = { 'Content-Type': 'application/json' };
|
|
754
|
-
|
|
763
|
+
var localBody = { path: context.path };
|
|
764
|
+
if (context.scope) localBody.scope = context.scope;
|
|
765
|
+
if (context.base) localBody.base = context.base;
|
|
766
|
+
fetchOptions.body = JSON.stringify(localBody);
|
|
767
|
+
} else if (mode === 'pr') {
|
|
768
|
+
// A `host` query param (set when opening a PR from a dashboard
|
|
769
|
+
// collection row or a pasted URL) tells setup which system the
|
|
770
|
+
// PR lives on, so the fetch binds there directly instead of
|
|
771
|
+
// probing. Sentinel: the literal "github" means github.com and
|
|
772
|
+
// maps to body host null; any other value is an api_host URL
|
|
773
|
+
// string (alt host) — an api_host is always a URL, so "github"
|
|
774
|
+
// is unambiguous. Absent = unknown; the server derives the host.
|
|
775
|
+
var setupHost = new URLSearchParams(window.location.search).get('host');
|
|
776
|
+
if (setupHost) {
|
|
777
|
+
fetchOptions.headers = { 'Content-Type': 'application/json' };
|
|
778
|
+
fetchOptions.body = JSON.stringify({ host: setupHost === 'github' ? null : setupHost });
|
|
779
|
+
}
|
|
755
780
|
}
|
|
756
781
|
|
|
757
782
|
var response = await fetch(postUrl, fetchOptions);
|
|
@@ -764,16 +789,13 @@
|
|
|
764
789
|
|
|
765
790
|
var data = await response.json();
|
|
766
791
|
|
|
767
|
-
// If the review already exists, redirect immediately
|
|
792
|
+
// If the review already exists, redirect immediately.
|
|
793
|
+
// Relay the full auto-analyze intent bundle (analyze /
|
|
794
|
+
// analysisConfigId / council / provider / model) so a delegated
|
|
795
|
+
// override or CLI council survives this hop to the review page.
|
|
768
796
|
if (data.existing && data.reviewUrl) {
|
|
769
797
|
var targetUrl = new URL(data.reviewUrl, window.location.origin);
|
|
770
|
-
|
|
771
|
-
if (qs.get('analyze') === 'true') targetUrl.searchParams.set('analyze', qs.get('analyze'));
|
|
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'));
|
|
798
|
+
window.carryAnalyzeParams(window.location.search, targetUrl);
|
|
777
799
|
window.location.href = targetUrl.toString();
|
|
778
800
|
return;
|
|
779
801
|
}
|
|
@@ -832,13 +854,10 @@
|
|
|
832
854
|
setTimeout(function() {
|
|
833
855
|
if (msg.reviewUrl) {
|
|
834
856
|
var targetUrl = new URL(msg.reviewUrl, window.location.origin);
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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'));
|
|
857
|
+
// Relay the full auto-analyze intent bundle (analyze /
|
|
858
|
+
// analysisConfigId / council / provider / model) so a
|
|
859
|
+
// delegated override or CLI council survives the hop.
|
|
860
|
+
window.carryAnalyzeParams(window.location.search, targetUrl);
|
|
842
861
|
window.location.href = targetUrl.toString();
|
|
843
862
|
}
|
|
844
863
|
}, 400);
|
package/src/ai/codex-provider.js
CHANGED
|
@@ -21,14 +21,18 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
21
21
|
/**
|
|
22
22
|
* Codex model definitions with tier mappings
|
|
23
23
|
*
|
|
24
|
-
* Based on OpenAI
|
|
24
|
+
* Based on OpenAI's GPT-5.6 launch and Models guide
|
|
25
|
+
* (openai.com/index/gpt-5-6 and developers.openai.com/api/docs/models)
|
|
26
|
+
* - gpt-5.6-sol: Flagship frontier model for complex professional work
|
|
27
|
+
* - gpt-5.6-terra: Balances intelligence and cost for everyday work
|
|
28
|
+
* - gpt-5.6-luna: Fast, affordable model for cost-sensitive, high-volume work
|
|
25
29
|
* - gpt-5.4-nano: Cheapest model ($0.20/$1.25 per MTok), good for surface scans
|
|
26
30
|
* - gpt-5.4-mini: Fast with 400k context ($0.75/$4.50 per MTok)
|
|
27
31
|
* - gpt-5.3-codex: Industry-leading coding model for complex engineering tasks
|
|
28
|
-
* - gpt-5.4
|
|
29
|
-
*
|
|
32
|
+
* - GPT-5.6, gpt-5.4, and gpt-5.5 models are exposed only through the requested
|
|
33
|
+
* explicit reasoning-effort variants.
|
|
30
34
|
*
|
|
31
|
-
* Reasoning-effort variants (-high / -xhigh) use `cli_model` to pass the base
|
|
35
|
+
* Reasoning-effort variants (-high / -xhigh / -max) use `cli_model` to pass the base
|
|
32
36
|
* model ID to `codex exec -m` and add `-c model_reasoning_effort="..."` via
|
|
33
37
|
* extra_args so Codex picks up the effort level through its config override.
|
|
34
38
|
*
|
|
@@ -36,17 +40,61 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
36
40
|
*/
|
|
37
41
|
const CODEX_MODELS = [
|
|
38
42
|
{
|
|
39
|
-
id: 'gpt-5.
|
|
40
|
-
cli_model: 'gpt-5.
|
|
43
|
+
id: 'gpt-5.6-sol-high',
|
|
44
|
+
cli_model: 'gpt-5.6-sol',
|
|
41
45
|
extra_args: ['-c', 'model_reasoning_effort="high"'],
|
|
42
|
-
name: 'GPT-5.
|
|
46
|
+
name: 'GPT-5.6 Sol High',
|
|
43
47
|
tier: 'thorough',
|
|
44
|
-
tagline: '
|
|
45
|
-
description: '
|
|
48
|
+
tagline: 'Frontier Review',
|
|
49
|
+
description: 'OpenAI flagship and best coding model yet, with high reasoning effort for demanding PR reviews, complex professional work, and cross-file analysis.',
|
|
46
50
|
badge: 'Recommended',
|
|
47
51
|
badgeClass: 'badge-recommended',
|
|
48
52
|
default: true
|
|
49
53
|
},
|
|
54
|
+
{
|
|
55
|
+
id: 'gpt-5.6-sol-xhigh',
|
|
56
|
+
cli_model: 'gpt-5.6-sol',
|
|
57
|
+
extra_args: ['-c', 'model_reasoning_effort="xhigh"'],
|
|
58
|
+
name: 'GPT-5.6 Sol XHigh',
|
|
59
|
+
tier: 'thorough',
|
|
60
|
+
tagline: 'Frontier Depth',
|
|
61
|
+
description: 'GPT-5.6 Sol with extra-high reasoning effort for difficult architectural reviews, subtle regressions, and security-sensitive changes.',
|
|
62
|
+
badge: 'Extra High',
|
|
63
|
+
badgeClass: 'badge-power'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'gpt-5.6-terra-xhigh',
|
|
67
|
+
cli_model: 'gpt-5.6-terra',
|
|
68
|
+
extra_args: ['-c', 'model_reasoning_effort="xhigh"'],
|
|
69
|
+
name: 'GPT-5.6 Terra XHigh',
|
|
70
|
+
tier: 'balanced',
|
|
71
|
+
tagline: 'Intelligence & Value',
|
|
72
|
+
description: 'GPT-5.6 balanced model with extra-high reasoning effort, combining strong intelligence and lower cost for careful everyday PR reviews.',
|
|
73
|
+
badge: 'Best Balance',
|
|
74
|
+
badgeClass: 'badge-balanced'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'gpt-5.6-luna-max',
|
|
78
|
+
cli_model: 'gpt-5.6-luna',
|
|
79
|
+
extra_args: ['-c', 'model_reasoning_effort="max"'],
|
|
80
|
+
name: 'GPT-5.6 Luna Max',
|
|
81
|
+
tier: 'balanced',
|
|
82
|
+
tagline: 'High-Volume Value',
|
|
83
|
+
description: 'GPT-5.6 fastest, most cost-efficient model with max reasoning effort for high-volume reviews and broad codebase scans.',
|
|
84
|
+
badge: 'Lowest Cost',
|
|
85
|
+
badgeClass: 'badge-speed'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'gpt-5.5-high',
|
|
89
|
+
cli_model: 'gpt-5.5',
|
|
90
|
+
extra_args: ['-c', 'model_reasoning_effort="high"'],
|
|
91
|
+
name: 'GPT-5.5 High',
|
|
92
|
+
tier: 'thorough',
|
|
93
|
+
tagline: 'Previous Flagship',
|
|
94
|
+
description: 'Previous-generation GPT model with high reasoning effort for demanding PR reviews, strong code understanding, and careful cross-file analysis.',
|
|
95
|
+
badge: 'Previous Gen',
|
|
96
|
+
badgeClass: 'badge-power'
|
|
97
|
+
},
|
|
50
98
|
{
|
|
51
99
|
id: 'gpt-5.5-xhigh',
|
|
52
100
|
cli_model: 'gpt-5.5',
|
|
@@ -122,7 +170,7 @@ class CodexProvider extends AIProvider {
|
|
|
122
170
|
* @param {Object} configOverrides.env - Additional environment variables
|
|
123
171
|
* @param {Object[]} configOverrides.models - Custom model definitions
|
|
124
172
|
*/
|
|
125
|
-
constructor(model = 'gpt-5.
|
|
173
|
+
constructor(model = 'gpt-5.6-sol-high', configOverrides = {}) {
|
|
126
174
|
super(model);
|
|
127
175
|
|
|
128
176
|
// Command precedence: ENV > config > default
|
|
@@ -845,7 +893,7 @@ class CodexProvider extends AIProvider {
|
|
|
845
893
|
}
|
|
846
894
|
|
|
847
895
|
static getDefaultModel() {
|
|
848
|
-
return 'gpt-5.
|
|
896
|
+
return 'gpt-5.6-sol-high';
|
|
849
897
|
}
|
|
850
898
|
|
|
851
899
|
static getInstallInstructions() {
|
package/src/config.js
CHANGED
|
@@ -534,26 +534,61 @@ function _resolveFeatures(apiHost, explicit) {
|
|
|
534
534
|
return out;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Whether a repo config describes an *exclusive* alt-host repo — one whose
|
|
539
|
+
* every PR lives on the configured `api_host` and which has no github.com
|
|
540
|
+
* presence. True iff `api_host` is a non-empty string AND `exclusive` is not
|
|
541
|
+
* explicitly `false`. Omitting `exclusive` therefore preserves today's
|
|
542
|
+
* behaviour (an `api_host` repo is alt-host-only). Setting `exclusive: false`
|
|
543
|
+
* marks a *dual* repo whose PRs may live on github.com OR the alt host.
|
|
544
|
+
*
|
|
545
|
+
* @param {Object|null|undefined} repoConfig - A single `repos[...]` entry
|
|
546
|
+
* @returns {boolean}
|
|
547
|
+
*/
|
|
548
|
+
function isExclusiveAltHost(repoConfig) {
|
|
549
|
+
if (!repoConfig || typeof repoConfig !== 'object') return false;
|
|
550
|
+
const apiHost = typeof repoConfig.api_host === 'string' && repoConfig.api_host;
|
|
551
|
+
if (!apiHost) return false;
|
|
552
|
+
return repoConfig.exclusive !== false;
|
|
553
|
+
}
|
|
554
|
+
|
|
537
555
|
/**
|
|
538
556
|
* Resolves the host binding for a given repository. The binding describes
|
|
539
557
|
* which API host pair-review should talk to, the token to authenticate
|
|
540
558
|
* with, and per-area dispatch flags.
|
|
541
559
|
*
|
|
542
|
-
*
|
|
560
|
+
* The optional `options.host` selects the binding *flavor* for repos that can
|
|
561
|
+
* live on more than one host (dual repos, `exclusive: false`):
|
|
562
|
+
* - `undefined` (or no options) — legacy + ambiguity rule: an EXCLUSIVE
|
|
563
|
+
* alt-host repo binds to its alt host (today's behaviour); a DUAL repo or
|
|
564
|
+
* a plain github.com repo binds to github.com.
|
|
565
|
+
* - `null` — force a github.com binding. For an EXCLUSIVE alt-host repo this
|
|
566
|
+
* is a caller bug (that repo has no github.com presence) and throws.
|
|
567
|
+
* - `'<url>'` — force an alt-host binding; the string must equal the repo's
|
|
568
|
+
* configured `api_host`, otherwise the stored host no longer matches
|
|
569
|
+
* config and this throws.
|
|
570
|
+
*
|
|
571
|
+
* Token resolution priority for a github.com (github-flavored) binding of a
|
|
572
|
+
* plain repo:
|
|
543
573
|
* 1. GITHUB_TOKEN environment variable
|
|
544
574
|
* 2. repo-level `token`
|
|
545
575
|
* 3. repo-level `token_command` (cached per (repo, command))
|
|
546
576
|
* 4. top-level `github_token`
|
|
547
577
|
* 5. top-level `github_token_command` (cached per (repo, command))
|
|
548
578
|
*
|
|
549
|
-
* For
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* the
|
|
553
|
-
*
|
|
554
|
-
* lookup returns an empty token so the caller can surface a clear
|
|
579
|
+
* For an alt-host binding, the github.com top-level credentials are NOT used —
|
|
580
|
+
* `GITHUB_TOKEN`, `config.github_token`, and `config.github_token_command` are
|
|
581
|
+
* all github.com-only and would be the wrong token for an alt-host endpoint.
|
|
582
|
+
* Only the repo-scoped `token` / `token_command` keys are consulted; missing
|
|
583
|
+
* those, the lookup returns an empty token so the caller can surface a clear
|
|
555
584
|
* "missing credential" error.
|
|
556
585
|
*
|
|
586
|
+
* For the github.com binding of a DUAL repo, the reverse holds: the repo-scoped
|
|
587
|
+
* `token` / `token_command` are alt-host credentials and are NOT used — only
|
|
588
|
+
* the top-level github.com chain (env → `github_token` → `github_token_command`)
|
|
589
|
+
* is consulted. The repo's explicit `features` block (written for the alt host)
|
|
590
|
+
* also does not apply to its github.com binding.
|
|
591
|
+
*
|
|
557
592
|
* Refreshable sources (`repo:token_command`, `config:github_token_command`)
|
|
558
593
|
* additionally carry a `refresh` closure on the returned binding. Calling
|
|
559
594
|
* `refresh()` busts the cached token for that exact source, re-runs the
|
|
@@ -564,58 +599,101 @@ function _resolveFeatures(apiHost, explicit) {
|
|
|
564
599
|
*
|
|
565
600
|
* @param {string|null|undefined} repository - "owner/repo" identifier, or null/undefined for no-repo fallback
|
|
566
601
|
* @param {Object} config - Configuration object from loadConfig()
|
|
567
|
-
* @
|
|
602
|
+
* @param {{ host?: string|null }} [options] - Per-PR host override (see above)
|
|
603
|
+
* @returns {{ apiHost: string|null, host: string|null, token: string, features: Object, source: string, refresh: (function(): string)|null }}
|
|
568
604
|
*/
|
|
569
|
-
function resolveHostBinding(repository, config) {
|
|
605
|
+
function resolveHostBinding(repository, config, options = {}) {
|
|
570
606
|
const safeConfig = config || {};
|
|
571
607
|
const repoConfig = repository ? getRepoConfig(safeConfig, repository) : null;
|
|
572
|
-
const
|
|
608
|
+
const configuredApiHost = (repoConfig && typeof repoConfig.api_host === 'string' && repoConfig.api_host)
|
|
573
609
|
? repoConfig.api_host
|
|
574
610
|
: null;
|
|
575
|
-
const
|
|
611
|
+
const requestedHost = options ? options.host : undefined;
|
|
612
|
+
const exclusive = isExclusiveAltHost(repoConfig);
|
|
613
|
+
|
|
614
|
+
// Decide the binding flavor from the requested host + repo config.
|
|
615
|
+
// `apiHost` null → github.com binding; non-null → alt-host binding.
|
|
616
|
+
let apiHost;
|
|
617
|
+
if (requestedHost === undefined) {
|
|
618
|
+
// Ambiguity rule: exclusive alt-host repo → alt binding (legacy);
|
|
619
|
+
// dual repo and plain github repo → github binding.
|
|
620
|
+
apiHost = exclusive ? configuredApiHost : null;
|
|
621
|
+
} else if (requestedHost === null) {
|
|
622
|
+
if (exclusive) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`resolveHostBinding: repository "${repository}" is an exclusive alt-host repo (api_host "${configuredApiHost}") and has no github.com presence, but a github.com binding was requested (host=null). This is a caller bug; pass the api_host string, or set "exclusive": false on the repo config.`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
apiHost = null;
|
|
628
|
+
} else {
|
|
629
|
+
// A specific alt host was requested; it must match this repo's config.
|
|
630
|
+
if (requestedHost !== configuredApiHost) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`resolveHostBinding: requested host "${requestedHost}" for repository "${repository}" does not match its configured api_host (${configuredApiHost === null ? 'none' : `"${configuredApiHost}"`}). The stored host no longer matches config — re-open the PR from a URL to re-resolve its host.`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
apiHost = configuredApiHost;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Binding flavor booleans:
|
|
639
|
+
// - alt binding uses ONLY repo-scoped credentials + the repo's features.
|
|
640
|
+
// - github binding uses ONLY the top-level github.com chain.
|
|
641
|
+
// - the github binding of a DUAL repo must skip the repo-scoped
|
|
642
|
+
// credentials/features (they belong to the alt host).
|
|
643
|
+
const isAltBinding = apiHost !== null;
|
|
644
|
+
const isDualGithubBinding = !isAltBinding && configuredApiHost !== null;
|
|
645
|
+
const useRepoScopedToken = !isDualGithubBinding;
|
|
646
|
+
const useGithubChain = !isAltBinding;
|
|
647
|
+
|
|
648
|
+
// A dual repo's explicit `features` block was authored for its alt host, so
|
|
649
|
+
// it does not apply to the github.com binding; use plain github defaults.
|
|
650
|
+
const explicitFeatures = isDualGithubBinding ? undefined : repoConfig?.features;
|
|
651
|
+
const features = _resolveFeatures(apiHost, explicitFeatures);
|
|
576
652
|
|
|
577
653
|
// Token resolution
|
|
578
654
|
let token = '';
|
|
579
655
|
let source = 'none';
|
|
580
656
|
|
|
581
|
-
// 1. GITHUB_TOKEN env var, only for github.com
|
|
582
|
-
if (
|
|
657
|
+
// 1. GITHUB_TOKEN env var, only for a github.com binding
|
|
658
|
+
if (useGithubChain && process.env.GITHUB_TOKEN) {
|
|
583
659
|
token = process.env.GITHUB_TOKEN;
|
|
584
660
|
source = 'env:GITHUB_TOKEN';
|
|
585
661
|
logger.debug('Using GitHub token from GITHUB_TOKEN environment variable');
|
|
586
|
-
return { apiHost, token, features, source, refresh: null };
|
|
662
|
+
return { apiHost, host: apiHost, token, features, source, refresh: null };
|
|
587
663
|
}
|
|
588
664
|
|
|
589
|
-
// 2. Repo-level literal token
|
|
590
|
-
|
|
665
|
+
// 2. Repo-level literal token (alt-host credential; not used for a dual
|
|
666
|
+
// repo's github.com binding)
|
|
667
|
+
if (useRepoScopedToken && repoConfig && typeof repoConfig.token === 'string' && repoConfig.token) {
|
|
591
668
|
token = repoConfig.token;
|
|
592
669
|
source = 'repo:token';
|
|
593
670
|
logger.debug(`Using token from repos[${repository}].token`);
|
|
594
|
-
return { apiHost, token, features, source, refresh: null };
|
|
671
|
+
return { apiHost, host: apiHost, token, features, source, refresh: null };
|
|
595
672
|
}
|
|
596
673
|
|
|
597
674
|
// 3. Repo-level token_command
|
|
598
|
-
if (repoConfig && typeof repoConfig.token_command === 'string' && repoConfig.token_command) {
|
|
675
|
+
if (useRepoScopedToken && repoConfig && typeof repoConfig.token_command === 'string' && repoConfig.token_command) {
|
|
599
676
|
const result = _runTokenCommand(repoConfig.token_command, repository, 'repo:token_command');
|
|
600
677
|
if (result) {
|
|
601
678
|
return {
|
|
602
679
|
apiHost,
|
|
680
|
+
host: apiHost,
|
|
603
681
|
token: result,
|
|
604
682
|
features,
|
|
605
683
|
source: 'repo:token_command',
|
|
606
|
-
refresh: _makeRefresh(repository, safeConfig, 'repo:token_command')
|
|
684
|
+
refresh: _makeRefresh(repository, safeConfig, 'repo:token_command', options)
|
|
607
685
|
};
|
|
608
686
|
}
|
|
609
687
|
}
|
|
610
688
|
|
|
611
|
-
// 4. Top-level github_token. Only consulted for github.com
|
|
689
|
+
// 4. Top-level github_token. Only consulted for a github.com binding —
|
|
612
690
|
// the top-level token is a github.com credential and would fail
|
|
613
691
|
// authentication when sent to an alt-host.
|
|
614
|
-
if (
|
|
692
|
+
if (useGithubChain && typeof safeConfig.github_token === 'string' && safeConfig.github_token) {
|
|
615
693
|
token = safeConfig.github_token;
|
|
616
694
|
source = 'config:github_token';
|
|
617
695
|
logger.debug('Using GitHub token from config.github_token');
|
|
618
|
-
return { apiHost, token, features, source, refresh: null };
|
|
696
|
+
return { apiHost, host: apiHost, token, features, source, refresh: null };
|
|
619
697
|
}
|
|
620
698
|
|
|
621
699
|
// 5. Top-level github_token_command. Like step 4, github.com-only.
|
|
@@ -623,25 +701,26 @@ function resolveHostBinding(repository, config) {
|
|
|
623
701
|
// command only — keying on repository would re-invoke the (often
|
|
624
702
|
// slow) command per repo per session. Repo-level `token_command`
|
|
625
703
|
// above keeps its per-(repo, command) cache key.
|
|
626
|
-
if (
|
|
704
|
+
if (useGithubChain && typeof safeConfig.github_token_command === 'string' && safeConfig.github_token_command) {
|
|
627
705
|
const result = _runTokenCommand(safeConfig.github_token_command, null, 'config:github_token_command');
|
|
628
706
|
if (result) {
|
|
629
707
|
return {
|
|
630
708
|
apiHost,
|
|
709
|
+
host: apiHost,
|
|
631
710
|
token: result,
|
|
632
711
|
features,
|
|
633
712
|
source: 'config:github_token_command',
|
|
634
|
-
refresh: _makeRefresh(repository, safeConfig, 'config:github_token_command')
|
|
713
|
+
refresh: _makeRefresh(repository, safeConfig, 'config:github_token_command', options)
|
|
635
714
|
};
|
|
636
715
|
}
|
|
637
716
|
}
|
|
638
717
|
|
|
639
|
-
if (
|
|
718
|
+
if (isAltBinding && repository) {
|
|
640
719
|
logger.debug(`No repo-scoped token resolved for alt-host repo ${repository} (${apiHost}); github.com top-level credentials are not used for alt-hosts`);
|
|
641
720
|
} else {
|
|
642
721
|
logger.debug('No token resolved for host binding');
|
|
643
722
|
}
|
|
644
|
-
return { apiHost, token: '', features, source: 'none', refresh: null };
|
|
723
|
+
return { apiHost, host: apiHost, token: '', features, source: 'none', refresh: null };
|
|
645
724
|
}
|
|
646
725
|
|
|
647
726
|
/**
|
|
@@ -656,14 +735,15 @@ function resolveHostBinding(repository, config) {
|
|
|
656
735
|
* @param {string|null|undefined} repository - "owner/repo" identifier as supplied to resolveHostBinding
|
|
657
736
|
* @param {Object} config - Configuration object from loadConfig()
|
|
658
737
|
* @param {('repo:token_command'|'config:github_token_command')} source - The refreshable source backing the binding
|
|
738
|
+
* @param {{ host?: string|null }} [options] - The same host override the binding was resolved with, so the refresh re-resolves the SAME flavor (a two-arg re-resolve would apply the ambiguity rule and could pick the wrong host for a dual repo)
|
|
659
739
|
* @returns {function(): string} - Closure resolving to the fresh token (empty string on failure)
|
|
660
740
|
*/
|
|
661
|
-
function _makeRefresh(repository, config, source) {
|
|
741
|
+
function _makeRefresh(repository, config, source, options = {}) {
|
|
662
742
|
return function refresh() {
|
|
663
743
|
invalidateTokenCache(repository, config, source);
|
|
664
744
|
// Re-resolve after invalidation so _runTokenCommand re-executes the
|
|
665
745
|
// command. Returns '' if the command now fails or yields nothing.
|
|
666
|
-
return resolveHostBinding(repository, config).token;
|
|
746
|
+
return resolveHostBinding(repository, config, options).token;
|
|
667
747
|
};
|
|
668
748
|
}
|
|
669
749
|
|
|
@@ -795,6 +875,22 @@ function validateRepoConfig(config) {
|
|
|
795
875
|
const apiHost = (typeof repoEntry.api_host === 'string' && repoEntry.api_host) ? repoEntry.api_host : null;
|
|
796
876
|
const features = (repoEntry.features && typeof repoEntry.features === 'object') ? repoEntry.features : {};
|
|
797
877
|
|
|
878
|
+
// `exclusive` marks whether an alt-host repo's PRs live ONLY on its
|
|
879
|
+
// `api_host` (default) or may also live on github.com (`exclusive: false`,
|
|
880
|
+
// a dual repo). It is meaningless without `api_host`.
|
|
881
|
+
if (repoEntry.exclusive !== undefined && repoEntry.exclusive !== null) {
|
|
882
|
+
if (typeof repoEntry.exclusive !== 'boolean') {
|
|
883
|
+
throw new Error(
|
|
884
|
+
`Invalid pair-review config: repos["${repoKey}"].exclusive must be a boolean.`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (!apiHost) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`Invalid pair-review config: repos["${repoKey}"].exclusive is only valid when api_host is set.`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
798
894
|
for (const [area, value] of Object.entries(features)) {
|
|
799
895
|
// Endpoint-override sub-keys (e.g. `pending_review_comments_endpoint`)
|
|
800
896
|
// are validated separately below. Reject anything that ends in
|
|
@@ -910,6 +1006,31 @@ function validateRepoConfig(config) {
|
|
|
910
1006
|
`Invalid pair-review config: repos["${repoKey}"].url_pattern is not a valid regular expression: ${err.message}`
|
|
911
1007
|
);
|
|
912
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
// An `api_host`-bearing pattern must never match a canonical github.com /
|
|
1011
|
+
// Graphite URL — doing so pre-pins a github PR to the alt host and bypasses
|
|
1012
|
+
// the setup probe (a silent, durable wrong binding). Warn (do NOT throw —
|
|
1013
|
+
// we must not break existing configs; parsePRUrl also guards this at
|
|
1014
|
+
// runtime by discarding such matches) when the pattern is over-broad.
|
|
1015
|
+
if (typeof repoEntry.api_host === 'string' && repoEntry.api_host) {
|
|
1016
|
+
try {
|
|
1017
|
+
const rx = new RegExp(repoEntry.url_pattern);
|
|
1018
|
+
const canaries = [
|
|
1019
|
+
'https://github.com/o/r/pull/1',
|
|
1020
|
+
'https://app.graphite.dev/github/pr/o/r/1',
|
|
1021
|
+
'https://app.graphite.com/github/o/r/pull/1'
|
|
1022
|
+
];
|
|
1023
|
+
if (canaries.some((u) => rx.test(u))) {
|
|
1024
|
+
logger.warn(
|
|
1025
|
+
`repos["${repoKey}"].url_pattern also matches canonical github.com / Graphite URLs; ` +
|
|
1026
|
+
`anchor it (e.g. start with "^https://<your-alt-host>/") so it never pre-pins a github.com ` +
|
|
1027
|
+
`PR to the alt host. pair-review will still route such URLs to github.com.`
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
} catch {
|
|
1031
|
+
// Invalid regex already reported above; nothing more to check.
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
913
1034
|
}
|
|
914
1035
|
|
|
915
1036
|
// Optional escape-hatch regex used by parseRepositoryFromURL to match
|
|
@@ -1512,6 +1633,7 @@ module.exports = {
|
|
|
1512
1633
|
validatePort,
|
|
1513
1634
|
getGitHubToken,
|
|
1514
1635
|
resolveHostBinding,
|
|
1636
|
+
isExclusiveAltHost,
|
|
1515
1637
|
invalidateTokenCache,
|
|
1516
1638
|
validateRepoConfig,
|
|
1517
1639
|
matchRepoByUrl,
|