claude-git-hooks 2.61.2 → 2.67.3

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.
@@ -24,8 +24,7 @@ import {
24
24
  discoverVersionFiles,
25
25
  incrementVersion,
26
26
  modifySuffix,
27
- updateVersionFiles,
28
- validateVersionFormat
27
+ updateVersionFiles
29
28
  } from '../utils/version-manager.js';
30
29
  import { createTag, pushTags, tagExists, formatTagName } from '../utils/git-tag-manager.js';
31
30
  import {
@@ -48,11 +47,10 @@ import {
48
47
  showError,
49
48
  showWarning,
50
49
  promptConfirmation,
51
- promptMenu,
52
- promptToggleList,
53
- promptEditField
50
+ promptToggleList
54
51
  } from '../utils/interactive-ui.js';
55
52
  import logger from '../utils/logger.js';
53
+ import { libraryModuleUrl, hasLibrary } from '../utils/library-resolver.js';
56
54
  import { colors, error, checkGitRepo } from './helpers.js';
57
55
  import {
58
56
  CONSOLE_WARNING_TEMPLATE,
@@ -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
-
313
- if (choice === 'a') {
314
- // All files selected
315
- return discovery.files;
316
- }
317
296
 
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
- );
345
-
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
- }
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
+ }));
355
303
 
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
  /**
@@ -464,20 +414,25 @@ export async function runBumpVersion(args) {
464
414
 
465
415
  showSuccess('Prerequisites validated');
466
416
 
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();
417
+ // Library verification gate — silent on clean, loud-warn on stale, never-hard-fail.
418
+ // Only runs when the working repo has its own .library/ (the tool ships without one).
419
+ if (!hasLibrary()) {
420
+ logger.debug('bump-version', 'No .library/ in current repo — skipping Library verification gate');
421
+ } else {
422
+ logger.debug('bump-version', 'Running Library verification gate');
423
+ try {
424
+ const { verify } = await import(libraryModuleUrl('librarian/index.js'));
425
+ const verifyResult = await verify();
472
426
 
473
- if (verifyResult.clean) {
474
- logger.debug('bump-version', 'Library is clean — no warning needed');
475
- } else {
476
- _emitLibraryWarning(verifyResult);
427
+ if (verifyResult.clean) {
428
+ logger.debug('bump-version', 'Library is clean — no warning needed');
429
+ } else {
430
+ _emitLibraryWarning(verifyResult);
431
+ }
432
+ } catch (verifyErr) {
433
+ const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING} ${verifyErr.message}${colors.reset}\n\n`;
434
+ process.stderr.write(msg);
477
435
  }
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
436
  }
482
437
 
483
438
  // Step 2: Discover all version files
@@ -504,15 +459,6 @@ export async function runBumpVersion(args) {
504
459
  // Display discovery table
505
460
  displayDiscoveryTable(discovery);
506
461
 
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
462
  // Step 3: File selection (if mismatch or interactive mode)
517
463
  let selectedFiles = discovery.files.filter((f) => f.selected);
518
464
 
@@ -528,6 +474,22 @@ export async function runBumpVersion(args) {
528
474
  console.log('');
529
475
  }
530
476
 
477
+ // Derive current version from selected files (not from discovery.resolvedVersion)
478
+ const selectedVersions = selectedFiles
479
+ .filter((f) => f.version !== null)
480
+ .map((f) => f.version);
481
+ const uniqueSelected = [...new Set(selectedVersions)];
482
+ const currentVersion = uniqueSelected.length === 1
483
+ ? uniqueSelected[0]
484
+ : discovery.resolvedVersion;
485
+
486
+ if (!currentVersion) {
487
+ showError('Could not determine a valid semver version from selected files');
488
+ process.exit(1);
489
+ }
490
+
491
+ showInfo(`Current version: ${currentVersion}`);
492
+
531
493
  // Step 4: Calculate new version or apply suffix operation
532
494
  logger.debug('bump-version', 'Step 4: Calculating new version');
533
495
  let newVersion;
@@ -35,6 +35,7 @@ import {
35
35
  promptEditField
36
36
  } from '../utils/interactive-ui.js';
37
37
  import logger from '../utils/logger.js';
38
+ import { libraryModuleUrl, hasLibrary } from '../utils/library-resolver.js';
38
39
  import { resolveLabels } from '../utils/label-resolver.js';
39
40
  import { colors, error, checkGitRepo } from './helpers.js';
40
41
 
@@ -356,26 +357,31 @@ export async function runCloseRelease(args) {
356
357
  console.log('');
357
358
 
358
359
  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);
360
+ // Library staleness gate — block release closure if books are stale.
361
+ // Only runs when the working repo has its own .library/ (the tool ships without one).
362
+ if (!hasLibrary()) {
363
+ logger.debug('close-release', 'No .library/ in current repo skipping Library staleness gate');
364
+ } else {
365
+ try {
366
+ showInfo('📚 Checking library staleness...');
367
+ const { runAll, formatHuman } = await import(libraryModuleUrl('tools/staleness.js'));
368
+ const { getSourceDir, getBooksDir } = await import(libraryModuleUrl('paths.js'));
369
+ const { getRepoRoot } = await import('../utils/git-operations.js');
370
+ const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
371
+ const hasDrift = stalenessResult.stale.length > 0 ||
372
+ stalenessResult.orphan_books.length > 0 ||
373
+ stalenessResult.unbooked_sources.length > 0;
374
+ if (hasDrift) {
375
+ error('📚 Library staleness detected — cannot close release with stale books');
376
+ process.stdout.write(formatHuman(stalenessResult));
377
+ error('Run: npm run library:regenerate');
378
+ process.exit(1);
379
+ }
380
+ showSuccess('📚 Library books are current');
381
+ } catch (err) {
382
+ showWarning('📚 Library staleness check unavailable — .library/ tools not found');
383
+ logger.debug('close-release', 'Library staleness check skipped', { error: err.message });
374
384
  }
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
385
  }
380
386
 
381
387
  // ── Step 7: git reset --soft origin/main ─────────────────────────────
@@ -24,6 +24,7 @@ import {
24
24
  } from '../utils/interactive-ui.js';
25
25
  import { getBranchPushStatus, pushBranch, stageFiles, createCommit, getRepoRoot } from '../utils/git-operations.js';
26
26
  import logger from '../utils/logger.js';
27
+ import { libraryModuleUrl } from '../utils/library-resolver.js';
27
28
  import { resolveLabels } from '../utils/label-resolver.js';
28
29
  import { CostTracker } from '../utils/cost-tracker.js';
29
30
  import { error, fatal, checkGitRepo } from './helpers.js';
@@ -364,7 +365,7 @@ export async function runCreatePr(args) {
364
365
  // Step 5.6: Version alignment validation (Issue #44)
365
366
  logger.debug('create-pr', 'Step 5.6: Validating version alignment');
366
367
  const { validateVersionAlignment } = await import('../utils/version-manager.js');
367
- const versionCheck = await validateVersionAlignment();
368
+ const versionCheck = await validateVersionAlignment(baseBranch);
368
369
 
369
370
  if (!versionCheck.aligned) {
370
371
  showWarning('Version misalignment detected:');
@@ -451,23 +452,163 @@ export async function runCreatePr(args) {
451
452
  }
452
453
  }
453
454
 
454
- // Step 5.7: Smart tag pushing (Issue #44)
455
- logger.debug('create-pr', 'Step 5.7: Checking and pushing unpushed tags');
455
+ // Step 5.7: Library maintenance pipeline (AUT-3764)
456
+ // Runs BEFORE tag push so that library regen is included in the tagged commit.
457
+ // Guard: only run if the current repo has its own .library/ setup.
458
+ // When claude-hooks is installed in a foreign repo (npm link / file: ref),
459
+ // the relative import resolves to git-hooks' .library/ — not this repo's.
460
+ let libraryCommitted = false;
461
+ const root = getRepoRoot();
462
+ const libraryResolverPath = path.join(root, '.library', 'resolver.yaml');
463
+ if (!fs.existsSync(libraryResolverPath)) {
464
+ logger.debug('create-pr', 'No .library/resolver.yaml in current repo — skipping Library pipeline');
465
+ } else {
466
+ logger.debug('create-pr', 'Step 5.7: Running Library maintenance pipeline');
467
+ try {
468
+ showInfo('Running Library maintenance pipeline...');
469
+ const { createPrPipeline } = await import(libraryModuleUrl('librarian/index.js'));
470
+
471
+ const pipelineSummary = await createPrPipeline({ repoRoot: root });
472
+ const {
473
+ modifiedFiles: libraryFiles,
474
+ perStep,
475
+ pendingDueToApiDown,
476
+ warnings: pipelineWarnings,
477
+ } = pipelineSummary;
478
+
479
+ // Surface pipeline summary
480
+ logger.debug('create-pr', 'Pipeline completed', {
481
+ modifiedCount: libraryFiles.length,
482
+ pendingDueToApiDown,
483
+ warningCount: pipelineWarnings.length,
484
+ });
485
+ showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
486
+ if (perStep.regen.changed > 0) {
487
+ showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
488
+ }
489
+ if (perStep.addRemoveRename.created > 0) {
490
+ showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
491
+ }
492
+ if (pendingDueToApiDown > 0) {
493
+ showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
494
+ }
495
+ for (const w of pipelineWarnings) {
496
+ showWarning(w);
497
+ }
498
+
499
+ if (libraryFiles.length === 0) {
500
+ showInfo('Library already in sync');
501
+ } else {
502
+ // Stage all modified Library files (stageFiles expects absolute paths)
503
+ const absFiles = libraryFiles.map(f => path.join(root, f));
504
+ const stageResult = stageFiles(absFiles);
505
+
506
+ if (!stageResult.success) {
507
+ showWarning(`Failed to stage Library files: ${stageResult.error}`);
508
+ } else {
509
+ // Commit with deterministic message — separate commit, not amend
510
+ const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
511
+ const commitResult = createCommit(libraryCommitMsg);
512
+
513
+ if (!commitResult.success) {
514
+ showWarning(`Failed to commit Library changes: ${commitResult.error}`);
515
+ } else {
516
+ libraryCommitted = true;
517
+ showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
518
+
519
+ // Push the Library commit to the PR branch's remote
520
+ const libraryPushResult = pushBranch(currentBranch);
521
+ if (!libraryPushResult.success) {
522
+ showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
523
+ } else {
524
+ showSuccess('Library commit pushed');
525
+ }
526
+ }
527
+ }
528
+ }
529
+ } catch (pipelineErr) {
530
+ // Pipeline failure is non-blocking — log and continue with PR creation
531
+ logger.warning('create-pr', 'Library pipeline failed, continuing', {
532
+ error: pipelineErr.message,
533
+ });
534
+ showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
535
+ }
536
+ }
537
+
538
+ // Step 5.75: mscope lessons-learned capture (automation-skills resume).
539
+ // Interactive knowledge-capture alongside the gotcha solicitation above:
540
+ // hands stdio to `automation-skills resume`, which records the branch's
541
+ // lessons to the skill repo's implementation-history.md. Skipped in
542
+ // headless mode (interactive by nature), config-gated via
543
+ // skillRegistry.resumeOnCreatePr, and never blocks PR creation.
544
+ if (!headless &&
545
+ config.skillRegistry?.enabled !== false &&
546
+ config.skillRegistry?.resumeOnCreatePr !== false) {
547
+ try {
548
+ const { isResumeCliAvailable, runResumeFlow } = await import('../utils/skill-registry/resume.js');
549
+ if (isResumeCliAvailable()) {
550
+ showInfo('Handing control to `automation-skills resume` to capture lessons-learned...');
551
+ const resumeOut = runResumeFlow({ repoRoot: root });
552
+ if (resumeOut.ran && resumeOut.exitCode === 0) {
553
+ showSuccess('Lessons captured to the mscope skill repo');
554
+ } else if (resumeOut.ran) {
555
+ showWarning('Lessons capture exited without recording (see output above)');
556
+ }
557
+ } else {
558
+ logger.debug('create-pr', 'automation-skills CLI not found — skipping lessons capture');
559
+ }
560
+ } catch (resumeErr) {
561
+ // Knowledge capture failure is non-blocking — log and continue.
562
+ showWarning(`Lessons capture unavailable: ${resumeErr.message}`);
563
+ }
564
+ }
565
+
566
+ // Step 5.8: Smart tag pushing (Issue #44)
567
+ // Runs AFTER library maintenance so tags include the library commit.
568
+ logger.debug('create-pr', 'Step 5.8: Checking and pushing unpushed tags');
456
569
  const {
457
570
  compareLocalAndRemoteTags,
458
571
  pushTags: pushTagsUtil,
459
- getLatestLocalTag,
460
- getLatestRemoteTag,
572
+ createTag: createTagUtil,
573
+ getLatestRemoteTagOnBranch,
461
574
  parseTagVersion
462
575
  } = await import('../utils/git-tag-manager.js');
463
576
  const { compareVersions } = await import('../utils/version-manager.js');
464
577
 
578
+ // If library was committed after bump-version created the tag,
579
+ // re-point unpushed tags to HEAD so the tag includes library books.
580
+ if (libraryCommitted) {
581
+ const unpushedTags = await compareLocalAndRemoteTags();
582
+ for (const tag of unpushedTags.localNewer) {
583
+ const version = parseTagVersion(tag);
584
+ if (version) {
585
+ logger.debug('create-pr', 'Re-pointing tag to include library commit', { tag });
586
+ let origTagMsg = '';
587
+ try { origTagMsg = execSync(`git tag -l --format="%(contents)" ${tag}`, { encoding: 'utf8' }).trim(); } catch { /* ignore */ }
588
+ const tagMsg = origTagMsg || `Release version ${version}`;
589
+ try {
590
+ await createTagUtil(version, tagMsg, { force: true });
591
+ showInfo(`Tag ${tag} moved to include library commit`);
592
+ } catch (tagErr) {
593
+ logger.warning('create-pr', 'Failed to re-point tag, continuing', { tag, error: tagErr.message });
594
+ }
595
+ }
596
+ }
597
+ }
598
+
465
599
  const tagComparison = await compareLocalAndRemoteTags();
466
600
 
467
601
  if (tagComparison.localNewer.length > 0) {
468
- // Get latest local and remote tags for comparison
469
- const latestLocalTag = getLatestLocalTag();
470
- const latestRemoteTag = await getLatestRemoteTag();
602
+ // Derive latest unpushed tag (scoped to what's actually being pushed)
603
+ const sortedUnpushed = tagComparison.localNewer
604
+ .map((t) => ({ tag: t, version: parseTagVersion(t) }))
605
+ .filter((t) => t.version !== null)
606
+ .sort((a, b) => compareVersions(a.version, b.version));
607
+ const latestLocalTag = sortedUnpushed.length > 0
608
+ ? sortedUnpushed[sortedUnpushed.length - 1].tag
609
+ : tagComparison.localNewer[0];
610
+ // Compare against latest remote tag on the BASE branch (not global latest)
611
+ const latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
471
612
 
472
613
  const localVersion = latestLocalTag ? parseTagVersion(latestLocalTag) : null;
473
614
  const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
@@ -483,7 +624,7 @@ export async function runCreatePr(args) {
483
624
  let shouldPushTags = false;
484
625
  let userChoice = null;
485
626
 
486
- // Case 1: Local tag > Remote tag → Auto-push
627
+ // Case 1: Local tag > Remote tag → Auto-push (normal bump-version flow)
487
628
  if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) > 0) {
488
629
  logger.debug('create-pr', 'Local version > remote version, auto-pushing', {
489
630
  localVersion,
@@ -494,7 +635,7 @@ export async function runCreatePr(args) {
494
635
  showInfo('Auto-pushing tag to remote...');
495
636
  shouldPushTags = true;
496
637
 
497
- // Case 2: Local tag = Remote tag → Prompt with warning
638
+ // Case 2: Local tag = Remote tag → Prompt with warning (may already be pushed)
498
639
  } else if (
499
640
  localVersion &&
500
641
  remoteVersion &&
@@ -534,7 +675,7 @@ export async function runCreatePr(args) {
534
675
  shouldPushTags = true;
535
676
  }
536
677
 
537
- // Case 3: Local tag < Remote tag → Prompt with error
678
+ // Case 3: Local tag < Remote tag → Prompt with warning (someone pushed newer)
538
679
  } else if (
539
680
  localVersion &&
540
681
  remoteVersion &&
@@ -664,78 +805,6 @@ export async function runCreatePr(args) {
664
805
  logger.debug('create-pr', 'No unpushed tags found, continuing');
665
806
  }
666
807
 
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
808
  // Step 6: Generate PR metadata using engine
740
809
  logger.debug('create-pr', 'Step 6: Generating PR metadata with engine');
741
810
  showInfo('Generating PR metadata with Claude...');
@@ -59,6 +59,7 @@ import {
59
59
  promptConfirmation
60
60
  } from '../utils/interactive-ui.js';
61
61
  import logger from '../utils/logger.js';
62
+ import { libraryModuleUrl, hasLibrary } from '../utils/library-resolver.js';
62
63
  import { colors, error, checkGitRepo } from './helpers.js';
63
64
  import {
64
65
  CONSOLE_WARNING_TEMPLATE,
@@ -372,21 +373,26 @@ export async function runCreateRelease(args) {
372
373
  showSuccess('Preconditions validated');
373
374
  console.log('');
374
375
 
375
- // Step 2: Library verification gate — silent on clean, loud-warn on stale, never blocks
376
+ // Step 2: Library verification gate — silent on clean, loud-warn on stale, never blocks.
377
+ // Only runs when the working repo has its own .library/ (the tool ships without one).
376
378
  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();
379
+ if (!hasLibrary()) {
380
+ logger.debug('create-release', 'No .library/ in current repo — skipping Library verification gate');
381
+ } else {
382
+ logger.debug('create-release', 'Step 2: Running Library verification gate');
383
+ try {
384
+ const { verify } = await import(libraryModuleUrl('librarian/index.js'));
385
+ verifyResult = await verify();
381
386
 
382
- if (verifyResult.clean) {
383
- logger.debug('create-release', 'Library is clean — no warning needed');
384
- } else {
385
- _emitLibraryWarning(verifyResult);
387
+ if (verifyResult.clean) {
388
+ logger.debug('create-release', 'Library is clean — no warning needed');
389
+ } else {
390
+ _emitLibraryWarning(verifyResult);
391
+ }
392
+ } catch (verifyErr) {
393
+ const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE} ${verifyErr.message}${colors.reset}\n\n`;
394
+ process.stderr.write(msg);
386
395
  }
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
396
  }
391
397
 
392
398
  // Step 3: Discover version files
@@ -511,7 +517,7 @@ export async function runCreateRelease(args) {
511
517
  logger.debug('create-release', 'Step 9: Running Library regeneration');
512
518
  showInfo('📚 Regenerating stale Library books...');
513
519
  try {
514
- const { createPrPipeline } = await import('../../.library/librarian/index.js');
520
+ const { createPrPipeline } = await import(libraryModuleUrl('librarian/index.js'));
515
521
  const root = getRepoRoot();
516
522
  const pipelineSummary = await createPrPipeline({ repoRoot: root });
517
523
 
@@ -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) {
package/lib/defaults.json CHANGED
@@ -112,6 +112,11 @@
112
112
  "model": "sonnet",
113
113
  "timeout": 360000
114
114
  },
115
+ "skillRegistry": {
116
+ "enabled": true,
117
+ "blockOn": "never",
118
+ "resumeOnCreatePr": true
119
+ },
115
120
  "orchestrator": {
116
121
  "model": "opus",
117
122
  "timeout": 60000,