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.
@@ -247,14 +247,21 @@ export function discoverVersionFiles(options = {}) {
247
247
  for (const fileType of fileTypes) {
248
248
  const registry = VERSION_FILE_TYPES[fileType];
249
249
  if (registry && entry.name === registry.filename) {
250
- const version = registry.readVersion(fullPath);
250
+ const rawVersion = registry.readVersion(fullPath);
251
+ const version = rawVersion !== null && validateVersionFormat(rawVersion) ? rawVersion : null;
252
+ if (rawVersion !== null && version === null) {
253
+ logger.debug('version-manager - discoverVersionFiles', 'Non-semver version skipped', {
254
+ relativePath: path.relative(repoRoot, fullPath),
255
+ rawVersion
256
+ });
257
+ }
251
258
  const descriptor = {
252
259
  path: fullPath,
253
260
  relativePath: path.relative(repoRoot, fullPath),
254
261
  type: fileType,
255
262
  projectLabel: registry.projectLabel,
256
263
  version,
257
- selected: true
264
+ selected: version !== null
258
265
  };
259
266
  discoveredFiles.push(descriptor);
260
267
  logger.debug('version-manager - discoverVersionFiles', 'Found version file', {
@@ -285,19 +292,19 @@ export function discoverVersionFiles(options = {}) {
285
292
  return a.relativePath.localeCompare(b.relativePath);
286
293
  });
287
294
 
288
- // Determine resolved version (prefer root-level file, then first found)
295
+ // Determine resolved version from semver files only (prefer root-level, then first found)
289
296
  let resolvedVersion = null;
290
- const rootFile = discoveredFiles.find((f) => !f.relativePath.includes(path.sep));
297
+ const semverFiles = discoveredFiles.filter((f) => f.selected);
298
+ const rootFile = semverFiles.find((f) => !f.relativePath.includes(path.sep));
291
299
  if (rootFile && rootFile.version) {
292
300
  resolvedVersion = rootFile.version;
293
- } else if (discoveredFiles.length > 0) {
294
- // Use first file's version as fallback
295
- const firstWithVersion = discoveredFiles.find((f) => f.version !== null);
301
+ } else if (semverFiles.length > 0) {
302
+ const firstWithVersion = semverFiles.find((f) => f.version !== null);
296
303
  resolvedVersion = firstWithVersion ? firstWithVersion.version : null;
297
304
  }
298
305
 
299
- // Check for version mismatch
300
- const versions = discoveredFiles.filter((f) => f.version !== null).map((f) => f.version);
306
+ // Check for version mismatch (only among semver files)
307
+ const versions = semverFiles.filter((f) => f.version !== null).map((f) => f.version);
301
308
  const uniqueVersions = [...new Set(versions)];
302
309
  const mismatch = uniqueVersions.length > 1;
303
310
 
@@ -1149,24 +1156,36 @@ export function compareVersions(version1, version2) {
1149
1156
  *
1150
1157
  * @returns {Promise<Object>} Validation result with alignment status and issues
1151
1158
  */
1152
- export async function validateVersionAlignment() {
1153
- logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment');
1159
+ export async function validateVersionAlignment(baseBranch = null) {
1160
+ logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment', {
1161
+ baseBranch
1162
+ });
1154
1163
 
1155
1164
  try {
1156
1165
  // Discover all version files
1157
1166
  const discovery = discoverVersionFiles();
1158
1167
 
1159
- // Get git tag version (parseTagVersion returns null for non-semver tags)
1160
- const { getLatestLocalTag, parseTagVersion } = await import('./git-tag-manager.js');
1161
- const latestTag = getLatestLocalTag();
1168
+ // Get git tag version scoped to HEAD when baseBranch is provided
1169
+ const {
1170
+ getLatestLocalTag, getLatestLocalTagOnBranch,
1171
+ parseTagVersion,
1172
+ getLatestRemoteTag, getLatestRemoteTagOnBranch
1173
+ } = await import('./git-tag-manager.js');
1174
+ const latestTag = baseBranch ? getLatestLocalTagOnBranch(baseBranch) : getLatestLocalTag();
1162
1175
  const tagVersion = latestTag ? parseTagVersion(latestTag) : null;
1163
1176
 
1164
1177
  // Get CHANGELOG version
1165
1178
  const changelogVersion = readChangelogVersion();
1166
-
1167
- // Get remote tag version (parseTagVersion returns null for non-semver tags)
1168
- const { getLatestRemoteTag } = await import('./git-tag-manager.js');
1169
- const latestRemoteTag = await getLatestRemoteTag();
1179
+ let latestRemoteTag;
1180
+ if (baseBranch) {
1181
+ latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
1182
+ // Fall back to global if branch-scoped lookup returns nothing
1183
+ if (!latestRemoteTag) {
1184
+ latestRemoteTag = await getLatestRemoteTag();
1185
+ }
1186
+ } else {
1187
+ latestRemoteTag = await getLatestRemoteTag();
1188
+ }
1170
1189
  const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
1171
1190
 
1172
1191
  // Collect all local versions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.61.2",
3
+ "version": "2.67.3",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
17
  "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
18
  "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
+ "test:e2e": "bash test/manual/sdlc-stability-check.sh",
19
20
  "lint": "eslint lib/ bin/claude-hooks .library/librarian/",
20
21
  "lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
21
22
  "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
@@ -30,7 +30,7 @@ OUTPUT_SCHEMA:
30
30
  "line":int,
31
31
  "method":"name",
32
32
  "message":"desc",
33
- "rule":"optional"
33
+ "rule":"JBE-NNN|UIK-NNN|STR-NNN|...|empty"
34
34
  }],
35
35
  "blockingIssues":[{
36
36
  "description":"text",
@@ -53,5 +53,6 @@ RULES:
53
53
  - ratings:A(0issues),B(1-2minor),C(1major|3-5minor),D(2+major|1critical),E(1+blocker|2+critical)
54
54
  - IMPORTANT: @Autowired usage in Spring is NOT a BLOCKER/CRITICAL issue (max severity: MAJOR)
55
55
  - Spring dependency injection patterns (@Autowired) should NOT block commits
56
+ - RULE CATALOGUE: if a `=== PLATFORM ANTIPATTERN CATALOGUE ===` section is present in this prompt, classify each finding against it. When a finding matches a catalogue entry, populate `details[].rule` with the rule ID exactly (e.g. "JBE-001", "UIK-002"). The catalogue's severity is the MINIMUM to assign — don't downgrade. Findings that don't match any catalogue entry leave `rule` empty (those become candidate skill-gaps for the team to review separately).
56
57
 
57
58
  ANALYZE_BELOW:
@@ -33,6 +33,12 @@
33
33
  "model": "opus"
34
34
  },
35
35
 
36
+ "skillRegistry": {
37
+ "enabled": true,
38
+ "blockOn": "never",
39
+ "resumeOnCreatePr": true
40
+ },
41
+
36
42
  "prAnalysis": {
37
43
  "model": "sonnet",
38
44
  "timeout": 300000,
@@ -90,6 +96,25 @@
90
96
  "use_case": "Override the default sonnet model for judge passes"
91
97
  },
92
98
 
99
+ "skillRegistry.enabled": {
100
+ "description": "Enable/disable the mscope automation-skills integration: deterministic rule checks in pre-commit, antipattern catalogue injection into the analysis prompt, and the skill-gap writer. NOTE the cross-repo side effect: when the sibling automation-skills repo is found, unclassified AI findings are appended as [skill-gap] candidates to that repo's skill-feedback.md (reviewed via `automation-skills retro`; never auto-merged into the registry).",
101
+ "default": "true",
102
+ "use_case": "Set to false to fully disable the skill-registry integration (it auto-skips anyway when the skill repo isn't found)"
103
+ },
104
+
105
+ "skillRegistry.blockOn": {
106
+ "description": "Severity threshold at which deterministic skill-registry findings block the commit",
107
+ "default": "never",
108
+ "examples": ["never", "critical", "high", "medium", "low"],
109
+ "use_case": "Set to 'critical' or 'high' once the team is ready to enforce the mscope rule catalogue"
110
+ },
111
+
112
+ "skillRegistry.resumeOnCreatePr": {
113
+ "description": "Run `automation-skills resume` (interactive lessons-learned capture to the skill repo's implementation-history.md) during create-pr, before tags are pushed. Skipped automatically in headless mode or when the automation-skills CLI is not installed.",
114
+ "default": "true",
115
+ "use_case": "Set to false if your team captures lessons through another channel"
116
+ },
117
+
93
118
  "prAnalysis.model": {
94
119
  "description": "Claude model for PR analysis",
95
120
  "default": "sonnet",