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.
- package/CHANGELOG.md +329 -3
- package/README.md +34 -0
- package/lib/commands/analyze-diff.js +26 -23
- package/lib/commands/analyze.js +3 -1
- package/lib/commands/back-merge.js +39 -33
- package/lib/commands/bump-version.js +52 -90
- package/lib/commands/close-release.js +25 -19
- package/lib/commands/create-pr.js +152 -83
- package/lib/commands/create-release.js +19 -13
- package/lib/commands/help.js +13 -3
- package/lib/defaults.json +5 -0
- package/lib/hooks/pre-commit.js +112 -32
- package/lib/messages/library-warnings.js +128 -10
- package/lib/utils/analysis-engine.js +7 -3
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/config-registry.js +1 -0
- package/lib/utils/git-tag-manager.js +104 -0
- package/lib/utils/judge.js +2 -1
- package/lib/utils/library-resolver.js +50 -0
- package/lib/utils/prompt-builder.js +15 -0
- package/lib/utils/skill-registry/catalogue.js +74 -0
- package/lib/utils/skill-registry/feedback-writer.js +196 -0
- package/lib/utils/skill-registry/index.js +254 -0
- package/lib/utils/skill-registry/parser.js +311 -0
- package/lib/utils/skill-registry/resume.js +81 -0
- package/lib/utils/skill-registry/runner.js +265 -0
- package/lib/utils/version-manager.js +37 -18
- package/package.json +2 -1
- package/templates/CLAUDE_ANALYSIS_PROMPT.md +2 -1
- package/templates/config.advanced.example.json +25 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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:
|
|
455
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
//
|
|
469
|
-
const
|
|
470
|
-
|
|
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
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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('
|
|
520
|
+
const { createPrPipeline } = await import(libraryModuleUrl('librarian/index.js'));
|
|
515
521
|
const root = getRepoRoot();
|
|
516
522
|
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
517
523
|
|
package/lib/commands/help.js
CHANGED
|
@@ -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
|
-
|
|
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