@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.
- 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/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/js/local.js
CHANGED
|
@@ -70,9 +70,17 @@ class LocalManager {
|
|
|
70
70
|
await this.loadLocalReview();
|
|
71
71
|
|
|
72
72
|
// Auto-trigger analysis if ?analyze=true is present
|
|
73
|
-
const
|
|
73
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
74
|
+
const autoAnalyze = searchParams.get('analyze');
|
|
74
75
|
if (autoAnalyze === 'true' && !window.prManager.isAnalyzing) {
|
|
75
76
|
const manager = window.prManager;
|
|
77
|
+
// Provider/model override carried on the URL by single-port delegation
|
|
78
|
+
// (the delegated-to server is a different process whose env never saw the
|
|
79
|
+
// CLI flag). Prepended ahead of repo settings in _buildDefaultAnalysisConfig.
|
|
80
|
+
const urlOverride = {
|
|
81
|
+
provider: searchParams.get('provider'),
|
|
82
|
+
model: searchParams.get('model')
|
|
83
|
+
};
|
|
76
84
|
try {
|
|
77
85
|
// Prefer a stashed bulk-analysis config (threaded via analysisConfigId).
|
|
78
86
|
// The CLI uses it to carry --instructions plus the resolved
|
|
@@ -97,15 +105,15 @@ class LocalManager {
|
|
|
97
105
|
manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
|
|
98
106
|
manager._getAppConfig()
|
|
99
107
|
]);
|
|
100
|
-
config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig);
|
|
108
|
+
config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig, null, urlOverride);
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
await this.startLocalAnalysis(null, config);
|
|
104
112
|
} finally {
|
|
105
113
|
const cleanUrl = new URL(window.location);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
// Strip the whole auto-analyze intent bundle (analyze/analysisConfigId/
|
|
115
|
+
// council/provider/model) so a manual refresh does not replay the intent.
|
|
116
|
+
window.stripAnalyzeParams(cleanUrl);
|
|
109
117
|
history.replaceState(null, '', cleanUrl);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
@@ -348,11 +356,13 @@ class LocalManager {
|
|
|
348
356
|
// Resolve provider and model as a MATCHED pair so the council/advanced tabs
|
|
349
357
|
// are never seeded with a cross-provider model (e.g. antigravity + opus), which
|
|
350
358
|
// would blank the model <select> and be rejected by the backend.
|
|
359
|
+
// buildProviderModelScopes prepends any CLI/env override ahead of repo
|
|
360
|
+
// settings so `--provider` seeds the modal as the default selection.
|
|
351
361
|
const providersInfo = await manager._getProvidersInfo();
|
|
352
|
-
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
363
|
+
window.buildProviderModelScopes(repoSettings, appConfig),
|
|
364
|
+
providersInfo
|
|
365
|
+
);
|
|
356
366
|
|
|
357
367
|
// Determine default tab (priority: localStorage > repo settings > 'single')
|
|
358
368
|
const tabStorageKey = `pair-review-tab:local-${reviewId}`;
|
package/public/js/pr.js
CHANGED
|
@@ -634,7 +634,7 @@ class PRManager {
|
|
|
634
634
|
* @param {Object} reviewSettings - Review settings from fetchLastReviewSettings()
|
|
635
635
|
* @returns {Promise<Object>} Config object suitable for startAnalysis / startLocalAnalysis
|
|
636
636
|
*/
|
|
637
|
-
async _buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig = {}, providersInfo = null) {
|
|
637
|
+
async _buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig = {}, providersInfo = null, urlOverride = null) {
|
|
638
638
|
const defaultTab = repoSettings?.default_tab || 'single';
|
|
639
639
|
const councilId = repoSettings?.default_council_id || reviewSettings?.last_council_id || null;
|
|
640
640
|
|
|
@@ -642,6 +642,8 @@ class PRManager {
|
|
|
642
642
|
// highest priority for council selection. When present we force the council
|
|
643
643
|
// branch regardless of default_tab/settings, and derive configType from the
|
|
644
644
|
// council's own type ('council' or 'advanced') rather than the repo default.
|
|
645
|
+
// An explicit council outranks a provider/model override here, matching the
|
|
646
|
+
// backend precedence in resolveReviewConfig where --council beats --provider.
|
|
645
647
|
const urlSearch = (typeof window !== 'undefined' && window.location && window.location.search) || '';
|
|
646
648
|
const urlCouncilId = new URLSearchParams(urlSearch).get('council');
|
|
647
649
|
if (urlCouncilId) {
|
|
@@ -676,7 +678,14 @@ class PRManager {
|
|
|
676
678
|
}
|
|
677
679
|
}
|
|
678
680
|
|
|
679
|
-
|
|
681
|
+
// A CLI/env or delegation-URL provider/model override names a single
|
|
682
|
+
// provider, which is incompatible with a multi-voice council. When an
|
|
683
|
+
// override is active we bypass the repo's *default* council and force the
|
|
684
|
+
// single-provider path so `--provider` is always honored. (An explicit
|
|
685
|
+
// `?council=` above still wins, matching the backend precedence.)
|
|
686
|
+
const overrideActive = window.hasProviderModelOverride(appConfig, urlOverride);
|
|
687
|
+
|
|
688
|
+
if (!overrideActive && (defaultTab === 'council' || defaultTab === 'advanced') && councilId) {
|
|
680
689
|
// Fetch the full council config so the progress modal can render correctly
|
|
681
690
|
let councilConfig = null;
|
|
682
691
|
let councilName = null;
|
|
@@ -705,11 +714,13 @@ class PRManager {
|
|
|
705
714
|
// independently (repo || app || hardcoded) can mix a provider from one
|
|
706
715
|
// scope with a model from another, yielding an invalid pair (e.g.
|
|
707
716
|
// antigravity/opus) that startAnalysis would forward to the backend as-is.
|
|
717
|
+
// buildProviderModelScopes prepends any CLI/env or delegation-URL override
|
|
718
|
+
// ahead of repo settings so `--provider` outranks a repo's saved default.
|
|
708
719
|
const providers = providersInfo || await this._getProvidersInfo();
|
|
709
|
-
const { provider, model } = window.resolveProviderModelPair(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
720
|
+
const { provider, model } = window.resolveProviderModelPair(
|
|
721
|
+
window.buildProviderModelScopes(repoSettings, appConfig, urlOverride),
|
|
722
|
+
providers
|
|
723
|
+
);
|
|
713
724
|
|
|
714
725
|
return {
|
|
715
726
|
provider,
|
|
@@ -750,10 +761,18 @@ class PRManager {
|
|
|
750
761
|
* @param {number} prNumber - PR number
|
|
751
762
|
*/
|
|
752
763
|
async _maybeAutoAnalyze(owner, repo, prNumber) {
|
|
753
|
-
const
|
|
764
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
765
|
+
const autoAnalyze = searchParams.get('analyze');
|
|
754
766
|
if (autoAnalyze === 'true' && !this.isAnalyzing) {
|
|
755
767
|
this._autoAnalyzeRequested = true;
|
|
756
768
|
let shouldCleanUrl = true;
|
|
769
|
+
// Provider/model override carried on the URL by single-port delegation
|
|
770
|
+
// (the delegated-to server is a different process whose env never saw the
|
|
771
|
+
// CLI flag). Prepended ahead of repo settings in _buildDefaultAnalysisConfig.
|
|
772
|
+
const urlOverride = {
|
|
773
|
+
provider: searchParams.get('provider'),
|
|
774
|
+
model: searchParams.get('model')
|
|
775
|
+
};
|
|
757
776
|
try {
|
|
758
777
|
// Skip refresh if we just loaded fresh data (loadPR sets _justLoaded = true).
|
|
759
778
|
// Otherwise, refresh to ensure we have the latest PR data in case the worktree
|
|
@@ -791,7 +810,7 @@ class PRManager {
|
|
|
791
810
|
this.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
|
|
792
811
|
this._getAppConfig()
|
|
793
812
|
]);
|
|
794
|
-
config = await this._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig);
|
|
813
|
+
config = await this._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig, null, urlOverride);
|
|
795
814
|
}
|
|
796
815
|
|
|
797
816
|
await this.startAnalysis(owner, repo, prNumber, null, config);
|
|
@@ -799,9 +818,9 @@ class PRManager {
|
|
|
799
818
|
this._autoAnalyzeRequested = false;
|
|
800
819
|
if (shouldCleanUrl) {
|
|
801
820
|
const cleanUrl = new URL(window.location);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
821
|
+
// Strip the whole auto-analyze intent bundle (analyze/analysisConfigId/
|
|
822
|
+
// council/provider/model) so a manual refresh does not replay the intent.
|
|
823
|
+
window.stripAnalyzeParams(cleanUrl);
|
|
805
824
|
history.replaceState(null, '', cleanUrl);
|
|
806
825
|
}
|
|
807
826
|
}
|
|
@@ -3055,11 +3074,13 @@ class PRManager {
|
|
|
3055
3074
|
// Resolve provider and model as a MATCHED pair so the council/advanced tabs
|
|
3056
3075
|
// are never seeded with a cross-provider model (e.g. antigravity + opus), which
|
|
3057
3076
|
// would blank the model <select> and be rejected by the backend.
|
|
3077
|
+
// buildProviderModelScopes prepends any CLI/env override ahead of repo
|
|
3078
|
+
// settings so `--provider` seeds the modal as the default selection.
|
|
3058
3079
|
const providersInfo = await this._getProvidersInfo();
|
|
3059
|
-
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3080
|
+
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
3081
|
+
window.buildProviderModelScopes(repoSettings, appConfig),
|
|
3082
|
+
providersInfo
|
|
3083
|
+
);
|
|
3063
3084
|
const tabStorageKey = PRManager.getRepoStorageKey('pair-review-tab', owner, repo);
|
|
3064
3085
|
const rememberedTab = localStorage.getItem(tabStorageKey);
|
|
3065
3086
|
const defaultTab = rememberedTab || repoSettings?.default_tab || 'single';
|
|
@@ -6848,11 +6869,13 @@ class PRManager {
|
|
|
6848
6869
|
// Resolve provider and model as a MATCHED pair so the council/advanced tabs
|
|
6849
6870
|
// are never seeded with a cross-provider model (e.g. antigravity + opus), which
|
|
6850
6871
|
// would blank the model <select> and be rejected by the backend.
|
|
6872
|
+
// buildProviderModelScopes prepends any CLI/env override ahead of repo
|
|
6873
|
+
// settings so `--provider` seeds the modal as the default selection.
|
|
6851
6874
|
const providersInfo = await this._getProvidersInfo();
|
|
6852
|
-
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6875
|
+
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
6876
|
+
window.buildProviderModelScopes(repoSettings, appConfig),
|
|
6877
|
+
providersInfo
|
|
6878
|
+
);
|
|
6856
6879
|
|
|
6857
6880
|
// Determine default tab (priority: localStorage > repo settings > 'single')
|
|
6858
6881
|
const tabStorageKey = PRManager.getRepoStorageKey('pair-review-tab', owner, repo);
|
|
@@ -7021,29 +7044,34 @@ class PRManager {
|
|
|
7021
7044
|
|
|
7022
7045
|
/**
|
|
7023
7046
|
* Build the worktree-not-found recovery URL. When the user arrived via
|
|
7024
|
-
* auto-analyze (?analyze=true), the reload link preserves the
|
|
7025
|
-
*
|
|
7026
|
-
*
|
|
7027
|
-
* `
|
|
7028
|
-
*
|
|
7029
|
-
* the repo/default analysis
|
|
7047
|
+
* auto-analyze (?analyze=true), the reload link preserves the whole
|
|
7048
|
+
* auto-analyze intent bundle (analyze/analysisConfigId/council/provider/model)
|
|
7049
|
+
* so analysis re-triggers with the same selection after worktree setup.
|
|
7050
|
+
* Carrying is delegated to the shared `carryAnalyzeParams` relay so this
|
|
7051
|
+
* hop stays in lockstep with the other three (see analyze-params.js);
|
|
7052
|
+
* dropping `council` would silently fall back to the repo/default analysis
|
|
7053
|
+
* config, and dropping `provider`/`model` would lose a `--provider` override.
|
|
7030
7054
|
* @param {string} owner - Repository owner
|
|
7031
7055
|
* @param {string} repo - Repository name
|
|
7032
7056
|
* @param {number} number - PR number
|
|
7033
7057
|
* @returns {string} The recovery URL (unescaped)
|
|
7034
7058
|
*/
|
|
7035
7059
|
_buildWorktreeRecoveryUrl(owner, repo, number) {
|
|
7036
|
-
|
|
7060
|
+
// A fixed base is fine — only pathname + search are used for the link, so
|
|
7061
|
+
// the origin is irrelevant (and this avoids depending on window.location).
|
|
7062
|
+
const setupUrl = new URL(
|
|
7063
|
+
`/pr/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(number)}`,
|
|
7064
|
+
'http://localhost'
|
|
7065
|
+
);
|
|
7037
7066
|
if (this._autoAnalyzeRequested) {
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
setupUrl += `?${params.toString()}`;
|
|
7067
|
+
// The current search still carries the bundle here — it isn't stripped
|
|
7068
|
+
// until _maybeAutoAnalyze's finally block, which runs after this handler.
|
|
7069
|
+
// Force analyze=true so the retry still auto-triggers even if the source
|
|
7070
|
+
// somehow lost it, then carry the rest of the bundle via the shared relay.
|
|
7071
|
+
setupUrl.searchParams.set('analyze', 'true');
|
|
7072
|
+
window.carryAnalyzeParams(window.location.search, setupUrl);
|
|
7045
7073
|
}
|
|
7046
|
-
return setupUrl;
|
|
7074
|
+
return setupUrl.pathname + setupUrl.search;
|
|
7047
7075
|
}
|
|
7048
7076
|
|
|
7049
7077
|
/**
|
|
@@ -7055,14 +7083,14 @@ class PRManager {
|
|
|
7055
7083
|
* @param {number} number - PR number
|
|
7056
7084
|
*/
|
|
7057
7085
|
showWorktreeNotFoundError(owner, repo, number) {
|
|
7058
|
-
const
|
|
7086
|
+
const href = this._buildWorktreeRecoveryUrl(owner, repo, number);
|
|
7059
7087
|
const container = document.getElementById('pr-container');
|
|
7060
7088
|
if (container) {
|
|
7061
7089
|
container.innerHTML = `
|
|
7062
7090
|
<div class="error-container">
|
|
7063
7091
|
<div class="error-icon">Warning</div>
|
|
7064
7092
|
<div class="error-message">Worktree not found. Please reload the PR to set up the worktree before running analysis.</div>
|
|
7065
|
-
<a class="btn btn-primary" href="${this.escapeHtml(
|
|
7093
|
+
<a class="btn btn-primary" href="${this.escapeHtml(href)}">Reload PR</a>
|
|
7066
7094
|
</div>
|
|
7067
7095
|
`;
|
|
7068
7096
|
container.style.display = 'block';
|
package/public/js/repo-links.js
CHANGED
|
@@ -246,9 +246,17 @@
|
|
|
246
246
|
_currentLinks = null;
|
|
247
247
|
_currentContext = context || null;
|
|
248
248
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
249
|
+
// Pass the PR number (when known) so the server can resolve the link set
|
|
250
|
+
// against this PR's host for a dual-host repo (github links for a
|
|
251
|
+
// github-hosted PR, the external link for an alt-hosted one). Local mode
|
|
252
|
+
// has no PR number, so the query is omitted and the server returns the
|
|
253
|
+
// repo-level default.
|
|
254
|
+
let url = '/api/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) + '/links';
|
|
255
|
+
const number = context && context.number;
|
|
256
|
+
if (number !== undefined && number !== null && number !== '') {
|
|
257
|
+
url += '?number=' + encodeURIComponent(String(number));
|
|
258
|
+
}
|
|
259
|
+
const response = await fetch(url);
|
|
252
260
|
if (!response.ok) {
|
|
253
261
|
console.warn('[repo-links] Failed to fetch repo links:', response.status);
|
|
254
262
|
return;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Shared "auto-analyze intent" URL-param relay.
|
|
4
|
+
*
|
|
5
|
+
* The params `analyze`, `analysisConfigId`, `council`, `provider`, and `model`
|
|
6
|
+
* form a single bundle that must travel TOGETHER through every browser hop
|
|
7
|
+
* between the single-port delegation URL and the review page that consumes them:
|
|
8
|
+
*
|
|
9
|
+
* delegation URL → setup.html redirect(s) → review page auto-analyze
|
|
10
|
+
* review page → "Reload PR" retry → setup.html → review page auto-analyze
|
|
11
|
+
*
|
|
12
|
+
* Each hop used to cherry-pick fields by name, so any hop that forgot one
|
|
13
|
+
* silently dropped it — which is exactly how a delegated `--provider` override
|
|
14
|
+
* got lost on its way to the running server (and how a `--council` selection
|
|
15
|
+
* would drop on the "Reload PR" retry). Centralizing the bundle here means
|
|
16
|
+
* adding a new intent param later is a one-line edit in ONE place instead of a
|
|
17
|
+
* fresh bug at every call site.
|
|
18
|
+
*
|
|
19
|
+
* Note: setup-internal params (e.g. `path`, consumed by the local-mode setup
|
|
20
|
+
* POST) are deliberately NOT in the bundle, so they never leak onto the review
|
|
21
|
+
* page URL.
|
|
22
|
+
*/
|
|
23
|
+
const ANALYZE_PARAM_KEYS = ['analyze', 'analysisConfigId', 'council', 'provider', 'model'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Copy the auto-analyze intent bundle from a source query onto a target URL.
|
|
27
|
+
* Only non-empty values present in the source are copied; other params already
|
|
28
|
+
* on `toUrl` are left untouched.
|
|
29
|
+
*
|
|
30
|
+
* @param {URLSearchParams|string} fromSearch - source query (e.g. window.location.search)
|
|
31
|
+
* @param {URL} toUrl - destination URL object, mutated in place
|
|
32
|
+
* @returns {URL} the same `toUrl`, for chaining
|
|
33
|
+
*/
|
|
34
|
+
function carryAnalyzeParams(fromSearch, toUrl) {
|
|
35
|
+
if (!toUrl) return toUrl;
|
|
36
|
+
const src = typeof fromSearch === 'string' ? new URLSearchParams(fromSearch) : fromSearch;
|
|
37
|
+
if (!src || typeof src.get !== 'function') return toUrl;
|
|
38
|
+
for (const key of ANALYZE_PARAM_KEYS) {
|
|
39
|
+
const value = src.get(key);
|
|
40
|
+
if (value !== null && value !== '') {
|
|
41
|
+
toUrl.searchParams.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return toUrl;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delete the auto-analyze intent bundle from a URL. Used by the consumer after
|
|
49
|
+
* it acts on the intent, so a manual page refresh does not replay it.
|
|
50
|
+
*
|
|
51
|
+
* @param {URL} url - URL object, mutated in place
|
|
52
|
+
* @returns {URL} the same `url`, for chaining
|
|
53
|
+
*/
|
|
54
|
+
function stripAnalyzeParams(url) {
|
|
55
|
+
if (!url) return url;
|
|
56
|
+
for (const key of ANALYZE_PARAM_KEYS) {
|
|
57
|
+
url.searchParams.delete(key);
|
|
58
|
+
}
|
|
59
|
+
return url;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof window !== 'undefined') {
|
|
63
|
+
window.carryAnalyzeParams = carryAnalyzeParams;
|
|
64
|
+
window.stripAnalyzeParams = stripAnalyzeParams;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
68
|
+
module.exports = { carryAnalyzeParams, stripAnalyzeParams, ANALYZE_PARAM_KEYS };
|
|
69
|
+
}
|
|
@@ -86,10 +86,75 @@ function resolveProviderModelPair(scopes, providersInfo) {
|
|
|
86
86
|
return { provider: 'claude', model: providerDefaultModel(claude) };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Build the ordered scope list for resolveProviderModelPair, injecting the
|
|
91
|
+
* CLI/env override AHEAD of saved repo settings.
|
|
92
|
+
*
|
|
93
|
+
* The override must outrank repo settings to honor the documented
|
|
94
|
+
* `CLI/env > repo settings` contract. Folding the override into appConfig's
|
|
95
|
+
* default_provider/default_model alone is NOT enough: every seed site passes
|
|
96
|
+
* repoSettings first, so an env-aware appConfig would still lose to a repo's
|
|
97
|
+
* saved default. Prepending the override scope is the only correct fix.
|
|
98
|
+
*
|
|
99
|
+
* Two override channels, highest precedence first:
|
|
100
|
+
* 1. `extraOverride` — a per-invocation override carried on the auto-analyze
|
|
101
|
+
* URL (single-port delegation: the flag can only reach the already-running
|
|
102
|
+
* server through the URL).
|
|
103
|
+
* 2. `appConfig.provider_override` / `appConfig.model_override` — the
|
|
104
|
+
* PAIR_REVIEW_PROVIDER / PAIR_REVIEW_MODEL env override surfaced by
|
|
105
|
+
* /api/config, for the process that actually received the CLI flag.
|
|
106
|
+
*
|
|
107
|
+
* A provider-only override (`{ provider: 'codex', model: null }`) resolves via
|
|
108
|
+
* resolveProviderModelPair to codex + codex's own default model, matching CLI
|
|
109
|
+
* semantics where `--provider codex` alone pins the provider.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} repoSettings - saved repo settings row (may be null)
|
|
112
|
+
* @param {Object} [appConfig] - /api/config response (or __pairReview-shaped equivalent)
|
|
113
|
+
* @param {{provider?: string, model?: string}} [extraOverride] - per-invocation override (URL params)
|
|
114
|
+
* @returns {Array<{provider?: string, model?: string}>} ordered scopes for resolveProviderModelPair
|
|
115
|
+
*/
|
|
116
|
+
function buildProviderModelScopes(repoSettings, appConfig = {}, extraOverride = null) {
|
|
117
|
+
const cfg = appConfig || {};
|
|
118
|
+
const scopes = [];
|
|
119
|
+
// 1. Per-invocation override (delegation URL params) — highest precedence.
|
|
120
|
+
if (extraOverride && (extraOverride.provider || extraOverride.model)) {
|
|
121
|
+
scopes.push({ provider: extraOverride.provider || null, model: extraOverride.model || null });
|
|
122
|
+
}
|
|
123
|
+
// 2. CLI/env override surfaced by /api/config (non-delegated invocations).
|
|
124
|
+
if (cfg.provider_override || cfg.model_override) {
|
|
125
|
+
scopes.push({ provider: cfg.provider_override || null, model: cfg.model_override || null });
|
|
126
|
+
}
|
|
127
|
+
// 3. Saved repo settings.
|
|
128
|
+
scopes.push({ provider: repoSettings?.default_provider, model: repoSettings?.default_model });
|
|
129
|
+
// 4. App/config defaults.
|
|
130
|
+
scopes.push({ provider: cfg.default_provider, model: cfg.default_model });
|
|
131
|
+
return scopes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether a CLI/env provider-or-model override is active (from either channel).
|
|
136
|
+
* Used to decide whether auto-analyze should bypass a council default and force
|
|
137
|
+
* the single-provider path (a single-provider override is incompatible with a
|
|
138
|
+
* multi-voice council).
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} [appConfig] - /api/config response (or __pairReview-shaped equivalent)
|
|
141
|
+
* @param {{provider?: string, model?: string}} [extraOverride] - per-invocation override (URL params)
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
function hasProviderModelOverride(appConfig = {}, extraOverride = null) {
|
|
145
|
+
const cfg = appConfig || {};
|
|
146
|
+
return !!(
|
|
147
|
+
(extraOverride && (extraOverride.provider || extraOverride.model)) ||
|
|
148
|
+
cfg.provider_override || cfg.model_override
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
89
152
|
if (typeof window !== 'undefined') {
|
|
90
153
|
window.resolveProviderModelPair = resolveProviderModelPair;
|
|
154
|
+
window.buildProviderModelScopes = buildProviderModelScopes;
|
|
155
|
+
window.hasProviderModelOverride = hasProviderModelOverride;
|
|
91
156
|
}
|
|
92
157
|
|
|
93
158
|
if (typeof module !== 'undefined' && module.exports) {
|
|
94
|
-
module.exports = { resolveProviderModelPair };
|
|
159
|
+
module.exports = { resolveProviderModelPair, buildProviderModelScopes, hasProviderModelOverride };
|
|
95
160
|
}
|