@in-the-loop-labs/pair-review 3.3.0 → 3.3.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@in-the-loop-labs/pair-review",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test:e2e:debug": "playwright test --debug",
|
|
33
33
|
"generate:skill-prompts": "node scripts/generate-skill-prompts.js",
|
|
34
34
|
"changeset": "changeset",
|
|
35
|
-
"version": "changeset version && pnpm install --lockfile-only && node scripts/sync-plugin-versions.js && git add package.json pnpm-lock.yaml CHANGELOG.md .changeset .claude-plugin/marketplace.json plugin/.claude-plugin/plugin.json plugin-code-critic/.claude-plugin/plugin.json && git commit -m \"RELEASING: v$(node -p \"require('./package.json').version\")\"",
|
|
35
|
+
"version": "changeset version && pnpm install --lockfile-only && bash scripts/generate-package-lock.sh && node scripts/sync-plugin-versions.js && git add package.json pnpm-lock.yaml package-lock.json CHANGELOG.md .changeset .claude-plugin/marketplace.json plugin/.claude-plugin/plugin.json plugin-code-critic/.claude-plugin/plugin.json && git commit -m \"RELEASING: v$(node -p \"require('./package.json').version\")\"",
|
|
36
36
|
"release": "npm whoami > /dev/null || { echo 'Error: Not logged in to npm. Run: npm login'; exit 1; } && pnpm run version && changeset tag && npm publish && git push && git push --tags"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
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.
|
|
3
|
+
"version": "3.3.2",
|
|
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",
|
|
@@ -231,15 +231,22 @@ class WorktreePoolLifecycle {
|
|
|
231
231
|
// Note: poolEntry was already atomically marked 'switching' by claimAvailable()
|
|
232
232
|
try {
|
|
233
233
|
const git = this._simpleGit(poolEntry.path);
|
|
234
|
+
const worktreeManager = new this._GitWorktreeManager(this.db);
|
|
234
235
|
|
|
235
236
|
// Resolve the remote
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
const remoteName = await worktreeManager.resolveRemoteForPR(git, prData, {
|
|
238
|
+
owner: prInfo.owner,
|
|
239
|
+
repo: prInfo.repo,
|
|
240
|
+
number: prInfo.prNumber,
|
|
241
|
+
});
|
|
239
242
|
|
|
240
|
-
// Fetch new PR refs (incremental -- cheap on a warm worktree)
|
|
243
|
+
// Fetch new PR refs (incremental -- cheap on a warm worktree) with fallback
|
|
241
244
|
logger.info(`Fetching PR #${prInfo.prNumber} refs into pool worktree ${poolEntry.id}`);
|
|
242
|
-
await
|
|
245
|
+
const fetchedHead = await worktreeManager.fetchPRHead(git, {
|
|
246
|
+
owner: prInfo.owner,
|
|
247
|
+
repo: prInfo.repo,
|
|
248
|
+
number: prInfo.prNumber,
|
|
249
|
+
}, prData, { remote: remoteName });
|
|
243
250
|
|
|
244
251
|
// Clean the working tree before switching PRs. Without this, untracked
|
|
245
252
|
// files (build artifacts, generated code) from the previous PR leak into
|
|
@@ -254,7 +261,7 @@ class WorktreePoolLifecycle {
|
|
|
254
261
|
if (targetSha) {
|
|
255
262
|
await git.checkout([targetSha]);
|
|
256
263
|
} else {
|
|
257
|
-
await git.checkout([
|
|
264
|
+
await git.checkout([fetchedHead.checkoutTarget]);
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
// Run reset_script if configured
|
|
@@ -273,7 +280,6 @@ class WorktreePoolLifecycle {
|
|
|
273
280
|
PR_NUMBER: String(prInfo.prNumber),
|
|
274
281
|
WORKTREE_PATH: poolEntry.path,
|
|
275
282
|
};
|
|
276
|
-
const worktreeManager = new this._GitWorktreeManager();
|
|
277
283
|
await worktreeManager.executeCheckoutScript(
|
|
278
284
|
options.resetScript, poolEntry.path, scriptEnv, options.checkoutTimeout
|
|
279
285
|
);
|
|
@@ -285,7 +291,6 @@ class WorktreePoolLifecycle {
|
|
|
285
291
|
|
|
286
292
|
// Best-effort disk cleanup for deleted non-pool worktree directories
|
|
287
293
|
if (deletedPaths && deletedPaths.length > 0) {
|
|
288
|
-
const worktreeManager = new this._GitWorktreeManager();
|
|
289
294
|
for (const deletedPath of deletedPaths) {
|
|
290
295
|
try {
|
|
291
296
|
await worktreeManager.cleanupWorktree(deletedPath);
|
package/src/git/worktree.js
CHANGED
|
@@ -66,8 +66,9 @@ class GitWorktreeManager {
|
|
|
66
66
|
/**
|
|
67
67
|
* Resolve which git remote points to the given repository URLs.
|
|
68
68
|
* Compares normalized URLs against all configured remotes. If no match is
|
|
69
|
-
* found,
|
|
70
|
-
*
|
|
69
|
+
* found, falls back to an existing non-managed remote instead of mutating the
|
|
70
|
+
* repository's git config. This preserves proxy/mirror setups where the
|
|
71
|
+
* canonical fetch URL may differ from GitHub's clone URL.
|
|
71
72
|
*
|
|
72
73
|
* @param {Object} git - simple-git instance
|
|
73
74
|
* @param {string} cloneUrl - HTTPS clone URL of the target repository
|
|
@@ -81,9 +82,7 @@ class GitWorktreeManager {
|
|
|
81
82
|
const remoteOutput = await git.raw(['remote', '-v']);
|
|
82
83
|
|
|
83
84
|
if (!remoteOutput || !remoteOutput.trim()) {
|
|
84
|
-
|
|
85
|
-
await git.addRemote(MANAGED_REMOTE, cloneUrl);
|
|
86
|
-
return MANAGED_REMOTE;
|
|
85
|
+
throw new Error(`No remotes configured — cannot resolve base repository for ${cloneUrl}`);
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
// Parse remote output into { name: url } map (fetch URLs only)
|
|
@@ -115,9 +114,16 @@ class GitWorktreeManager {
|
|
|
115
114
|
|
|
116
115
|
const normalizedCloneUrl = normalizeUrl(cloneUrl);
|
|
117
116
|
const normalizedSshUrl = sshUrl ? normalizeUrl(sshUrl) : '';
|
|
117
|
+
const remoteNames = Object.keys(remotes);
|
|
118
|
+
const fallbackRemote = remotes.origin
|
|
119
|
+
? 'origin'
|
|
120
|
+
: remoteNames.find((name) => name !== MANAGED_REMOTE) || 'origin';
|
|
118
121
|
|
|
119
|
-
// Check each remote for a match
|
|
122
|
+
// Check each non-managed remote for a direct URL match
|
|
120
123
|
for (const [name, url] of Object.entries(remotes)) {
|
|
124
|
+
if (name === MANAGED_REMOTE) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
121
127
|
const normalizedRemoteUrl = normalizeUrl(url);
|
|
122
128
|
if (normalizedRemoteUrl === normalizedCloneUrl ||
|
|
123
129
|
(normalizedSshUrl && normalizedRemoteUrl === normalizedSshUrl)) {
|
|
@@ -126,16 +132,13 @@ class GitWorktreeManager {
|
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return MANAGED_REMOTE;
|
|
135
|
+
console.warn(
|
|
136
|
+
`No configured remote matched ${cloneUrl}; using existing remote '${fallbackRemote}' without modifying git config`
|
|
137
|
+
);
|
|
138
|
+
// NOTE: For fork PRs this may return a remote that does not point to the
|
|
139
|
+
// base repository. Callers rely on fetchPRHead's SHA-fallback path and
|
|
140
|
+
// tolerant base-branch fetching to handle this without mutating git config.
|
|
141
|
+
return fallbackRemote;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
/**
|
|
@@ -170,6 +173,97 @@ class GitWorktreeManager {
|
|
|
170
173
|
return this.resolveRemoteForRepo(git, cloneUrl, sshUrl);
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Extract a PR number from either { number } or { prNumber } shapes.
|
|
178
|
+
* @param {Object|null} prInfo
|
|
179
|
+
* @returns {number|null}
|
|
180
|
+
*/
|
|
181
|
+
getPRNumber(prInfo) {
|
|
182
|
+
if (!prInfo) return null;
|
|
183
|
+
return prInfo.number || prInfo.prNumber || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract the PR head branch name from either REST or stored PR metadata.
|
|
188
|
+
* @param {Object|null} prData
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
getPRHeadBranch(prData) {
|
|
192
|
+
return prData?.head?.ref || prData?.head_branch || '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract the PR head SHA from either REST or stored PR metadata.
|
|
197
|
+
* @param {Object|null} prData
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
getPRHeadSha(prData) {
|
|
201
|
+
return prData?.head?.sha || prData?.head_sha || '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Detect whether a fetch failed because the remote does not expose a PR ref.
|
|
206
|
+
* @param {Error} error
|
|
207
|
+
* @returns {boolean}
|
|
208
|
+
*/
|
|
209
|
+
isMissingRemoteRefError(error) {
|
|
210
|
+
const message = String(error?.message || '').toLowerCase();
|
|
211
|
+
return message.includes('couldn\'t find remote ref') ||
|
|
212
|
+
message.includes('could not find remote ref') ||
|
|
213
|
+
message.includes('remote ref does not exist') ||
|
|
214
|
+
message.includes('fatal: invalid refspec');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Fetch a PR head into a stable tracking ref, falling back from GitHub PR
|
|
219
|
+
* refs to a direct SHA fetch when the git transport does not expose
|
|
220
|
+
* refs/pull/* (for example, alternate internal fetch backends).
|
|
221
|
+
*
|
|
222
|
+
* @param {Object} git - simple-git instance
|
|
223
|
+
* @param {Object} prInfo - PR info { owner, repo, number } or { prNumber }
|
|
224
|
+
* @param {Object} prData - PR data from GitHub API or stored metadata
|
|
225
|
+
* @param {Object} [options={}]
|
|
226
|
+
* @param {string|null} [options.remote] - Base repository remote to use for PR-ref fetch
|
|
227
|
+
* @returns {Promise<{remote: string, trackingRef: string|null, checkoutTarget: string}>}
|
|
228
|
+
*/
|
|
229
|
+
async fetchPRHead(git, prInfo, prData, options = {}) {
|
|
230
|
+
const prNumber = this.getPRNumber(prInfo);
|
|
231
|
+
const headSha = this.getPRHeadSha(prData);
|
|
232
|
+
const baseRemote = options.remote || await this.resolveRemoteForPR(git, prData, prInfo);
|
|
233
|
+
|
|
234
|
+
if (!prNumber) {
|
|
235
|
+
throw new Error('Cannot fetch PR head without a PR number');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const prTrackingRef = `refs/remotes/${baseRemote}/pr-${prNumber}`;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await git.fetch([baseRemote, `+refs/pull/${prNumber}/head:${prTrackingRef}`]);
|
|
242
|
+
return {
|
|
243
|
+
remote: baseRemote,
|
|
244
|
+
trackingRef: prTrackingRef,
|
|
245
|
+
checkoutTarget: prTrackingRef
|
|
246
|
+
};
|
|
247
|
+
} catch (prRefError) {
|
|
248
|
+
if (!this.isMissingRemoteRefError(prRefError)) {
|
|
249
|
+
throw prRefError;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.warn(`PR ref fetch unavailable for PR #${prNumber} on remote ${baseRemote}, falling back to SHA fetch: ${prRefError.message}`);
|
|
253
|
+
|
|
254
|
+
if (!headSha) {
|
|
255
|
+
throw prRefError;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await git.raw(['fetch', baseRemote, headSha]);
|
|
259
|
+
return {
|
|
260
|
+
remote: baseRemote,
|
|
261
|
+
trackingRef: null,
|
|
262
|
+
checkoutTarget: headSha
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
173
267
|
/**
|
|
174
268
|
* Execute a user-provided checkout script in the worktree.
|
|
175
269
|
* The script receives PR context as environment variables and is responsible
|
|
@@ -427,20 +521,21 @@ class GitWorktreeManager {
|
|
|
427
521
|
console.log(`Base SHA fetch not needed or already available: ${fetchError.message}`);
|
|
428
522
|
}
|
|
429
523
|
|
|
430
|
-
// Fetch the PR head using
|
|
524
|
+
// Fetch the PR head using PR refs when available, with a branch/SHA fallback
|
|
431
525
|
console.log(`Fetching PR #${prInfo.number} head...`);
|
|
432
|
-
await
|
|
526
|
+
const fetchedHead = await this.fetchPRHead(worktreeGit, prInfo, prData, { remote });
|
|
433
527
|
|
|
434
528
|
// Execute checkout script if configured (before checkout so sparse-checkout is set up)
|
|
435
529
|
if (checkoutScript) {
|
|
436
530
|
// Fetch the actual head branch by name (for checkout scripts that expect branch refs)
|
|
437
531
|
// This may fail for fork PRs where the branch is in a different repo - that's okay
|
|
438
|
-
|
|
532
|
+
const headBranch = this.getPRHeadBranch(prData);
|
|
533
|
+
if (headBranch) {
|
|
439
534
|
try {
|
|
440
|
-
console.log(`Fetching head branch ${
|
|
441
|
-
await worktreeGit.fetch([remote, `+refs/heads/${
|
|
535
|
+
console.log(`Fetching head branch ${headBranch}...`);
|
|
536
|
+
await worktreeGit.fetch([remote, `+refs/heads/${headBranch}:refs/remotes/${remote}/${headBranch}`]);
|
|
442
537
|
// Create/update a local branch pointing to the fetched ref so tooling can reference it by name
|
|
443
|
-
await worktreeGit.branch(['-f',
|
|
538
|
+
await worktreeGit.branch(['-f', headBranch, `${remote}/${headBranch}`]);
|
|
444
539
|
} catch (branchFetchError) {
|
|
445
540
|
// Expected for fork PRs - the branch exists in the fork, not the base repo
|
|
446
541
|
console.log(`Could not fetch head branch (may be from a fork): ${branchFetchError.message}`);
|
|
@@ -450,9 +545,9 @@ class GitWorktreeManager {
|
|
|
450
545
|
console.log(`Executing checkout script: ${checkoutScript}`);
|
|
451
546
|
const scriptEnv = {
|
|
452
547
|
BASE_BRANCH: prData.base_branch,
|
|
453
|
-
HEAD_BRANCH:
|
|
548
|
+
HEAD_BRANCH: headBranch,
|
|
454
549
|
BASE_SHA: prData.base_sha,
|
|
455
|
-
HEAD_SHA: prData
|
|
550
|
+
HEAD_SHA: this.getPRHeadSha(prData),
|
|
456
551
|
PR_NUMBER: String(prInfo.number),
|
|
457
552
|
WORKTREE_PATH: worktreePath
|
|
458
553
|
};
|
|
@@ -461,13 +556,13 @@ class GitWorktreeManager {
|
|
|
461
556
|
}
|
|
462
557
|
|
|
463
558
|
// Checkout to PR head commit
|
|
464
|
-
const targetSha = prData
|
|
559
|
+
const targetSha = this.getPRHeadSha(prData);
|
|
465
560
|
if (targetSha) {
|
|
466
561
|
console.log(`Checking out to PR head commit ${targetSha}...`);
|
|
467
562
|
await worktreeGit.checkout([targetSha]);
|
|
468
563
|
} else {
|
|
469
|
-
console.log(`Checking out to PR head ref ${
|
|
470
|
-
await worktreeGit.checkout([
|
|
564
|
+
console.log(`Checking out to PR head ref ${fetchedHead.checkoutTarget}...`);
|
|
565
|
+
await worktreeGit.checkout([fetchedHead.checkoutTarget]);
|
|
471
566
|
}
|
|
472
567
|
|
|
473
568
|
// Verify we're at the correct commit
|
|
@@ -540,13 +635,13 @@ class GitWorktreeManager {
|
|
|
540
635
|
console.log(`Fetching latest changes from ${remote}...`);
|
|
541
636
|
await worktreeGit.fetch([remote, '--prune']);
|
|
542
637
|
|
|
543
|
-
// Fetch the PR head using
|
|
638
|
+
// Fetch the PR head using PR refs when available, with a branch/SHA fallback
|
|
544
639
|
console.log(`Fetching PR #${number} head...`);
|
|
545
|
-
await
|
|
640
|
+
const fetchedHead = await this.fetchPRHead(worktreeGit, prInfo, prData, { remote });
|
|
546
641
|
|
|
547
642
|
// Checkout to PR head commit
|
|
548
643
|
console.log(`Checking out to PR head commit ${headSha}...`);
|
|
549
|
-
await worktreeGit.checkout([
|
|
644
|
+
await worktreeGit.checkout([fetchedHead.checkoutTarget]);
|
|
550
645
|
|
|
551
646
|
// Verify we're at the correct commit
|
|
552
647
|
const currentCommit = await worktreeGit.revparse(['HEAD']);
|
|
@@ -919,11 +1014,11 @@ class GitWorktreeManager {
|
|
|
919
1014
|
|
|
920
1015
|
// Fetch the latest PR head from remote
|
|
921
1016
|
console.log(`Fetching PR #${prNumber} head from ${remote}...`);
|
|
922
|
-
await
|
|
1017
|
+
const fetchedHead = await this.fetchPRHead(git, prInfo || { number: prNumber }, prData, { remote });
|
|
923
1018
|
|
|
924
1019
|
// Reset to the fetched PR head
|
|
925
1020
|
console.log(`Resetting worktree to PR head...`);
|
|
926
|
-
await git.raw(['reset', '--hard',
|
|
1021
|
+
await git.raw(['reset', '--hard', fetchedHead.checkoutTarget]);
|
|
927
1022
|
|
|
928
1023
|
// Update last_accessed_at in database
|
|
929
1024
|
if (this.worktreeRepo) {
|
|
@@ -977,13 +1072,13 @@ class GitWorktreeManager {
|
|
|
977
1072
|
? await this.resolveRemoteForPR(git, prData, prInfo)
|
|
978
1073
|
: defaultRemote;
|
|
979
1074
|
|
|
980
|
-
// 3. Fetch PR head into a persistent ref
|
|
981
|
-
console.log(`Fetching PR #${prNumber} head from ${remote}
|
|
982
|
-
await
|
|
1075
|
+
// 3. Fetch PR head into a persistent ref (or by SHA when refs are unavailable)
|
|
1076
|
+
console.log(`Fetching PR #${prNumber} head from ${remote}...`);
|
|
1077
|
+
const fetchedHead = await this.fetchPRHead(git, prInfo || { number: prNumber }, prData, { remote });
|
|
983
1078
|
|
|
984
1079
|
// 4. Reset worktree to the fetched ref
|
|
985
|
-
console.log(`Resetting worktree to
|
|
986
|
-
await git.raw(['reset', '--hard',
|
|
1080
|
+
console.log(`Resetting worktree to ${fetchedHead.checkoutTarget}...`);
|
|
1081
|
+
await git.raw(['reset', '--hard', fetchedHead.checkoutTarget]);
|
|
987
1082
|
|
|
988
1083
|
// 5. Return the new HEAD SHA
|
|
989
1084
|
const headSha = (await git.revparse(['HEAD'])).trim();
|
|
@@ -1219,4 +1314,4 @@ class GitWorktreeManager {
|
|
|
1219
1314
|
}
|
|
1220
1315
|
}
|
|
1221
1316
|
|
|
1222
|
-
module.exports = { GitWorktreeManager };
|
|
1317
|
+
module.exports = { GitWorktreeManager };
|