@in-the-loop-labs/pair-review 3.3.0 → 3.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
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.0",
3
+ "version": "3.3.1",
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.0",
3
+ "version": "3.3.1",
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 remotes = await git.getRemotes();
237
- const remote = remotes.find(r => r.name === 'origin') || remotes[0];
238
- const remoteName = remote ? remote.name : 'origin';
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 git.fetch([remoteName, `+refs/pull/${prInfo.prNumber}/head:refs/remotes/${remoteName}/pr-${prInfo.prNumber}`]);
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([`refs/remotes/${remoteName}/pr-${prInfo.prNumber}`]);
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);
@@ -170,6 +170,97 @@ class GitWorktreeManager {
170
170
  return this.resolveRemoteForRepo(git, cloneUrl, sshUrl);
171
171
  }
172
172
 
173
+ /**
174
+ * Extract a PR number from either { number } or { prNumber } shapes.
175
+ * @param {Object|null} prInfo
176
+ * @returns {number|null}
177
+ */
178
+ getPRNumber(prInfo) {
179
+ if (!prInfo) return null;
180
+ return prInfo.number || prInfo.prNumber || null;
181
+ }
182
+
183
+ /**
184
+ * Extract the PR head branch name from either REST or stored PR metadata.
185
+ * @param {Object|null} prData
186
+ * @returns {string}
187
+ */
188
+ getPRHeadBranch(prData) {
189
+ return prData?.head?.ref || prData?.head_branch || '';
190
+ }
191
+
192
+ /**
193
+ * Extract the PR head SHA from either REST or stored PR metadata.
194
+ * @param {Object|null} prData
195
+ * @returns {string}
196
+ */
197
+ getPRHeadSha(prData) {
198
+ return prData?.head?.sha || prData?.head_sha || '';
199
+ }
200
+
201
+ /**
202
+ * Detect whether a fetch failed because the remote does not expose a PR ref.
203
+ * @param {Error} error
204
+ * @returns {boolean}
205
+ */
206
+ isMissingRemoteRefError(error) {
207
+ const message = String(error?.message || '').toLowerCase();
208
+ return message.includes('couldn\'t find remote ref') ||
209
+ message.includes('could not find remote ref') ||
210
+ message.includes('remote ref does not exist') ||
211
+ message.includes('fatal: invalid refspec');
212
+ }
213
+
214
+ /**
215
+ * Fetch a PR head into a stable tracking ref, falling back from GitHub PR
216
+ * refs to a direct SHA fetch when the git transport does not expose
217
+ * refs/pull/* (for example, alternate internal fetch backends).
218
+ *
219
+ * @param {Object} git - simple-git instance
220
+ * @param {Object} prInfo - PR info { owner, repo, number } or { prNumber }
221
+ * @param {Object} prData - PR data from GitHub API or stored metadata
222
+ * @param {Object} [options={}]
223
+ * @param {string|null} [options.remote] - Base repository remote to use for PR-ref fetch
224
+ * @returns {Promise<{remote: string, trackingRef: string|null, checkoutTarget: string}>}
225
+ */
226
+ async fetchPRHead(git, prInfo, prData, options = {}) {
227
+ const prNumber = this.getPRNumber(prInfo);
228
+ const headSha = this.getPRHeadSha(prData);
229
+ const baseRemote = options.remote || await this.resolveRemoteForPR(git, prData, prInfo);
230
+
231
+ if (!prNumber) {
232
+ throw new Error('Cannot fetch PR head without a PR number');
233
+ }
234
+
235
+ const prTrackingRef = `refs/remotes/${baseRemote}/pr-${prNumber}`;
236
+
237
+ try {
238
+ await git.fetch([baseRemote, `+refs/pull/${prNumber}/head:${prTrackingRef}`]);
239
+ return {
240
+ remote: baseRemote,
241
+ trackingRef: prTrackingRef,
242
+ checkoutTarget: prTrackingRef
243
+ };
244
+ } catch (prRefError) {
245
+ if (!this.isMissingRemoteRefError(prRefError)) {
246
+ throw prRefError;
247
+ }
248
+
249
+ console.warn(`PR ref fetch unavailable for PR #${prNumber} on remote ${baseRemote}, falling back to SHA fetch: ${prRefError.message}`);
250
+
251
+ if (!headSha) {
252
+ throw prRefError;
253
+ }
254
+
255
+ await git.raw(['fetch', baseRemote, headSha]);
256
+ return {
257
+ remote: baseRemote,
258
+ trackingRef: null,
259
+ checkoutTarget: headSha
260
+ };
261
+ }
262
+ }
263
+
173
264
  /**
174
265
  * Execute a user-provided checkout script in the worktree.
175
266
  * The script receives PR context as environment variables and is responsible
@@ -427,20 +518,21 @@ class GitWorktreeManager {
427
518
  console.log(`Base SHA fetch not needed or already available: ${fetchError.message}`);
428
519
  }
429
520
 
430
- // Fetch the PR head using GitHub's pull request refs (more reliable than branch names)
521
+ // Fetch the PR head using PR refs when available, with a branch/SHA fallback
431
522
  console.log(`Fetching PR #${prInfo.number} head...`);
432
- await worktreeGit.fetch([remote, `+refs/pull/${prInfo.number}/head:refs/remotes/${remote}/pr-${prInfo.number}`]);
523
+ const fetchedHead = await this.fetchPRHead(worktreeGit, prInfo, prData, { remote });
433
524
 
434
525
  // Execute checkout script if configured (before checkout so sparse-checkout is set up)
435
526
  if (checkoutScript) {
436
527
  // Fetch the actual head branch by name (for checkout scripts that expect branch refs)
437
528
  // This may fail for fork PRs where the branch is in a different repo - that's okay
438
- if (prData.head_branch) {
529
+ const headBranch = this.getPRHeadBranch(prData);
530
+ if (headBranch) {
439
531
  try {
440
- console.log(`Fetching head branch ${prData.head_branch}...`);
441
- await worktreeGit.fetch([remote, `+refs/heads/${prData.head_branch}:refs/remotes/${remote}/${prData.head_branch}`]);
532
+ console.log(`Fetching head branch ${headBranch}...`);
533
+ await worktreeGit.fetch([remote, `+refs/heads/${headBranch}:refs/remotes/${remote}/${headBranch}`]);
442
534
  // Create/update a local branch pointing to the fetched ref so tooling can reference it by name
443
- await worktreeGit.branch(['-f', prData.head_branch, `${remote}/${prData.head_branch}`]);
535
+ await worktreeGit.branch(['-f', headBranch, `${remote}/${headBranch}`]);
444
536
  } catch (branchFetchError) {
445
537
  // Expected for fork PRs - the branch exists in the fork, not the base repo
446
538
  console.log(`Could not fetch head branch (may be from a fork): ${branchFetchError.message}`);
@@ -450,9 +542,9 @@ class GitWorktreeManager {
450
542
  console.log(`Executing checkout script: ${checkoutScript}`);
451
543
  const scriptEnv = {
452
544
  BASE_BRANCH: prData.base_branch,
453
- HEAD_BRANCH: prData.head_branch,
545
+ HEAD_BRANCH: headBranch,
454
546
  BASE_SHA: prData.base_sha,
455
- HEAD_SHA: prData.head_sha,
547
+ HEAD_SHA: this.getPRHeadSha(prData),
456
548
  PR_NUMBER: String(prInfo.number),
457
549
  WORKTREE_PATH: worktreePath
458
550
  };
@@ -461,13 +553,13 @@ class GitWorktreeManager {
461
553
  }
462
554
 
463
555
  // Checkout to PR head commit
464
- const targetSha = prData.head_sha;
556
+ const targetSha = this.getPRHeadSha(prData);
465
557
  if (targetSha) {
466
558
  console.log(`Checking out to PR head commit ${targetSha}...`);
467
559
  await worktreeGit.checkout([targetSha]);
468
560
  } else {
469
- console.log(`Checking out to PR head ref ${remote}/pr-${prInfo.number}...`);
470
- await worktreeGit.checkout([`${remote}/pr-${prInfo.number}`]);
561
+ console.log(`Checking out to PR head ref ${fetchedHead.checkoutTarget}...`);
562
+ await worktreeGit.checkout([fetchedHead.checkoutTarget]);
471
563
  }
472
564
 
473
565
  // Verify we're at the correct commit
@@ -540,13 +632,13 @@ class GitWorktreeManager {
540
632
  console.log(`Fetching latest changes from ${remote}...`);
541
633
  await worktreeGit.fetch([remote, '--prune']);
542
634
 
543
- // Fetch the PR head using GitHub's pull request refs
635
+ // Fetch the PR head using PR refs when available, with a branch/SHA fallback
544
636
  console.log(`Fetching PR #${number} head...`);
545
- await worktreeGit.fetch([remote, `+refs/pull/${number}/head:refs/remotes/${remote}/pr-${number}`]);
637
+ const fetchedHead = await this.fetchPRHead(worktreeGit, prInfo, prData, { remote });
546
638
 
547
639
  // Checkout to PR head commit
548
640
  console.log(`Checking out to PR head commit ${headSha}...`);
549
- await worktreeGit.checkout([`${remote}/pr-${number}`]);
641
+ await worktreeGit.checkout([fetchedHead.checkoutTarget]);
550
642
 
551
643
  // Verify we're at the correct commit
552
644
  const currentCommit = await worktreeGit.revparse(['HEAD']);
@@ -919,11 +1011,11 @@ class GitWorktreeManager {
919
1011
 
920
1012
  // Fetch the latest PR head from remote
921
1013
  console.log(`Fetching PR #${prNumber} head from ${remote}...`);
922
- await git.fetch([remote, `pull/${prNumber}/head`]);
1014
+ const fetchedHead = await this.fetchPRHead(git, prInfo || { number: prNumber }, prData, { remote });
923
1015
 
924
1016
  // Reset to the fetched PR head
925
1017
  console.log(`Resetting worktree to PR head...`);
926
- await git.raw(['reset', '--hard', 'FETCH_HEAD']);
1018
+ await git.raw(['reset', '--hard', fetchedHead.checkoutTarget]);
927
1019
 
928
1020
  // Update last_accessed_at in database
929
1021
  if (this.worktreeRepo) {
@@ -977,13 +1069,13 @@ class GitWorktreeManager {
977
1069
  ? await this.resolveRemoteForPR(git, prData, prInfo)
978
1070
  : defaultRemote;
979
1071
 
980
- // 3. Fetch PR head into a persistent ref
981
- console.log(`Fetching PR #${prNumber} head from ${remote} into refs/remotes/${remote}/pr-${prNumber}...`);
982
- await git.fetch([remote, `+refs/pull/${prNumber}/head:refs/remotes/${remote}/pr-${prNumber}`]);
1072
+ // 3. Fetch PR head into a persistent ref (or by SHA when refs are unavailable)
1073
+ console.log(`Fetching PR #${prNumber} head from ${remote}...`);
1074
+ const fetchedHead = await this.fetchPRHead(git, prInfo || { number: prNumber }, prData, { remote });
983
1075
 
984
1076
  // 4. Reset worktree to the fetched ref
985
- console.log(`Resetting worktree to refs/remotes/${remote}/pr-${prNumber}...`);
986
- await git.raw(['reset', '--hard', `refs/remotes/${remote}/pr-${prNumber}`]);
1077
+ console.log(`Resetting worktree to ${fetchedHead.checkoutTarget}...`);
1078
+ await git.raw(['reset', '--hard', fetchedHead.checkoutTarget]);
987
1079
 
988
1080
  // 5. Return the new HEAD SHA
989
1081
  const headSha = (await git.revparse(['HEAD'])).trim();
@@ -1219,4 +1311,4 @@ class GitWorktreeManager {
1219
1311
  }
1220
1312
  }
1221
1313
 
1222
- module.exports = { GitWorktreeManager };
1314
+ module.exports = { GitWorktreeManager };