claude-git-hooks 2.51.2 → 2.61.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.
@@ -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
@@ -54,6 +54,10 @@ import {
54
54
  } from '../utils/interactive-ui.js';
55
55
  import logger from '../utils/logger.js';
56
56
  import { colors, error, checkGitRepo } from './helpers.js';
57
+ import {
58
+ CONSOLE_WARNING_TEMPLATE,
59
+ LIBRARY_VERIFY_SKIPPED_WARNING
60
+ } from '../messages/library-warnings.js';
57
61
 
58
62
  /**
59
63
  * Validates prerequisites before version bump
@@ -374,6 +378,25 @@ function restoreSnapshot(snapshot) {
374
378
  });
375
379
  }
376
380
 
381
+ /**
382
+ * Emit a loud Library staleness warning to stderr.
383
+ * Content comes from the wording constant + verify result data.
384
+ * Formatting uses ANSI codes for visual emphasis on TTY stderr.
385
+ *
386
+ * @param {import('../../.library/librarian/index.js').VerifyResult} result
387
+ * @private
388
+ */
389
+ function _emitLibraryWarning(result) {
390
+ const y = colors.yellow;
391
+ const r = colors.reset;
392
+ const bar = `${y}${'='.repeat(63)}${r}`;
393
+ const content = CONSOLE_WARNING_TEMPLATE(result, { autoRegen: 'deferred' });
394
+
395
+ const lines = ['', bar, '', `${y}${content}${r}`, '', bar, ''];
396
+
397
+ process.stderr.write(lines.join('\n'));
398
+ }
399
+
377
400
  /**
378
401
  * Bump version command
379
402
  * @param {Array<string>} args - Command arguments
@@ -441,6 +464,22 @@ export async function runBumpVersion(args) {
441
464
 
442
465
  showSuccess('Prerequisites validated');
443
466
 
467
+ // Library verification gate — silent on clean, loud-warn on stale, never-hard-fail
468
+ logger.debug('bump-version', 'Running Library verification gate');
469
+ try {
470
+ const { verify } = await import('../../.library/librarian/index.js');
471
+ const verifyResult = await verify();
472
+
473
+ if (verifyResult.clean) {
474
+ logger.debug('bump-version', 'Library is clean — no warning needed');
475
+ } else {
476
+ _emitLibraryWarning(verifyResult);
477
+ }
478
+ } catch (verifyErr) {
479
+ const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING} ${verifyErr.message}${colors.reset}\n\n`;
480
+ process.stderr.write(msg);
481
+ }
482
+
444
483
  // Step 2: Discover all version files
445
484
  logger.debug('bump-version', 'Step 2: Discovering version files');
446
485
  const discovery = discoverVersionFiles();
@@ -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';
@@ -664,6 +664,78 @@ export async function runCreatePr(args) {
664
664
  logger.debug('create-pr', 'No unpushed tags found, continuing');
665
665
  }
666
666
 
667
+ // Step 5.8: Library maintenance pipeline (AUT-3764)
668
+ logger.debug('create-pr', 'Step 5.8: Running Library maintenance pipeline');
669
+ try {
670
+ showInfo('Running Library maintenance pipeline...');
671
+ const { createPrPipeline } = await import('../../.library/librarian/index.js');
672
+ const root = getRepoRoot();
673
+
674
+ const pipelineSummary = await createPrPipeline({ repoRoot: root });
675
+ const {
676
+ modifiedFiles: libraryFiles,
677
+ perStep,
678
+ pendingDueToApiDown,
679
+ warnings: pipelineWarnings,
680
+ } = pipelineSummary;
681
+
682
+ // Surface pipeline summary
683
+ logger.debug('create-pr', 'Pipeline completed', {
684
+ modifiedCount: libraryFiles.length,
685
+ pendingDueToApiDown,
686
+ warningCount: pipelineWarnings.length,
687
+ });
688
+ showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
689
+ if (perStep.regen.changed > 0) {
690
+ showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
691
+ }
692
+ if (perStep.addRemoveRename.created > 0) {
693
+ showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
694
+ }
695
+ if (pendingDueToApiDown > 0) {
696
+ showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
697
+ }
698
+ for (const w of pipelineWarnings) {
699
+ showWarning(w);
700
+ }
701
+
702
+ if (libraryFiles.length === 0) {
703
+ showInfo('Library already in sync');
704
+ } else {
705
+ // Stage all modified Library files (stageFiles expects absolute paths)
706
+ const absFiles = libraryFiles.map(f => path.join(root, f));
707
+ const stageResult = stageFiles(absFiles);
708
+
709
+ if (!stageResult.success) {
710
+ showWarning(`Failed to stage Library files: ${stageResult.error}`);
711
+ } else {
712
+ // Commit with deterministic message — separate commit, not amend
713
+ const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
714
+ const commitResult = createCommit(libraryCommitMsg);
715
+
716
+ if (!commitResult.success) {
717
+ showWarning(`Failed to commit Library changes: ${commitResult.error}`);
718
+ } else {
719
+ showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
720
+
721
+ // Push the Library commit to the PR branch's remote
722
+ const libraryPushResult = pushBranch(currentBranch);
723
+ if (!libraryPushResult.success) {
724
+ showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
725
+ } else {
726
+ showSuccess('Library commit pushed');
727
+ }
728
+ }
729
+ }
730
+ }
731
+ } catch (pipelineErr) {
732
+ // Pipeline failure is non-blocking — log and continue with PR creation
733
+ logger.warning('create-pr', 'Library pipeline failed, continuing', {
734
+ error: pipelineErr.message,
735
+ });
736
+ showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
737
+ }
738
+
667
739
  // Step 6: Generate PR metadata using engine
668
740
  logger.debug('create-pr', 'Step 6: Generating PR metadata with engine');
669
741
  showInfo('Generating PR metadata with Claude...');
@@ -765,38 +837,6 @@ export async function runCreatePr(args) {
765
837
  finalBody = `> ⚠️ This PR must be merged with **merge commit** (not squash)\n\n${prBody}`;
766
838
  }
767
839
 
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
840
  // Step 10: Get reviewers from team + config fallback
801
841
  logger.debug('create-pr', 'Step 10: Selecting reviewers via team resolution');
802
842
  const reviewers = await selectReviewers({