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.
- package/CHANGELOG.md +329 -3
- package/README.md +34 -0
- package/lib/commands/analyze-diff.js +26 -23
- package/lib/commands/analyze.js +3 -1
- package/lib/commands/back-merge.js +39 -33
- package/lib/commands/bump-version.js +52 -90
- package/lib/commands/close-release.js +25 -19
- package/lib/commands/create-pr.js +152 -83
- package/lib/commands/create-release.js +19 -13
- package/lib/commands/help.js +13 -3
- package/lib/defaults.json +5 -0
- package/lib/hooks/pre-commit.js +112 -32
- package/lib/messages/library-warnings.js +128 -10
- package/lib/utils/analysis-engine.js +7 -3
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/config-registry.js +1 -0
- package/lib/utils/git-tag-manager.js +104 -0
- package/lib/utils/judge.js +2 -1
- package/lib/utils/library-resolver.js +50 -0
- package/lib/utils/prompt-builder.js +15 -0
- package/lib/utils/skill-registry/catalogue.js +74 -0
- package/lib/utils/skill-registry/feedback-writer.js +196 -0
- package/lib/utils/skill-registry/index.js +254 -0
- package/lib/utils/skill-registry/parser.js +311 -0
- package/lib/utils/skill-registry/resume.js +81 -0
- package/lib/utils/skill-registry/runner.js +265 -0
- package/lib/utils/version-manager.js +37 -18
- package/package.json +2 -1
- package/templates/CLAUDE_ANALYSIS_PROMPT.md +2 -1
- package/templates/config.advanced.example.json +25 -0
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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:
|
|
3
|
+
* Purpose: Staleness-warning wording and rendering for Library verification
|
|
4
|
+
* gates in claude-hooks.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
package/lib/utils/judge.js
CHANGED
|
@@ -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
|
|