claude-git-hooks 2.51.2 → 2.66.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.
@@ -39,7 +39,8 @@ import {
39
39
  checkoutBranch,
40
40
  pushBranch,
41
41
  deleteRemoteBranch,
42
- createCommit
42
+ createCommit,
43
+ stageFiles
43
44
  } from '../utils/git-operations.js';
44
45
  import {
45
46
  getLatestLocalTag,
@@ -650,6 +651,53 @@ export async function runBackMerge(args) {
650
651
  showSuccess(`✓ Merge committed: ${commitMsg}`);
651
652
  console.log('');
652
653
 
654
+ // 14b. Co-change correlation pipeline (AUT-3777)
655
+ let coChangeReport = null;
656
+ try {
657
+ const { coChangePipeline } = await import('../../.library/librarian/index.js');
658
+
659
+ showInfo('Running co-change correlation pipeline...');
660
+ const summary = coChangePipeline({ repoCtx: {} });
661
+ const { modifiedFiles, perStep, report, warnings, mode } = summary;
662
+
663
+ // Surface summary to stdout
664
+ showInfo(
665
+ `Co-change: mode=${mode}, detected=${perStep.detection.modifications}, ` +
666
+ `injected=${perStep.injection.modifications}, warnings=${warnings.length}`
667
+ );
668
+ for (const w of warnings) {
669
+ showWarning(w);
670
+ }
671
+
672
+ // Save report for PR body attachment after push
673
+ coChangeReport = report;
674
+
675
+ if (modifiedFiles.length === 0) {
676
+ showInfo('Co-change pipeline produced no Library changes');
677
+ } else {
678
+ const root = getRepoRoot();
679
+ const absFiles = modifiedFiles.map(f => path.isAbsolute(f) ? f : path.join(root, f));
680
+ const stageResult = stageFiles(absFiles);
681
+
682
+ if (!stageResult.success) {
683
+ showWarning(`Failed to stage co-change files: ${stageResult.error}`);
684
+ } else {
685
+ const windowDesc = tagName ? `${tagName}..HEAD` : 'full history';
686
+ const libCommitMsg = `chore(library): co-change correlations for ${windowDesc}`;
687
+ const libCommit = createCommit(libCommitMsg);
688
+
689
+ if (!libCommit.success) {
690
+ showWarning(`Failed to commit co-change files: ${libCommit.error}`);
691
+ } else {
692
+ showSuccess(`Co-change committed: ${libCommitMsg} (${modifiedFiles.length} file(s))`);
693
+ }
694
+ }
695
+ }
696
+ } catch (err) {
697
+ logger.warning(`co-change pipeline skipped: ${err.message}`);
698
+ logger.debug('back-merge', 'Co-change pipeline skipped', { error: err.message });
699
+ }
700
+
653
701
  // 15. Verify sync
654
702
  try {
655
703
  const postMergeDivergence = getDivergence(opts.into, `origin/${opts.from}`);
@@ -719,18 +767,50 @@ export async function runBackMerge(args) {
719
767
  await _revertFollowup(rcBranchForLog, repoRoot);
720
768
  }
721
769
 
722
- // 18b. Co-change injection refresh library references with newly merged history
723
- if (pushStatus === 'pushed') {
770
+ // 18b. Attach co-change report to PR body (AUT-3777)
771
+ if (coChangeReport && pushStatus === 'pushed') {
724
772
  try {
725
- const { main: injectCoChange } = await import('../../.library/tools/inject-co-change.js');
726
- showInfo('📚 Refreshing library co-change references...');
727
- await injectCoChange(['node', 'inject-co-change.js']);
728
- showSuccess('✓ Co-change references updated');
729
- } catch (err) {
730
- showWarning('📚 Co-change injection unavailable — .library/ tools not found');
731
- logger.debug('back-merge', 'Co-change injection skipped', { error: err.message });
773
+ const { parseGitHubRepo } = await import('../utils/github-client.js');
774
+ const { findExistingPR, updatePullRequestBody } = await import('../utils/github-api.js');
775
+ const repoInfo = parseGitHubRepo();
776
+
777
+ const existingPR = await findExistingPR({
778
+ owner: repoInfo.owner,
779
+ repo: repoInfo.repo,
780
+ head: opts.from,
781
+ base: opts.into,
782
+ });
783
+
784
+ if (existingPR) {
785
+ const OPEN_MARKER = '<!-- LIBRARY_COCHANGE_REPORT -->';
786
+ const CLOSE_MARKER = '<!-- /LIBRARY_COCHANGE_REPORT -->';
787
+ const section = `${OPEN_MARKER}\n${coChangeReport}\n${CLOSE_MARKER}`;
788
+ const markerRegex = new RegExp(
789
+ `${OPEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${CLOSE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
790
+ );
791
+
792
+ let newBody;
793
+ if (markerRegex.test(existingPR.body || '')) {
794
+ // Re-run: replace existing section
795
+ newBody = (existingPR.body).replace(markerRegex, section);
796
+ } else {
797
+ // First run: append section
798
+ newBody = `${existingPR.body || ''}\n\n${section}`;
799
+ }
800
+
801
+ await updatePullRequestBody(repoInfo.owner, repoInfo.repo, existingPR.number, newBody);
802
+ showSuccess('Co-change report attached to PR body');
803
+ } else {
804
+ logger.debug('back-merge', 'No existing PR found for report attachment', {
805
+ head: opts.from,
806
+ base: opts.into,
807
+ });
808
+ }
809
+ } catch (prErr) {
810
+ logger.debug('back-merge', 'Could not attach co-change report to PR', {
811
+ error: prErr.message,
812
+ });
732
813
  }
733
- console.log('');
734
814
  }
735
815
 
736
816
  // 19. Summary
@@ -48,12 +48,14 @@ import {
48
48
  showError,
49
49
  showWarning,
50
50
  promptConfirmation,
51
- promptMenu,
52
- promptToggleList,
53
- promptEditField
51
+ promptToggleList
54
52
  } from '../utils/interactive-ui.js';
55
53
  import logger from '../utils/logger.js';
56
54
  import { colors, error, checkGitRepo } from './helpers.js';
55
+ import {
56
+ CONSOLE_WARNING_TEMPLATE,
57
+ LIBRARY_VERIFY_SKIPPED_WARNING
58
+ } from '../messages/library-warnings.js';
57
59
 
58
60
  /**
59
61
  * Validates prerequisites before version bump
@@ -205,7 +207,9 @@ function displayDiscoveryTable(discovery) {
205
207
  );
206
208
  console.log('');
207
209
 
208
- if (discovery.files.length === 0) {
210
+ const visibleFiles = discovery.files.filter((f) => f.selected);
211
+
212
+ if (visibleFiles.length === 0) {
209
213
  console.log(' No version files found.');
210
214
  console.log('');
211
215
  return;
@@ -217,8 +221,8 @@ function displayDiscoveryTable(discovery) {
217
221
  );
218
222
  console.log(` ${'-'.repeat(3)} ${'-'.repeat(35)} ${'-'.repeat(15)} ${'-'.repeat(15)}`);
219
223
 
220
- // Table rows
221
- discovery.files.forEach((file, index) => {
224
+ // Table rows (only semver files)
225
+ visibleFiles.forEach((file, index) => {
222
226
  const num = `${index + 1}`.padEnd(3);
223
227
  const filePath = file.relativePath.padEnd(35);
224
228
  const fileType = file.projectLabel.padEnd(15);
@@ -289,70 +293,20 @@ function showDryRunPreview(info) {
289
293
  */
290
294
  async function promptFileSelection(discovery) {
291
295
  console.log('');
292
- showWarning('Version mismatch detected or interactive mode enabled');
293
- console.log('');
294
296
 
295
- const options = [
296
- { key: 'a', label: 'Update all files' },
297
- { key: 's', label: 'Select which files to update' },
298
- { key: 'e', label: 'Edit version per file (advanced)' },
299
- { key: 'c', label: 'Cancel' }
300
- ];
301
-
302
- const choice = await promptMenu('Choose action:', options, 'a');
303
-
304
- if (choice === 'c') {
305
- showInfo('Version bump cancelled');
306
- process.exit(0);
307
- }
308
-
309
- if (choice === 'a') {
310
- // All files selected
311
- return discovery.files;
312
- }
313
-
314
- if (choice === 's') {
315
- // Toggle list selection
316
- const items = discovery.files.map((file, index) => ({
317
- key: `${index}`,
318
- label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
319
- selected: file.selected
320
- }));
321
-
322
- const selectedKeys = await promptToggleList(
323
- 'Select files to update (type number to toggle, Enter to confirm):',
324
- items
325
- );
297
+ const semverFiles = discovery.files.filter((f) => f.selected);
298
+ const items = semverFiles.map((file, index) => ({
299
+ key: `${index}`,
300
+ label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
301
+ selected: true
302
+ }));
326
303
 
327
- // Filter files by selected keys
328
- return discovery.files.filter((_, index) => selectedKeys.includes(`${index}`));
329
- }
330
-
331
- if (choice === 'e') {
332
- // Per-file version editing
333
- showInfo('Enter target version for each file (press Enter to keep calculated version)');
334
- console.log('');
335
-
336
- for (const file of discovery.files) {
337
- const input = await promptEditField(
338
- `${file.relativePath} (${file.projectLabel})`,
339
- file.version || 'N/A'
340
- );
341
-
342
- // If user entered something different from current version, store it
343
- if (input !== file.version && input !== 'N/A') {
344
- if (!validateVersionFormat(input)) {
345
- showWarning(`Invalid version format: ${input} — skipping ${file.relativePath}`);
346
- continue;
347
- }
348
- file.targetVersion = input;
349
- }
350
- }
351
-
352
- return discovery.files;
353
- }
304
+ const selectedKeys = await promptToggleList(
305
+ 'Select files to update (type number to toggle, Enter to confirm):',
306
+ items
307
+ );
354
308
 
355
- return discovery.files;
309
+ return semverFiles.filter((_, index) => selectedKeys.includes(`${index}`));
356
310
  }
357
311
 
358
312
  /**
@@ -374,6 +328,25 @@ function restoreSnapshot(snapshot) {
374
328
  });
375
329
  }
376
330
 
331
+ /**
332
+ * Emit a loud Library staleness warning to stderr.
333
+ * Content comes from the wording constant + verify result data.
334
+ * Formatting uses ANSI codes for visual emphasis on TTY stderr.
335
+ *
336
+ * @param {import('../../.library/librarian/index.js').VerifyResult} result
337
+ * @private
338
+ */
339
+ function _emitLibraryWarning(result) {
340
+ const y = colors.yellow;
341
+ const r = colors.reset;
342
+ const bar = `${y}${'='.repeat(63)}${r}`;
343
+ const content = CONSOLE_WARNING_TEMPLATE(result, { autoRegen: 'deferred' });
344
+
345
+ const lines = ['', bar, '', `${y}${content}${r}`, '', bar, ''];
346
+
347
+ process.stderr.write(lines.join('\n'));
348
+ }
349
+
377
350
  /**
378
351
  * Bump version command
379
352
  * @param {Array<string>} args - Command arguments
@@ -441,6 +414,22 @@ export async function runBumpVersion(args) {
441
414
 
442
415
  showSuccess('Prerequisites validated');
443
416
 
417
+ // Library verification gate — silent on clean, loud-warn on stale, never-hard-fail
418
+ logger.debug('bump-version', 'Running Library verification gate');
419
+ try {
420
+ const { verify } = await import('../../.library/librarian/index.js');
421
+ const verifyResult = await verify();
422
+
423
+ if (verifyResult.clean) {
424
+ logger.debug('bump-version', 'Library is clean — no warning needed');
425
+ } else {
426
+ _emitLibraryWarning(verifyResult);
427
+ }
428
+ } catch (verifyErr) {
429
+ const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING} ${verifyErr.message}${colors.reset}\n\n`;
430
+ process.stderr.write(msg);
431
+ }
432
+
444
433
  // Step 2: Discover all version files
445
434
  logger.debug('bump-version', 'Step 2: Discovering version files');
446
435
  const discovery = discoverVersionFiles();
@@ -465,15 +454,6 @@ export async function runBumpVersion(args) {
465
454
  // Display discovery table
466
455
  displayDiscoveryTable(discovery);
467
456
 
468
- const currentVersion = discovery.resolvedVersion;
469
-
470
- if (!currentVersion) {
471
- showError('Could not determine current version from discovered files');
472
- process.exit(1);
473
- }
474
-
475
- showInfo(`Current version: ${currentVersion}`);
476
-
477
457
  // Step 3: File selection (if mismatch or interactive mode)
478
458
  let selectedFiles = discovery.files.filter((f) => f.selected);
479
459
 
@@ -489,6 +469,22 @@ export async function runBumpVersion(args) {
489
469
  console.log('');
490
470
  }
491
471
 
472
+ // Derive current version from selected files (not from discovery.resolvedVersion)
473
+ const selectedVersions = selectedFiles
474
+ .filter((f) => f.version !== null)
475
+ .map((f) => f.version);
476
+ const uniqueSelected = [...new Set(selectedVersions)];
477
+ const currentVersion = uniqueSelected.length === 1
478
+ ? uniqueSelected[0]
479
+ : discovery.resolvedVersion;
480
+
481
+ if (!currentVersion || !validateVersionFormat(currentVersion)) {
482
+ showError('Could not determine a valid semver version from selected files');
483
+ process.exit(1);
484
+ }
485
+
486
+ showInfo(`Current version: ${currentVersion}`);
487
+
492
488
  // Step 4: Calculate new version or apply suffix operation
493
489
  logger.debug('bump-version', 'Step 4: Calculating new version');
494
490
  let newVersion;
@@ -22,7 +22,7 @@ import {
22
22
  showWarning,
23
23
  promptConfirmation
24
24
  } from '../utils/interactive-ui.js';
25
- import { getBranchPushStatus, pushBranch } from '../utils/git-operations.js';
25
+ import { getBranchPushStatus, pushBranch, stageFiles, createCommit, getRepoRoot } from '../utils/git-operations.js';
26
26
  import logger from '../utils/logger.js';
27
27
  import { resolveLabels } from '../utils/label-resolver.js';
28
28
  import { CostTracker } from '../utils/cost-tracker.js';
@@ -364,7 +364,7 @@ export async function runCreatePr(args) {
364
364
  // Step 5.6: Version alignment validation (Issue #44)
365
365
  logger.debug('create-pr', 'Step 5.6: Validating version alignment');
366
366
  const { validateVersionAlignment } = await import('../utils/version-manager.js');
367
- const versionCheck = await validateVersionAlignment();
367
+ const versionCheck = await validateVersionAlignment(baseBranch);
368
368
 
369
369
  if (!versionCheck.aligned) {
370
370
  showWarning('Version misalignment detected:');
@@ -451,23 +451,135 @@ export async function runCreatePr(args) {
451
451
  }
452
452
  }
453
453
 
454
- // Step 5.7: Smart tag pushing (Issue #44)
455
- logger.debug('create-pr', 'Step 5.7: Checking and pushing unpushed tags');
454
+ // Step 5.7: Library maintenance pipeline (AUT-3764)
455
+ // Runs BEFORE tag push so that library regen is included in the tagged commit.
456
+ // Guard: only run if the current repo has its own .library/ setup.
457
+ // When claude-hooks is installed in a foreign repo (npm link / file: ref),
458
+ // the relative import resolves to git-hooks' .library/ — not this repo's.
459
+ let libraryCommitted = false;
460
+ const root = getRepoRoot();
461
+ const libraryResolverPath = path.join(root, '.library', 'resolver.yaml');
462
+ if (!fs.existsSync(libraryResolverPath)) {
463
+ logger.debug('create-pr', 'No .library/resolver.yaml in current repo — skipping Library pipeline');
464
+ } else {
465
+ logger.debug('create-pr', 'Step 5.7: Running Library maintenance pipeline');
466
+ try {
467
+ showInfo('Running Library maintenance pipeline...');
468
+ const { createPrPipeline } = await import('../../.library/librarian/index.js');
469
+
470
+ const pipelineSummary = await createPrPipeline({ repoRoot: root });
471
+ const {
472
+ modifiedFiles: libraryFiles,
473
+ perStep,
474
+ pendingDueToApiDown,
475
+ warnings: pipelineWarnings,
476
+ } = pipelineSummary;
477
+
478
+ // Surface pipeline summary
479
+ logger.debug('create-pr', 'Pipeline completed', {
480
+ modifiedCount: libraryFiles.length,
481
+ pendingDueToApiDown,
482
+ warningCount: pipelineWarnings.length,
483
+ });
484
+ showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
485
+ if (perStep.regen.changed > 0) {
486
+ showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
487
+ }
488
+ if (perStep.addRemoveRename.created > 0) {
489
+ showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
490
+ }
491
+ if (pendingDueToApiDown > 0) {
492
+ showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
493
+ }
494
+ for (const w of pipelineWarnings) {
495
+ showWarning(w);
496
+ }
497
+
498
+ if (libraryFiles.length === 0) {
499
+ showInfo('Library already in sync');
500
+ } else {
501
+ // Stage all modified Library files (stageFiles expects absolute paths)
502
+ const absFiles = libraryFiles.map(f => path.join(root, f));
503
+ const stageResult = stageFiles(absFiles);
504
+
505
+ if (!stageResult.success) {
506
+ showWarning(`Failed to stage Library files: ${stageResult.error}`);
507
+ } else {
508
+ // Commit with deterministic message — separate commit, not amend
509
+ const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
510
+ const commitResult = createCommit(libraryCommitMsg);
511
+
512
+ if (!commitResult.success) {
513
+ showWarning(`Failed to commit Library changes: ${commitResult.error}`);
514
+ } else {
515
+ libraryCommitted = true;
516
+ showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
517
+
518
+ // Push the Library commit to the PR branch's remote
519
+ const libraryPushResult = pushBranch(currentBranch);
520
+ if (!libraryPushResult.success) {
521
+ showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
522
+ } else {
523
+ showSuccess('Library commit pushed');
524
+ }
525
+ }
526
+ }
527
+ }
528
+ } catch (pipelineErr) {
529
+ // Pipeline failure is non-blocking — log and continue with PR creation
530
+ logger.warning('create-pr', 'Library pipeline failed, continuing', {
531
+ error: pipelineErr.message,
532
+ });
533
+ showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
534
+ }
535
+ }
536
+
537
+ // Step 5.8: Smart tag pushing (Issue #44)
538
+ // Runs AFTER library maintenance so tags include the library commit.
539
+ logger.debug('create-pr', 'Step 5.8: Checking and pushing unpushed tags');
456
540
  const {
457
541
  compareLocalAndRemoteTags,
458
542
  pushTags: pushTagsUtil,
459
- getLatestLocalTag,
460
- getLatestRemoteTag,
543
+ createTag: createTagUtil,
544
+ getLatestRemoteTagOnBranch,
461
545
  parseTagVersion
462
546
  } = await import('../utils/git-tag-manager.js');
463
547
  const { compareVersions } = await import('../utils/version-manager.js');
464
548
 
549
+ // If library was committed after bump-version created the tag,
550
+ // re-point unpushed tags to HEAD so the tag includes library books.
551
+ if (libraryCommitted) {
552
+ const unpushedTags = await compareLocalAndRemoteTags();
553
+ for (const tag of unpushedTags.localNewer) {
554
+ const version = parseTagVersion(tag);
555
+ if (version) {
556
+ logger.debug('create-pr', 'Re-pointing tag to include library commit', { tag });
557
+ let origTagMsg = '';
558
+ try { origTagMsg = execSync(`git tag -l --format="%(contents)" ${tag}`, { encoding: 'utf8' }).trim(); } catch { /* ignore */ }
559
+ const tagMsg = origTagMsg || `Release version ${version}`;
560
+ try {
561
+ await createTagUtil(version, tagMsg, { force: true });
562
+ showInfo(`Tag ${tag} moved to include library commit`);
563
+ } catch (tagErr) {
564
+ logger.warning('create-pr', 'Failed to re-point tag, continuing', { tag, error: tagErr.message });
565
+ }
566
+ }
567
+ }
568
+ }
569
+
465
570
  const tagComparison = await compareLocalAndRemoteTags();
466
571
 
467
572
  if (tagComparison.localNewer.length > 0) {
468
- // Get latest local and remote tags for comparison
469
- const latestLocalTag = getLatestLocalTag();
470
- const latestRemoteTag = await getLatestRemoteTag();
573
+ // Derive latest unpushed tag (scoped to what's actually being pushed)
574
+ const sortedUnpushed = tagComparison.localNewer
575
+ .map((t) => ({ tag: t, version: parseTagVersion(t) }))
576
+ .filter((t) => t.version !== null)
577
+ .sort((a, b) => compareVersions(a.version, b.version));
578
+ const latestLocalTag = sortedUnpushed.length > 0
579
+ ? sortedUnpushed[sortedUnpushed.length - 1].tag
580
+ : tagComparison.localNewer[0];
581
+ // Compare against latest remote tag on the BASE branch (not global latest)
582
+ const latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
471
583
 
472
584
  const localVersion = latestLocalTag ? parseTagVersion(latestLocalTag) : null;
473
585
  const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
@@ -483,7 +595,7 @@ export async function runCreatePr(args) {
483
595
  let shouldPushTags = false;
484
596
  let userChoice = null;
485
597
 
486
- // Case 1: Local tag > Remote tag → Auto-push
598
+ // Case 1: Local tag > Remote tag → Auto-push (normal bump-version flow)
487
599
  if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) > 0) {
488
600
  logger.debug('create-pr', 'Local version > remote version, auto-pushing', {
489
601
  localVersion,
@@ -494,7 +606,7 @@ export async function runCreatePr(args) {
494
606
  showInfo('Auto-pushing tag to remote...');
495
607
  shouldPushTags = true;
496
608
 
497
- // Case 2: Local tag = Remote tag → Prompt with warning
609
+ // Case 2: Local tag = Remote tag → Prompt with warning (may already be pushed)
498
610
  } else if (
499
611
  localVersion &&
500
612
  remoteVersion &&
@@ -534,7 +646,7 @@ export async function runCreatePr(args) {
534
646
  shouldPushTags = true;
535
647
  }
536
648
 
537
- // Case 3: Local tag < Remote tag → Prompt with error
649
+ // Case 3: Local tag < Remote tag → Prompt with warning (someone pushed newer)
538
650
  } else if (
539
651
  localVersion &&
540
652
  remoteVersion &&
@@ -765,38 +877,6 @@ export async function runCreatePr(args) {
765
877
  finalBody = `> ⚠️ This PR must be merged with **merge commit** (not squash)\n\n${prBody}`;
766
878
  }
767
879
 
768
- // Library staleness section — informational, non-blocking
769
- try {
770
- showInfo('📚 Checking library staleness...');
771
- const { checkBook } = await import('../../.library/tools/staleness.js');
772
- const { getBooksDir } = await import('../../.library/paths.js');
773
- const { getRepoRoot } = await import('../utils/git-operations.js');
774
- const booksDir = getBooksDir();
775
- const root = getRepoRoot();
776
- const changedSourceFiles = (filesArray || [])
777
- .map(f => f.path || f)
778
- .filter(p => p.startsWith('lib/') && p.endsWith('.js'));
779
- const staleBooks = [];
780
- for (const srcPath of changedSourceFiles) {
781
- const bookName = `${srcPath.replace(/^lib\/.*\//, '').replace(/\.js$/, '')}.md`;
782
- try {
783
- const result = await checkBook(path.join(booksDir, bookName), root);
784
- if (result.status === 'stale') staleBooks.push(bookName);
785
- } catch {
786
- // skip
787
- }
788
- }
789
- if (staleBooks.length > 0) {
790
- const bookList = staleBooks.map(b => `- \`${b}\``).join('\n');
791
- finalBody += `\n\n---\n\n### 📚 Library Staleness\n\nThe following library books may need regeneration:\n\n${bookList}\n\nRun: \`npm run library:regenerate\``;
792
- showWarning(`📚 ${staleBooks.length} stale book(s) — section added to PR body`);
793
- } else if (changedSourceFiles.length > 0) {
794
- showSuccess('📚 Library books are current');
795
- }
796
- } catch {
797
- showWarning('📚 Library staleness check unavailable — .library/ tools not found');
798
- }
799
-
800
880
  // Step 10: Get reviewers from team + config fallback
801
881
  logger.debug('create-pr', 'Step 10: Selecting reviewers via team resolution');
802
882
  const reviewers = await selectReviewers({