claude-git-hooks 2.45.0 → 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.
package/CLAUDE.md CHANGED
@@ -1,8 +1,8 @@
1
- # Repository Context
1
+ # claude-git-hooks
2
2
 
3
- `claude-git-hooks` — intelligent Git hooks system integrating Claude CLI for code analysis, commit message generation, PR creation, and release workflow automation.
3
+ Intelligent Git hooks system integrating Claude CLI for code analysis, commit message generation, PR creation, and release workflow automation.
4
4
 
5
- ## Context Acquisition
5
+ ## Library
6
6
 
7
7
  All project knowledge lives in [`.library/`](.library/). Start at [`.library/index.md`](.library/index.md).
8
8
 
@@ -10,15 +10,16 @@ Load context based on your current task:
10
10
 
11
11
  | Task | Load |
12
12
  |------|------|
13
- | Writing or modifying code | [`.library/conventions.md`](.library/conventions.md) — coding standards, testing patterns |
14
- | Understanding a source module | [`.library/by-code/`](.library/by-code/) — find the book for that file |
15
- | Understanding a business workflow | [`.library/by-domain/`](.library/by-domain/) — commit pipeline, release management, PR analysis, GitHub integration |
16
- | Adding a new CLI command | [`.library/by-task-type/add-new-command.md`](.library/by-task-type/add-new-command.md) — 7-book reading sequence |
17
- | Updating the library after code changes | [`.library/README.md`](.library/README.md) — book schema, template, creation steps |
13
+ | Writing or modifying code | `conventions.md` (Library root) — coding standards, testing patterns |
14
+ | Understanding a source module | `@by-code/` — find the book for that file |
15
+ | Understanding a business workflow | `@by-domain/` — commit pipeline, release management, PR analysis, GitHub integration |
16
+ | Adding a new CLI command | `@by-task-type/add-new-command.md` — 7-book reading sequence |
18
17
 
19
- Books follow a standard template at `.library/templates/book-template.md`. After modifying a module, update its book and the relevant shelf index.
18
+ **Programmatic access**: `fetchLibraryContent(taskOrTopic)` in `.library/librarian/index.js` loads CLAUDE.md + index.md + conventions.md as fixed context, then routes the topic to the relevant index.
20
19
 
21
- ## Global Rules
20
+ **Budget-aware loading**: Every index carries `reading_cost_tokens`; task-type sequences split into core vs conditional books. Check token budgets before loading full sequences.
21
+
22
+ ## Rules
22
23
 
23
24
  These behavioral rules apply to all work in this repository:
24
25
 
@@ -31,5 +32,3 @@ These behavioral rules apply to all work in this repository:
31
32
  7. **Do NOT use `console.log`** — always use `logger.js`: `info()`, `warning()`, `error()`, `debug()`
32
33
  8. **Platform-specific care** — use `path.join()` (no hardcoded `/`); test on Windows, Linux, macOS
33
34
  9. **Input sanitization** — use `sanitize.js` for user inputs in shell commands; avoid `shell: true` in `spawn()`
34
-
35
- > See [`CLAUDE-MIGRATION.md`](CLAUDE-MIGRATION.md) for a mapping of where each original CLAUDE.md section moved.
package/README.md CHANGED
@@ -529,6 +529,13 @@ claude-git-hooks/
529
529
  │ ├── diff-analysis-orchestrator.js # Intelligent batch orchestration
530
530
  │ ├── judge.js # Auto-fix judge (v2.20.0)
531
531
  │ └── token-store.js # Token persistence - settings.local.json
532
+ ├── .library/ # Code Knowledge Library - auto-generated module docs
533
+ │ ├── books/ # One book per source module (auto + manual sections)
534
+ │ ├── extractor/ # Tree-sitter AST tooling
535
+ │ │ ├── extract.js # Source → book auto-section generator
536
+ │ │ ├── parser.js # WASM parser init and grammar loading
537
+ │ │ └── adapters/ # Language-specific CST → normalized AST
538
+ │ └── templates/ # Book schema and category reference
532
539
  ├── templates/
533
540
  │ ├── pre-commit # Bash wrapper - invokes Node.js
534
541
  │ ├── prepare-commit-msg # Bash wrapper - invokes Node.js
@@ -93,6 +93,33 @@ export async function runAnalyzeDiff(args) {
93
93
  console.log(result.testingNotes);
94
94
  }
95
95
 
96
+ // Library staleness section
97
+ try {
98
+ const { runAll } = await import('../../.library/tools/staleness.js');
99
+ const { getSourceDir, getBooksDir } = await import('../../.library/paths.js');
100
+ const { getRepoRoot } = await import('../utils/git-operations.js');
101
+ const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
102
+ const staleCount = stalenessResult.stale.length +
103
+ stalenessResult.orphan_books.length +
104
+ stalenessResult.unbooked_sources.length;
105
+ if (staleCount > 0) {
106
+ console.log('');
107
+ console.log(`📚 ${colors.yellow}Library Staleness:${colors.reset} ${staleCount} book(s) need attention`);
108
+ for (const item of stalenessResult.stale) {
109
+ console.log(` └─ stale: ${item.book}`);
110
+ }
111
+ for (const item of stalenessResult.orphan_books) {
112
+ console.log(` └─ orphan: ${item.book}`);
113
+ }
114
+ for (const src of stalenessResult.unbooked_sources) {
115
+ console.log(` └─ unbooked: ${src}`);
116
+ }
117
+ console.log(' Run: npm run library:regenerate');
118
+ }
119
+ } catch {
120
+ logger.warning('📚 Library staleness check unavailable — .library/ tools not found');
121
+ }
122
+
96
123
  // Save the results in a file with context
97
124
  const outputData = {
98
125
  prTitle: result.prTitle,
@@ -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,6 +767,52 @@ export async function runBackMerge(args) {
719
767
  await _revertFollowup(rcBranchForLog, repoRoot);
720
768
  }
721
769
 
770
+ // 18b. Attach co-change report to PR body (AUT-3777)
771
+ if (coChangeReport && pushStatus === 'pushed') {
772
+ try {
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
+ });
813
+ }
814
+ }
815
+
722
816
  // 19. Summary
723
817
  console.log('');
724
818
  console.log(`${colors.green}═════════════════════════════════════════════════${colors.reset}`);
@@ -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();
@@ -356,6 +356,28 @@ export async function runCloseRelease(args) {
356
356
  console.log('');
357
357
 
358
358
  try {
359
+ // Library staleness gate — block release closure if books are stale
360
+ try {
361
+ showInfo('📚 Checking library staleness...');
362
+ const { runAll, formatHuman } = await import('../../.library/tools/staleness.js');
363
+ const { getSourceDir, getBooksDir } = await import('../../.library/paths.js');
364
+ const { getRepoRoot } = await import('../utils/git-operations.js');
365
+ const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
366
+ const hasDrift = stalenessResult.stale.length > 0 ||
367
+ stalenessResult.orphan_books.length > 0 ||
368
+ stalenessResult.unbooked_sources.length > 0;
369
+ if (hasDrift) {
370
+ error('📚 Library staleness detected — cannot close release with stale books');
371
+ process.stdout.write(formatHuman(stalenessResult));
372
+ error('Run: npm run library:regenerate');
373
+ process.exit(1);
374
+ }
375
+ showSuccess('📚 Library books are current');
376
+ } catch (err) {
377
+ showWarning('📚 Library staleness check unavailable — .library/ tools not found');
378
+ logger.debug('close-release', 'Library staleness check skipped', { error: err.message });
379
+ }
380
+
359
381
  // ── Step 7: git reset --soft origin/main ─────────────────────────────
360
382
 
361
383
  showInfo('Resetting to origin/main (--soft)...');
@@ -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...');