claude-git-hooks 2.30.2 → 2.33.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.
- package/CHANGELOG.md +80 -9
- package/CLAUDE.md +139 -87
- package/README.md +117 -93
- package/lib/commands/close-release.js +7 -7
- package/lib/commands/create-pr.js +47 -21
- package/lib/commands/diff-batch-info.js +0 -9
- package/lib/commands/telemetry-cmd.js +0 -15
- package/lib/config.js +0 -1
- package/lib/hooks/pre-commit.js +43 -0
- package/lib/hooks/prepare-commit-msg.js +15 -0
- package/lib/utils/analysis-engine.js +10 -0
- package/lib/utils/authorization.js +6 -7
- package/lib/utils/github-api.js +92 -60
- package/lib/utils/github-client.js +5 -105
- package/lib/utils/judge.js +12 -5
- package/lib/utils/label-resolver.js +232 -0
- package/lib/utils/metrics.js +185 -0
- package/lib/utils/pr-statistics.js +15 -0
- package/lib/utils/remote-config.js +102 -0
- package/lib/utils/reviewer-selector.js +154 -0
- package/lib/utils/telemetry.js +12 -39
- package/package.json +1 -1
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -32,6 +32,7 @@ import { loadPreset } from '../utils/preset-loader.js';
|
|
|
32
32
|
import { getVersion } from '../utils/package-info.js';
|
|
33
33
|
import logger from '../utils/logger.js';
|
|
34
34
|
import { getConfig } from '../config.js';
|
|
35
|
+
import { recordMetric } from '../utils/metrics.js';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Configuration loaded from lib/config.js
|
|
@@ -218,6 +219,23 @@ const main = async () => {
|
|
|
218
219
|
console.log();
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
// Record blocked analysis metric
|
|
223
|
+
recordMetric('analysis.completed', {
|
|
224
|
+
files: validFiles.map(f => f.path),
|
|
225
|
+
fileCount: validFiles.length,
|
|
226
|
+
issues: result.issues,
|
|
227
|
+
details: (result.details || []).map(d => ({
|
|
228
|
+
severity: d.severity, category: d.category,
|
|
229
|
+
description: d.description, file: d.file
|
|
230
|
+
})),
|
|
231
|
+
blocked: true,
|
|
232
|
+
duration: Date.now() - startTime,
|
|
233
|
+
preset: presetName,
|
|
234
|
+
orchestrated: filesData.length >= 3
|
|
235
|
+
}).catch((err) => {
|
|
236
|
+
logger.debug('pre-commit - main', 'Metrics recording failed (blocked)', err);
|
|
237
|
+
});
|
|
238
|
+
|
|
221
239
|
// Generate resolution prompt if needed
|
|
222
240
|
if (shouldGeneratePrompt(result)) {
|
|
223
241
|
const resolutionPath = await generateResolutionPrompt(result, {
|
|
@@ -235,6 +253,23 @@ const main = async () => {
|
|
|
235
253
|
process.exit(1);
|
|
236
254
|
}
|
|
237
255
|
|
|
256
|
+
// Record successful analysis metric
|
|
257
|
+
recordMetric('analysis.completed', {
|
|
258
|
+
files: validFiles.map(f => f.path),
|
|
259
|
+
fileCount: validFiles.length,
|
|
260
|
+
issues: result.issues,
|
|
261
|
+
details: (result.details || []).map(d => ({
|
|
262
|
+
severity: d.severity, category: d.category,
|
|
263
|
+
description: d.description, file: d.file
|
|
264
|
+
})),
|
|
265
|
+
blocked: false,
|
|
266
|
+
duration: Date.now() - startTime,
|
|
267
|
+
preset: presetName,
|
|
268
|
+
orchestrated: filesData.length >= 3
|
|
269
|
+
}).catch((err) => {
|
|
270
|
+
logger.debug('pre-commit - main', 'Metrics recording failed (success)', err);
|
|
271
|
+
});
|
|
272
|
+
|
|
238
273
|
// Success
|
|
239
274
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
240
275
|
console.log(`\n⏱️ Analysis time: ${duration}s`);
|
|
@@ -242,6 +277,14 @@ const main = async () => {
|
|
|
242
277
|
|
|
243
278
|
process.exit(0);
|
|
244
279
|
} catch (error) {
|
|
280
|
+
// Record analysis failure metric
|
|
281
|
+
recordMetric('analysis.failed', {
|
|
282
|
+
errorMessage: error.message,
|
|
283
|
+
duration: Date.now() - startTime
|
|
284
|
+
}).catch((err) => {
|
|
285
|
+
logger.debug('pre-commit - main', 'Metrics recording failed (error)', err);
|
|
286
|
+
});
|
|
287
|
+
|
|
245
288
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
246
289
|
console.log(`\n⏱️ Analysis time: ${duration}s`);
|
|
247
290
|
|
|
@@ -25,6 +25,7 @@ import { getVersion } from '../utils/package-info.js';
|
|
|
25
25
|
import logger from '../utils/logger.js';
|
|
26
26
|
import { getConfig } from '../config.js';
|
|
27
27
|
import { getOrPromptTaskId, formatWithTaskId } from '../utils/task-id.js';
|
|
28
|
+
import { recordMetric } from '../utils/metrics.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Builds commit message generation prompt
|
|
@@ -265,6 +266,14 @@ const main = async () => {
|
|
|
265
266
|
// Write to commit message file
|
|
266
267
|
await fs.writeFile(commitMsgFile, `${message}\n`, 'utf8');
|
|
267
268
|
|
|
269
|
+
// Record commit generation metric
|
|
270
|
+
recordMetric('commit.generated', {
|
|
271
|
+
fileCount: filesData.length,
|
|
272
|
+
files: filesData.map(f => f.path),
|
|
273
|
+
duration: Date.now() - startTime,
|
|
274
|
+
success: true
|
|
275
|
+
}).catch(() => {});
|
|
276
|
+
|
|
268
277
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
269
278
|
console.error(`⏱️ Message generation completed in ${duration}s`);
|
|
270
279
|
|
|
@@ -272,6 +281,12 @@ const main = async () => {
|
|
|
272
281
|
|
|
273
282
|
process.exit(0);
|
|
274
283
|
} catch (error) {
|
|
284
|
+
// Record commit failure metric
|
|
285
|
+
recordMetric('commit.failed', {
|
|
286
|
+
errorMessage: error.message,
|
|
287
|
+
duration: Date.now() - startTime
|
|
288
|
+
}).catch(() => {});
|
|
289
|
+
|
|
275
290
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
276
291
|
console.error(`⏱️ Message generation failed after ${duration}s`);
|
|
277
292
|
|
|
@@ -34,6 +34,7 @@ import { analyzeCode } from './claude-client.js';
|
|
|
34
34
|
import { orchestrateBatches } from './diff-analysis-orchestrator.js';
|
|
35
35
|
import { buildAnalysisPrompt } from './prompt-builder.js';
|
|
36
36
|
import logger from './logger.js';
|
|
37
|
+
import { recordMetric } from './metrics.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Standard file data schema used throughout the analysis pipeline
|
|
@@ -448,6 +449,15 @@ export const runAnalysis = async (filesData, config, options = {}) => {
|
|
|
448
449
|
|
|
449
450
|
result = consolidateResults(results);
|
|
450
451
|
|
|
452
|
+
// Record orchestration metric
|
|
453
|
+
recordMetric('orchestration.completed', {
|
|
454
|
+
fileCount: filesData.length,
|
|
455
|
+
batchCount: batches.length,
|
|
456
|
+
batchBreakdown: batches.map(b => ({ files: b.files.length, model: b.model })),
|
|
457
|
+
orchestrationTime,
|
|
458
|
+
hook
|
|
459
|
+
}).catch(() => {});
|
|
460
|
+
|
|
451
461
|
if (saveDebug) {
|
|
452
462
|
const { saveDebugResponse } = await import('./claude-client.js');
|
|
453
463
|
await saveDebugResponse(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Purpose: Role-based authorization for protected workflow commands
|
|
4
4
|
*
|
|
5
5
|
* Design:
|
|
6
|
-
* - Permissions are sourced from a centralized GitHub repo (mscope-S-L/
|
|
6
|
+
* - Permissions are sourced from a centralized GitHub repo (mscope-S-L/git-hooks-config)
|
|
7
7
|
* - Role hierarchy is defined in permissions.json (ordered lowest → highest)
|
|
8
8
|
* - Commands not in commandPermissions are open to all
|
|
9
9
|
* - All failure scenarios are fail-closed (block on any error)
|
|
@@ -31,14 +31,13 @@ import {
|
|
|
31
31
|
} from './github-api.js';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Constants for the centralized
|
|
34
|
+
* Constants for the centralized config repository
|
|
35
35
|
*
|
|
36
|
-
* PERMISSIONS_REPO_OWNER/NAME: where the permissions file lives (
|
|
37
|
-
*
|
|
38
|
-
* PERMISSIONS_ORG: the GitHub org used for membership checks — stable regardless of repo location.
|
|
36
|
+
* PERMISSIONS_REPO_OWNER/NAME: where the permissions file lives (mscope-S-L org).
|
|
37
|
+
* PERMISSIONS_ORG: the GitHub org used for membership checks.
|
|
39
38
|
*/
|
|
40
|
-
const PERMISSIONS_REPO_OWNER = '
|
|
41
|
-
const PERMISSIONS_REPO_NAME = '
|
|
39
|
+
const PERMISSIONS_REPO_OWNER = 'mscope-S-L';
|
|
40
|
+
const PERMISSIONS_REPO_NAME = 'git-hooks-config';
|
|
42
41
|
const PERMISSIONS_FILE_PATH = 'permissions.json';
|
|
43
42
|
const PERMISSIONS_ORG = 'mscope-S-L';
|
|
44
43
|
|
package/lib/utils/github-api.js
CHANGED
|
@@ -212,7 +212,8 @@ export const createPullRequest = async ({
|
|
|
212
212
|
base,
|
|
213
213
|
draft = false,
|
|
214
214
|
labels = [],
|
|
215
|
-
reviewers = []
|
|
215
|
+
reviewers = [],
|
|
216
|
+
teamReviewers = []
|
|
216
217
|
}) => {
|
|
217
218
|
logger.debug('github-api - createPullRequest', 'Starting PR creation', {
|
|
218
219
|
owner,
|
|
@@ -286,28 +287,32 @@ export const createPullRequest = async ({
|
|
|
286
287
|
logger.debug('github-api - createPullRequest', 'No labels to add');
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
// Step 3: Request reviewers (if any)
|
|
290
|
-
if (reviewers.length > 0) {
|
|
290
|
+
// Step 3: Request reviewers (if any — individual and/or team)
|
|
291
|
+
if (reviewers.length > 0 || teamReviewers.length > 0) {
|
|
291
292
|
logger.debug('github-api - createPullRequest', 'Requesting reviewers', {
|
|
292
293
|
prNumber: pr.number,
|
|
293
|
-
reviewers
|
|
294
|
+
reviewers,
|
|
295
|
+
teamReviewers
|
|
294
296
|
});
|
|
295
297
|
try {
|
|
296
298
|
await octokit.pulls.requestReviewers({
|
|
297
299
|
owner,
|
|
298
300
|
repo,
|
|
299
301
|
pull_number: pr.number,
|
|
300
|
-
reviewers
|
|
302
|
+
reviewers,
|
|
303
|
+
team_reviewers: teamReviewers
|
|
301
304
|
});
|
|
302
305
|
logger.debug('github-api - createPullRequest', 'Reviewers requested successfully', {
|
|
303
|
-
reviewers
|
|
306
|
+
reviewers,
|
|
307
|
+
teamReviewers
|
|
304
308
|
});
|
|
305
309
|
} catch (reviewerError) {
|
|
306
310
|
// Non-fatal: PR was created, reviewer request failed
|
|
307
311
|
logger.warning(`Could not request reviewers: ${reviewerError.message}`);
|
|
308
312
|
logger.debug('github-api - createPullRequest', 'Reviewer request failed', {
|
|
309
313
|
error: reviewerError.message,
|
|
310
|
-
reviewers
|
|
314
|
+
reviewers,
|
|
315
|
+
teamReviewers
|
|
311
316
|
});
|
|
312
317
|
}
|
|
313
318
|
} else {
|
|
@@ -324,7 +329,8 @@ export const createPullRequest = async ({
|
|
|
324
329
|
head: pr.head.ref,
|
|
325
330
|
base: pr.base.ref,
|
|
326
331
|
labels,
|
|
327
|
-
reviewers
|
|
332
|
+
reviewers,
|
|
333
|
+
teamReviewers
|
|
328
334
|
};
|
|
329
335
|
|
|
330
336
|
logger.debug('github-api - createPullRequest', 'PR creation completed', result);
|
|
@@ -758,69 +764,95 @@ export const createPullRequestReview = async (
|
|
|
758
764
|
};
|
|
759
765
|
|
|
760
766
|
/**
|
|
761
|
-
*
|
|
767
|
+
* List teams with access to a repository
|
|
768
|
+
* Why: Needed by reviewer-selector to resolve team-based reviewers
|
|
762
769
|
*
|
|
763
|
-
* @param {
|
|
764
|
-
* @param {string}
|
|
765
|
-
* @
|
|
766
|
-
* @
|
|
767
|
-
* @returns {Promise<string|null>} CODEOWNERS content or null if not found
|
|
770
|
+
* @param {string} owner - Repository owner
|
|
771
|
+
* @param {string} repo - Repository name
|
|
772
|
+
* @returns {Promise<Array<{slug: string, name: string, permission: string}>>} Teams with access
|
|
773
|
+
* @throws {GitHubAPIError} On failure
|
|
768
774
|
*/
|
|
769
|
-
export const
|
|
770
|
-
logger.debug('github-api -
|
|
775
|
+
export const listRepoTeams = async (owner, repo) => {
|
|
776
|
+
logger.debug('github-api - listRepoTeams', 'Listing repo teams', { owner, repo });
|
|
771
777
|
|
|
772
778
|
const octokit = getOctokit();
|
|
773
779
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
try {
|
|
777
|
-
const { data: repoData } = await octokit.repos.get({ owner, repo });
|
|
778
|
-
branch = repoData.default_branch;
|
|
779
|
-
logger.debug('github-api - readCodeowners', 'Using default branch', { branch });
|
|
780
|
-
} catch (error) {
|
|
781
|
-
branch = 'main'; // Fallback
|
|
782
|
-
logger.debug(
|
|
783
|
-
'github-api - readCodeowners',
|
|
784
|
-
'Could not get default branch, using main',
|
|
785
|
-
{ error: error.message }
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
780
|
+
try {
|
|
781
|
+
const { data } = await octokit.repos.listTeams({ owner, repo });
|
|
789
782
|
|
|
790
|
-
|
|
791
|
-
|
|
783
|
+
const teams = data.map((t) => ({
|
|
784
|
+
slug: t.slug,
|
|
785
|
+
name: t.name,
|
|
786
|
+
permission: t.permission
|
|
787
|
+
}));
|
|
792
788
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
789
|
+
logger.debug('github-api - listRepoTeams', 'Teams fetched', {
|
|
790
|
+
count: teams.length,
|
|
791
|
+
slugs: teams.map((t) => t.slug)
|
|
792
|
+
});
|
|
796
793
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
794
|
+
return teams;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
const statusCode = error.status || error.response?.status;
|
|
797
|
+
logger.debug('github-api - listRepoTeams', 'Failed to list repo teams', {
|
|
798
|
+
owner,
|
|
799
|
+
repo,
|
|
800
|
+
status: statusCode,
|
|
801
|
+
message: error.message
|
|
802
|
+
});
|
|
803
803
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
lines: content.split('\n').length
|
|
810
|
-
});
|
|
811
|
-
return content;
|
|
812
|
-
}
|
|
813
|
-
} catch (error) {
|
|
814
|
-
logger.debug('github-api - readCodeowners', 'Path not found', {
|
|
815
|
-
path,
|
|
816
|
-
error: error.message
|
|
817
|
-
});
|
|
818
|
-
// Continue to next path
|
|
819
|
-
}
|
|
804
|
+
throw new GitHubAPIError(`Failed to list teams for ${owner}/${repo}: ${error.message}`, {
|
|
805
|
+
cause: error,
|
|
806
|
+
statusCode,
|
|
807
|
+
context: { owner, repo }
|
|
808
|
+
});
|
|
820
809
|
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* List members of a GitHub team within an organization
|
|
814
|
+
* Why: Needed by reviewer-selector to resolve team slug to individual usernames
|
|
815
|
+
*
|
|
816
|
+
* @param {string} org - GitHub organization name
|
|
817
|
+
* @param {string} teamSlug - Team slug (e.g. 'automation', not '@org/automation')
|
|
818
|
+
* @returns {Promise<Array<{login: string}>>} Team members
|
|
819
|
+
* @throws {GitHubAPIError} On failure
|
|
820
|
+
*/
|
|
821
|
+
export const listTeamMembers = async (org, teamSlug) => {
|
|
822
|
+
logger.debug('github-api - listTeamMembers', 'Listing team members', { org, teamSlug });
|
|
823
|
+
|
|
824
|
+
const octokit = getOctokit();
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
const { data } = await octokit.teams.listMembersInOrg({
|
|
828
|
+
org,
|
|
829
|
+
team_slug: teamSlug,
|
|
830
|
+
per_page: 100
|
|
831
|
+
});
|
|
821
832
|
|
|
822
|
-
|
|
823
|
-
|
|
833
|
+
const members = data.map((m) => ({ login: m.login }));
|
|
834
|
+
|
|
835
|
+
logger.debug('github-api - listTeamMembers', 'Members fetched', {
|
|
836
|
+
teamSlug,
|
|
837
|
+
count: members.length,
|
|
838
|
+
logins: members.map((m) => m.login)
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
return members;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
const statusCode = error.status || error.response?.status;
|
|
844
|
+
logger.debug('github-api - listTeamMembers', 'Failed to list team members', {
|
|
845
|
+
org,
|
|
846
|
+
teamSlug,
|
|
847
|
+
status: statusCode,
|
|
848
|
+
message: error.message
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
throw new GitHubAPIError(
|
|
852
|
+
`Failed to list members of team '${teamSlug}' in ${org}: ${error.message}`,
|
|
853
|
+
{ cause: error, statusCode, context: { org, teamSlug } }
|
|
854
|
+
);
|
|
855
|
+
}
|
|
824
856
|
};
|
|
825
857
|
|
|
826
858
|
/**
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File: github-client.js
|
|
3
|
-
* Purpose: GitHub utilities for
|
|
3
|
+
* Purpose: GitHub utilities for repository parsing and config-based reviewer detection
|
|
4
4
|
*
|
|
5
5
|
* Note: PR creation and API calls are in github-api.js (Octokit-based)
|
|
6
|
+
* Team-based reviewer selection is in reviewer-selector.js
|
|
6
7
|
* This module handles:
|
|
7
8
|
* - Repository URL parsing
|
|
8
|
-
* -
|
|
9
|
-
* - Reviewer detection from CODEOWNERS and config
|
|
9
|
+
* - Config-based reviewer detection (fallback for team-based selection)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execSync } from 'child_process';
|
|
@@ -57,93 +57,6 @@ export const parseGitHubRepo = () => {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
* Read CODEOWNERS file from repository
|
|
62
|
-
* Why: Auto-detect reviewers based on file changes
|
|
63
|
-
*
|
|
64
|
-
* @returns {Promise<string|null>} - CODEOWNERS content or null if not found
|
|
65
|
-
*/
|
|
66
|
-
export const readCodeowners = async () => {
|
|
67
|
-
logger.debug('github-client - readCodeowners', 'Reading CODEOWNERS file');
|
|
68
|
-
|
|
69
|
-
const repo = parseGitHubRepo();
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
// Use Octokit-based implementation from github-api.js
|
|
73
|
-
const { readCodeowners: readCodeownersOctokit } = await import('./github-api.js');
|
|
74
|
-
const content = await readCodeownersOctokit({
|
|
75
|
-
owner: repo.owner,
|
|
76
|
-
repo: repo.repo
|
|
77
|
-
});
|
|
78
|
-
return content;
|
|
79
|
-
} catch (error) {
|
|
80
|
-
logger.debug('github-client - readCodeowners', 'Could not read CODEOWNERS', {
|
|
81
|
-
error: error.message
|
|
82
|
-
});
|
|
83
|
-
return null; // Non-critical failure
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Parse CODEOWNERS file to get reviewers for files
|
|
89
|
-
* Why: Extract reviewer info from CODEOWNERS content
|
|
90
|
-
*
|
|
91
|
-
* @param {string} codeownersContent - CODEOWNERS file content
|
|
92
|
-
* @param {Array<string>} files - Files changed in PR
|
|
93
|
-
* @returns {Array<string>} - GitHub usernames of reviewers
|
|
94
|
-
*/
|
|
95
|
-
export const parseCodeownersReviewers = (codeownersContent, files = []) => {
|
|
96
|
-
if (!codeownersContent) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
logger.debug('github-client - parseCodeownersReviewers', 'Parsing CODEOWNERS', {
|
|
101
|
-
filesCount: files.length
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const reviewers = new Set();
|
|
105
|
-
const lines = codeownersContent.split('\n');
|
|
106
|
-
|
|
107
|
-
// Parse CODEOWNERS format:
|
|
108
|
-
// pattern @username1 @username2
|
|
109
|
-
// Example: *.js @frontend-team @tech-lead
|
|
110
|
-
|
|
111
|
-
for (const line of lines) {
|
|
112
|
-
const trimmed = line.trim();
|
|
113
|
-
|
|
114
|
-
// Skip comments and empty lines
|
|
115
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const parts = trimmed.split(/\s+/);
|
|
120
|
-
if (parts.length < 2) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const pattern = parts[0];
|
|
125
|
-
const owners = parts.slice(1).filter((o) => o.startsWith('@'));
|
|
126
|
-
|
|
127
|
-
// Check if any file matches this pattern
|
|
128
|
-
const patternRegex = new RegExp(
|
|
129
|
-
pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.')
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
for (const file of files) {
|
|
133
|
-
if (patternRegex.test(file)) {
|
|
134
|
-
owners.forEach((owner) => reviewers.add(owner.substring(1))); // Remove @
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const reviewersList = Array.from(reviewers);
|
|
140
|
-
logger.debug('github-client - parseCodeownersReviewers', 'Found reviewers', {
|
|
141
|
-
reviewers: reviewersList
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
return reviewersList;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
60
|
/**
|
|
148
61
|
* Get reviewers for PR based on changed files
|
|
149
62
|
* Why: Auto-detect appropriate reviewers
|
|
@@ -159,20 +72,7 @@ export const getReviewersForFiles = async (files = [], config = {}) => {
|
|
|
159
72
|
|
|
160
73
|
const reviewers = new Set();
|
|
161
74
|
|
|
162
|
-
//
|
|
163
|
-
try {
|
|
164
|
-
const codeowners = await readCodeowners();
|
|
165
|
-
if (codeowners) {
|
|
166
|
-
const codeownersReviewers = parseCodeownersReviewers(codeowners, files);
|
|
167
|
-
codeownersReviewers.forEach((r) => reviewers.add(r));
|
|
168
|
-
}
|
|
169
|
-
} catch (error) {
|
|
170
|
-
logger.debug('github-client - getReviewersForFiles', 'Could not read CODEOWNERS', {
|
|
171
|
-
error: error.message
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Option 2: Use config-based reviewers
|
|
75
|
+
// Config-based reviewers (flat array or object)
|
|
176
76
|
if (config.reviewers) {
|
|
177
77
|
const configReviewers = Array.isArray(config.reviewers)
|
|
178
78
|
? config.reviewers
|
|
@@ -181,7 +81,7 @@ export const getReviewersForFiles = async (files = [], config = {}) => {
|
|
|
181
81
|
configReviewers.forEach((r) => reviewers.add(r));
|
|
182
82
|
}
|
|
183
83
|
|
|
184
|
-
//
|
|
84
|
+
// Reviewer rules (pattern-based)
|
|
185
85
|
if (config.reviewerRules && Array.isArray(config.reviewerRules)) {
|
|
186
86
|
for (const rule of config.reviewerRules) {
|
|
187
87
|
const patternRegex = new RegExp(rule.pattern);
|
package/lib/utils/judge.js
CHANGED
|
@@ -25,7 +25,7 @@ import { loadPrompt } from './prompt-builder.js';
|
|
|
25
25
|
import { formatBlockingIssues, getAffectedFiles, formatFileContents } from './resolution-prompt.js';
|
|
26
26
|
import { getRepoRoot, getRepoName, getCurrentBranch } from './git-operations.js';
|
|
27
27
|
import logger from './logger.js';
|
|
28
|
-
import {
|
|
28
|
+
import { recordMetric } from './metrics.js';
|
|
29
29
|
|
|
30
30
|
const JUDGE_DEFAULT_MODEL = 'sonnet';
|
|
31
31
|
const JUDGE_TIMEOUT = 120000;
|
|
@@ -239,14 +239,21 @@ const judgeAndFix = async (analysisResult, filesData, config) => {
|
|
|
239
239
|
);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
// Record
|
|
243
|
-
|
|
242
|
+
// Record metrics
|
|
243
|
+
recordMetric('judge.completed', {
|
|
244
244
|
model,
|
|
245
245
|
issueCount: allIssues.length,
|
|
246
246
|
fixedCount,
|
|
247
247
|
falsePositiveCount,
|
|
248
|
-
unresolvedCount
|
|
249
|
-
|
|
248
|
+
unresolvedCount,
|
|
249
|
+
falsePositives: fixes.filter(f => f.assessment === 'FALSE_POSITIVE')
|
|
250
|
+
.map(f => ({ explanation: f.explanation || '' })),
|
|
251
|
+
unresolved: remainingIssues.map(i => ({
|
|
252
|
+
severity: i.severity, description: i.description || i.message || '', file: i.file
|
|
253
|
+
})),
|
|
254
|
+
fixed: fixes.filter((f, idx) => f.assessment === 'TRUE_ISSUE' && resolvedIndices.has(idx))
|
|
255
|
+
.map(f => ({ file: f.file, explanation: f.explanation || '' }))
|
|
256
|
+
}).catch(() => {});
|
|
250
257
|
|
|
251
258
|
return { fixedCount, falsePositiveCount, remainingIssues, verdicts };
|
|
252
259
|
};
|