@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/src/local-review.js
CHANGED
|
@@ -11,7 +11,7 @@ const { fireHooks, hasHooks } = require('./hooks/hook-runner');
|
|
|
11
11
|
const { buildReviewStartedPayload, buildReviewLoadedPayload, getCachedUser } = require('./hooks/payloads');
|
|
12
12
|
|
|
13
13
|
const execAsync = promisify(exec);
|
|
14
|
-
const { STOPS, scopeIncludes, includesBranch, DEFAULT_SCOPE, scopeLabel, reviewScope } = require('./local-scope');
|
|
14
|
+
const { STOPS, scopeIncludes, includesBranch, DEFAULT_SCOPE, scopeLabel, reviewScope, parseScopeArg } = require('./local-scope');
|
|
15
15
|
const { initializeDatabase, ReviewRepository, RepoSettingsRepository } = require('./database');
|
|
16
16
|
const { resolveCouncilHandle } = require('./councils/resolve-council');
|
|
17
17
|
const { prepareInteractiveAnalysisConfig } = require('./interactive-analysis-config');
|
|
@@ -21,6 +21,10 @@ const summaryGenerator = require('./ai/summary-generator');
|
|
|
21
21
|
const tourGenerator = require('./ai/tour-generator');
|
|
22
22
|
const { getShaAbbrevLength } = require('./git/sha-abbrev');
|
|
23
23
|
const { GIT_DIFF_FLAGS, GIT_DIFF_FLAGS_ARRAY } = require('./git/diff-flags');
|
|
24
|
+
// Namespace import (not destructured) so detectBaseBranch is resolved off the
|
|
25
|
+
// module object at call time — this keeps it interceptable by vi.spyOn on the
|
|
26
|
+
// base-branch module in tests, unlike a destructured binding captured at load.
|
|
27
|
+
const baseBranchModule = require('./git/base-branch');
|
|
24
28
|
const open = (...args) => process.env.PAIR_REVIEW_NO_OPEN ? Promise.resolve() : import('open').then(({ default: open }) => open(...args));
|
|
25
29
|
|
|
26
30
|
// Design note: This module uses execSync for git commands despite async function signatures.
|
|
@@ -694,6 +698,112 @@ async function generateLocalDiff(repoPath, options = {}) {
|
|
|
694
698
|
};
|
|
695
699
|
}
|
|
696
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Resolve the effective scope and base branch for a local review session,
|
|
703
|
+
* applying an explicit `--scope`/`--base` (or delegated query) override on top
|
|
704
|
+
* of the persisted/default scope.
|
|
705
|
+
*
|
|
706
|
+
* Shared by the CLI seam (setupLocalReviewSession) and the web setup seam
|
|
707
|
+
* (src/setup/local-setup.js) so a scoped launch behaves identically whether it
|
|
708
|
+
* cold-starts a server or is delegated to a running one. Callers MUST have
|
|
709
|
+
* validated flags.scope/flags.base first (parseScopeArg + branch-name regex +
|
|
710
|
+
* "base requires branch scope"); an invalid flags.scope is treated as "no
|
|
711
|
+
* override" here rather than throwing.
|
|
712
|
+
*
|
|
713
|
+
* Precedence — scope: explicit override > persisted review scope > DEFAULT_SCOPE.
|
|
714
|
+
* Precedence — base: explicit --base > persisted local_base_branch >
|
|
715
|
+
* detectBaseBranch. The base is nulled for any non-branch scope so a stale value
|
|
716
|
+
* from a previously branch-scoped session never leaks into the diff/persist/return.
|
|
717
|
+
*
|
|
718
|
+
* @param {Object} params
|
|
719
|
+
* @param {Object|null} params.existingReview - Persisted review row, or null for a fresh session
|
|
720
|
+
* @param {Object} [params.flags] - { scope?, base? }
|
|
721
|
+
* @param {string} params.repoPath
|
|
722
|
+
* @param {string} params.branch - Current branch name
|
|
723
|
+
* @param {string} params.repository - owner/repo (for host binding / PR base lookup)
|
|
724
|
+
* @param {Object} params.config
|
|
725
|
+
* @returns {Promise<{scopeStart: string, scopeEnd: string, baseBranch: string|null, scopeOverridden: boolean}>}
|
|
726
|
+
*/
|
|
727
|
+
async function resolveScopeAndBase({ existingReview, flags = {}, repoPath, branch, repository, config }) {
|
|
728
|
+
const scopeOverride = flags.scope ? parseScopeArg(flags.scope) : null;
|
|
729
|
+
let scopeStart, scopeEnd;
|
|
730
|
+
if (scopeOverride) {
|
|
731
|
+
({ start: scopeStart, end: scopeEnd } = scopeOverride);
|
|
732
|
+
} else if (existingReview) {
|
|
733
|
+
({ start: scopeStart, end: scopeEnd } = reviewScope(existingReview));
|
|
734
|
+
} else {
|
|
735
|
+
({ start: scopeStart, end: scopeEnd } = DEFAULT_SCOPE);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let baseBranch = existingReview?.local_base_branch || null;
|
|
739
|
+
if (scopeOverride && includesBranch(scopeStart)) {
|
|
740
|
+
if (flags.base) {
|
|
741
|
+
baseBranch = flags.base;
|
|
742
|
+
} else if (!baseBranch) {
|
|
743
|
+
const branchBinding = repository ? resolveHostBinding(repository, config) : null;
|
|
744
|
+
const token = branchBinding?.token || getGitHubToken(config);
|
|
745
|
+
const detection = await baseBranchModule.detectBaseBranch(repoPath, branch, {
|
|
746
|
+
repository,
|
|
747
|
+
enableGraphite: config?.enable_graphite === true,
|
|
748
|
+
_deps: (token || branchBinding) ? {
|
|
749
|
+
getGitHubToken: () => token || '',
|
|
750
|
+
getHostBinding: () => branchBinding || null
|
|
751
|
+
} : undefined
|
|
752
|
+
});
|
|
753
|
+
if (!detection) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Could not detect a base branch for scope '${scopeStart}..${scopeEnd}'. ` +
|
|
756
|
+
'Pass --base <branch> to specify one explicitly.'
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
baseBranch = detection.baseBranch;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// A non-branch scope has no base branch. Drop any value seeded from a
|
|
764
|
+
// previously branch-scoped session so the stale base never reaches the diff,
|
|
765
|
+
// the persisted row, or the returned session — matching the web set-scope
|
|
766
|
+
// route, which persists null when leaving branch scope.
|
|
767
|
+
if (!includesBranch(scopeStart)) {
|
|
768
|
+
baseBranch = null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return { scopeStart, scopeEnd, baseBranch, scopeOverridden: !!scopeOverride };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Persist an explicitly-overridden scope (and its resolved base) and auto-name
|
|
776
|
+
* the review when a branch scope is newly applied to an unnamed review. Call
|
|
777
|
+
* ONLY after generateScopedDiff succeeds, so a scope whose diff fails (e.g. a
|
|
778
|
+
* bad --base) never sticks. Shared by the CLI and web setup seams. Caller gates
|
|
779
|
+
* this on `scopeOverridden` — it always writes when invoked.
|
|
780
|
+
*
|
|
781
|
+
* @param {Object} params
|
|
782
|
+
* @param {Object} params.reviewRepo - ReviewRepository instance
|
|
783
|
+
* @param {number} params.sessionId
|
|
784
|
+
* @param {Object|null} params.existingReview
|
|
785
|
+
* @param {string} params.scopeStart
|
|
786
|
+
* @param {string} params.scopeEnd
|
|
787
|
+
* @param {string|null} params.baseBranch
|
|
788
|
+
* @param {string} params.branch - Current branch (stored as head branch for branch scopes)
|
|
789
|
+
* @param {string} params.repoPath
|
|
790
|
+
*/
|
|
791
|
+
async function persistScopeSelection({ reviewRepo, sessionId, existingReview, scopeStart, scopeEnd, baseBranch, branch, repoPath }) {
|
|
792
|
+
await reviewRepo.updateLocalScope(sessionId, scopeStart, scopeEnd, baseBranch, branch);
|
|
793
|
+
|
|
794
|
+
// Keep CLI and web set-scope in sync: when a branch scope is newly applied to
|
|
795
|
+
// an as-yet-unnamed review, auto-name it from the first commit subject.
|
|
796
|
+
// Mirrors routes/local.js set-scope. A fresh session has no existing review,
|
|
797
|
+
// which counts as both unnamed and previously non-branch.
|
|
798
|
+
const oldScopeStart = existingReview ? reviewScope(existingReview).start : DEFAULT_SCOPE.start;
|
|
799
|
+
if (!existingReview?.name && includesBranch(scopeStart) && !includesBranch(oldScopeStart) && baseBranch) {
|
|
800
|
+
const firstSubject = await module.exports.getFirstCommitSubject(repoPath, baseBranch);
|
|
801
|
+
if (firstSubject) {
|
|
802
|
+
await reviewRepo.updateReview(sessionId, { name: firstSubject.slice(0, 200) });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
697
807
|
/**
|
|
698
808
|
* Set up a local review session: resolve git state, persist the diff,
|
|
699
809
|
* and enqueue the background summary job. Caller is responsible for
|
|
@@ -777,7 +887,11 @@ async function setupLocalReviewSession({ db, config, repoPath, flags = {}, start
|
|
|
777
887
|
console.log(`Created new review session (ID: ${sessionId})`);
|
|
778
888
|
}
|
|
779
889
|
|
|
780
|
-
|
|
890
|
+
// Resolve scope + base (explicit --scope/--base override > persisted > default)
|
|
891
|
+
// via the shared helper so the CLI and delegated web-setup seam stay identical.
|
|
892
|
+
const { scopeStart, scopeEnd, baseBranch, scopeOverridden } = await resolveScopeAndBase({
|
|
893
|
+
existingReview, flags, repoPath, branch, repository, config
|
|
894
|
+
});
|
|
781
895
|
|
|
782
896
|
const hookEvent = existingReview ? 'review.loaded' : 'review.started';
|
|
783
897
|
if (hasHooks(hookEvent, config)) {
|
|
@@ -790,11 +904,20 @@ async function setupLocalReviewSession({ db, config, repoPath, flags = {}, start
|
|
|
790
904
|
fireHooks(hookEvent, payload, config);
|
|
791
905
|
}).catch(err => { logger.warn(`Review hook failed: ${err.message}`); });
|
|
792
906
|
}
|
|
793
|
-
const baseBranch = existingReview?.local_base_branch || null;
|
|
794
907
|
|
|
795
908
|
console.log(`Generating diff for scope: ${scopeLabel(scopeStart, scopeEnd)}...`);
|
|
796
909
|
const { diff, stats } = await module.exports.generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch);
|
|
797
910
|
|
|
911
|
+
// Persist an explicitly-requested scope (and its resolved base) so the web UI
|
|
912
|
+
// later opens with the same scope. Only after generateScopedDiff succeeds, so a
|
|
913
|
+
// scope whose diff fails (e.g. a bad --base) never sticks. Never touch persisted
|
|
914
|
+
// scope when no override was supplied.
|
|
915
|
+
if (scopeOverridden) {
|
|
916
|
+
await persistScopeSelection({
|
|
917
|
+
reviewRepo, sessionId, existingReview, scopeStart, scopeEnd, baseBranch, branch, repoPath
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
798
921
|
let branchInfo = null;
|
|
799
922
|
if (!includesBranch(scopeStart)) {
|
|
800
923
|
const untrackedFiles = await getUntrackedFiles(repoPath);
|
|
@@ -932,6 +1055,11 @@ async function handleLocalReview(targetPath, flags = {}) {
|
|
|
932
1055
|
if (flags.model && !flags.council) {
|
|
933
1056
|
process.env.PAIR_REVIEW_MODEL = flags.model;
|
|
934
1057
|
}
|
|
1058
|
+
// Mirror --provider too so the local web/UI routes (which read
|
|
1059
|
+
// PAIR_REVIEW_PROVIDER via getProvider(req)) honor it, matching --model above.
|
|
1060
|
+
if (flags.provider) {
|
|
1061
|
+
process.env.PAIR_REVIEW_PROVIDER = flags.provider;
|
|
1062
|
+
}
|
|
935
1063
|
|
|
936
1064
|
console.log('Starting server...');
|
|
937
1065
|
const port = await startServer(db);
|
|
@@ -1085,14 +1213,13 @@ async function detectAndBuildBranchInfo(repoPath, branch, options = {}) {
|
|
|
1085
1213
|
if (untrackedFiles && untrackedFiles.length > 0) return null;
|
|
1086
1214
|
|
|
1087
1215
|
try {
|
|
1088
|
-
const { detectBaseBranch } = require('./git/base-branch');
|
|
1089
1216
|
const depsOverride = githubToken || hostBinding
|
|
1090
1217
|
? {
|
|
1091
1218
|
getGitHubToken: () => githubToken || '',
|
|
1092
1219
|
getHostBinding: () => hostBinding || null
|
|
1093
1220
|
}
|
|
1094
1221
|
: undefined;
|
|
1095
|
-
const detection = await detectBaseBranch(repoPath, branch, {
|
|
1222
|
+
const detection = await baseBranchModule.detectBaseBranch(repoPath, branch, {
|
|
1096
1223
|
repository,
|
|
1097
1224
|
enableGraphite,
|
|
1098
1225
|
_deps: depsOverride
|
|
@@ -1117,6 +1244,8 @@ async function detectAndBuildBranchInfo(repoPath, branch, options = {}) {
|
|
|
1117
1244
|
module.exports = {
|
|
1118
1245
|
handleLocalReview,
|
|
1119
1246
|
setupLocalReviewSession,
|
|
1247
|
+
resolveScopeAndBase,
|
|
1248
|
+
persistScopeSelection,
|
|
1120
1249
|
findGitRoot,
|
|
1121
1250
|
findMainGitRoot,
|
|
1122
1251
|
getHeadSha,
|
package/src/local-scope.js
CHANGED
|
@@ -15,6 +15,42 @@ function isValidScope(start, end) {
|
|
|
15
15
|
return si !== -1 && ei !== -1 && si <= ei && si <= UNSTAGED_INDEX && ei >= UNSTAGED_INDEX;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The full set of valid scope ranges, formatted as `start..end` strings.
|
|
20
|
+
* Computed from STOPS + isValidScope so it stays the single source of truth.
|
|
21
|
+
* Ordered by STOPS position (branch-first).
|
|
22
|
+
* @type {string[]}
|
|
23
|
+
*/
|
|
24
|
+
const VALID_SCOPE_RANGES = (() => {
|
|
25
|
+
const ranges = [];
|
|
26
|
+
for (const start of STOPS) {
|
|
27
|
+
for (const end of STOPS) {
|
|
28
|
+
if (isValidScope(start, end)) ranges.push(`${start}..${end}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return ranges;
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a `--scope` CLI argument of the form `<start>..<end>` into a scope
|
|
36
|
+
* object. Splits on `..`, trims each side, and delegates validation to
|
|
37
|
+
* isValidScope (the single source of truth). Single tokens, missing `..`,
|
|
38
|
+
* unknown stops, non-contiguous ranges, ranges excluding 'unstaged', and
|
|
39
|
+
* reversed ranges all return null.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} value - Raw CLI value (e.g. 'branch..untracked')
|
|
42
|
+
* @returns {{ start: string, end: string }|null} Parsed scope, or null if invalid
|
|
43
|
+
*/
|
|
44
|
+
function parseScopeArg(value) {
|
|
45
|
+
if (typeof value !== 'string') return null;
|
|
46
|
+
const parts = value.split('..');
|
|
47
|
+
if (parts.length !== 2) return null;
|
|
48
|
+
const start = parts[0].trim();
|
|
49
|
+
const end = parts[1].trim();
|
|
50
|
+
if (!isValidScope(start, end)) return null;
|
|
51
|
+
return { start, end };
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
function normalizeScope(start, end) {
|
|
19
55
|
if (isValidScope(start, end)) return { start, end };
|
|
20
56
|
const si = STOPS.indexOf(start);
|
|
@@ -130,7 +166,9 @@ function scopeGitHints(start, end, baseBranch) {
|
|
|
130
166
|
const LocalScope = {
|
|
131
167
|
STOPS,
|
|
132
168
|
DEFAULT_SCOPE,
|
|
169
|
+
VALID_SCOPE_RANGES,
|
|
133
170
|
isValidScope,
|
|
171
|
+
parseScopeArg,
|
|
134
172
|
normalizeScope,
|
|
135
173
|
reviewScope,
|
|
136
174
|
scopeIncludes,
|