claude-git-hooks 2.61.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.
package/CHANGELOG.md CHANGED
@@ -5,45 +5,283 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
5
5
  El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.66.1] - 2026-06-10
9
+
10
+ ### 🐛 Fixed
11
+ - Normalized path separators to forward slashes in staleness checker for cross-platform compatibility
12
+
13
+
14
+ ## [2.66.0] - 2026-06-10
15
+
16
+ ### ✨ Added
17
+ - Multi-language extractor registry with pluggable per-language architecture [AUT-3742]
18
+ - End-to-end SDLC stability check test suite
19
+ - Version alignment validation now scoped to base branch tags (#185)
20
+ - Extractor notes template for documenting per-language extractors
21
+
22
+ ### 🔧 Changed
23
+ - Moved arrow-function extraction from monolithic extractor to pluggable extractor module [AUT-3742]
24
+ - Decoupled `lib/` from `.library/` static imports with ESLint enforcement rule [AUT-3742]
25
+ - Library maintenance pipeline now runs before tag push in `create-pr`, ensuring library books are included in tagged commits
26
+ - Simplified `bump-version` interactive file selection to direct toggle list, removing multi-choice menu
27
+ - Tags are force-repointed to HEAD after library commit so tagged version includes regenerated books
28
+
29
+ ### 🐛 Fixed
30
+ - Fixed `create-pr` gotcha solicitation receiving empty file paths instead of actual book content (#190)
31
+ - Fixed `create-pr` tag comparison using global tag list instead of base branch scope
32
+ - Fixed `create-pr` library pipeline leaking host-repo paths into foreign repositories (#186)
33
+ - Fixed `bump-version` crashing on non-semver version files such as Maven `${revision}` placeholders (#187)
34
+ - Fixed version tag lookup scanning all branches instead of only the HEAD branch (#185)
35
+ - Fixed stale git worktree leftovers causing integration test failures
36
+
37
+ ### 🗑️ Removed
38
+ - Removed per-file custom version entry option (`promptEditField`) from `bump-version` interactive selection
39
+
40
+
41
+ ## [2.65.0] - 2026-06-08
42
+
43
+ ### ✨ Added
44
+ - Multi-language extractor registry with pluggable per-language extractors (AUT-3742)
45
+ - Extractor interface contract and runtime validator for language extractor plugins (AUT-3742)
46
+ - JS extractor notes template for documenting new extractors (AUT-3742)
47
+ - Test harness with JS fixtures for extractor validation (AUT-3742)
48
+ - Version alignment now scoped to base branch tags instead of global tag lookup
49
+
50
+ ### 🔧 Changed
51
+ - Arrow-function extraction moved from monolithic extract.js to pluggable JS extractor module (AUT-3742)
52
+ - Decoupled lib/ from .library/ static imports with ESLint enforcement — lib/ must use dynamic import() for Library integration (AUT-3742)
53
+ - Library maintenance pipeline in create-pr now runs before tag push so tags include regenerated books
54
+ - Unpushed tags are force-repointed to HEAD after library commit to include book changes
55
+ - Gotcha solicitation now receives full book content instead of just file paths
56
+ - bump-version interactive file selection simplified — goes directly to toggle list, non-semver files pre-deselected
57
+
58
+ ### 🐛 Fixed
59
+ - Fixed create-pr library pipeline running incorrectly in foreign repos by deriving paths from resolver config (#186)
60
+ - Fixed bump-version crash when non-semver files (e.g., Maven `${revision}`) reached version parser (#187)
61
+ - Fixed create-pr comparing tags against global scope instead of base branch (#185, #189)
62
+ - Fixed create-pr using stale tag reference after library pipeline commit
63
+ - Fixed local tag lookup not scoped to HEAD branch, causing cross-branch version mismatches
64
+
65
+ ### 🗑️ Removed
66
+ - Removed per-file custom version editing (option `e`) from bump-version interactive selection
67
+ - Removed `promptMenu` and `promptEditField` usage from bump-version flow
68
+
69
+
70
+ ## [2.64.4] - 2026-06-08
71
+
72
+ ### ✨ Added
73
+ - Added multi-language extractor registry with pluggable architecture and runtime interface validation (AUT-3742, #184)
74
+ - Added JS extractor as first pluggable extractor with dedicated test harness and fixtures (AUT-3742)
75
+ - Added extractor notes template for documenting per-language extractors (AUT-3742)
76
+ - Added branch-scoped tag lookup functions `getLatestLocalTagOnBranch()` and `getLatestRemoteTagOnBranch()` to git-tag-manager (#185)
77
+ - Added branch-scoped version alignment validation in `create-pr` (#185)
78
+
79
+ ### 🔧 Changed
80
+ - Decoupled `lib/` from `.library/` static imports with ESLint restricted-imports guard (AUT-3742)
81
+ - Moved arrow-function extraction from monolithic extractor into pluggable JS extractor module (AUT-3742)
82
+ - Simplified `bump-version` interactive file selection — removed menu, goes directly to toggle list with non-semver files pre-deselected (#187)
83
+ - Library pipeline in `create-pr` now derives paths from resolver config, preventing git-hooks paths from leaking into foreign repos (#186)
84
+ - Reordered `create-pr` steps so library pipeline runs before tag push, then re-points unpushed tags to HEAD to include library commits
85
+
86
+ ### 🐛 Fixed
87
+ - Fixed `bump-version` crash when non-semver files (e.g., Maven `${revision}`) were included in version resolution (#187)
88
+ - Fixed `create-pr` library pipeline using hardcoded paths that broke in foreign repositories (#186)
89
+ - Fixed version alignment and tag lookup returning results from unrelated branches instead of scoping to base branch (#185)
90
+ - Fixed `create-pr` tag becoming stale after library book regeneration by running the pipeline before tag push and re-pointing tags
91
+
92
+ ### 🗑️ Removed
93
+ - Removed `promptMenu` and `promptEditField` options from `bump-version` interactive file selection (#187)
94
+
95
+
96
+ ## [2.64.3] - 2026-06-08
97
+
98
+ ### ✨ Added
99
+ - Added multi-language extractor registry with pluggable interface, including JS extractor and notes template (AUT-3742)
100
+ - Added library maintenance pipeline step in create-pr that regenerates books before tag push
101
+ - Added branch-scoped tag lookup functions (`getLatestLocalTagOnBranch`, `getLatestRemoteTagOnBranch`) to git-tag-manager
102
+ - Added ESLint rule preventing `lib/` from statically importing `.library/` modules (AUT-3742)
103
+ - Added extractor test harness with JS fixtures (AUT-3742)
104
+
105
+ ### 🔧 Changed
106
+ - Moved arrow-function extraction from monolithic `extract.js` into pluggable extractor under `librarian/extractors/registered/js/` (AUT-3742)
107
+ - Scoped version alignment validation to base branch tags instead of global tag lookup
108
+ - Simplified bump-version interactive file selection — goes directly to toggle list, non-semver files pre-deselected
109
+ - Library pipeline now derives booksDir/sourceDir from resolver config when repoRoot is overridden, preventing path leakage into foreign repos
110
+ - Unpushed tags are force-repointed to HEAD after library commit so tags include regenerated books
111
+
112
+ ### 🐛 Fixed
113
+ - Fixed create-pr using stale tag for comparison after library regeneration created a new commit
114
+ - Fixed create-pr running library pipeline in foreign repos where `.library/` paths were invalid (#186)
115
+ - Fixed bump-version crashing on non-semver version files (e.g., Maven `${revision}`) by excluding them from version resolution (#187)
116
+ - Fixed local tag lookup scanning tags from all branches instead of only the current HEAD branch (#185)
117
+
118
+ ### 🗑️ Removed
119
+ - Removed per-file custom version editing (`promptEditField` option) from bump-version interactive selection
120
+ - Removed `promptMenu` four-option flow from bump-version in favor of direct toggle list
121
+
122
+
123
+ ## [2.64.2] - 2026-06-08
124
+
125
+ ### ✨ Added
126
+ - Multi-language extractor registry with pluggable extractor interface and JS extractor (AUT-3742, #184)
127
+ - Extractor notes template and per-extractor documentation standard (AUT-3742)
128
+ - Test harness for extractors with JS fixtures (AUT-3742)
129
+ - ESLint rule preventing lib/ from statically importing .library/ modules (AUT-3742)
130
+ - Version alignment now scoped to base branch tags for accurate cross-branch comparisons (#185)
131
+ - Library maintenance pipeline step in create-pr to regenerate books before tagging (#187)
132
+
133
+ ### 🔧 Changed
134
+ - Moved arrow-function extraction from monolithic extractor to pluggable JS extractor module (AUT-3742)
135
+ - Decoupled lib/ from .library/ imports — library integration now uses dynamic import with graceful degradation (AUT-3742)
136
+ - Library pipeline in create-pr derives paths from resolver.yaml config instead of hardcoded values (#186)
137
+ - Simplified bump-version interactive file selection to use toggle list directly, removing menu options (a/s/e/c) (#187)
138
+ - Tags are re-pointed to HEAD after library commit so tagged releases include regenerated books (#187)
139
+
140
+ ### 🐛 Fixed
141
+ - Fixed bump-version crashing on non-semver version files (e.g., Maven `${revision}`) by excluding them from version resolution (#187)
142
+ - Fixed create-pr library pipeline leaking git-hooks paths into foreign repos (#186)
143
+ - Fixed local tag lookup scanning all branches instead of scoping to HEAD branch (#185)
144
+ - Fixed stale tags in create-pr when library pipeline generates a new commit after tag creation (#187)
145
+
146
+ ### 🗑️ Removed
147
+ - Removed per-file custom version editing (option `e`) from bump-version interactive selection (#187)
148
+
149
+
150
+ ## [2.64.1] - 2026-06-08
151
+
152
+ ### ✨ Added
153
+ - Multi-language extractor registry with pluggable architecture for Library book generation [AUT-3742] (#184)
154
+ - JS extractor as first pluggable extractor with Tree-sitter parsing, extractor notes template, and test harness [AUT-3742] (#184)
155
+ - ESLint rule preventing `lib/` from statically importing `.library/` modules [AUT-3742] (#184)
156
+ - Branch-scoped tag lookup functions `getLatestLocalTagOnBranch()` and `getLatestRemoteTagOnBranch()` in git-tag-manager (#185)
157
+
158
+ ### 🔧 Changed
159
+ - Library pipeline (`createPrPipeline`) now derives `booksDir`/`sourceDir` from overridden `repoRoot`, preventing git-hooks paths from leaking into foreign repos (#186)
160
+ - Version alignment scoped to base branch tags instead of global tag list (#185)
161
+ - Bump-version interactive file selection simplified to direct toggle list with non-semver files pre-deselected
162
+ - Moved arrow-function extraction from monolithic `extract.js` to pluggable extractor module [AUT-3742]
163
+
164
+ ### 🐛 Fixed
165
+ - Excluded non-semver files (e.g., Maven `${revision}`) from version resolution in bump-version command, preventing `parseVersion()` failures
166
+ - Fixed library pipeline using hardcoded git-hooks paths when running in foreign repos (#186)
167
+ - Scoped local tag lookup to HEAD branch to avoid cross-branch version pollution (#185)
168
+
169
+ ### 🗑️ Removed
170
+ - Removed menu-based file selection (all/select/edit/cancel) from bump-version in favor of direct toggle list
171
+ - Removed per-file custom version entry (`targetVersion`) from interactive file selection
172
+
173
+
174
+ ## [2.64.0] - 2026-06-08
175
+
176
+ ### ✨ Added
177
+ - Multi-language extractor registry with pluggable interface and JS extractor (AUT-3742)
178
+ - Extractor notes template for documenting per-language extractors (AUT-3742)
179
+ - Test harness with JS fixtures for extractor validation (AUT-3742)
180
+ - Branch-scoped tag lookup methods `getLatestLocalTagOnBranch()` and `getLatestRemoteTagOnBranch()` in git-tag-manager
181
+ - ESLint `no-restricted-imports` rule preventing `lib/` from statically importing `.library/` modules (AUT-3742)
182
+
183
+ ### 🔧 Changed
184
+ - Moved arrow-function extraction from monolithic `extract.js` to pluggable extractor module under `librarian/extractors/registered/js/` (AUT-3742)
185
+ - `validateVersionAlignment()` now accepts a `baseBranch` parameter, scoping version checks to the relevant branch tags instead of all tags
186
+ - Library pipeline (`createPrPipeline`) now derives `booksDir`/`sourceDir` from resolver config when `repoRoot` is overridden, preventing path leakage into foreign repos
187
+
188
+ ### 🐛 Fixed
189
+ - Fixed local tag lookup returning tags from unrelated branches instead of scoping to HEAD branch
190
+ - Fixed library pipeline leaking git-hooks internal paths into foreign repos during PR creation (#185)
191
+
192
+
193
+ ## [2.63.1] - 2026-06-03
194
+
195
+ ### ✨ Added
196
+
197
+ - Multi-language extractor registry with pluggable interface and runtime validation (AUT-3742, #184)
198
+ - JS extractor as first pluggable extractor with full EXTRACTOR_NOTES documentation (AUT-3742)
199
+ - Extractor notes template for documenting new language extractors (AUT-3742)
200
+ - Test harness with JS fixtures for extractor validation (AUT-3742)
201
+ - Version alignment scoped to base branch tags for accurate cross-branch versioning
202
+
203
+ ### 🔧 Changed
204
+
205
+ - Decoupled lib/ from .library/ imports with ESLint no-restricted-imports rule to enforce package boundary (AUT-3742)
206
+ - Refactored arrow-function extraction from monolithic extract.js into pluggable JS extractor module (AUT-3742)
207
+
208
+ ### 🐛 Fixed
209
+
210
+ - Scoped local tag lookup to HEAD branch to prevent cross-branch version conflicts
211
+
212
+ ## [2.63.0] - 2026-06-03
213
+
214
+ ### ✨ Added
215
+
216
+ - Added multi-language extractor registry with convention-driven loader, priority-based lookup, and warn-and-skip error handling (AUT-3742)
217
+ - Added Extractor interface definition with JSDoc types and runtime validator for pluggable per-language extractors (AUT-3742)
218
+ - Added JS/ESM pluggable extractor conforming to the new Extractor interface (AUT-3742)
219
+ - Added extractor notes template and canonical JS extractor documentation covering supported constructs, known limitations, and test fixtures (AUT-3742)
220
+ - Added ESLint `no-restricted-imports` rule preventing `lib/` from statically importing `.library/` modules (AUT-3742)
221
+ - Added extractor test harness with JS fixtures for eight construct categories (AUT-3742)
222
+
223
+ ### 🔧 Changed
224
+
225
+ - Refactored arrow-function extraction from monolithic `extract.js` into standalone pluggable extractor module under `librarian/extractors/registered/js/` (AUT-3742)
226
+ - Decoupled `lib/` source code from `.library/` imports — runtime integration now requires dynamic `import()` with graceful degradation (AUT-3742)
227
+
228
+ ## [2.62.0] - 2026-06-02
229
+
230
+ ### ✨ Added
231
+
232
+ - Added structure detector pipeline for Library bootstrap — 3-stage detection (filesystem scan → Haiku inference → interactive console) that generates `.library/structure.yaml` config (AUT-3741)
233
+ - Added deterministic filesystem scanner with configurable depth/child limits, BOM file detection, and language-by-layer counting (AUT-3741)
234
+ - Added Haiku-based structure inference with exponential-backoff retry, scanner-detected language reconciliation, and graceful degradation to stub config on API failure (AUT-3741)
235
+ - Added interactive 3-option console flow (Accept / Reject / Retry) with re-run safety supporting Keep / Overwrite / Merge of existing configs (AUT-3741)
236
+ - Added structure detector documentation to Library README including sequence diagram, consumer contract, config format, and worked example (AUT-3741)
237
+
238
+ ### 🐛 Fixed
239
+
240
+ - Fixed tag staleness during Library regeneration (#179)
241
+
8
242
  ## [2.61.2] - 2026-06-01
9
243
 
10
244
  ### 🐛 Fixed
11
- - Fixed incorrect import path for gotcha solicitation in PR pipeline
12
245
 
246
+ - Fixed incorrect import path for gotcha solicitation in PR pipeline
13
247
 
14
248
  ## [2.61.1] - 2026-06-01
15
249
 
16
250
  ### ✨ Added
251
+
17
252
  - Auto-regeneration of stale Library books during `create-release` — stale books are now regenerated before the release is tagged, keeping the tag accurate (#178, AUT-3738)
18
253
 
19
254
  ### 🔧 Changed
255
+
20
256
  - Staleness warning templates are now context-aware: `CONSOLE_WARNING_TEMPLATE` and `PR_BODY_SECTION_TEMPLATE` accept an `autoRegen` option to tailor messaging per consumer (`will-run` for create-release, `deferred` for bump-version, `completed`/`failed` in PR body)
21
257
  - PR body staleness section now reflects the actual auto-regeneration outcome instead of always listing remediation scripts
22
258
  - Console staleness warning in `bump-version` now tells the user that Library will be auto-regenerated when they run `create-pr`
23
259
 
24
260
  ### 🗑️ Removed
25
- - Removed static recommended-scripts list from console staleness warnings (replaced by context-aware auto-regeneration messages)
26
261
 
262
+ - Removed static recommended-scripts list from console staleness warnings (replaced by context-aware auto-regeneration messages)
27
263
 
28
264
  ## [2.61.0] - 2026-06-01
29
265
 
30
266
  ### ✨ Added
267
+
31
268
  - Added canonical staleness-warning templates (`CONSOLE_WARNING_TEMPLATE`, `PR_BODY_SECTION_TEMPLATE`, `PR_TAG_VALUE`) for consistent Library staleness messaging across all consumer commands (AUT-3738)
32
269
  - Added non-blocking Library staleness verification gate to the `create-release` workflow — warns operators when Library books are out of date without blocking the release (AUT-3738)
33
270
  - Added automatic release PR creation step in `create-release` with idempotency check, staleness section in PR body wrapped in marker comments, and `library-stale` label when applicable (AUT-3738)
34
271
  - Added `LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE` constant for release-specific Library verification skip messaging (AUT-3738)
35
272
 
36
273
  ### 🔧 Changed
274
+
37
275
  - Refactored `bump-version` Library warning to use the canonical `CONSOLE_WARNING_TEMPLATE` instead of inline formatting (AUT-3738)
38
276
  - Changed `create-release` Library staleness gate from blocking (exit on stale) to non-blocking (warn and continue), using the librarian `verify()` API instead of raw staleness tools (AUT-3738)
39
277
  - Refactored `library-warnings.js` to re-export canonical wording from the librarian messages module instead of defining constants inline (AUT-3738)
40
278
  - Updated Library books for `create-release`, `bump-version`, and `library-warnings` to reflect new dependencies and exports (AUT-3738)
41
279
 
42
280
  ### 🗑️ Removed
281
+
43
282
  - Removed blocking Library staleness check from `create-release` that imported `.library/tools/staleness.js` directly and aborted on drift (AUT-3738)
44
283
  - Removed `LIBRARY_STALE_WARNING` constant from `library-warnings.js`, replaced by canonical `CONSOLE_WARNING_TEMPLATE` (AUT-3738)
45
284
 
46
-
47
285
  ## [2.61.0] - 2026-05-29
48
286
 
49
287
  ### ✨ Added
@@ -48,9 +48,7 @@ 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';
@@ -209,7 +207,9 @@ function displayDiscoveryTable(discovery) {
209
207
  );
210
208
  console.log('');
211
209
 
212
- if (discovery.files.length === 0) {
210
+ const visibleFiles = discovery.files.filter((f) => f.selected);
211
+
212
+ if (visibleFiles.length === 0) {
213
213
  console.log(' No version files found.');
214
214
  console.log('');
215
215
  return;
@@ -221,8 +221,8 @@ function displayDiscoveryTable(discovery) {
221
221
  );
222
222
  console.log(` ${'-'.repeat(3)} ${'-'.repeat(35)} ${'-'.repeat(15)} ${'-'.repeat(15)}`);
223
223
 
224
- // Table rows
225
- discovery.files.forEach((file, index) => {
224
+ // Table rows (only semver files)
225
+ visibleFiles.forEach((file, index) => {
226
226
  const num = `${index + 1}`.padEnd(3);
227
227
  const filePath = file.relativePath.padEnd(35);
228
228
  const fileType = file.projectLabel.padEnd(15);
@@ -293,70 +293,20 @@ function showDryRunPreview(info) {
293
293
  */
294
294
  async function promptFileSelection(discovery) {
295
295
  console.log('');
296
- showWarning('Version mismatch detected or interactive mode enabled');
297
- console.log('');
298
-
299
- const options = [
300
- { key: 'a', label: 'Update all files' },
301
- { key: 's', label: 'Select which files to update' },
302
- { key: 'e', label: 'Edit version per file (advanced)' },
303
- { key: 'c', label: 'Cancel' }
304
- ];
305
-
306
- const choice = await promptMenu('Choose action:', options, 'a');
307
-
308
- if (choice === 'c') {
309
- showInfo('Version bump cancelled');
310
- process.exit(0);
311
- }
312
296
 
313
- if (choice === 'a') {
314
- // All files selected
315
- return discovery.files;
316
- }
317
-
318
- if (choice === 's') {
319
- // Toggle list selection
320
- const items = discovery.files.map((file, index) => ({
321
- key: `${index}`,
322
- label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
323
- selected: file.selected
324
- }));
325
-
326
- const selectedKeys = await promptToggleList(
327
- 'Select files to update (type number to toggle, Enter to confirm):',
328
- items
329
- );
330
-
331
- // Filter files by selected keys
332
- return discovery.files.filter((_, index) => selectedKeys.includes(`${index}`));
333
- }
334
-
335
- if (choice === 'e') {
336
- // Per-file version editing
337
- showInfo('Enter target version for each file (press Enter to keep calculated version)');
338
- console.log('');
339
-
340
- for (const file of discovery.files) {
341
- const input = await promptEditField(
342
- `${file.relativePath} (${file.projectLabel})`,
343
- file.version || 'N/A'
344
- );
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
+ }));
345
303
 
346
- // If user entered something different from current version, store it
347
- if (input !== file.version && input !== 'N/A') {
348
- if (!validateVersionFormat(input)) {
349
- showWarning(`Invalid version format: ${input} — skipping ${file.relativePath}`);
350
- continue;
351
- }
352
- file.targetVersion = input;
353
- }
354
- }
355
-
356
- return discovery.files;
357
- }
304
+ const selectedKeys = await promptToggleList(
305
+ 'Select files to update (type number to toggle, Enter to confirm):',
306
+ items
307
+ );
358
308
 
359
- return discovery.files;
309
+ return semverFiles.filter((_, index) => selectedKeys.includes(`${index}`));
360
310
  }
361
311
 
362
312
  /**
@@ -504,15 +454,6 @@ export async function runBumpVersion(args) {
504
454
  // Display discovery table
505
455
  displayDiscoveryTable(discovery);
506
456
 
507
- const currentVersion = discovery.resolvedVersion;
508
-
509
- if (!currentVersion) {
510
- showError('Could not determine current version from discovered files');
511
- process.exit(1);
512
- }
513
-
514
- showInfo(`Current version: ${currentVersion}`);
515
-
516
457
  // Step 3: File selection (if mismatch or interactive mode)
517
458
  let selectedFiles = discovery.files.filter((f) => f.selected);
518
459
 
@@ -528,6 +469,22 @@ export async function runBumpVersion(args) {
528
469
  console.log('');
529
470
  }
530
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
+
531
488
  // Step 4: Calculate new version or apply suffix operation
532
489
  logger.debug('bump-version', 'Step 4: Calculating new version');
533
490
  let newVersion;
@@ -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 &&
@@ -664,78 +776,6 @@ export async function runCreatePr(args) {
664
776
  logger.debug('create-pr', 'No unpushed tags found, continuing');
665
777
  }
666
778
 
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
-
739
779
  // Step 6: Generate PR metadata using engine
740
780
  logger.debug('create-pr', 'Step 6: Generating PR metadata with engine');
741
781
  showInfo('Generating PR metadata with Claude...');
@@ -18,8 +18,6 @@ import { fetchFileContent, fetchDirectoryListing, createIssue } from '../utils/g
18
18
  import { promptMenu, promptEditField, promptConfirmation } from '../utils/interactive-ui.js';
19
19
  import logger from '../utils/logger.js';
20
20
  import { commands } from '../cli-metadata.js';
21
- import { fetchLibraryContent as librarianFetch } from '../../.library/librarian/index.js';
22
-
23
21
  /**
24
22
  * Get claude-hooks source repo coordinates from package.json
25
23
  * Why: AI help must always fetch from the tool's own repo, not the user's current repo
@@ -192,10 +190,22 @@ const _readPackageFile = async (relativePath) => {
192
190
  * Why: The catalog provides navigational context for the AI librarian (Pass 1).
193
191
  * Delegates to the librarian module for directory discovery and routing.
194
192
  *
193
+ * Uses dynamic import() because .library/ is not shipped in the npm package —
194
+ * it only exists in the source repo. When running from a global install,
195
+ * the import fails gracefully and the help command falls back to static help.
196
+ *
195
197
  * @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
196
198
  */
197
199
  const readLibraryCatalog = async () => {
198
- // Fetching delegated to the librarian module; see .library/librarian/
200
+ let librarianFetch;
201
+ try {
202
+ const mod = await import('../../.library/librarian/index.js');
203
+ librarianFetch = mod.fetchLibraryContent;
204
+ } catch {
205
+ logger.debug('help - readLibraryCatalog', 'Library not available (expected in global install)');
206
+ return null;
207
+ }
208
+
199
209
  try {
200
210
  const result = await librarianFetch(null, { repoRoot: _packageRoot, full: true });
201
211
  if (result.catalog) {
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * File: library-warnings.js
3
- * Purpose: Warning wording for Library verification gates in claude-hooks.
3
+ * Purpose: Staleness-warning wording and rendering for Library verification
4
+ * gates in claude-hooks.
4
5
  *
5
- * Staleness wording is canonical in the librarian messages module
6
- * (.library/librarian/messages/staleness-warnings.js) this file
7
- * re-exports it for claude-hooks consumers. Do not edit wording here;
8
- * edit the librarian template instead.
6
+ * These are consumer rendering functions they take structured VerifyResult
7
+ * data from the Library and produce display-ready text (console) or Markdown
8
+ * (PR body). The Library produces the data; this module formats it.
9
+ *
10
+ * Constraints:
11
+ * - Pure functions of (VerifyResult, opts) — no I/O, no globals, no environment
12
+ * - Do NOT import from .library/ — lib/ must not depend on unshipped paths
9
13
  *
10
14
  * Related tickets:
11
15
  * AUT-3767 — original placeholder (retired by AUT-3769)
@@ -14,11 +18,125 @@
14
18
  * AUT-3738 — parent user story
15
19
  */
16
20
 
17
- export {
18
- CONSOLE_WARNING_TEMPLATE,
19
- PR_BODY_SECTION_TEMPLATE,
20
- PR_TAG_VALUE
21
- } from '../../.library/librarian/messages/staleness-warnings.js';
21
+ /**
22
+ * Render an assertive console warning for Library staleness.
23
+ *
24
+ * Returns plain text with a marker and the list of stale books.
25
+ * The closing section varies by consumer context via `opts.autoRegen`:
26
+ * - `'will-run'` — create-release: regen runs in the same command
27
+ * - `'deferred'` — bump-version: regen runs later via create-pr
28
+ *
29
+ * Consumers apply visual formatting (box, color, stderr routing) on top.
30
+ *
31
+ * Returns an empty string when the Library is clean.
32
+ *
33
+ * @param {Object} verifyResult
34
+ * @param {boolean} verifyResult.clean
35
+ * @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
36
+ * @param {string[]} verifyResult.recommendedScripts
37
+ * @param {Object} opts
38
+ * @param {'will-run'|'deferred'} opts.autoRegen — controls the call-to-action
39
+ * @returns {string} Plain-text console warning, or empty string if clean
40
+ */
41
+ export function CONSOLE_WARNING_TEMPLATE(verifyResult, opts = {}) {
42
+ if (verifyResult.clean) {
43
+ return '';
44
+ }
45
+
46
+ const lines = [];
47
+
48
+ lines.push('⚠️ Library is stale');
49
+ lines.push('');
50
+ lines.push('The following books are out of date:');
51
+ lines.push('');
52
+
53
+ for (const book of verifyResult.staleBooks) {
54
+ lines.push(` • ${book.path}`);
55
+ for (const reason of book.reasons) {
56
+ lines.push(` - ${reason}`);
57
+ }
58
+ }
59
+
60
+ lines.push('');
61
+
62
+ if (opts.autoRegen === 'will-run') {
63
+ lines.push('Auto-regeneration will run before the release is tagged.');
64
+ } else if (opts.autoRegen === 'deferred') {
65
+ lines.push('Library will be auto-regenerated when you run create-pr.');
66
+ }
67
+
68
+ return lines.join('\n');
69
+ }
70
+
71
+ /**
72
+ * Render a Markdown section for the PR body when Library is stale.
73
+ *
74
+ * The section heading is exactly `## ⚠️ Library is stale` — do not
75
+ * change this; downstream tooling may key on it. Includes stale-books
76
+ * list with reasons and a remediation section that varies by
77
+ * `opts.autoRegen`:
78
+ * - `'completed'` — auto-regen ran successfully; no scripts listed
79
+ * - `'failed'` — auto-regen failed; scripts listed as fallback
80
+ *
81
+ * The acknowledgement line is always present.
82
+ *
83
+ * Returns an empty string when the Library is clean.
84
+ *
85
+ * @param {Object} verifyResult
86
+ * @param {boolean} verifyResult.clean
87
+ * @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
88
+ * @param {string[]} verifyResult.recommendedScripts
89
+ * @param {Object} opts
90
+ * @param {'completed'|'failed'} opts.autoRegen — controls the remediation section
91
+ * @returns {string} Markdown section, or empty string if clean
92
+ */
93
+ export function PR_BODY_SECTION_TEMPLATE(verifyResult, opts = {}) {
94
+ if (verifyResult.clean) {
95
+ return '';
96
+ }
97
+
98
+ const lines = [];
99
+
100
+ lines.push('## ⚠️ Library is stale');
101
+ lines.push('');
102
+ lines.push('The following Library books are out of date:');
103
+ lines.push('');
104
+
105
+ for (const book of verifyResult.staleBooks) {
106
+ const reasonText = book.reasons.join('; ');
107
+ lines.push(`- \`${book.path}\` — ${reasonText}`);
108
+ }
109
+
110
+ lines.push('');
111
+
112
+ if (opts.autoRegen === 'completed') {
113
+ lines.push('**Auto-regeneration was performed.** The books listed above were regenerated in the release commit.');
114
+ } else if (opts.autoRegen === 'failed') {
115
+ lines.push('**Recommended remediation scripts:**');
116
+ lines.push('');
117
+ for (const script of verifyResult.recommendedScripts) {
118
+ lines.push(`- \`${script}\``);
119
+ }
120
+ lines.push('');
121
+ lines.push('_Auto-regeneration was attempted but failed. Run the scripts manually._');
122
+ }
123
+
124
+ lines.push('');
125
+ lines.push(
126
+ '_This release was tagged via claude-hooks. The Library lifecycle pipeline is integrated with `create-pr` and `back-merge` — staleness at release time indicates a path that bypassed the pipeline._'
127
+ );
128
+
129
+ return lines.join('\n');
130
+ }
131
+
132
+ /**
133
+ * Tag value used to mark a PR or Linear issue as library-stale.
134
+ *
135
+ * Applied as a Linear label slug or a PR-title prefix by consumers.
136
+ *
137
+ * @type {string}
138
+ */
139
+ export const PR_TAG_VALUE = 'library-stale';
22
140
 
23
141
  export const LIBRARY_VERIFY_SKIPPED_WARNING =
24
142
  'Library verification skipped due to an unexpected error. ' +
@@ -502,10 +502,11 @@ async function verifySDKConnection() {
502
502
  * @param {number} options.timeout - Timeout in milliseconds (default: 120000 = 2 minutes)
503
503
  * @param {string} options.model - Claude model override (e.g., 'haiku', 'sonnet', 'opus')
504
504
  * @param {boolean} options.headless - Use SDK instead of CLI (default: false)
505
+ * @param {boolean} options.print - Use CLI print mode (-p): text-only output, no tool use (default: false)
505
506
  * @returns {Promise<string>} Claude's response
506
507
  * @throws {ClaudeClientError} If execution fails or times out
507
508
  */
508
- const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null } = {}) => {
509
+ const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null, print = false } = {}) => {
509
510
  // Headless mode: use Anthropic SDK directly (GH#133)
510
511
  // Branch here (not in executeClaudeWithRetry) because analyzeCode calls
511
512
  // executeClaude directly via withRetry — branching here covers all paths.
@@ -548,10 +549,14 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = nu
548
549
  // executed inside bash --login so that .profile/.bashrc set up the correct PATH.
549
550
  // All CLI flags are embedded in the bash -c command string (no spaces in flag values).
550
551
  const claudeParts = [loginShell.path];
552
+ if (print) claudeParts.push('-p');
551
553
  if (allowedTools.length > 0) claudeParts.push(`--allowedTools ${allowedTools.join(',')}`);
552
554
  if (model) claudeParts.push(`--model ${model}`);
553
555
  finalArgs.push('bash', '-lc', claudeParts.join(' '));
554
556
  } else {
557
+ if (print) {
558
+ finalArgs.push('-p');
559
+ }
555
560
  if (allowedTools.length > 0) {
556
561
  // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
557
562
  finalArgs.push('--allowedTools', allowedTools.join(','));
@@ -196,6 +196,51 @@ export function getLatestLocalTag() {
196
196
  }
197
197
  }
198
198
 
199
+ /**
200
+ * Gets latest local tag reachable from HEAD
201
+ * Why: Excludes rogue tags from unrelated branches that were fetched locally
202
+ *
203
+ * @returns {string|null} Latest semver tag reachable from HEAD, or null
204
+ */
205
+ export function getLatestLocalTagOnBranch() {
206
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'Getting latest local tag on HEAD');
207
+
208
+ try {
209
+ const output = execGitTagCommand('git tag --merged HEAD --sort=-v:refname');
210
+
211
+ if (!output) {
212
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'No tags on HEAD');
213
+ return null;
214
+ }
215
+
216
+ const tags = output.split(/\r?\n/).filter((t) => t.length > 0);
217
+ const semverTags = tags.filter(isSemverTag);
218
+
219
+ if (semverTags.length === 0) {
220
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'No semver tags on HEAD', {
221
+ totalTags: tags.length
222
+ });
223
+ return null;
224
+ }
225
+
226
+ const latestTag = semverTags[0];
227
+
228
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'Latest tag on HEAD', {
229
+ latestTag,
230
+ semverTags: semverTags.length
231
+ });
232
+
233
+ return latestTag;
234
+ } catch (error) {
235
+ logger.error(
236
+ 'git-tag-manager - getLatestLocalTagOnBranch',
237
+ 'Failed to get tags on HEAD',
238
+ error
239
+ );
240
+ return null;
241
+ }
242
+ }
243
+
199
244
  /**
200
245
  * Gets all remote tags
201
246
  * Why: Compare local tags with remote for push status
@@ -311,6 +356,65 @@ export async function getLatestRemoteTag(remoteName = null) {
311
356
  }
312
357
  }
313
358
 
359
+ /**
360
+ * Gets latest remote tag reachable from a specific branch
361
+ * Why: Scoped comparison — avoids rogue tags pushed from unmerged feature branches
362
+ *
363
+ * @param {string} baseBranch - Branch to scope tags to (e.g., 'develop')
364
+ * @param {string} remoteName - Remote name (default: 'origin')
365
+ * @returns {string|null} Latest semver tag name reachable from the branch, or null
366
+ */
367
+ export function getLatestRemoteTagOnBranch(baseBranch, remoteName = null) {
368
+ const remote = remoteName || getRemoteName();
369
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'Getting latest tag on branch', {
370
+ baseBranch,
371
+ remote
372
+ });
373
+
374
+ try {
375
+ // Ensure remote branch ref is fresh
376
+ execGitTagCommand(`git fetch ${remote} ${baseBranch} --quiet`);
377
+
378
+ // Get tags merged into the remote branch, sorted by version descending
379
+ const output = execGitTagCommand(
380
+ `git tag --merged ${remote}/${baseBranch} --sort=-v:refname`
381
+ );
382
+
383
+ if (!output) {
384
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'No tags on branch');
385
+ return null;
386
+ }
387
+
388
+ const tags = output.split(/\r?\n/).filter((line) => line.length > 0);
389
+ const semverTags = tags.filter(isSemverTag);
390
+
391
+ if (semverTags.length === 0) {
392
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'No semver tags on branch', {
393
+ totalTags: tags.length
394
+ });
395
+ return null;
396
+ }
397
+
398
+ // Already sorted by git --sort=-v:refname (descending), first is latest
399
+ const latestTag = semverTags[0];
400
+
401
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'Latest tag on branch', {
402
+ baseBranch,
403
+ latestTag,
404
+ semverTags: semverTags.length
405
+ });
406
+
407
+ return latestTag;
408
+ } catch (error) {
409
+ logger.error(
410
+ 'git-tag-manager - getLatestRemoteTagOnBranch',
411
+ 'Failed to get tags on branch',
412
+ error
413
+ );
414
+ return null;
415
+ }
416
+ }
417
+
314
418
  /**
315
419
  * Checks if tag exists
316
420
  * Why: Prevents duplicate tag creation
@@ -139,7 +139,8 @@ const judgeAndFix = async (analysisResult, filesData, config, { headless = false
139
139
  const response = await executeClaudeWithRetry(prompt, {
140
140
  model,
141
141
  timeout: judgeTimeout,
142
- headless
142
+ headless,
143
+ print: true
143
144
  });
144
145
 
145
146
  const parsed = extractJSON(response);
@@ -248,13 +248,14 @@ export function discoverVersionFiles(options = {}) {
248
248
  const registry = VERSION_FILE_TYPES[fileType];
249
249
  if (registry && entry.name === registry.filename) {
250
250
  const version = registry.readVersion(fullPath);
251
+ const isSemver = version !== null && validateVersionFormat(version);
251
252
  const descriptor = {
252
253
  path: fullPath,
253
254
  relativePath: path.relative(repoRoot, fullPath),
254
255
  type: fileType,
255
256
  projectLabel: registry.projectLabel,
256
257
  version,
257
- selected: true
258
+ selected: isSemver
258
259
  };
259
260
  discoveredFiles.push(descriptor);
260
261
  logger.debug('version-manager - discoverVersionFiles', 'Found version file', {
@@ -285,19 +286,19 @@ export function discoverVersionFiles(options = {}) {
285
286
  return a.relativePath.localeCompare(b.relativePath);
286
287
  });
287
288
 
288
- // Determine resolved version (prefer root-level file, then first found)
289
+ // Determine resolved version from semver files only (prefer root-level, then first found)
289
290
  let resolvedVersion = null;
290
- const rootFile = discoveredFiles.find((f) => !f.relativePath.includes(path.sep));
291
+ const semverFiles = discoveredFiles.filter((f) => f.selected);
292
+ const rootFile = semverFiles.find((f) => !f.relativePath.includes(path.sep));
291
293
  if (rootFile && rootFile.version) {
292
294
  resolvedVersion = rootFile.version;
293
- } else if (discoveredFiles.length > 0) {
294
- // Use first file's version as fallback
295
- const firstWithVersion = discoveredFiles.find((f) => f.version !== null);
295
+ } else if (semverFiles.length > 0) {
296
+ const firstWithVersion = semverFiles.find((f) => f.version !== null);
296
297
  resolvedVersion = firstWithVersion ? firstWithVersion.version : null;
297
298
  }
298
299
 
299
- // Check for version mismatch
300
- const versions = discoveredFiles.filter((f) => f.version !== null).map((f) => f.version);
300
+ // Check for version mismatch (only among semver files)
301
+ const versions = semverFiles.filter((f) => f.version !== null).map((f) => f.version);
301
302
  const uniqueVersions = [...new Set(versions)];
302
303
  const mismatch = uniqueVersions.length > 1;
303
304
 
@@ -1149,24 +1150,36 @@ export function compareVersions(version1, version2) {
1149
1150
  *
1150
1151
  * @returns {Promise<Object>} Validation result with alignment status and issues
1151
1152
  */
1152
- export async function validateVersionAlignment() {
1153
- logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment');
1153
+ export async function validateVersionAlignment(baseBranch = null) {
1154
+ logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment', {
1155
+ baseBranch
1156
+ });
1154
1157
 
1155
1158
  try {
1156
1159
  // Discover all version files
1157
1160
  const discovery = discoverVersionFiles();
1158
1161
 
1159
- // Get git tag version (parseTagVersion returns null for non-semver tags)
1160
- const { getLatestLocalTag, parseTagVersion } = await import('./git-tag-manager.js');
1161
- const latestTag = getLatestLocalTag();
1162
+ // Get git tag version scoped to HEAD when baseBranch is provided
1163
+ const {
1164
+ getLatestLocalTag, getLatestLocalTagOnBranch,
1165
+ parseTagVersion,
1166
+ getLatestRemoteTag, getLatestRemoteTagOnBranch
1167
+ } = await import('./git-tag-manager.js');
1168
+ const latestTag = baseBranch ? getLatestLocalTagOnBranch(baseBranch) : getLatestLocalTag();
1162
1169
  const tagVersion = latestTag ? parseTagVersion(latestTag) : null;
1163
1170
 
1164
1171
  // Get CHANGELOG version
1165
1172
  const changelogVersion = readChangelogVersion();
1166
-
1167
- // Get remote tag version (parseTagVersion returns null for non-semver tags)
1168
- const { getLatestRemoteTag } = await import('./git-tag-manager.js');
1169
- const latestRemoteTag = await getLatestRemoteTag();
1173
+ let latestRemoteTag;
1174
+ if (baseBranch) {
1175
+ latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
1176
+ // Fall back to global if branch-scoped lookup returns nothing
1177
+ if (!latestRemoteTag) {
1178
+ latestRemoteTag = await getLatestRemoteTag();
1179
+ }
1180
+ } else {
1181
+ latestRemoteTag = await getLatestRemoteTag();
1182
+ }
1170
1183
  const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
1171
1184
 
1172
1185
  // Collect all local versions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.61.2",
3
+ "version": "2.66.1",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,8 @@
16
16
  "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
17
  "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
18
  "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
+ "test:e2e": "bash test/manual/sdlc-stability-check.sh",
20
+ "test:full": "npm run test:all && npm run test:e2e",
19
21
  "lint": "eslint lib/ bin/claude-hooks .library/librarian/",
20
22
  "lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
21
23
  "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",