claude-git-hooks 2.51.2 → 2.66.1

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.
@@ -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
@@ -940,6 +940,36 @@ export const findExistingPR = async ({ owner, repo, head, base }) => {
940
940
  }
941
941
  };
942
942
 
943
+ /**
944
+ * Update the body of an existing pull request
945
+ * @param {string} owner - Repository owner
946
+ * @param {string} repo - Repository name
947
+ * @param {number} number - PR number
948
+ * @param {string} body - New PR body content
949
+ * @returns {Promise<Object>} Updated PR data
950
+ */
951
+ export const updatePullRequestBody = async (owner, repo, number, body) => {
952
+ logger.debug('github-api - updatePullRequestBody', 'Updating PR body', {
953
+ owner, repo, number, bodyLength: body.length
954
+ });
955
+
956
+ const octokit = getOctokit();
957
+
958
+ const { data } = await octokit.pulls.update({
959
+ owner,
960
+ repo,
961
+ pull_number: number,
962
+ body
963
+ });
964
+
965
+ logger.debug('github-api - updatePullRequestBody', 'PR body updated', {
966
+ number: data.number,
967
+ url: data.html_url
968
+ });
969
+
970
+ return data;
971
+ };
972
+
943
973
  /**
944
974
  * Get repository information
945
975
  * Why: Fetch repo metadata for validation and context
@@ -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);
@@ -45,6 +45,12 @@ export function parseEslintOutput(stdout) {
45
45
 
46
46
  for (const fileResult of results) {
47
47
  for (const msg of fileResult.messages || []) {
48
+ // Skip ESLint meta-warnings about ignored files — not code quality issues.
49
+ // Dot-directory files (e.g. .library/) trigger "File ignored by default"
50
+ // even when ignorePatterns negation is set, because ESLint's default
51
+ // dot-directory ignore takes precedence for explicitly passed file paths.
52
+ if (msg.message && msg.message.startsWith('File ignored')) continue;
53
+
48
54
  const issue = {
49
55
  file: fileResult.filePath || '',
50
56
  line: msg.line,
@@ -248,13 +248,14 @@ export function discoverVersionFiles(options = {}) {
248
248
  const registry = VERSION_FILE_TYPES[fileType];
249
249
  if (registry && entry.name === registry.filename) {
250
250
  const version = registry.readVersion(fullPath);
251
+ const isSemver = version !== null && validateVersionFormat(version);
251
252
  const descriptor = {
252
253
  path: fullPath,
253
254
  relativePath: path.relative(repoRoot, fullPath),
254
255
  type: fileType,
255
256
  projectLabel: registry.projectLabel,
256
257
  version,
257
- selected: true
258
+ selected: isSemver
258
259
  };
259
260
  discoveredFiles.push(descriptor);
260
261
  logger.debug('version-manager - discoverVersionFiles', 'Found version file', {
@@ -285,19 +286,19 @@ export function discoverVersionFiles(options = {}) {
285
286
  return a.relativePath.localeCompare(b.relativePath);
286
287
  });
287
288
 
288
- // Determine resolved version (prefer root-level file, then first found)
289
+ // Determine resolved version from semver files only (prefer root-level, then first found)
289
290
  let resolvedVersion = null;
290
- const rootFile = discoveredFiles.find((f) => !f.relativePath.includes(path.sep));
291
+ const semverFiles = discoveredFiles.filter((f) => f.selected);
292
+ const rootFile = semverFiles.find((f) => !f.relativePath.includes(path.sep));
291
293
  if (rootFile && rootFile.version) {
292
294
  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);
295
+ } else if (semverFiles.length > 0) {
296
+ const firstWithVersion = semverFiles.find((f) => f.version !== null);
296
297
  resolvedVersion = firstWithVersion ? firstWithVersion.version : null;
297
298
  }
298
299
 
299
- // Check for version mismatch
300
- const versions = discoveredFiles.filter((f) => f.version !== null).map((f) => f.version);
300
+ // Check for version mismatch (only among semver files)
301
+ const versions = semverFiles.filter((f) => f.version !== null).map((f) => f.version);
301
302
  const uniqueVersions = [...new Set(versions)];
302
303
  const mismatch = uniqueVersions.length > 1;
303
304
 
@@ -1149,24 +1150,36 @@ export function compareVersions(version1, version2) {
1149
1150
  *
1150
1151
  * @returns {Promise<Object>} Validation result with alignment status and issues
1151
1152
  */
1152
- export async function validateVersionAlignment() {
1153
- logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment');
1153
+ export async function validateVersionAlignment(baseBranch = null) {
1154
+ logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment', {
1155
+ baseBranch
1156
+ });
1154
1157
 
1155
1158
  try {
1156
1159
  // Discover all version files
1157
1160
  const discovery = discoverVersionFiles();
1158
1161
 
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();
1162
+ // Get git tag version scoped to HEAD when baseBranch is provided
1163
+ const {
1164
+ getLatestLocalTag, getLatestLocalTagOnBranch,
1165
+ parseTagVersion,
1166
+ getLatestRemoteTag, getLatestRemoteTagOnBranch
1167
+ } = await import('./git-tag-manager.js');
1168
+ const latestTag = baseBranch ? getLatestLocalTagOnBranch(baseBranch) : getLatestLocalTag();
1162
1169
  const tagVersion = latestTag ? parseTagVersion(latestTag) : null;
1163
1170
 
1164
1171
  // Get CHANGELOG version
1165
1172
  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();
1173
+ let latestRemoteTag;
1174
+ if (baseBranch) {
1175
+ latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
1176
+ // Fall back to global if branch-scoped lookup returns nothing
1177
+ if (!latestRemoteTag) {
1178
+ latestRemoteTag = await getLatestRemoteTag();
1179
+ }
1180
+ } else {
1181
+ latestRemoteTag = await getLatestRemoteTag();
1182
+ }
1170
1183
  const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
1171
1184
 
1172
1185
  // Collect all local versions
package/package.json CHANGED
@@ -1,83 +1,85 @@
1
- {
2
- "name": "claude-git-hooks",
3
- "version": "2.51.2",
4
- "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
- "type": "module",
6
- "bin": {
7
- "claude-hooks": "./bin/claude-hooks"
8
- },
9
- "scripts": {
10
- "test": "npm run test:all",
11
- "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
- "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
- "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
- "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
- "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
- "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
- "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
- "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
- "lint": "eslint lib/ bin/claude-hooks",
20
- "lint:fix": "eslint lib/ bin/claude-hooks --fix",
21
- "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
22
- "precommit": "npm run lint && npm run test:smoke",
23
- "prepublishOnly": "npm run test:all",
24
- "library:check": "node .library/bin/library check",
25
- "library:regenerate": "node .library/bin/library regenerate",
26
- "library:extract": "node .library/bin/library extract",
27
- "library:tokens": "node .library/bin/library tokens",
28
- "library:graph": "node .library/bin/library graph",
29
- "library:inject": "node .library/bin/library inject",
30
- "library:validate": "node .library/bin/library validate",
31
- "library:report": "node .library/bin/library report"
32
- },
33
- "keywords": [
34
- "git",
35
- "hooks",
36
- "claude",
37
- "ai",
38
- "code-review",
39
- "commit-messages",
40
- "pre-commit",
41
- "automation"
42
- ],
43
- "author": "Pablo Rovito",
44
- "license": "MIT",
45
- "repository": {
46
- "type": "git",
47
- "url": "https://github.com/mscope-S-L/git-hooks.git"
48
- },
49
- "engines": {
50
- "node": ">=16.9.0"
51
- },
52
- "engineStrict": false,
53
- "os": [
54
- "darwin",
55
- "linux",
56
- "win32"
57
- ],
58
- "preferGlobal": true,
59
- "files": [
60
- "bin/",
61
- "lib/",
62
- "templates/",
63
- "README.md",
64
- "CHANGELOG.md",
65
- "CLAUDE.md",
66
- "LICENSE"
67
- ],
68
- "dependencies": {
69
- "@anthropic-ai/sdk": "^0.91.0",
70
- "@octokit/rest": "^21.0.0",
71
- "langfuse": "^3.38.20"
72
- },
73
- "devDependencies": {
74
- "@types/jest": "^29.5.0",
75
- "eslint": "^8.57.1",
76
- "jest": "^29.7.0",
77
- "js-tiktoken": "^1.0.18",
78
- "madge": "^8.0.0",
79
- "prettier": "^3.2.0",
80
- "tree-sitter-wasms": "^0.1.13",
81
- "web-tree-sitter": "^0.24.7"
82
- }
83
- }
1
+ {
2
+ "name": "claude-git-hooks",
3
+ "version": "2.66.1",
4
+ "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-hooks": "./bin/claude-hooks"
8
+ },
9
+ "scripts": {
10
+ "test": "npm run test:all",
11
+ "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
+ "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
+ "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
+ "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
+ "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
+ "test:e2e": "bash test/manual/sdlc-stability-check.sh",
20
+ "test:full": "npm run test:all && npm run test:e2e",
21
+ "lint": "eslint lib/ bin/claude-hooks .library/librarian/",
22
+ "lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
23
+ "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
24
+ "precommit": "npm run lint && npm run test:smoke",
25
+ "prepublishOnly": "npm run test:all",
26
+ "library:check": "node .library/bin/library check",
27
+ "library:regenerate": "node .library/bin/library regenerate",
28
+ "library:extract": "node .library/bin/library extract",
29
+ "library:tokens": "node .library/bin/library tokens",
30
+ "library:graph": "node .library/bin/library graph",
31
+ "library:inject": "node .library/bin/library inject",
32
+ "library:validate": "node .library/bin/library validate",
33
+ "library:report": "node .library/bin/library report"
34
+ },
35
+ "keywords": [
36
+ "git",
37
+ "hooks",
38
+ "claude",
39
+ "ai",
40
+ "code-review",
41
+ "commit-messages",
42
+ "pre-commit",
43
+ "automation"
44
+ ],
45
+ "author": "Pablo Rovito",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/mscope-S-L/git-hooks.git"
50
+ },
51
+ "engines": {
52
+ "node": ">=16.9.0"
53
+ },
54
+ "engineStrict": false,
55
+ "os": [
56
+ "darwin",
57
+ "linux",
58
+ "win32"
59
+ ],
60
+ "preferGlobal": true,
61
+ "files": [
62
+ "bin/",
63
+ "lib/",
64
+ "templates/",
65
+ "README.md",
66
+ "CHANGELOG.md",
67
+ "CLAUDE.md",
68
+ "LICENSE"
69
+ ],
70
+ "dependencies": {
71
+ "@anthropic-ai/sdk": "^0.91.0",
72
+ "@octokit/rest": "^21.0.0",
73
+ "langfuse": "^3.38.20"
74
+ },
75
+ "devDependencies": {
76
+ "@types/jest": "^29.5.0",
77
+ "eslint": "^8.57.1",
78
+ "jest": "^29.7.0",
79
+ "js-tiktoken": "^1.0.18",
80
+ "madge": "^8.0.0",
81
+ "prettier": "^3.2.0",
82
+ "tree-sitter-wasms": "^0.1.13",
83
+ "web-tree-sitter": "^0.24.7"
84
+ }
85
+ }