claude-git-hooks 2.66.1 → 2.68.0
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 +117 -8
- package/README.md +34 -0
- package/bin/claude-hooks +19 -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 +20 -15
- package/lib/commands/close-release.js +25 -19
- package/lib/commands/create-pr.js +30 -1
- package/lib/commands/create-release.js +19 -13
- package/lib/commands/install.js +9 -19
- package/lib/commands/update.js +14 -28
- package/lib/defaults.json +9 -0
- package/lib/hooks/pre-commit.js +112 -32
- package/lib/utils/analysis-engine.js +7 -3
- package/lib/utils/auto-update.js +198 -0
- package/lib/utils/config-registry.js +1 -0
- 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 +9 -3
- package/package.json +84 -85
- package/templates/CLAUDE_ANALYSIS_PROMPT.md +2 -1
- package/templates/config.advanced.example.json +42 -0
|
@@ -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';
|
|
@@ -465,7 +466,7 @@ export async function runCreatePr(args) {
|
|
|
465
466
|
logger.debug('create-pr', 'Step 5.7: Running Library maintenance pipeline');
|
|
466
467
|
try {
|
|
467
468
|
showInfo('Running Library maintenance pipeline...');
|
|
468
|
-
const { createPrPipeline } = await import('
|
|
469
|
+
const { createPrPipeline } = await import(libraryModuleUrl('librarian/index.js'));
|
|
469
470
|
|
|
470
471
|
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
471
472
|
const {
|
|
@@ -534,6 +535,34 @@ export async function runCreatePr(args) {
|
|
|
534
535
|
}
|
|
535
536
|
}
|
|
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
|
+
|
|
537
566
|
// Step 5.8: Smart tag pushing (Issue #44)
|
|
538
567
|
// Runs AFTER library maintenance so tags include the library commit.
|
|
539
568
|
logger.debug('create-pr', 'Step 5.8: Checking and pushing unpushed tags');
|
|
@@ -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/install.js
CHANGED
|
@@ -27,10 +27,9 @@ import {
|
|
|
27
27
|
getGitHooksPath,
|
|
28
28
|
isWindows,
|
|
29
29
|
getClaudeCommand,
|
|
30
|
-
getPackageJson,
|
|
31
|
-
getLatestVersion,
|
|
32
30
|
Entertainment
|
|
33
31
|
} from './helpers.js';
|
|
32
|
+
import { getUpdateStatus, performUpdate } from '../utils/auto-update.js';
|
|
34
33
|
import { runSetupGitHub } from './setup-github.js';
|
|
35
34
|
import { generateCompletionData } from '../cli-metadata.js';
|
|
36
35
|
import { getConfig } from '../config.js';
|
|
@@ -41,11 +40,12 @@ import { getConfig } from '../config.js';
|
|
|
41
40
|
*/
|
|
42
41
|
async function checkVersionAndPromptUpdate() {
|
|
43
42
|
try {
|
|
44
|
-
const currentVersion =
|
|
45
|
-
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
43
|
+
const { currentVersion, latestVersion, isNewer } = await getUpdateStatus();
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// isNewer guards against the `===` downgrade bug: a dev build ahead of npm
|
|
46
|
+
// (current > latest) must NOT prompt to "update" to an older published version.
|
|
47
|
+
if (!isNewer) {
|
|
48
|
+
return true; // Already on latest (or a dev build ahead of npm)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
console.log('');
|
|
@@ -58,13 +58,13 @@ async function checkVersionAndPromptUpdate() {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
return new Promise((resolve) => {
|
|
61
|
-
rl.question('Do you want to update now? (y/n): ', (answer) => {
|
|
61
|
+
rl.question('Do you want to update now? (y/n): ', async (answer) => {
|
|
62
62
|
rl.close();
|
|
63
63
|
|
|
64
64
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
65
|
-
info('Updating claude-git-hooks...');
|
|
66
65
|
try {
|
|
67
|
-
|
|
66
|
+
// install is about to run, so skip the hook reinstall here
|
|
67
|
+
await performUpdate({ reinstallHooks: false, silent: false });
|
|
68
68
|
success('Update completed. Please run your command again.');
|
|
69
69
|
process.exit(0); // Exit so user restarts the process
|
|
70
70
|
} catch (e) {
|
|
@@ -463,16 +463,6 @@ export async function runInstall(args) {
|
|
|
463
463
|
success(`${hook} installed`);
|
|
464
464
|
});
|
|
465
465
|
|
|
466
|
-
// Copy version verification script with LF line endings
|
|
467
|
-
const checkVersionSource = path.join(templatesPath, 'check-version.sh');
|
|
468
|
-
const checkVersionDest = path.join(hooksPath, 'check-version.sh');
|
|
469
|
-
|
|
470
|
-
if (fs.existsSync(checkVersionSource)) {
|
|
471
|
-
copyWithLF(checkVersionSource, checkVersionDest);
|
|
472
|
-
fs.chmodSync(checkVersionDest, '755');
|
|
473
|
-
success('Version verification script installed');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
466
|
// Create .claude directory if it doesn't exist
|
|
477
467
|
const claudeDir = '.claude';
|
|
478
468
|
if (!fs.existsSync(claudeDir)) {
|
package/lib/commands/update.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File: update.js
|
|
3
3
|
* Purpose: Update command - update to the latest version
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper over the shared auto-update logic in lib/utils/auto-update.js.
|
|
6
|
+
* Runs verbosely (silent: false) — this is a user-initiated update.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
error,
|
|
9
|
-
success,
|
|
10
|
-
info,
|
|
11
|
-
warning,
|
|
12
|
-
getPackageJson,
|
|
13
|
-
getLatestVersion,
|
|
14
|
-
compareVersions
|
|
15
|
-
} from './helpers.js';
|
|
16
|
-
import { runInstall } from './install.js';
|
|
9
|
+
import { error, success, info, warning } from './helpers.js';
|
|
10
|
+
import { getUpdateStatus, performUpdate } from '../utils/auto-update.js';
|
|
17
11
|
|
|
18
12
|
/**
|
|
19
13
|
* Update command - update to the latest version
|
|
@@ -22,33 +16,26 @@ export async function runUpdate() {
|
|
|
22
16
|
info('Checking latest available version...');
|
|
23
17
|
|
|
24
18
|
try {
|
|
25
|
-
const currentVersion =
|
|
26
|
-
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
19
|
+
const { currentVersion, latestVersion, isNewer, isDev } = await getUpdateStatus();
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (comparison === 0) {
|
|
31
|
-
success(`You already have the latest version installed (${currentVersion})`);
|
|
32
|
-
return;
|
|
33
|
-
} else if (comparison > 0) {
|
|
21
|
+
if (isDev) {
|
|
34
22
|
info(`You are using a development version (${currentVersion})`);
|
|
35
23
|
info(`Latest published version: ${latestVersion}`);
|
|
36
24
|
success(`You already have the latest version installed (${currentVersion})`);
|
|
37
25
|
return;
|
|
38
26
|
}
|
|
39
27
|
|
|
28
|
+
if (!isNewer) {
|
|
29
|
+
success(`You already have the latest version installed (${currentVersion})`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
40
33
|
info(`Current version: ${currentVersion}`);
|
|
41
34
|
info(`Available version: ${latestVersion}`);
|
|
42
35
|
|
|
43
|
-
// Update the package
|
|
44
|
-
info('Updating claude-git-hooks...');
|
|
45
36
|
try {
|
|
46
|
-
|
|
37
|
+
await performUpdate({ reinstallHooks: true, silent: false });
|
|
47
38
|
success(`Successfully updated to version ${latestVersion}`);
|
|
48
|
-
|
|
49
|
-
// Reinstall hooks with the new version
|
|
50
|
-
info('Reinstalling hooks with the new version...');
|
|
51
|
-
await runInstall(['--force']);
|
|
52
39
|
} catch (updateError) {
|
|
53
40
|
error('Error updating. Try running: npm install -g claude-git-hooks@latest');
|
|
54
41
|
}
|
|
@@ -56,9 +43,8 @@ export async function runUpdate() {
|
|
|
56
43
|
warning('Could not check the latest available version');
|
|
57
44
|
warning('Trying to update anyway...');
|
|
58
45
|
try {
|
|
59
|
-
|
|
46
|
+
await performUpdate({ reinstallHooks: true, silent: false });
|
|
60
47
|
success('Update completed');
|
|
61
|
-
await runInstall(['--force']);
|
|
62
48
|
} catch (updateError) {
|
|
63
49
|
error(`Error updating: ${updateError.message}`);
|
|
64
50
|
}
|
package/lib/defaults.json
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"debug": false,
|
|
34
34
|
"wslCheckTimeout": 15000
|
|
35
35
|
},
|
|
36
|
+
"autoUpdate": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"intervalHours": 24
|
|
39
|
+
},
|
|
36
40
|
"git": {
|
|
37
41
|
"diffFilter": "ACM"
|
|
38
42
|
},
|
|
@@ -112,6 +116,11 @@
|
|
|
112
116
|
"model": "sonnet",
|
|
113
117
|
"timeout": 360000
|
|
114
118
|
},
|
|
119
|
+
"skillRegistry": {
|
|
120
|
+
"enabled": true,
|
|
121
|
+
"blockOn": "never",
|
|
122
|
+
"resumeOnCreatePr": true
|
|
123
|
+
},
|
|
115
124
|
"orchestrator": {
|
|
116
125
|
"model": "opus",
|
|
117
126
|
"timeout": 60000,
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* - resolution-prompt: Issue resolution generation
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { join } from 'path';
|
|
23
|
-
import { getStagedFiles, getRepoRoot, getStagedTreeSha } from '../utils/git-operations.js';
|
|
22
|
+
import { join, basename } from 'path';
|
|
23
|
+
import { getStagedFiles, getRepoRoot, getStagedTreeSha, getCurrentBranch } from '../utils/git-operations.js';
|
|
24
24
|
import { writeMarker } from '../utils/hooks-verified-marker.js';
|
|
25
25
|
import { filterFiles } from '../utils/file-operations.js';
|
|
26
26
|
import {
|
|
@@ -33,9 +33,11 @@ import { generateResolutionPrompt, shouldGeneratePrompt } from '../utils/resolut
|
|
|
33
33
|
import { loadPreset } from '../utils/preset-loader.js';
|
|
34
34
|
import { getVersion } from '../utils/package-info.js';
|
|
35
35
|
import logger from '../utils/logger.js';
|
|
36
|
+
import { libraryModuleUrl, hasLibrary } from '../utils/library-resolver.js';
|
|
36
37
|
import { getConfig } from '../config.js';
|
|
37
38
|
import { recordMetric } from '../utils/metrics.js';
|
|
38
39
|
import { runLinters, displayLintResults, lintIssuesToAnalysisDetails } from '../utils/linter-runner.js';
|
|
40
|
+
import * as skillRegistry from '../utils/skill-registry/index.js';
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Configuration loaded from lib/config.js
|
|
@@ -151,39 +153,44 @@ const main = async () => {
|
|
|
151
153
|
process.exit(0);
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
// Library staleness check — non-blocking warning
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
// Library staleness check — non-blocking warning.
|
|
157
|
+
// Only runs when the working repo has its own .library/ (the tool ships without one).
|
|
158
|
+
if (!hasLibrary()) {
|
|
159
|
+
logger.debug('pre-commit', 'No .library/ in current repo — skipping Library staleness check');
|
|
160
|
+
} else {
|
|
161
|
+
try {
|
|
162
|
+
const { checkBook } = await import(libraryModuleUrl('tools/staleness.js'));
|
|
163
|
+
const { getBooksDir } = await import(libraryModuleUrl('paths.js'));
|
|
164
|
+
const booksDir = getBooksDir();
|
|
165
|
+
const sourceFiles = validFiles
|
|
166
|
+
.map(f => (typeof f === 'string' ? f : f.path))
|
|
167
|
+
.filter(p => p.startsWith('lib/'));
|
|
168
|
+
|
|
169
|
+
if (sourceFiles.length > 0) {
|
|
170
|
+
const staleBooks = [];
|
|
171
|
+
for (const srcPath of sourceFiles) {
|
|
172
|
+
const bookName = `${srcPath.replace(/^lib\/(?:.*\/)?/, '').replace(/\.js$/, '')}.md`;
|
|
173
|
+
const bookPath = join(booksDir, bookName);
|
|
174
|
+
try {
|
|
175
|
+
const result = await checkBook(bookPath, getRepoRoot());
|
|
176
|
+
if (result.status === 'stale') {
|
|
177
|
+
staleBooks.push(result.book);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Book doesn't exist or check failed — skip silently
|
|
172
181
|
}
|
|
173
|
-
} catch {
|
|
174
|
-
// Book doesn't exist or check failed — skip silently
|
|
175
182
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
if (staleBooks.length > 0) {
|
|
184
|
+
logger.warning(`📚 ${staleBooks.length} library book(s) will become stale after this commit`);
|
|
185
|
+
for (const book of staleBooks) {
|
|
186
|
+
logger.warning(` └─ ${book}`);
|
|
187
|
+
}
|
|
188
|
+
logger.warning(' Run: npm run library:regenerate');
|
|
181
189
|
}
|
|
182
|
-
logger.warning(' Run: npm run library:regenerate');
|
|
183
190
|
}
|
|
191
|
+
} catch {
|
|
192
|
+
logger.warning('📚 Library staleness check unavailable — .library/ tools not found');
|
|
184
193
|
}
|
|
185
|
-
} catch {
|
|
186
|
-
logger.warning('📚 Library staleness check unavailable — .library/ tools not found');
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
// Step 3: Run linters (fast, deterministic — before Claude analysis)
|
|
@@ -220,6 +227,43 @@ const main = async () => {
|
|
|
220
227
|
}
|
|
221
228
|
}
|
|
222
229
|
|
|
230
|
+
// Step 3.5: mscope skill-registry check (deterministic; fast; warn-only by default).
|
|
231
|
+
// The facade runs the JBE-NNN / UIK-NNN catalogue's `Verify:` greps
|
|
232
|
+
// against the staged file contents, displays findings alongside lint +
|
|
233
|
+
// AI output, and returns the blockOn gating decision. Exiting on a
|
|
234
|
+
// blocked commit stays here in the hook — utils never exit.
|
|
235
|
+
//
|
|
236
|
+
// Silent skip if the skill repo isn't locatable — the integration is
|
|
237
|
+
// value-add, never a hard dep.
|
|
238
|
+
//
|
|
239
|
+
// skillRoot is hoisted to function-scope so Step 9.5 (skill-gap writer)
|
|
240
|
+
// can reuse the resolution (registry load is memoized in the facade).
|
|
241
|
+
let skillRoot = null;
|
|
242
|
+
let skillCheck = null;
|
|
243
|
+
if (config.skillRegistry?.enabled !== false) {
|
|
244
|
+
try {
|
|
245
|
+
const absPaths = validFiles
|
|
246
|
+
.map((f) => (typeof f === 'string' ? f : f.path))
|
|
247
|
+
.map((p) => (p.startsWith('/') || /^[A-Za-z]:/.test(p) ? p : join(repoRoot, p)));
|
|
248
|
+
|
|
249
|
+
skillCheck = skillRegistry.runSkillChecks(absPaths, { repoRoot, config });
|
|
250
|
+
skillRoot = skillCheck.skillRoot;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
// Never break the hook for skill-registry failures.
|
|
253
|
+
logger.debug('pre-commit - main', 'Skill registry check failed', { error: err?.message });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The escape hatch is the config knob — `--no-verify` is
|
|
258
|
+
// intentionally not advertised (project convention discourages
|
|
259
|
+
// skipping hooks; the config is the supported override).
|
|
260
|
+
// process.exit stays outside the try-catch per repo convention.
|
|
261
|
+
if (skillCheck?.shouldBlock) {
|
|
262
|
+
logger.error(`Skill registry: ${skillCheck.triggeringCount} finding(s) at or above '${config.skillRegistry?.blockOn}' threshold — commit blocked.`);
|
|
263
|
+
logger.error('To downgrade the threshold, set config.skillRegistry.blockOn = "never" (or a less strict severity).');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
223
267
|
// Step 4: Build file data using shared engine
|
|
224
268
|
logger.debug('pre-commit - main', 'Building file data for analysis');
|
|
225
269
|
const filesData = buildFilesData(validFiles, { staged: true });
|
|
@@ -232,11 +276,17 @@ const main = async () => {
|
|
|
232
276
|
logger.info('⚡ Intelligent orchestration: grouping files and assigning models');
|
|
233
277
|
}
|
|
234
278
|
|
|
235
|
-
// Step 6: Run analysis using shared engine
|
|
279
|
+
// Step 6: Run analysis using shared engine. The skill-registry
|
|
280
|
+
// catalogue (memoized load — free after Step 3.5) lets Claude classify
|
|
281
|
+
// findings against the mscope rule IDs; unmatched findings become
|
|
282
|
+
// skill-gap candidates in Step 9.5.
|
|
236
283
|
const result = await runAnalysis(filesData, config, {
|
|
237
284
|
hook: 'pre-commit',
|
|
238
285
|
saveDebug: config.system.debug,
|
|
239
|
-
headless: isHeadless
|
|
286
|
+
headless: isHeadless,
|
|
287
|
+
catalogueSection: config.skillRegistry?.enabled !== false
|
|
288
|
+
? skillRegistry.getCatalogueSection({ repoRoot })
|
|
289
|
+
: null
|
|
240
290
|
});
|
|
241
291
|
|
|
242
292
|
// Step 7: Display results using shared function
|
|
@@ -308,6 +358,36 @@ const main = async () => {
|
|
|
308
358
|
}
|
|
309
359
|
}
|
|
310
360
|
|
|
361
|
+
// Step 9.5: Skill-gap writer (Option E2). For each detail Claude left
|
|
362
|
+
// un-classified (no detail.rule from the catalogue), append a
|
|
363
|
+
// `[skill-gap]` candidate to the appropriate skill-feedback.md.
|
|
364
|
+
// Per the maintainer's `feedback_skill_improvements_user_approval`
|
|
365
|
+
// rule, we never auto-merge candidates into the registry — they're
|
|
366
|
+
// surfaced for review during `automation-skills retro`.
|
|
367
|
+
//
|
|
368
|
+
// Reuses `skillRoot` resolved in Step 3.5 — no second loadRegistry()
|
|
369
|
+
// round-trip. Silent skip if the skill repo wasn't found or AI
|
|
370
|
+
// returned no details.
|
|
371
|
+
if (config.skillRegistry?.enabled !== false &&
|
|
372
|
+
skillRoot &&
|
|
373
|
+
Array.isArray(result.details) && result.details.length > 0) {
|
|
374
|
+
try {
|
|
375
|
+
const out = skillRegistry.recordSkillGaps(result, {
|
|
376
|
+
skillRoot,
|
|
377
|
+
branch: getCurrentBranch(),
|
|
378
|
+
repoName: basename(repoRoot),
|
|
379
|
+
});
|
|
380
|
+
if (out.written > 0) {
|
|
381
|
+
logger.info(`📝 ${out.written} skill-gap candidate(s) recorded to skill-feedback.md (${out.skippedDuplicate} duplicate(s) skipped, ${out.skippedClassified} classified to existing rules).`);
|
|
382
|
+
logger.info(' Review with: automation-skills retro');
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
// Warning (not debug): a failed write means feedback candidates
|
|
386
|
+
// were silently lost — the user should know. Commit continues.
|
|
387
|
+
logger.warning(`Skill-gap writer failed — candidates not recorded: ${err?.message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
311
391
|
// Step 10: Check quality gate
|
|
312
392
|
const qualityGatePassed = result.QUALITY_GATE === 'PASSED';
|
|
313
393
|
const approved = result.approved !== false;
|
|
@@ -376,10 +376,12 @@ const ORCHESTRATOR_THRESHOLD = getDefaultSection('orchestrator').threshold;
|
|
|
376
376
|
* @param {Object} options - Analysis options
|
|
377
377
|
* @param {boolean} options.saveDebug - Save debug output (default: from config)
|
|
378
378
|
* @param {string} options.hook - Hook name for telemetry (default: 'analysis')
|
|
379
|
+
* @param {string|null} options.catalogueSection - Rendered skill-registry catalogue forwarded
|
|
380
|
+
* to the prompt builder (opaque string; default: null)
|
|
379
381
|
* @returns {Promise<Object>} Analysis result
|
|
380
382
|
*/
|
|
381
383
|
export const runAnalysis = async (filesData, config, options = {}) => {
|
|
382
|
-
const { saveDebug = config.system?.debug, hook = 'analysis', headless = false, costTracker = null } = options;
|
|
384
|
+
const { saveDebug = config.system?.debug, hook = 'analysis', headless = false, costTracker = null, catalogueSection = null } = options;
|
|
383
385
|
|
|
384
386
|
if (filesData.length === 0) {
|
|
385
387
|
logger.debug('analysis-engine - runAnalysis', 'No files to analyze');
|
|
@@ -425,7 +427,8 @@ export const runAnalysis = async (filesData, config, options = {}) => {
|
|
|
425
427
|
BRANCH_NAME: getCurrentBranch()
|
|
426
428
|
},
|
|
427
429
|
commonContext,
|
|
428
|
-
batchRationale: batch.rationale
|
|
430
|
+
batchRationale: batch.rationale,
|
|
431
|
+
catalogueSection
|
|
429
432
|
})
|
|
430
433
|
)
|
|
431
434
|
);
|
|
@@ -484,7 +487,8 @@ export const runAnalysis = async (filesData, config, options = {}) => {
|
|
|
484
487
|
metadata: {
|
|
485
488
|
REPO_NAME: getRepoName(),
|
|
486
489
|
BRANCH_NAME: getCurrentBranch()
|
|
487
|
-
}
|
|
490
|
+
},
|
|
491
|
+
catalogueSection
|
|
488
492
|
});
|
|
489
493
|
|
|
490
494
|
const telemetryContext = {
|