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.
@@ -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
- try {
361
- showInfo('📚 Checking library staleness...');
362
- const { runAll, formatHuman } = await import('../../.library/tools/staleness.js');
363
- const { getSourceDir, getBooksDir } = await import('../../.library/paths.js');
364
- const { getRepoRoot } = await import('../utils/git-operations.js');
365
- const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
366
- const hasDrift = stalenessResult.stale.length > 0 ||
367
- stalenessResult.orphan_books.length > 0 ||
368
- stalenessResult.unbooked_sources.length > 0;
369
- if (hasDrift) {
370
- error('📚 Library staleness detected cannot close release with stale books');
371
- process.stdout.write(formatHuman(stalenessResult));
372
- error('Run: npm run library:regenerate');
373
- process.exit(1);
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('../../.library/librarian/index.js');
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
- logger.debug('create-release', 'Step 2: Running Library verification gate');
378
- try {
379
- const { verify } = await import('../../.library/librarian/index.js');
380
- verifyResult = await verify();
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
- if (verifyResult.clean) {
383
- logger.debug('create-release', 'Library is clean — no warning needed');
384
- } else {
385
- _emitLibraryWarning(verifyResult);
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('../../.library/librarian/index.js');
520
+ const { createPrPipeline } = await import(libraryModuleUrl('librarian/index.js'));
515
521
  const root = getRepoRoot();
516
522
  const pipelineSummary = await createPrPipeline({ repoRoot: root });
517
523
 
@@ -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 = getPackageJson().version;
45
- const latestVersion = await getLatestVersion('claude-git-hooks');
43
+ const { currentVersion, latestVersion, isNewer } = await getUpdateStatus();
46
44
 
47
- if (currentVersion === latestVersion) {
48
- return true; // Already updated
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
- execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
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)) {
@@ -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 { execSync } from 'child_process';
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 = getPackageJson().version;
26
- const latestVersion = await getLatestVersion('claude-git-hooks');
19
+ const { currentVersion, latestVersion, isNewer, isDev } = await getUpdateStatus();
27
20
 
28
- const comparison = compareVersions(currentVersion, latestVersion);
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
- execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
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
- execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
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,
@@ -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;
@@ -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 = {