claude-git-hooks 2.66.1 → 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 +96 -8
- 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 +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/defaults.json +5 -0
- package/lib/hooks/pre-commit.js +112 -32
- package/lib/utils/analysis-engine.js +7 -3
- 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 +1 -2
- package/templates/CLAUDE_ANALYSIS_PROMPT.md +2 -1
- package/templates/config.advanced.example.json +25 -0
|
@@ -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/defaults.json
CHANGED
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 = {
|
|
@@ -70,6 +70,7 @@ const SECTION_REMOTE_MAP = {
|
|
|
70
70
|
analysis: _settingsEntry('analysis'),
|
|
71
71
|
commitMessage: _settingsEntry('commitMessage'),
|
|
72
72
|
judge: _settingsEntry('judge'),
|
|
73
|
+
skillRegistry: _settingsEntry('skillRegistry'),
|
|
73
74
|
orchestrator: _settingsEntry('orchestrator'),
|
|
74
75
|
prMetadata: _settingsEntry('prMetadata'),
|
|
75
76
|
linting: _settingsEntry('linting')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: library-resolver.js
|
|
3
|
+
* Purpose: Resolve the Library (`.library/`) of the repository that claude-hooks
|
|
4
|
+
* is currently running in — never the bundled tool's own copy.
|
|
5
|
+
*
|
|
6
|
+
* Why: claude-git-hooks is a tool. When installed (globally or as a dependency)
|
|
7
|
+
* it intentionally ships WITHOUT a `.library/` (see package.json `files`). The
|
|
8
|
+
* Library that the pipeline maintains always belongs to the repo under work —
|
|
9
|
+
* e.g. running `create-pr` inside `mscope-transactions` maintains that repo's
|
|
10
|
+
* docs. The only time it touches git-hooks' own Library is when dog-fooding
|
|
11
|
+
* (editing git-hooks itself), in which case the working repo IS git-hooks.
|
|
12
|
+
*
|
|
13
|
+
* Consumers must therefore resolve `.library/` from the working repo root, not
|
|
14
|
+
* relative to their own (package) location. This module centralizes that.
|
|
15
|
+
*
|
|
16
|
+
* Cross-platform: getRepoRoot() returns a git path (`/mnt/c/...` under WSL/Linux,
|
|
17
|
+
* `C:/...` under native Windows). `path.join` + `pathToFileURL` produce a valid
|
|
18
|
+
* `file://` URL for the current runtime, so `import()` of an absolute path works
|
|
19
|
+
* everywhere (a bare `import('C:\\...')` is invalid on Windows).
|
|
20
|
+
*
|
|
21
|
+
* Dependencies:
|
|
22
|
+
* - git-operations: getRepoRoot() — the single source of truth for the repo root
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync } from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import { pathToFileURL } from 'url';
|
|
28
|
+
import { getRepoRoot } from './git-operations.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a file:// URL for a module inside the current repo's `.library/`,
|
|
32
|
+
* suitable for dynamic `import()`.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} subpath - Path inside `.library/` (e.g. 'librarian/index.js')
|
|
35
|
+
* @returns {string} A `file://` URL string
|
|
36
|
+
*/
|
|
37
|
+
export function libraryModuleUrl(subpath) {
|
|
38
|
+
return pathToFileURL(path.join(getRepoRoot(), '.library', subpath)).href;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether the current repo has its own Library to maintain.
|
|
43
|
+
* Cheap pre-check so consumers can skip the pipeline cleanly (no scary warning)
|
|
44
|
+
* in repos that simply have no `.library/`.
|
|
45
|
+
*
|
|
46
|
+
* @returns {boolean} True when `.library/resolver.yaml` exists in the repo root
|
|
47
|
+
*/
|
|
48
|
+
export function hasLibrary() {
|
|
49
|
+
return existsSync(path.join(getRepoRoot(), '.library', 'resolver.yaml'));
|
|
50
|
+
}
|
|
@@ -194,6 +194,8 @@ const formatFileSection = ({ path, diff, content, isNew }) => {
|
|
|
194
194
|
* @param {Object} options.metadata - Additional metadata (repo name, branch, etc.)
|
|
195
195
|
* @param {string|null} options.commonContext - Shared commit overview injected before file diffs (optional)
|
|
196
196
|
* @param {string|null} options.batchRationale - Orchestrator's rationale for this batch (optional)
|
|
197
|
+
* @param {string|null} options.catalogueSection - Rendered platform antipattern catalogue (optional;
|
|
198
|
+
* callers obtain it via skill-registry getCatalogueSection())
|
|
197
199
|
* @returns {Promise<string>} Complete analysis prompt
|
|
198
200
|
*
|
|
199
201
|
* File data object structure:
|
|
@@ -211,6 +213,7 @@ const buildAnalysisPrompt = async ({
|
|
|
211
213
|
metadata = {},
|
|
212
214
|
commonContext = null,
|
|
213
215
|
batchRationale = null,
|
|
216
|
+
catalogueSection = null,
|
|
214
217
|
baseDir = '.claude/prompts'
|
|
215
218
|
} = {}) => {
|
|
216
219
|
logger.debug('prompt-builder - buildAnalysisPrompt', 'Building analysis prompt', {
|
|
@@ -219,6 +222,7 @@ const buildAnalysisPrompt = async ({
|
|
|
219
222
|
fileCount: files.length,
|
|
220
223
|
hasCommonContext: !!commonContext,
|
|
221
224
|
hasBatchRationale: !!batchRationale,
|
|
225
|
+
hasCatalogueSection: !!catalogueSection,
|
|
222
226
|
baseDir
|
|
223
227
|
});
|
|
224
228
|
|
|
@@ -247,6 +251,17 @@ const buildAnalysisPrompt = async ({
|
|
|
247
251
|
prompt += '\n\n=== EVALUATION GUIDELINES ===\n';
|
|
248
252
|
prompt += guidelines;
|
|
249
253
|
|
|
254
|
+
// Add the mscope platform antipattern catalogue if the caller supplied
|
|
255
|
+
// one (rendered via skill-registry getCatalogueSection()). The catalogue
|
|
256
|
+
// lists every JBE-NNN / UIK-NNN / etc. rule with severity; Claude is
|
|
257
|
+
// instructed to populate details[].rule with the catalogue ID when a
|
|
258
|
+
// finding matches. Unmatched findings (rule unset) become skill-gap
|
|
259
|
+
// candidates downstream.
|
|
260
|
+
if (catalogueSection) {
|
|
261
|
+
prompt += '\n\n=== PLATFORM ANTIPATTERN CATALOGUE ===\n';
|
|
262
|
+
prompt += catalogueSection;
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
// Add changes section
|
|
251
266
|
prompt += '\n\n=== CHANGES TO REVIEW ===\n';
|
|
252
267
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: skill-registry/catalogue.js
|
|
3
|
+
* Purpose: Render the parsed JBE/UIK rule index as a compact catalogue
|
|
4
|
+
* string suitable for injection into Claude's analysis prompt.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (Option E2): when Claude analyses code, we want it to
|
|
7
|
+
* label findings with the canonical rule ID (e.g. `JBE-001`) directly,
|
|
8
|
+
* rather than relying on fuzzy post-processing to guess. We do this by
|
|
9
|
+
* injecting a compact catalogue into the analysis prompt — one line per
|
|
10
|
+
* rule, grouped by scope, ~80 chars per line. The prompt's existing
|
|
11
|
+
* `details[].rule` field (already declared "optional" in the schema)
|
|
12
|
+
* becomes load-bearing: populated = covered; empty = skill-gap candidate.
|
|
13
|
+
*
|
|
14
|
+
* Token budget: ~95 rules × ~80 chars = ~7.6 KB → ~1,900 tokens added to
|
|
15
|
+
* each analysis prompt. Negligible against the 200k context window.
|
|
16
|
+
*
|
|
17
|
+
* Why a separate module: keeps catalogue formatting decisions (one-line
|
|
18
|
+
* vs full, severity tags, scope grouping) in one place where they can be
|
|
19
|
+
* tuned without touching prompt-builder or pre-commit.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render the rule catalogue as a compact string for prompt injection.
|
|
24
|
+
* @param {Array<object>} rules - From parser.loadRegistry().rules
|
|
25
|
+
* @returns {string} catalogue text (empty string if no rules)
|
|
26
|
+
*/
|
|
27
|
+
export function renderCatalogue(rules) {
|
|
28
|
+
if (!rules || rules.length === 0) return '';
|
|
29
|
+
|
|
30
|
+
const byScope = {};
|
|
31
|
+
for (const r of rules) {
|
|
32
|
+
(byScope[r.scope] ||= []).push(r);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push('Each finding MUST be classified against the platform antipattern catalogue below.');
|
|
37
|
+
lines.push('When a finding matches a catalogue entry, populate `details[].rule` with the rule ID');
|
|
38
|
+
lines.push('(e.g. "JBE-001"). Findings that don\'t match any entry leave `rule` empty — those');
|
|
39
|
+
lines.push('are candidate skill-gaps the team will review separately.');
|
|
40
|
+
lines.push('');
|
|
41
|
+
|
|
42
|
+
const SCOPE_LABELS = {
|
|
43
|
+
backend: 'BACKEND (Java / Spring Boot / JPA / SQL Server) — applies to .java and .sql files',
|
|
44
|
+
frontend: 'FRONTEND (React / @mscope/uikit / RHF / TanStack Query) — applies to .jsx/.tsx/.js/.css',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (const scope of ['backend', 'frontend']) {
|
|
48
|
+
const list = byScope[scope];
|
|
49
|
+
if (!list || list.length === 0) continue;
|
|
50
|
+
lines.push(SCOPE_LABELS[scope] || scope.toUpperCase());
|
|
51
|
+
|
|
52
|
+
// Sort: id ascending (group prefix together: JBE-001..JBE-NNN, UIK-001..)
|
|
53
|
+
list.sort((a, b) => a.id.localeCompare(b.id, 'en', { numeric: true }));
|
|
54
|
+
|
|
55
|
+
for (const r of list) {
|
|
56
|
+
const sevTag = (`[${ r.severity || 'unknown' }]`).padEnd(11);
|
|
57
|
+
const title = (r.title || '').replace(/`/g, '').slice(0, 110);
|
|
58
|
+
lines.push(` ${r.id.padEnd(9)} ${sevTag} ${title}`);
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lines.push('SEVERITY → JSON mapping:');
|
|
64
|
+
lines.push(' critical → BLOCKER (commit blocked)');
|
|
65
|
+
lines.push(' high → CRITICAL (commit blocked)');
|
|
66
|
+
lines.push(' medium → MAJOR (warn)');
|
|
67
|
+
lines.push(' low → MINOR (warn)');
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push('When you flag a finding, the catalogue entry\'s severity tells you the MIN severity');
|
|
70
|
+
lines.push('to assign — don\'t downgrade. You may upgrade if context warrants (e.g. a JBE-001');
|
|
71
|
+
lines.push('printStackTrace inside a financial calculation path is BLOCKER, not CRITICAL).');
|
|
72
|
+
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|