claude-git-hooks 2.45.0 → 2.61.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +386 -21
- package/CLAUDE.md +11 -12
- package/README.md +7 -0
- package/lib/commands/analyze-diff.js +27 -0
- package/lib/commands/back-merge.js +95 -1
- package/lib/commands/bump-version.js +39 -0
- package/lib/commands/close-release.js +22 -0
- package/lib/commands/create-pr.js +73 -1
- package/lib/commands/create-release.js +198 -30
- package/lib/commands/help.js +16 -60
- package/lib/hooks/pre-commit.js +36 -0
- package/lib/messages/library-warnings.js +29 -0
- package/lib/utils/github-api.js +30 -0
- package/lib/utils/linter-runner.js +6 -0
- package/lib/utils/version-manager.js +6 -8
- package/package.json +83 -75
|
@@ -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.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
11
|
-
* 6.
|
|
12
|
-
* 7.
|
|
13
|
-
* 8.
|
|
14
|
-
* 9.
|
|
15
|
-
* 10.
|
|
16
|
-
* 11.
|
|
17
|
-
* 12.
|
|
18
|
-
* 13.
|
|
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:
|
|
322
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
392
|
-
logger.debug('create-release', 'Step
|
|
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
|
|
412
|
-
logger.debug('create-release', 'Step
|
|
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
|
|
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
|
|
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
|
|
500
|
-
logger.debug('create-release', 'Step
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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');
|
package/lib/commands/help.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
/**
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -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.';
|
package/lib/utils/github-api.js
CHANGED
|
@@ -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
|
|
381
|
-
* Why: Applies version update to
|
|
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
|
|
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)) {
|