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.
@@ -4,18 +4,21 @@
4
4
  *
5
5
  * Workflow:
6
6
  * 1. Validate preconditions (clean tree, on develop, up-to-date, no existing RC)
7
- * 2. Discover version files — abort on mismatch
8
- * 3. Calculate next version
9
- * 4. [dry-run] Preview + return
10
- * 5. Confirm with user
11
- * 6. Create RC branch from develop
12
- * 7. Bump version files + commit (--no-verify)
13
- * 8. [optional] Generate CHANGELOG
14
- * 9. Create Git tag (skip if already exists)
15
- * 10. Push RC branch (unless --skip-push)
16
- * 11. Deploy to shadow (unless --no-shadow or --skip-push)
17
- * 12. Return to RC branch
18
- * 13. Display summary
7
+ * 2. Library verification gate (non-blocking)
8
+ * 3. Discover version files — abort on mismatch
9
+ * 4. Calculate next version
10
+ * 5. [dry-run] Preview + return
11
+ * 6. Confirm with user
12
+ * 7. Create RC branch from develop
13
+ * 8. Bump version files
14
+ * 9. [if stale] Library regeneration (keeps tag accurate)
15
+ * 10. [optional] Generate CHANGELOG
16
+ * 11. Stage all + commit (--no-verify)
17
+ * 12. Create Git tag (skip if already exists)
18
+ * 13. Push RC branch (unless --skip-push)
19
+ * 14. Create release PR (unless --skip-push or push failed)
20
+ * 15. Deploy to shadow (unless --no-shadow or --skip-push)
21
+ * 16. Return to RC branch + display summary
19
22
  */
20
23
 
21
24
  import { execSync } from 'child_process';
@@ -57,6 +60,12 @@ import {
57
60
  } from '../utils/interactive-ui.js';
58
61
  import logger from '../utils/logger.js';
59
62
  import { colors, error, checkGitRepo } from './helpers.js';
63
+ import {
64
+ CONSOLE_WARNING_TEMPLATE,
65
+ PR_BODY_SECTION_TEMPLATE,
66
+ PR_TAG_VALUE,
67
+ LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE
68
+ } from '../messages/library-warnings.js';
60
69
 
61
70
  /** Source branch for RC creation */
62
71
  const SOURCE_BRANCH = 'develop';
@@ -264,6 +273,51 @@ function restoreSnapshot(snapshot) {
264
273
  });
265
274
  }
266
275
 
276
+ /**
277
+ * Emit a loud Library staleness warning to stderr.
278
+ * Content comes from the canonical template + verify result data.
279
+ * Formatting uses ANSI codes for visual emphasis on TTY stderr.
280
+ *
281
+ * @param {import('../../.library/librarian/index.js').VerifyResult} result
282
+ * @private
283
+ */
284
+ function _emitLibraryWarning(result) {
285
+ const y = colors.yellow;
286
+ const r = colors.reset;
287
+ const bar = `${y}${'='.repeat(63)}${r}`;
288
+ const content = CONSOLE_WARNING_TEMPLATE(result, { autoRegen: 'will-run' });
289
+
290
+ const lines = ['', bar, '', `${y}${content}${r}`, '', bar, ''];
291
+
292
+ process.stderr.write(lines.join('\n'));
293
+ }
294
+
295
+ /** Marker comment pair wrapping the staleness section in PR bodies */
296
+ const STALENESS_MARKER_OPEN = '<!-- LIBRARY_STALENESS_SECTION -->';
297
+ const STALENESS_MARKER_CLOSE = '<!-- /LIBRARY_STALENESS_SECTION -->';
298
+
299
+ /**
300
+ * Build the release PR body, optionally appending the staleness section.
301
+ *
302
+ * @param {string} nextVersion
303
+ * @param {import('../../.library/librarian/index.js').VerifyResult|null} verifyResult
304
+ * @param {'completed'|'failed'|null} regenOutcome - result of auto-regeneration
305
+ * @returns {string}
306
+ * @private
307
+ */
308
+ function _buildReleasePrBody(nextVersion, verifyResult, regenOutcome) {
309
+ let body = `## Release V${nextVersion}\n\nRelease-candidate branch created from \`${SOURCE_BRANCH}\`.`;
310
+
311
+ if (verifyResult && !verifyResult.clean) {
312
+ const section = PR_BODY_SECTION_TEMPLATE(verifyResult, {
313
+ autoRegen: regenOutcome || 'failed'
314
+ });
315
+ body += `\n\n${STALENESS_MARKER_OPEN}\n${section}\n${STALENESS_MARKER_CLOSE}`;
316
+ }
317
+
318
+ return body;
319
+ }
320
+
267
321
  /**
268
322
  * create-release command
269
323
  * Creates a release-candidate branch from develop, bumps version, and deploys to shadow.
@@ -318,8 +372,25 @@ export async function runCreateRelease(args) {
318
372
  showSuccess('Preconditions validated');
319
373
  console.log('');
320
374
 
321
- // Step 2: Discover version files
322
- logger.debug('create-release', 'Step 2: Discovering version files');
375
+ // Step 2: Library verification gate — silent on clean, loud-warn on stale, never blocks
376
+ let verifyResult = null;
377
+ logger.debug('create-release', 'Step 2: Running Library verification gate');
378
+ try {
379
+ const { verify } = await import('../../.library/librarian/index.js');
380
+ verifyResult = await verify();
381
+
382
+ if (verifyResult.clean) {
383
+ logger.debug('create-release', 'Library is clean — no warning needed');
384
+ } else {
385
+ _emitLibraryWarning(verifyResult);
386
+ }
387
+ } catch (verifyErr) {
388
+ const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE} ${verifyErr.message}${colors.reset}\n\n`;
389
+ process.stderr.write(msg);
390
+ }
391
+
392
+ // Step 3: Discover version files
393
+ logger.debug('create-release', 'Step 3: Discovering version files');
323
394
  const discovery = discoverVersionFiles();
324
395
 
325
396
  if (discovery.files.length === 0) {
@@ -356,7 +427,7 @@ export async function runCreateRelease(args) {
356
427
  process.exit(1);
357
428
  }
358
429
 
359
- // Step 3: Calculate next version
430
+ // Step 4: Calculate next version
360
431
  const nextVersion = incrementVersion(currentVersion, options.bumpType);
361
432
  const rcBranch = `${RC_PREFIX}/V${nextVersion}`;
362
433
  const tagName = formatTagName(nextVersion);
@@ -366,13 +437,13 @@ export async function runCreateRelease(args) {
366
437
  showInfo(`Branch: ${rcBranch}`);
367
438
  console.log('');
368
439
 
369
- // Step 4: Dry-run
440
+ // Step 5: Dry-run
370
441
  if (options.dryRun) {
371
442
  showDryRunPreview({ bumpType: options.bumpType, currentVersion, nextVersion, rcBranch, options });
372
443
  return;
373
444
  }
374
445
 
375
- // Step 5: Confirm with user
446
+ // Step 6: Confirm with user
376
447
  const confirmed = await promptConfirmation(
377
448
  `Create ${rcBranch} and bump version to ${nextVersion}?`,
378
449
  true
@@ -388,8 +459,8 @@ export async function runCreateRelease(args) {
388
459
  const config = await getConfig();
389
460
  const selectedFiles = discovery.files.filter((f) => f.selected);
390
461
 
391
- // Step 6: Create RC branch from develop
392
- logger.debug('create-release', 'Step 6: Creating RC branch', { rcBranch });
462
+ // Step 7: Create RC branch from develop
463
+ logger.debug('create-release', 'Step 7: Creating RC branch', { rcBranch });
393
464
  showInfo(`Creating branch ${rcBranch} from ${SOURCE_BRANCH}...`);
394
465
  checkoutBranch(rcBranch, { create: true, startPoint: SOURCE_BRANCH });
395
466
  showSuccess(`✓ Branch created: ${rcBranch}`);
@@ -408,8 +479,8 @@ export async function runCreateRelease(args) {
408
479
  }
409
480
  });
410
481
 
411
- // Step 7: Bump version files
412
- logger.debug('create-release', 'Step 7: Bumping version files');
482
+ // Step 8: Bump version files
483
+ logger.debug('create-release', 'Step 8: Bumping version files');
413
484
  showInfo('Updating version files...');
414
485
  try {
415
486
  updateVersionFiles(selectedFiles, nextVersion);
@@ -433,10 +504,41 @@ export async function runCreateRelease(args) {
433
504
  process.exit(1);
434
505
  }
435
506
 
436
- // Step 8: Optional CHANGELOG
507
+ // Step 9: Library regeneration (if stale — keeps tag accurate)
508
+ let libraryFiles = [];
509
+ let regenOutcome = null;
510
+ if (verifyResult && !verifyResult.clean) {
511
+ logger.debug('create-release', 'Step 9: Running Library regeneration');
512
+ showInfo('📚 Regenerating stale Library books...');
513
+ try {
514
+ const { createPrPipeline } = await import('../../.library/librarian/index.js');
515
+ const root = getRepoRoot();
516
+ const pipelineSummary = await createPrPipeline({ repoRoot: root });
517
+
518
+ libraryFiles = pipelineSummary.modifiedFiles.map(
519
+ (f) => path.join(root, f)
520
+ );
521
+
522
+ regenOutcome = 'completed';
523
+ if (libraryFiles.length > 0) {
524
+ showSuccess(`✓ Library regenerated (${libraryFiles.length} file(s))`);
525
+ } else {
526
+ showInfo('Library pipeline ran — no files changed');
527
+ }
528
+ } catch (regenErr) {
529
+ regenOutcome = 'failed';
530
+ showWarning(`Library regeneration failed: ${regenErr.message}`);
531
+ logger.debug('create-release', 'Library regen failed', {
532
+ error: regenErr.message
533
+ });
534
+ }
535
+ process.stdout.write('\n');
536
+ }
537
+
538
+ // Step 10: Optional CHANGELOG
437
539
  let selectedChangelogPath = null;
438
540
  if (options.updateChangelog) {
439
- logger.debug('create-release', 'Step 8: Generating CHANGELOG');
541
+ logger.debug('create-release', 'Step 10: Generating CHANGELOG');
440
542
  showInfo('Generating CHANGELOG entry...');
441
543
  const changelogResult = await generateChangelogEntry({
442
544
  version: nextVersion,
@@ -462,6 +564,9 @@ export async function runCreateRelease(args) {
462
564
  logger.debug('create-release', 'Staging and committing version bump');
463
565
  showInfo('Staging and committing...');
464
566
  const filesToStage = selectedFiles.map((f) => f.path);
567
+ if (libraryFiles.length > 0) {
568
+ filesToStage.push(...libraryFiles);
569
+ }
465
570
  if (options.updateChangelog) {
466
571
  const changelogPath =
467
572
  selectedChangelogPath || path.join(getRepoRoot(), 'CHANGELOG.md');
@@ -496,8 +601,8 @@ export async function runCreateRelease(args) {
496
601
  showSuccess('✓ Version bump committed');
497
602
  console.log('');
498
603
 
499
- // Step 9: Create Git tag (skip silently if already exists)
500
- logger.debug('create-release', 'Step 9: Creating Git tag', { tagName });
604
+ // Step 11: Create Git tag (skip silently if already exists)
605
+ logger.debug('create-release', 'Step 11: Creating Git tag', { tagName });
501
606
  const tagAlreadyExists = await tagExists(tagName, 'local');
502
607
  if (tagAlreadyExists) {
503
608
  showWarning(`Tag ${tagName} already exists locally — skipping tag creation`);
@@ -511,7 +616,7 @@ export async function runCreateRelease(args) {
511
616
  }
512
617
  console.log('');
513
618
 
514
- // Step 10: Push RC branch (unless --skip-push)
619
+ // Step 12: Push RC branch (unless --skip-push)
515
620
  let pushStatus = 'skipped (--skip-push)';
516
621
  if (!options.skipPush) {
517
622
  showInfo(`Pushing ${rcBranch} to remote...`);
@@ -529,7 +634,66 @@ export async function runCreateRelease(args) {
529
634
  console.log('');
530
635
  }
531
636
 
532
- // Step 11: Deploy to shadow (unless --no-shadow or --skip-push)
637
+ // Step 13: Create release PR (unless --skip-push or push failed)
638
+ let prUrl = null;
639
+ if (!options.skipPush && pushStatus === 'pushed') {
640
+ logger.debug('create-release', 'Step 13: Creating release PR');
641
+ try {
642
+ const { createPullRequest, validateToken, findExistingPR } =
643
+ await import('../utils/github-api.js');
644
+ const { parseGitHubRepo } = await import('../utils/github-client.js');
645
+
646
+ const tokenValidation = await validateToken();
647
+ if (!tokenValidation.valid) {
648
+ showWarning(
649
+ `GitHub token invalid — release PR not created: ${tokenValidation.error}`
650
+ );
651
+ console.log('');
652
+ console.log('Create the PR manually:');
653
+ console.log(` gh pr create --base main --head ${rcBranch}`);
654
+ } else {
655
+ const repoInfo = parseGitHubRepo();
656
+
657
+ // Idempotency: skip if PR already exists for this head → main
658
+ const existingPR = await findExistingPR({
659
+ owner: repoInfo.owner,
660
+ repo: repoInfo.repo,
661
+ head: rcBranch,
662
+ base: 'main'
663
+ });
664
+
665
+ if (existingPR) {
666
+ showInfo(`Release PR already exists: ${existingPR.html_url}`);
667
+ prUrl = existingPR.html_url;
668
+ } else {
669
+ const prBody = _buildReleasePrBody(nextVersion, verifyResult, regenOutcome);
670
+ const labels = verifyResult && !verifyResult.clean
671
+ ? [PR_TAG_VALUE]
672
+ : [];
673
+
674
+ const pr = await createPullRequest({
675
+ owner: repoInfo.owner,
676
+ repo: repoInfo.repo,
677
+ title: `Release V${nextVersion}`,
678
+ body: prBody,
679
+ head: rcBranch,
680
+ base: 'main',
681
+ labels
682
+ });
683
+ prUrl = pr.html_url;
684
+ showSuccess(`✓ Release PR created: ${prUrl}`);
685
+ }
686
+ }
687
+ } catch (prErr) {
688
+ showWarning(`Release PR creation failed: ${prErr.message}`);
689
+ console.log('');
690
+ console.log('Create the PR manually:');
691
+ console.log(` gh pr create --base main --head ${rcBranch}`);
692
+ }
693
+ console.log('');
694
+ }
695
+
696
+ // Step 14: Deploy to shadow (unless --no-shadow or --skip-push)
533
697
  let shadowStatus = 'skipped';
534
698
  if (options.noShadow) {
535
699
  showInfo('Shadow deployment skipped (--no-shadow)');
@@ -551,7 +715,7 @@ export async function runCreateRelease(args) {
551
715
  }
552
716
  }
553
717
 
554
- // Step 12: Ensure we land on RC branch
718
+ // Step 15: Ensure we land on RC branch
555
719
  const currentBranchAfter = getCurrentBranch();
556
720
  if (currentBranchAfter !== rcBranch) {
557
721
  try {
@@ -561,7 +725,7 @@ export async function runCreateRelease(args) {
561
725
  }
562
726
  }
563
727
 
564
- // Step 13: Summary
728
+ // Step 16: Summary
565
729
  console.log('');
566
730
  console.log(
567
731
  `${colors.green}════════════════════════════════════════════════════${colors.reset}`
@@ -579,13 +743,17 @@ export async function runCreateRelease(args) {
579
743
  `${colors.blue}Tag:${colors.reset} ${tagAlreadyExists ? `${tagName} (pre-existing, skipped)` : tagName}`
580
744
  );
581
745
  console.log(`${colors.blue}Push:${colors.reset} ${pushStatus}`);
746
+ console.log(
747
+ `${colors.blue}PR:${colors.reset} ${prUrl || 'skipped'}`
748
+ );
582
749
  console.log(`${colors.blue}Shadow:${colors.reset} ${shadowStatus}`);
583
750
  console.log('');
584
751
 
585
752
  if (options.skipPush) {
586
753
  console.log('Next steps:');
587
754
  console.log(` 1. Push when ready: git push -u origin ${rcBranch}`);
588
- console.log(` 2. Sync shadow: claude-hooks shadow sync ${rcBranch}`);
755
+ console.log(` 2. Create PR: gh pr create --base main --head ${rcBranch}`);
756
+ console.log(` 3. Sync shadow: claude-hooks shadow sync ${rcBranch}`);
589
757
  } else {
590
758
  console.log('Next steps:');
591
759
  console.log(' 1. Verify shadow deployment is running');
@@ -18,6 +18,7 @@ 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';
21
22
 
22
23
  /**
23
24
  * Get claude-hooks source repo coordinates from package.json
@@ -170,12 +171,7 @@ const _packageRoot = path.join(__dirname, '..', '..');
170
171
  */
171
172
  const MAX_BOOKS = 5;
172
173
 
173
- /**
174
- * Catalog directories within .library/ to auto-discover
175
- * Why: These contain index and shelf files that form the navigational catalog.
176
- * books/ is excluded — those are fetched on-demand in Pass 2.
177
- */
178
- const _CATALOG_DIRS = ['by-code', 'by-domain', 'by-task-type'];
174
+ // Catalog routing delegated to the librarian module; see .library/librarian/
179
175
 
180
176
  /**
181
177
  * Read a single file from package root, returning null on failure
@@ -194,67 +190,27 @@ const _readPackageFile = async (relativePath) => {
194
190
  /**
195
191
  * Read the project catalog from .library/ and CLAUDE.md
196
192
  * Why: The catalog provides navigational context for the AI librarian (Pass 1).
197
- * Auto-discovers index/shelf files so the catalog stays current as .library/ grows.
198
- *
199
- * Reads:
200
- * - CLAUDE.md (global rules)
201
- * - .library/index.md (repo identity, capabilities)
202
- * - .library/conventions.md (coding standards)
203
- * - .library/by-code/*.md (source-path shelves)
204
- * - .library/by-domain/*.md (workflow reading lists)
205
- * - .library/by-task-type/*.md (task guidance)
193
+ * Delegates to the librarian module for directory discovery and routing.
206
194
  *
207
195
  * @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
208
196
  */
209
197
  const readLibraryCatalog = async () => {
210
- const sections = [];
211
-
212
- // Fixed files: CLAUDE.md + core library files
213
- const fixedFiles = [
214
- { label: 'CLAUDE.md', path: 'CLAUDE.md' },
215
- { label: '.library/index.md', path: '.library/index.md' },
216
- { label: '.library/conventions.md', path: '.library/conventions.md' }
217
- ];
218
-
219
- for (const file of fixedFiles) {
220
- const content = await _readPackageFile(file.path);
221
- if (content) {
222
- sections.push(`--- ${file.label} ---\n${content}`);
223
- }
224
- }
225
-
226
- // Auto-discover .md files in each catalog directory
227
- for (const dir of _CATALOG_DIRS) {
228
- const dirPath = path.join(_packageRoot, '.library', dir);
229
- let entries;
230
- try {
231
- entries = await fs.readdir(dirPath);
232
- } catch {
233
- logger.debug('help - readLibraryCatalog', `Could not read .library/${dir}/`);
234
- continue;
235
- }
236
-
237
- const mdFiles = entries.filter((f) => f.endsWith('.md')).sort();
238
- for (const file of mdFiles) {
239
- const relativePath = `.library/${dir}/${file}`;
240
- const content = await _readPackageFile(relativePath);
241
- if (content) {
242
- sections.push(`--- ${relativePath} ---\n${content}`);
243
- }
198
+ // Fetching delegated to the librarian module; see .library/librarian/
199
+ try {
200
+ const result = await librarianFetch(null, { repoRoot: _packageRoot, full: true });
201
+ if (result.catalog) {
202
+ logger.debug('help - readLibraryCatalog', 'Catalog loaded via librarian', {
203
+ files: result.readingList.length,
204
+ length: result.catalog.length
205
+ });
244
206
  }
245
- }
246
-
247
- if (sections.length === 0) {
248
- logger.debug('help - readLibraryCatalog', 'No catalog files found');
207
+ return result.catalog;
208
+ } catch (err) {
209
+ logger.debug('help - readLibraryCatalog', 'Librarian fetch failed', {
210
+ error: err.message
211
+ });
249
212
  return null;
250
213
  }
251
-
252
- const catalog = sections.join('\n\n');
253
- logger.debug('help - readLibraryCatalog', 'Catalog loaded', {
254
- sections: sections.length,
255
- length: catalog.length
256
- });
257
- return catalog;
258
214
  };
259
215
 
260
216
  /**
@@ -19,6 +19,7 @@
19
19
  * - resolution-prompt: Issue resolution generation
20
20
  */
21
21
 
22
+ import { join } from 'path';
22
23
  import { getStagedFiles, getRepoRoot, getStagedTreeSha } from '../utils/git-operations.js';
23
24
  import { writeMarker } from '../utils/hooks-verified-marker.js';
24
25
  import { filterFiles } from '../utils/file-operations.js';
@@ -150,6 +151,41 @@ const main = async () => {
150
151
  process.exit(0);
151
152
  }
152
153
 
154
+ // Library staleness check — non-blocking warning
155
+ try {
156
+ const { checkBook } = await import('../../.library/tools/staleness.js');
157
+ const { getBooksDir } = await import('../../.library/paths.js');
158
+ const booksDir = getBooksDir();
159
+ const sourceFiles = validFiles
160
+ .map(f => (typeof f === 'string' ? f : f.path))
161
+ .filter(p => p.startsWith('lib/'));
162
+
163
+ if (sourceFiles.length > 0) {
164
+ const staleBooks = [];
165
+ for (const srcPath of sourceFiles) {
166
+ const bookName = `${srcPath.replace(/^lib\/(?:.*\/)?/, '').replace(/\.js$/, '')}.md`;
167
+ const bookPath = join(booksDir, bookName);
168
+ try {
169
+ const result = await checkBook(bookPath, getRepoRoot());
170
+ if (result.status === 'stale') {
171
+ staleBooks.push(result.book);
172
+ }
173
+ } catch {
174
+ // Book doesn't exist or check failed — skip silently
175
+ }
176
+ }
177
+ if (staleBooks.length > 0) {
178
+ logger.warning(`📚 ${staleBooks.length} library book(s) will become stale after this commit`);
179
+ for (const book of staleBooks) {
180
+ logger.warning(` └─ ${book}`);
181
+ }
182
+ logger.warning(' Run: npm run library:regenerate');
183
+ }
184
+ }
185
+ } catch {
186
+ logger.warning('📚 Library staleness check unavailable — .library/ tools not found');
187
+ }
188
+
153
189
  // Step 3: Run linters (fast, deterministic — before Claude analysis)
154
190
  // Unfixable lint issues are forwarded to the judge for semantic resolution
155
191
  let unfixableLintDetails = [];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * File: library-warnings.js
3
+ * Purpose: Warning wording for Library verification gates in claude-hooks.
4
+ *
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.
9
+ *
10
+ * Related tickets:
11
+ * AUT-3767 — original placeholder (retired by AUT-3769)
12
+ * AUT-3769 — finalized wording + librarian messages module
13
+ * AUT-3770 — create-release integration (PR body, label, console)
14
+ * AUT-3738 — parent user story
15
+ */
16
+
17
+ export {
18
+ CONSOLE_WARNING_TEMPLATE,
19
+ PR_BODY_SECTION_TEMPLATE,
20
+ PR_TAG_VALUE
21
+ } from '../../.library/librarian/messages/staleness-warnings.js';
22
+
23
+ export const LIBRARY_VERIFY_SKIPPED_WARNING =
24
+ 'Library verification skipped due to an unexpected error. ' +
25
+ 'The version bump will proceed normally.';
26
+
27
+ export const LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE =
28
+ 'Library verification skipped due to an unexpected error. ' +
29
+ 'The release will proceed normally.';
@@ -940,6 +940,36 @@ export const findExistingPR = async ({ owner, repo, head, base }) => {
940
940
  }
941
941
  };
942
942
 
943
+ /**
944
+ * Update the body of an existing pull request
945
+ * @param {string} owner - Repository owner
946
+ * @param {string} repo - Repository name
947
+ * @param {number} number - PR number
948
+ * @param {string} body - New PR body content
949
+ * @returns {Promise<Object>} Updated PR data
950
+ */
951
+ export const updatePullRequestBody = async (owner, repo, number, body) => {
952
+ logger.debug('github-api - updatePullRequestBody', 'Updating PR body', {
953
+ owner, repo, number, bodyLength: body.length
954
+ });
955
+
956
+ const octokit = getOctokit();
957
+
958
+ const { data } = await octokit.pulls.update({
959
+ owner,
960
+ repo,
961
+ pull_number: number,
962
+ body
963
+ });
964
+
965
+ logger.debug('github-api - updatePullRequestBody', 'PR body updated', {
966
+ number: data.number,
967
+ url: data.html_url
968
+ });
969
+
970
+ return data;
971
+ };
972
+
943
973
  /**
944
974
  * Get repository information
945
975
  * Why: Fetch repo metadata for validation and context
@@ -45,6 +45,12 @@ export function parseEslintOutput(stdout) {
45
45
 
46
46
  for (const fileResult of results) {
47
47
  for (const msg of fileResult.messages || []) {
48
+ // Skip ESLint meta-warnings about ignored files — not code quality issues.
49
+ // Dot-directory files (e.g. .library/) trigger "File ignored by default"
50
+ // even when ignorePatterns negation is set, because ESLint's default
51
+ // dot-directory ignore takes precedence for explicitly passed file paths.
52
+ if (msg.message && msg.message.startsWith('File ignored')) continue;
53
+
48
54
  const issue = {
49
55
  file: fileResult.filePath || '',
50
56
  line: msg.line,
@@ -377,11 +377,13 @@ export function writeVersionToFile(filePath, type, newVersion) {
377
377
  }
378
378
 
379
379
  /**
380
- * Updates version in selected files
381
- * Why: Applies version update to specific VersionFileDescriptor[] subset
380
+ * Updates version in the given files (resolved mutations from the selector)
381
+ * Why: Applies version update to the concrete list of files the caller provides.
382
+ * Every item in the array is a mutation to apply — the caller is responsible
383
+ * for filtering before calling this function.
382
384
  *
383
- * @param {Array} files - Array of VersionFileDescriptor objects with selected=true
384
- * @param {string} newVersion - New version string
385
+ * @param {Array} files - Array of VersionFileDescriptor objects to update
386
+ * @param {string} newVersion - New version string (used when file has no targetVersion)
385
387
  */
386
388
  export function updateVersionFiles(files, newVersion) {
387
389
  logger.debug('version-manager - updateVersionFiles', 'Updating version files', {
@@ -402,10 +404,6 @@ export function updateVersionFiles(files, newVersion) {
402
404
  const errors = [];
403
405
 
404
406
  for (const file of files) {
405
- if (!file.selected) {
406
- continue; // Skip unselected files
407
- }
408
-
409
407
  try {
410
408
  // Verify file still exists
411
409
  if (!fs.existsSync(file.path)) {