@in-the-loop-labs/pair-review 3.2.3 → 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.
@@ -0,0 +1,216 @@
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
+ 'use strict';
3
+
4
+ const logger = require('../utils/logger');
5
+
6
+ const GRACE_PERIOD_MS = 30_000; // 30 seconds after last WS disconnect
7
+
8
+ /**
9
+ * In-memory tracker that determines whether a pool worktree is "in use".
10
+ *
11
+ * A pool worktree is considered in-use when any of:
12
+ * - At least one WebSocket session is subscribed to its review topic
13
+ * - An AI analysis is running against it
14
+ * - The grace period after the last session disconnect hasn't expired
15
+ *
16
+ * When a worktree becomes idle (all of the above are false), the `onIdle`
17
+ * callback fires so the pool manager can mark it available.
18
+ */
19
+ class WorktreePoolUsageTracker {
20
+ constructor() {
21
+ /** @type {Map<string, Set<string>>} worktreeId -> Set of active session keys */
22
+ this._sessions = new Map();
23
+ /** @type {Map<string, Set<string>>} worktreeId -> Set of active analysis IDs */
24
+ this._analyses = new Map();
25
+ /** @type {Map<string, NodeJS.Timeout>} worktreeId -> grace period timer */
26
+ this._graceTimers = new Map();
27
+ /** @type {Function|null} Callback when a worktree becomes idle: (worktreeId) => void */
28
+ this.onIdle = null;
29
+ }
30
+
31
+ /**
32
+ * Register an active WebSocket session for a worktree.
33
+ * Clears any pending grace-period timer.
34
+ * @param {string} worktreeId - Pool worktree ID
35
+ * @param {string} sessionKey - Unique key for this WS connection
36
+ */
37
+ addSession(worktreeId, sessionKey) {
38
+ if (!this._sessions.has(worktreeId)) {
39
+ this._sessions.set(worktreeId, new Set());
40
+ }
41
+ this._sessions.get(worktreeId).add(sessionKey);
42
+
43
+ // Cancel any pending grace timer
44
+ const timer = this._graceTimers.get(worktreeId);
45
+ if (timer) {
46
+ clearTimeout(timer);
47
+ this._graceTimers.delete(worktreeId);
48
+ logger.debug(`Grace period cancelled for pool worktree ${worktreeId} — new session connected`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Remove a WebSocket session. Starts grace period if no sessions remain
54
+ * and no analyses are running.
55
+ * @param {string} worktreeId
56
+ * @param {string} sessionKey
57
+ */
58
+ removeSession(worktreeId, sessionKey) {
59
+ const sessions = this._sessions.get(worktreeId);
60
+ if (!sessions) return;
61
+
62
+ sessions.delete(sessionKey);
63
+ if (sessions.size === 0) {
64
+ this._sessions.delete(worktreeId);
65
+ }
66
+
67
+ this._checkIdle(worktreeId);
68
+ }
69
+
70
+ /**
71
+ * Register an active analysis for a worktree.
72
+ * Clears any pending grace-period timer.
73
+ * @param {string} worktreeId - Pool worktree ID
74
+ * @param {string} analysisId - Unique analysis identifier
75
+ */
76
+ addAnalysis(worktreeId, analysisId) {
77
+ if (!this._analyses.has(worktreeId)) {
78
+ this._analyses.set(worktreeId, new Set());
79
+ }
80
+ this._analyses.get(worktreeId).add(analysisId);
81
+
82
+ // Cancel any pending grace timer
83
+ const timer = this._graceTimers.get(worktreeId);
84
+ if (timer) {
85
+ clearTimeout(timer);
86
+ this._graceTimers.delete(worktreeId);
87
+ logger.debug(`Grace period cancelled for pool worktree ${worktreeId} — new analysis started`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Remove an active analysis hold.
93
+ * @param {string} worktreeId
94
+ * @param {string} analysisId
95
+ */
96
+ removeAnalysis(worktreeId, analysisId) {
97
+ const analyses = this._analyses.get(worktreeId);
98
+ if (!analyses) return;
99
+
100
+ analyses.delete(analysisId);
101
+ if (analyses.size === 0) {
102
+ this._analyses.delete(worktreeId);
103
+ }
104
+
105
+ this._checkIdle(worktreeId);
106
+ }
107
+
108
+ /**
109
+ * Remove an analysis hold by analysisId only (without knowing worktreeId).
110
+ * Searches all worktrees for the analysis.
111
+ * @param {string} analysisId
112
+ */
113
+ removeAnalysisById(analysisId) {
114
+ for (const [worktreeId, analyses] of this._analyses) {
115
+ if (analyses.has(analysisId)) {
116
+ this.removeAnalysis(worktreeId, analysisId);
117
+ return;
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Check if a worktree is currently in use.
124
+ * @param {string} worktreeId
125
+ * @returns {boolean}
126
+ */
127
+ isInUse(worktreeId) {
128
+ const hasSessions = (this._sessions.get(worktreeId)?.size || 0) > 0;
129
+ const hasAnalyses = (this._analyses.get(worktreeId)?.size || 0) > 0;
130
+ const hasGraceTimer = this._graceTimers.has(worktreeId);
131
+ return hasSessions || hasAnalyses || hasGraceTimer;
132
+ }
133
+
134
+ /**
135
+ * Return the set of active analysis IDs for a worktree (may be empty).
136
+ * @param {string} worktreeId
137
+ * @returns {Set<string>}
138
+ */
139
+ getActiveAnalyses(worktreeId) {
140
+ return new Set(this._analyses.get(worktreeId) || []);
141
+ }
142
+
143
+ /**
144
+ * Forcefully clear all tracking state for a single worktree.
145
+ *
146
+ * Removes sessions, analyses, and grace timers.
147
+ * Does NOT fire the onIdle callback — the caller is assumed to be
148
+ * handling the worktree lifecycle directly (e.g., deleting or
149
+ * marking it available in the pool).
150
+ *
151
+ * @param {string} worktreeId - Pool worktree ID to purge
152
+ */
153
+ clearWorktree(worktreeId) {
154
+ this._sessions.delete(worktreeId);
155
+ this._analyses.delete(worktreeId);
156
+ const timer = this._graceTimers.get(worktreeId);
157
+ if (timer) {
158
+ clearTimeout(timer);
159
+ this._graceTimers.delete(worktreeId);
160
+ }
161
+ logger.debug(`Cleared all tracking state for pool worktree ${worktreeId}`);
162
+ }
163
+
164
+ /**
165
+ * Internal: check if a worktree has become idle and start grace period or fire callback.
166
+ * @param {string} worktreeId
167
+ * @private
168
+ */
169
+ _checkIdle(worktreeId) {
170
+ const hasSessions = (this._sessions.get(worktreeId)?.size || 0) > 0;
171
+ const hasAnalyses = (this._analyses.get(worktreeId)?.size || 0) > 0;
172
+
173
+ if (hasSessions || hasAnalyses) return; // still in use
174
+
175
+ // Already have a grace timer? Let it run
176
+ if (this._graceTimers.has(worktreeId)) return;
177
+
178
+ // Start grace period
179
+ logger.debug(`Starting ${GRACE_PERIOD_MS / 1000}s grace period for pool worktree ${worktreeId}`);
180
+ const timer = setTimeout(async () => {
181
+ this._graceTimers.delete(worktreeId);
182
+ // Double-check still idle (a new session could have connected during grace)
183
+ const stillHasSessions = (this._sessions.get(worktreeId)?.size || 0) > 0;
184
+ const stillHasAnalyses = (this._analyses.get(worktreeId)?.size || 0) > 0;
185
+ if (!stillHasSessions && !stillHasAnalyses) {
186
+ logger.info(`Pool worktree ${worktreeId} idle after grace period`);
187
+ if (this.onIdle) {
188
+ try {
189
+ await this.onIdle(worktreeId);
190
+ } catch (err) {
191
+ logger.error(`onIdle callback failed for ${worktreeId}: ${err.message}`);
192
+ }
193
+ }
194
+ }
195
+ }, GRACE_PERIOD_MS);
196
+
197
+ // Don't hold the process open for grace timers
198
+ if (timer.unref) timer.unref();
199
+ this._graceTimers.set(worktreeId, timer);
200
+ }
201
+
202
+ /**
203
+ * Clear all tracking state. Useful for testing.
204
+ */
205
+ reset() {
206
+ this._sessions.clear();
207
+ this._analyses.clear();
208
+ for (const timer of this._graceTimers.values()) {
209
+ clearTimeout(timer);
210
+ }
211
+ this._graceTimers.clear();
212
+ this.onIdle = null;
213
+ }
214
+ }
215
+
216
+ module.exports = { WorktreePoolUsageTracker, GRACE_PERIOD_MS };
@@ -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
@@ -241,10 +332,12 @@ class GitWorktreeManager {
241
332
  * When set, worktree is created with --no-checkout from the main git root (no sparse-checkout inheritance),
242
333
  * and the script is executed before checkout with PR context as environment variables.
243
334
  * @param {number} [options.checkoutTimeout] - Timeout in ms for checkout script (default: 300000 = 5 minutes)
244
- * @returns {Promise<string>} Path to created worktree
335
+ * @param {string} [options.explicitId] - When provided, use this ID for the worktrees-table record
336
+ * instead of generating one. Used by the worktree pool to align the worktrees-table ID with the pool ID.
337
+ * @returns {Promise<{ path: string, id: string }>} Path and database ID of created worktree
245
338
  */
246
339
  async createWorktreeForPR(prInfo, prData, repositoryPath, options = {}) {
247
- const { worktreeSourcePath, checkoutScript, checkoutTimeout } = options;
340
+ const { worktreeSourcePath, checkoutScript, checkoutTimeout, explicitId } = options;
248
341
  // Check if worktree already exists in DB
249
342
  const repository = normalizeRepository(prInfo.owner, prInfo.repo);
250
343
  let worktreePath;
@@ -265,7 +358,25 @@ class GitWorktreeManager {
265
358
  // Try to reuse existing worktree by refreshing it
266
359
  console.log(`Found existing worktree for PR #${prInfo.number} at ${worktreePath}`);
267
360
  try {
268
- return await this.refreshWorktree(worktreeRecord, prInfo.number, prData, prInfo);
361
+ const refreshedPath = await this.refreshWorktree(worktreeRecord, prInfo.number, prData, prInfo);
362
+ let returnId = worktreeRecord.id;
363
+
364
+ // If explicitId is provided and differs from the existing record's ID,
365
+ // migrate the worktrees-table row to use the pool ID. This happens when
366
+ // pool mode is enabled for a repo that already has legacy worktree records.
367
+ if (explicitId && worktreeRecord.id !== explicitId && this.worktreeRepo) {
368
+ const migrated = await this.worktreeRepo.getOrCreate({
369
+ prNumber: prInfo.number,
370
+ repository,
371
+ branch: prData.head_branch || prData.base_branch,
372
+ path: refreshedPath,
373
+ explicitId,
374
+ });
375
+ returnId = migrated.id;
376
+ console.log(`Migrated worktree record from ${worktreeRecord.id} to ${explicitId}`);
377
+ }
378
+
379
+ return { path: refreshedPath, id: returnId };
269
380
  } catch (refreshError) {
270
381
  // If refresh fails due to uncommitted changes, propagate that error
271
382
  if (refreshError.message.includes('uncommitted changes')) {
@@ -289,20 +400,23 @@ class GitWorktreeManager {
289
400
  if (legacyExists && await this.isValidGitWorktree(legacyPath)) {
290
401
  console.log(`Found legacy worktree for PR #${prInfo.number} at ${legacyPath}, adopting it`);
291
402
 
292
- // Create DB record for the legacy worktree
403
+ // Create DB record for the legacy worktree — pass explicitId so the record
404
+ // is created with the pool ID when pool mode is active
293
405
  if (this.worktreeRepo) {
294
406
  worktreeRecord = await this.worktreeRepo.getOrCreate({
295
407
  prNumber: prInfo.number,
296
408
  repository,
297
409
  branch: prData.head_branch || prData.base_branch,
298
- path: legacyPath
410
+ path: legacyPath,
411
+ explicitId,
299
412
  });
300
413
  console.log(`Created database record for legacy worktree`);
301
414
  }
302
415
 
303
416
  // Try to refresh and reuse the legacy worktree
304
417
  try {
305
- return await this.refreshWorktree({ path: legacyPath, id: worktreeRecord?.id }, prInfo.number, prData, prInfo);
418
+ const refreshedPath = await this.refreshWorktree({ path: legacyPath, id: worktreeRecord?.id }, prInfo.number, prData, prInfo);
419
+ return { path: refreshedPath, id: worktreeRecord?.id };
306
420
  } catch (refreshError) {
307
421
  // If refresh fails due to uncommitted changes, propagate that error
308
422
  if (refreshError.message.includes('uncommitted changes')) {
@@ -404,20 +518,21 @@ class GitWorktreeManager {
404
518
  console.log(`Base SHA fetch not needed or already available: ${fetchError.message}`);
405
519
  }
406
520
 
407
- // 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
408
522
  console.log(`Fetching PR #${prInfo.number} head...`);
409
- 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 });
410
524
 
411
525
  // Execute checkout script if configured (before checkout so sparse-checkout is set up)
412
526
  if (checkoutScript) {
413
527
  // Fetch the actual head branch by name (for checkout scripts that expect branch refs)
414
528
  // This may fail for fork PRs where the branch is in a different repo - that's okay
415
- if (prData.head_branch) {
529
+ const headBranch = this.getPRHeadBranch(prData);
530
+ if (headBranch) {
416
531
  try {
417
- console.log(`Fetching head branch ${prData.head_branch}...`);
418
- 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}`]);
419
534
  // Create/update a local branch pointing to the fetched ref so tooling can reference it by name
420
- await worktreeGit.branch(['-f', prData.head_branch, `${remote}/${prData.head_branch}`]);
535
+ await worktreeGit.branch(['-f', headBranch, `${remote}/${headBranch}`]);
421
536
  } catch (branchFetchError) {
422
537
  // Expected for fork PRs - the branch exists in the fork, not the base repo
423
538
  console.log(`Could not fetch head branch (may be from a fork): ${branchFetchError.message}`);
@@ -427,9 +542,9 @@ class GitWorktreeManager {
427
542
  console.log(`Executing checkout script: ${checkoutScript}`);
428
543
  const scriptEnv = {
429
544
  BASE_BRANCH: prData.base_branch,
430
- HEAD_BRANCH: prData.head_branch,
545
+ HEAD_BRANCH: headBranch,
431
546
  BASE_SHA: prData.base_sha,
432
- HEAD_SHA: prData.head_sha,
547
+ HEAD_SHA: this.getPRHeadSha(prData),
433
548
  PR_NUMBER: String(prInfo.number),
434
549
  WORKTREE_PATH: worktreePath
435
550
  };
@@ -438,8 +553,14 @@ class GitWorktreeManager {
438
553
  }
439
554
 
440
555
  // Checkout to PR head commit
441
- console.log(`Checking out to PR head commit ${prData.head_sha}...`);
442
- await worktreeGit.checkout([`${remote}/pr-${prInfo.number}`]);
556
+ const targetSha = this.getPRHeadSha(prData);
557
+ if (targetSha) {
558
+ console.log(`Checking out to PR head commit ${targetSha}...`);
559
+ await worktreeGit.checkout([targetSha]);
560
+ } else {
561
+ console.log(`Checking out to PR head ref ${fetchedHead.checkoutTarget}...`);
562
+ await worktreeGit.checkout([fetchedHead.checkoutTarget]);
563
+ }
443
564
 
444
565
  // Verify we're at the correct commit
445
566
  const currentCommit = await worktreeGit.revparse(['HEAD']);
@@ -448,18 +569,21 @@ class GitWorktreeManager {
448
569
  }
449
570
 
450
571
  // Store/update worktree record in database
572
+ let worktreeDbId;
451
573
  if (this.worktreeRepo) {
452
- await this.worktreeRepo.getOrCreate({
574
+ const record = await this.worktreeRepo.getOrCreate({
453
575
  prNumber: prInfo.number,
454
576
  repository,
455
577
  branch: prData.head_branch || prData.base_branch,
456
- path: worktreePath
578
+ path: worktreePath,
579
+ explicitId,
457
580
  });
581
+ worktreeDbId = record.id;
458
582
  console.log(`Worktree record stored in database`);
459
583
  }
460
584
 
461
585
  console.log(`Worktree created successfully at ${worktreePath}`);
462
- return worktreePath;
586
+ return { path: worktreePath, id: worktreeDbId };
463
587
 
464
588
  } catch (error) {
465
589
  console.error('Error creating worktree:', error);
@@ -503,17 +627,18 @@ class GitWorktreeManager {
503
627
  // Resolve which remote points to the PR's base repository (handles forks)
504
628
  const remote = await this.resolveRemoteForPR(worktreeGit, prData, prInfo);
505
629
 
506
- // Fetch the latest from the resolved remote
630
+ // Fetch the latest from the resolved remote (--prune removes stale
631
+ // tracking refs that would otherwise block fetch on ref hierarchy conflicts)
507
632
  console.log(`Fetching latest changes from ${remote}...`);
508
- await worktreeGit.fetch([remote]);
633
+ await worktreeGit.fetch([remote, '--prune']);
509
634
 
510
- // 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
511
636
  console.log(`Fetching PR #${number} head...`);
512
- await worktreeGit.fetch([remote, `+refs/pull/${number}/head:refs/remotes/${remote}/pr-${number}`]);
637
+ const fetchedHead = await this.fetchPRHead(worktreeGit, prInfo, prData, { remote });
513
638
 
514
639
  // Checkout to PR head commit
515
640
  console.log(`Checking out to PR head commit ${headSha}...`);
516
- await worktreeGit.checkout([`${remote}/pr-${number}`]);
641
+ await worktreeGit.checkout([fetchedHead.checkoutTarget]);
517
642
 
518
643
  // Verify we're at the correct commit
519
644
  const currentCommit = await worktreeGit.revparse(['HEAD']);
@@ -886,11 +1011,11 @@ class GitWorktreeManager {
886
1011
 
887
1012
  // Fetch the latest PR head from remote
888
1013
  console.log(`Fetching PR #${prNumber} head from ${remote}...`);
889
- await git.fetch([remote, `pull/${prNumber}/head`]);
1014
+ const fetchedHead = await this.fetchPRHead(git, prInfo || { number: prNumber }, prData, { remote });
890
1015
 
891
1016
  // Reset to the fetched PR head
892
1017
  console.log(`Resetting worktree to PR head...`);
893
- await git.raw(['reset', '--hard', 'FETCH_HEAD']);
1018
+ await git.raw(['reset', '--hard', fetchedHead.checkoutTarget]);
894
1019
 
895
1020
  // Update last_accessed_at in database
896
1021
  if (this.worktreeRepo) {
@@ -944,13 +1069,13 @@ class GitWorktreeManager {
944
1069
  ? await this.resolveRemoteForPR(git, prData, prInfo)
945
1070
  : defaultRemote;
946
1071
 
947
- // 3. Fetch PR head into a persistent ref
948
- console.log(`Fetching PR #${prNumber} head from ${remote} into refs/remotes/${remote}/pr-${prNumber}...`);
949
- 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 });
950
1075
 
951
1076
  // 4. Reset worktree to the fetched ref
952
- console.log(`Resetting worktree to refs/remotes/${remote}/pr-${prNumber}...`);
953
- 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]);
954
1079
 
955
1080
  // 5. Return the new HEAD SHA
956
1081
  const headSha = (await git.revparse(['HEAD'])).trim();
@@ -1186,4 +1311,4 @@ class GitWorktreeManager {
1186
1311
  }
1187
1312
  }
1188
1313
 
1189
- module.exports = { GitWorktreeManager };
1314
+ module.exports = { GitWorktreeManager };