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.
@@ -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
- 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);
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
- 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}`);
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;
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * File: library-warnings.js
3
- * Purpose: Warning wording for Library verification gates in claude-hooks.
3
+ * Purpose: Staleness-warning wording and rendering for Library verification
4
+ * gates in claude-hooks.
4
5
  *
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.
6
+ * These are consumer rendering functions they take structured VerifyResult
7
+ * data from the Library and produce display-ready text (console) or Markdown
8
+ * (PR body). The Library produces the data; this module formats it.
9
+ *
10
+ * Constraints:
11
+ * - Pure functions of (VerifyResult, opts) — no I/O, no globals, no environment
12
+ * - Do NOT import from .library/ — lib/ must not depend on unshipped paths
9
13
  *
10
14
  * Related tickets:
11
15
  * AUT-3767 — original placeholder (retired by AUT-3769)
@@ -14,11 +18,125 @@
14
18
  * AUT-3738 — parent user story
15
19
  */
16
20
 
17
- export {
18
- CONSOLE_WARNING_TEMPLATE,
19
- PR_BODY_SECTION_TEMPLATE,
20
- PR_TAG_VALUE
21
- } from '../../.library/librarian/messages/staleness-warnings.js';
21
+ /**
22
+ * Render an assertive console warning for Library staleness.
23
+ *
24
+ * Returns plain text with a marker and the list of stale books.
25
+ * The closing section varies by consumer context via `opts.autoRegen`:
26
+ * - `'will-run'` — create-release: regen runs in the same command
27
+ * - `'deferred'` — bump-version: regen runs later via create-pr
28
+ *
29
+ * Consumers apply visual formatting (box, color, stderr routing) on top.
30
+ *
31
+ * Returns an empty string when the Library is clean.
32
+ *
33
+ * @param {Object} verifyResult
34
+ * @param {boolean} verifyResult.clean
35
+ * @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
36
+ * @param {string[]} verifyResult.recommendedScripts
37
+ * @param {Object} opts
38
+ * @param {'will-run'|'deferred'} opts.autoRegen — controls the call-to-action
39
+ * @returns {string} Plain-text console warning, or empty string if clean
40
+ */
41
+ export function CONSOLE_WARNING_TEMPLATE(verifyResult, opts = {}) {
42
+ if (verifyResult.clean) {
43
+ return '';
44
+ }
45
+
46
+ const lines = [];
47
+
48
+ lines.push('⚠️ Library is stale');
49
+ lines.push('');
50
+ lines.push('The following books are out of date:');
51
+ lines.push('');
52
+
53
+ for (const book of verifyResult.staleBooks) {
54
+ lines.push(` • ${book.path}`);
55
+ for (const reason of book.reasons) {
56
+ lines.push(` - ${reason}`);
57
+ }
58
+ }
59
+
60
+ lines.push('');
61
+
62
+ if (opts.autoRegen === 'will-run') {
63
+ lines.push('Auto-regeneration will run before the release is tagged.');
64
+ } else if (opts.autoRegen === 'deferred') {
65
+ lines.push('Library will be auto-regenerated when you run create-pr.');
66
+ }
67
+
68
+ return lines.join('\n');
69
+ }
70
+
71
+ /**
72
+ * Render a Markdown section for the PR body when Library is stale.
73
+ *
74
+ * The section heading is exactly `## ⚠️ Library is stale` — do not
75
+ * change this; downstream tooling may key on it. Includes stale-books
76
+ * list with reasons and a remediation section that varies by
77
+ * `opts.autoRegen`:
78
+ * - `'completed'` — auto-regen ran successfully; no scripts listed
79
+ * - `'failed'` — auto-regen failed; scripts listed as fallback
80
+ *
81
+ * The acknowledgement line is always present.
82
+ *
83
+ * Returns an empty string when the Library is clean.
84
+ *
85
+ * @param {Object} verifyResult
86
+ * @param {boolean} verifyResult.clean
87
+ * @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
88
+ * @param {string[]} verifyResult.recommendedScripts
89
+ * @param {Object} opts
90
+ * @param {'completed'|'failed'} opts.autoRegen — controls the remediation section
91
+ * @returns {string} Markdown section, or empty string if clean
92
+ */
93
+ export function PR_BODY_SECTION_TEMPLATE(verifyResult, opts = {}) {
94
+ if (verifyResult.clean) {
95
+ return '';
96
+ }
97
+
98
+ const lines = [];
99
+
100
+ lines.push('## ⚠️ Library is stale');
101
+ lines.push('');
102
+ lines.push('The following Library books are out of date:');
103
+ lines.push('');
104
+
105
+ for (const book of verifyResult.staleBooks) {
106
+ const reasonText = book.reasons.join('; ');
107
+ lines.push(`- \`${book.path}\` — ${reasonText}`);
108
+ }
109
+
110
+ lines.push('');
111
+
112
+ if (opts.autoRegen === 'completed') {
113
+ lines.push('**Auto-regeneration was performed.** The books listed above were regenerated in the release commit.');
114
+ } else if (opts.autoRegen === 'failed') {
115
+ lines.push('**Recommended remediation scripts:**');
116
+ lines.push('');
117
+ for (const script of verifyResult.recommendedScripts) {
118
+ lines.push(`- \`${script}\``);
119
+ }
120
+ lines.push('');
121
+ lines.push('_Auto-regeneration was attempted but failed. Run the scripts manually._');
122
+ }
123
+
124
+ lines.push('');
125
+ lines.push(
126
+ '_This release was tagged via claude-hooks. The Library lifecycle pipeline is integrated with `create-pr` and `back-merge` — staleness at release time indicates a path that bypassed the pipeline._'
127
+ );
128
+
129
+ return lines.join('\n');
130
+ }
131
+
132
+ /**
133
+ * Tag value used to mark a PR or Linear issue as library-stale.
134
+ *
135
+ * Applied as a Linear label slug or a PR-title prefix by consumers.
136
+ *
137
+ * @type {string}
138
+ */
139
+ export const PR_TAG_VALUE = 'library-stale';
22
140
 
23
141
  export const LIBRARY_VERIFY_SKIPPED_WARNING =
24
142
  'Library verification skipped due to an unexpected error. ' +
@@ -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 = {
@@ -502,10 +502,11 @@ async function verifySDKConnection() {
502
502
  * @param {number} options.timeout - Timeout in milliseconds (default: 120000 = 2 minutes)
503
503
  * @param {string} options.model - Claude model override (e.g., 'haiku', 'sonnet', 'opus')
504
504
  * @param {boolean} options.headless - Use SDK instead of CLI (default: false)
505
+ * @param {boolean} options.print - Use CLI print mode (-p): text-only output, no tool use (default: false)
505
506
  * @returns {Promise<string>} Claude's response
506
507
  * @throws {ClaudeClientError} If execution fails or times out
507
508
  */
508
- const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null } = {}) => {
509
+ const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null, print = false } = {}) => {
509
510
  // Headless mode: use Anthropic SDK directly (GH#133)
510
511
  // Branch here (not in executeClaudeWithRetry) because analyzeCode calls
511
512
  // executeClaude directly via withRetry — branching here covers all paths.
@@ -548,10 +549,14 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = nu
548
549
  // executed inside bash --login so that .profile/.bashrc set up the correct PATH.
549
550
  // All CLI flags are embedded in the bash -c command string (no spaces in flag values).
550
551
  const claudeParts = [loginShell.path];
552
+ if (print) claudeParts.push('-p');
551
553
  if (allowedTools.length > 0) claudeParts.push(`--allowedTools ${allowedTools.join(',')}`);
552
554
  if (model) claudeParts.push(`--model ${model}`);
553
555
  finalArgs.push('bash', '-lc', claudeParts.join(' '));
554
556
  } else {
557
+ if (print) {
558
+ finalArgs.push('-p');
559
+ }
555
560
  if (allowedTools.length > 0) {
556
561
  // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
557
562
  finalArgs.push('--allowedTools', allowedTools.join(','));
@@ -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')
@@ -196,6 +196,51 @@ export function getLatestLocalTag() {
196
196
  }
197
197
  }
198
198
 
199
+ /**
200
+ * Gets latest local tag reachable from HEAD
201
+ * Why: Excludes rogue tags from unrelated branches that were fetched locally
202
+ *
203
+ * @returns {string|null} Latest semver tag reachable from HEAD, or null
204
+ */
205
+ export function getLatestLocalTagOnBranch() {
206
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'Getting latest local tag on HEAD');
207
+
208
+ try {
209
+ const output = execGitTagCommand('git tag --merged HEAD --sort=-v:refname');
210
+
211
+ if (!output) {
212
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'No tags on HEAD');
213
+ return null;
214
+ }
215
+
216
+ const tags = output.split(/\r?\n/).filter((t) => t.length > 0);
217
+ const semverTags = tags.filter(isSemverTag);
218
+
219
+ if (semverTags.length === 0) {
220
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'No semver tags on HEAD', {
221
+ totalTags: tags.length
222
+ });
223
+ return null;
224
+ }
225
+
226
+ const latestTag = semverTags[0];
227
+
228
+ logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'Latest tag on HEAD', {
229
+ latestTag,
230
+ semverTags: semverTags.length
231
+ });
232
+
233
+ return latestTag;
234
+ } catch (error) {
235
+ logger.error(
236
+ 'git-tag-manager - getLatestLocalTagOnBranch',
237
+ 'Failed to get tags on HEAD',
238
+ error
239
+ );
240
+ return null;
241
+ }
242
+ }
243
+
199
244
  /**
200
245
  * Gets all remote tags
201
246
  * Why: Compare local tags with remote for push status
@@ -311,6 +356,65 @@ export async function getLatestRemoteTag(remoteName = null) {
311
356
  }
312
357
  }
313
358
 
359
+ /**
360
+ * Gets latest remote tag reachable from a specific branch
361
+ * Why: Scoped comparison — avoids rogue tags pushed from unmerged feature branches
362
+ *
363
+ * @param {string} baseBranch - Branch to scope tags to (e.g., 'develop')
364
+ * @param {string} remoteName - Remote name (default: 'origin')
365
+ * @returns {string|null} Latest semver tag name reachable from the branch, or null
366
+ */
367
+ export function getLatestRemoteTagOnBranch(baseBranch, remoteName = null) {
368
+ const remote = remoteName || getRemoteName();
369
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'Getting latest tag on branch', {
370
+ baseBranch,
371
+ remote
372
+ });
373
+
374
+ try {
375
+ // Ensure remote branch ref is fresh
376
+ execGitTagCommand(`git fetch ${remote} ${baseBranch} --quiet`);
377
+
378
+ // Get tags merged into the remote branch, sorted by version descending
379
+ const output = execGitTagCommand(
380
+ `git tag --merged ${remote}/${baseBranch} --sort=-v:refname`
381
+ );
382
+
383
+ if (!output) {
384
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'No tags on branch');
385
+ return null;
386
+ }
387
+
388
+ const tags = output.split(/\r?\n/).filter((line) => line.length > 0);
389
+ const semverTags = tags.filter(isSemverTag);
390
+
391
+ if (semverTags.length === 0) {
392
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'No semver tags on branch', {
393
+ totalTags: tags.length
394
+ });
395
+ return null;
396
+ }
397
+
398
+ // Already sorted by git --sort=-v:refname (descending), first is latest
399
+ const latestTag = semverTags[0];
400
+
401
+ logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'Latest tag on branch', {
402
+ baseBranch,
403
+ latestTag,
404
+ semverTags: semverTags.length
405
+ });
406
+
407
+ return latestTag;
408
+ } catch (error) {
409
+ logger.error(
410
+ 'git-tag-manager - getLatestRemoteTagOnBranch',
411
+ 'Failed to get tags on branch',
412
+ error
413
+ );
414
+ return null;
415
+ }
416
+ }
417
+
314
418
  /**
315
419
  * Checks if tag exists
316
420
  * Why: Prevents duplicate tag creation
@@ -139,7 +139,8 @@ const judgeAndFix = async (analysisResult, filesData, config, { headless = false
139
139
  const response = await executeClaudeWithRetry(prompt, {
140
140
  model,
141
141
  timeout: judgeTimeout,
142
- headless
142
+ headless,
143
+ print: true
143
144
  });
144
145
 
145
146
  const parsed = extractJSON(response);
@@ -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