claude-git-hooks 2.1.0 → 2.3.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/README.md +280 -78
  3. package/bin/claude-hooks +295 -119
  4. package/lib/config.js +164 -0
  5. package/lib/hooks/pre-commit.js +180 -67
  6. package/lib/hooks/prepare-commit-msg.js +47 -41
  7. package/lib/utils/claude-client.js +107 -16
  8. package/lib/utils/claude-diagnostics.js +266 -0
  9. package/lib/utils/file-operations.js +1 -65
  10. package/lib/utils/file-utils.js +65 -0
  11. package/lib/utils/installation-diagnostics.js +145 -0
  12. package/lib/utils/package-info.js +75 -0
  13. package/lib/utils/preset-loader.js +214 -0
  14. package/lib/utils/prompt-builder.js +83 -67
  15. package/lib/utils/resolution-prompt.js +12 -2
  16. package/package.json +49 -50
  17. package/templates/ANALYZE_DIFF.md +33 -0
  18. package/templates/COMMIT_MESSAGE.md +24 -0
  19. package/templates/CUSTOMIZATION_GUIDE.md +656 -0
  20. package/templates/SUBAGENT_INSTRUCTION.md +1 -0
  21. package/templates/config.example.json +41 -0
  22. package/templates/pre-commit +40 -2
  23. package/templates/prepare-commit-msg +40 -2
  24. package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
  25. package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
  26. package/templates/presets/ai/config.json +12 -0
  27. package/templates/presets/ai/preset.json +42 -0
  28. package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
  29. package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
  30. package/templates/presets/backend/config.json +12 -0
  31. package/templates/presets/backend/preset.json +49 -0
  32. package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
  33. package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
  34. package/templates/presets/database/config.json +12 -0
  35. package/templates/presets/database/preset.json +38 -0
  36. package/templates/presets/default/config.json +12 -0
  37. package/templates/presets/default/preset.json +53 -0
  38. package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
  39. package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
  40. package/templates/presets/frontend/config.json +12 -0
  41. package/templates/presets/frontend/preset.json +50 -0
  42. package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
  43. package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
  44. package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
  45. package/templates/presets/fullstack/config.json +12 -0
  46. package/templates/presets/fullstack/preset.json +55 -0
  47. package/templates/shared/ANALYSIS_PROMPT.md +103 -0
  48. package/templates/shared/ANALYZE_DIFF.md +33 -0
  49. package/templates/shared/COMMIT_MESSAGE.md +24 -0
  50. package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
  51. package/templates/shared/RESOLUTION_PROMPT.md +32 -0
  52. package/templates/check-version.sh +0 -266
@@ -0,0 +1,145 @@
1
+ /**
2
+ * File: installation-diagnostics.js
3
+ * Purpose: Reusable error diagnostics and formatting utilities
4
+ *
5
+ * Key features:
6
+ * - Generic error formatting with installation diagnostics
7
+ * - Detects common installation issues
8
+ * - Provides actionable remediation steps
9
+ * - Extensible for future diagnostic checks
10
+ *
11
+ * Usage:
12
+ * import { formatError } from './installation-diagnostics.js';
13
+ *
14
+ * try {
15
+ * // ... operation that might fail
16
+ * } catch (error) {
17
+ * console.error(formatError('Presets not found'));
18
+ * process.exit(1);
19
+ * }
20
+ */
21
+
22
+ import fs from 'fs';
23
+ import path from 'path';
24
+ import { getRepoRoot } from './git-operations.js';
25
+
26
+ /**
27
+ * Gets installation diagnostics
28
+ * Why: Centralized logic to detect common installation issues
29
+ *
30
+ * Future enhancements:
31
+ * - Check file permissions
32
+ * - Verify Claude CLI installation
33
+ * - Check Node.js version compatibility
34
+ * - Validate .gitignore entries
35
+ * - Check hook file integrity
36
+ * - Verify template files exist
37
+ * - Check config.json validity
38
+ *
39
+ * @returns {Object} Diagnostic information
40
+ */
41
+ export const getInstallationDiagnostics = () => {
42
+ const diagnostics = {
43
+ currentDir: process.cwd(),
44
+ repoRoot: null,
45
+ isInRepoRoot: false,
46
+ claudeDirExists: false,
47
+ claudeDirPath: null,
48
+ presetsDirExists: false,
49
+ presetsDirPath: null,
50
+ gitHooksExists: false,
51
+ };
52
+
53
+ try {
54
+ diagnostics.repoRoot = getRepoRoot();
55
+ diagnostics.isInRepoRoot = diagnostics.currentDir === diagnostics.repoRoot;
56
+
57
+ diagnostics.claudeDirPath = path.join(diagnostics.repoRoot, '.claude');
58
+ diagnostics.claudeDirExists = fs.existsSync(diagnostics.claudeDirPath);
59
+
60
+ diagnostics.presetsDirPath = path.join(diagnostics.claudeDirPath, 'presets');
61
+ diagnostics.presetsDirExists = fs.existsSync(diagnostics.presetsDirPath);
62
+
63
+ const gitHooksPath = path.join(diagnostics.repoRoot, '.git', 'hooks');
64
+ diagnostics.gitHooksExists = fs.existsSync(gitHooksPath);
65
+ } catch (error) {
66
+ // Not in a git repository - diagnostics.repoRoot will be null
67
+ }
68
+
69
+ return diagnostics;
70
+ };
71
+
72
+ /**
73
+ * Formats error message with diagnostics and remediation steps
74
+ * Why: Provides consistent, actionable error messages across all errors
75
+ *
76
+ * @param {string} errorMessage - Description of what failed (e.g., "Presets not found", "Template file missing")
77
+ * @param {string[]} additionalContext - Optional additional context lines
78
+ * @returns {string} Formatted error message with diagnostics and remediation steps
79
+ */
80
+ export const formatError = (errorMessage, additionalContext = []) => {
81
+ const diagnostics = getInstallationDiagnostics();
82
+ const lines = [];
83
+
84
+ lines.push(`⚠️ ${errorMessage}`);
85
+ lines.push('');
86
+
87
+ // Add any additional context first
88
+ if (additionalContext.length > 0) {
89
+ lines.push(...additionalContext);
90
+ lines.push('');
91
+ }
92
+
93
+ // Diagnostic information
94
+ lines.push('Installation diagnostics:');
95
+ lines.push(` Current directory: ${diagnostics.currentDir}`);
96
+ if (diagnostics.repoRoot) {
97
+ lines.push(` Repository root: ${diagnostics.repoRoot}`);
98
+ lines.push(` .claude/ exists: ${diagnostics.claudeDirExists ? '✓' : '✗'}`);
99
+ lines.push(` presets/ exists: ${diagnostics.presetsDirExists ? '✓' : '✗'}`);
100
+ } else {
101
+ lines.push(` Repository root: [Not in a git repository]`);
102
+ }
103
+ lines.push('');
104
+
105
+ // Remediation steps based on detected issues
106
+ lines.push('Recommended solution:');
107
+ if (!diagnostics.repoRoot) {
108
+ lines.push(' Not in a git repository');
109
+ lines.push(' → Navigate to your repository and try again');
110
+ } else if (!diagnostics.claudeDirExists) {
111
+ lines.push(' claude-hooks not installed');
112
+ if (!diagnostics.isInRepoRoot) {
113
+ lines.push(` → cd ${diagnostics.repoRoot}`);
114
+ lines.push(' → claude-hooks install');
115
+ } else {
116
+ lines.push(' → claude-hooks install');
117
+ }
118
+ } else if (!diagnostics.isInRepoRoot) {
119
+ lines.push(' Running from subdirectory (may cause path issues)');
120
+ lines.push(` → cd ${diagnostics.repoRoot}`);
121
+ lines.push(' → claude-hooks uninstall');
122
+ lines.push(' → claude-hooks install');
123
+ } else if (!diagnostics.presetsDirExists) {
124
+ lines.push(' Incomplete installation (presets missing)');
125
+ lines.push(' → claude-hooks install --force');
126
+ } else {
127
+ lines.push(' Unknown issue detected');
128
+ lines.push(' → claude-hooks install --force');
129
+ }
130
+
131
+ return lines.join('\n');
132
+ };
133
+
134
+ /**
135
+ * Checks if installation appears healthy
136
+ * Why: Quick validation before operations that require full installation
137
+ *
138
+ * @returns {boolean} True if installation looks healthy
139
+ */
140
+ export const isInstallationHealthy = () => {
141
+ const diagnostics = getInstallationDiagnostics();
142
+ return diagnostics.claudeDirExists &&
143
+ diagnostics.presetsDirExists &&
144
+ diagnostics.gitHooksExists;
145
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * File: package-info.js
3
+ * Purpose: Utility for reading package.json information
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ /**
14
+ * @typedef {Object} PackageData
15
+ * @property {string} name - Package name
16
+ * @property {string} version - Package version
17
+ * @property {string} [description] - Package description
18
+ */
19
+
20
+ /**
21
+ * Cache for package.json to avoid repeated file reads
22
+ * @type {PackageData|undefined}
23
+ */
24
+ let packageCache;
25
+
26
+ /**
27
+ * Gets package.json data with caching (async)
28
+ * @returns {Promise<PackageData>} Package data with name and version
29
+ * @throws {Error} If package.json cannot be read
30
+ */
31
+ export const getPackageJson = async () => {
32
+ // Return cached value if available
33
+ if (packageCache) {
34
+ return packageCache;
35
+ }
36
+
37
+ try {
38
+ const packagePath = path.join(__dirname, '..', '..', 'package.json');
39
+ const content = await fs.readFile(packagePath, 'utf8');
40
+ packageCache = JSON.parse(content);
41
+ return packageCache;
42
+ } catch (error) {
43
+ throw new Error(`Failed to read package.json: ${error.message}`);
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Gets package version (async)
49
+ * @returns {Promise<string>} Package version
50
+ */
51
+ export const getVersion = async () => {
52
+ const pkg = await getPackageJson();
53
+ return pkg.version;
54
+ };
55
+
56
+ /**
57
+ * Gets package name (async)
58
+ * @returns {Promise<string>} Package name
59
+ */
60
+ export const getPackageName = async () => {
61
+ const pkg = await getPackageJson();
62
+ return pkg.name;
63
+ };
64
+
65
+ /**
66
+ * Calculates batch information for subagent processing
67
+ * @param {number} fileCount - Number of files to process
68
+ * @param {number} batchSize - Size of each batch
69
+ * @returns {Object} Batch information { numBatches, shouldShowBatches }
70
+ */
71
+ export const calculateBatches = (fileCount, batchSize) => {
72
+ const numBatches = Math.ceil(fileCount / batchSize);
73
+ const shouldShowBatches = fileCount > batchSize;
74
+ return { numBatches, shouldShowBatches };
75
+ };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * File: preset-loader.js
3
+ * Purpose: Loads preset configurations and metadata
4
+ *
5
+ * Key responsibilities:
6
+ * - Load preset.json (metadata)
7
+ * - Load config.json (overrides)
8
+ * - Resolve template paths
9
+ * - Replace placeholders in templates
10
+ *
11
+ * Dependencies:
12
+ * - fs/promises: Async file operations
13
+ * - path: Cross-platform path handling
14
+ * - git-operations: For getRepoRoot()
15
+ * - logger: Debug and error logging
16
+ * - installation-diagnostics: Error formatting with remediation steps
17
+ */
18
+
19
+ import fs from 'fs/promises';
20
+ import path from 'path';
21
+ import { getRepoRoot } from './git-operations.js';
22
+ import logger from './logger.js';
23
+ import { formatError } from './installation-diagnostics.js';
24
+
25
+ /**
26
+ * Custom error for preset loading failures
27
+ */
28
+ class PresetError extends Error {
29
+ constructor(message, { presetName, cause } = {}) {
30
+ super(message);
31
+ this.name = 'PresetError';
32
+ this.presetName = presetName;
33
+ this.cause = cause;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Loads preset metadata + config
39
+ * @param {string} presetName - Name of preset (backend, frontend, etc.)
40
+ * @returns {Promise<Object>} { metadata, config, templates }
41
+ */
42
+ export async function loadPreset(presetName) {
43
+ logger.debug(
44
+ 'preset-loader - loadPreset',
45
+ 'Loading preset',
46
+ { presetName }
47
+ );
48
+
49
+ const repoRoot = getRepoRoot();
50
+
51
+ // Only try .claude/presets/{name} (installed by claude-hooks install)
52
+ const presetDir = path.join(repoRoot, '.claude', 'presets', presetName);
53
+
54
+ logger.debug('preset-loader - loadPreset', 'Loading preset from', { presetDir });
55
+
56
+ try {
57
+ // Load preset.json (metadata)
58
+ const presetJsonPath = path.join(presetDir, 'preset.json');
59
+ const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
60
+ const metadata = JSON.parse(metadataRaw);
61
+
62
+ // Load config.json (overrides)
63
+ const configJsonPath = path.join(presetDir, 'config.json');
64
+ const configRaw = await fs.readFile(configJsonPath, 'utf8');
65
+ const config = JSON.parse(configRaw);
66
+
67
+ // Resolve template paths
68
+ const templates = {};
69
+ for (const [key, templatePath] of Object.entries(metadata.templates)) {
70
+ templates[key] = path.join(presetDir, templatePath);
71
+ }
72
+
73
+ logger.debug(
74
+ 'preset-loader - loadPreset',
75
+ 'Preset loaded successfully',
76
+ {
77
+ presetName,
78
+ displayName: metadata.displayName,
79
+ fileExtensions: metadata.fileExtensions,
80
+ templateCount: Object.keys(templates).length
81
+ }
82
+ );
83
+
84
+ return { metadata, config, templates };
85
+
86
+ } catch (error) {
87
+ logger.error(
88
+ 'preset-loader - loadPreset',
89
+ `Failed to load preset: ${presetName}`,
90
+ error
91
+ );
92
+
93
+ throw new PresetError(`Preset "${presetName}" not found or invalid`, {
94
+ presetName,
95
+ cause: error
96
+ });
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Loads a template file and replaces placeholders
102
+ * @param {string} templatePath - Absolute path to template file
103
+ * @param {Object} metadata - Preset metadata for placeholder replacement
104
+ * @returns {Promise<string>} Template content with placeholders replaced
105
+ */
106
+ export async function loadTemplate(templatePath, metadata) {
107
+ logger.debug(
108
+ 'preset-loader - loadTemplate',
109
+ 'Loading template',
110
+ { templatePath }
111
+ );
112
+
113
+ try {
114
+ let template = await fs.readFile(templatePath, 'utf8');
115
+
116
+ // Replace placeholders
117
+ template = template.replace(
118
+ /{{TECH_STACK}}/g,
119
+ metadata.techStack.join(', ')
120
+ );
121
+ template = template.replace(
122
+ /{{FOCUS_AREAS}}/g,
123
+ metadata.focusAreas.join(', ')
124
+ );
125
+ template = template.replace(
126
+ /{{FILE_EXTENSIONS}}/g,
127
+ metadata.fileExtensions.join(', ')
128
+ );
129
+ template = template.replace(
130
+ /{{PRESET_NAME}}/g,
131
+ metadata.name
132
+ );
133
+
134
+ logger.debug(
135
+ 'preset-loader - loadTemplate',
136
+ 'Template loaded and processed',
137
+ {
138
+ templatePath,
139
+ originalLength: template.length
140
+ }
141
+ );
142
+
143
+ return template;
144
+
145
+ } catch (error) {
146
+ logger.error(
147
+ 'preset-loader - loadTemplate',
148
+ `Failed to load template: ${templatePath}`,
149
+ error
150
+ );
151
+
152
+ throw new PresetError(`Template not found: ${templatePath}`, {
153
+ cause: error
154
+ });
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Lists all available presets in .claude/presets/
160
+ * @returns {Promise<Array<Object>>} Array of preset metadata
161
+ * Returns: [{ name, displayName, description }]
162
+ */
163
+ export async function listPresets() {
164
+ logger.debug('preset-loader - listPresets', 'Listing available presets');
165
+
166
+ const repoRoot = getRepoRoot();
167
+ const presets = [];
168
+
169
+ // Load all presets from .claude/presets/ (installed by claude-hooks install)
170
+ const presetsDir = path.join(repoRoot, '.claude', 'presets');
171
+ try {
172
+ const presetNames = await fs.readdir(presetsDir);
173
+
174
+ for (const name of presetNames) {
175
+ const presetJsonPath = path.join(presetsDir, name, 'preset.json');
176
+ try {
177
+ const metadataRaw = await fs.readFile(presetJsonPath, 'utf8');
178
+ const metadata = JSON.parse(metadataRaw);
179
+
180
+ presets.push({
181
+ name: metadata.name,
182
+ displayName: metadata.displayName,
183
+ description: metadata.description
184
+ });
185
+ } catch (error) {
186
+ logger.debug(
187
+ 'preset-loader - listPresets',
188
+ `Skipping invalid preset: ${name}`,
189
+ error
190
+ );
191
+ }
192
+ }
193
+ } catch (error) {
194
+ const errorMsg = formatError('No presets directory found', [
195
+ `Expected location: ${presetsDir}`
196
+ ]);
197
+ logger.warning(errorMsg);
198
+ logger.debug(
199
+ 'preset-loader - listPresets',
200
+ 'Failed to read presets directory',
201
+ error
202
+ );
203
+ }
204
+
205
+ logger.debug(
206
+ 'preset-loader - listPresets',
207
+ 'Presets listed',
208
+ { count: presets.length }
209
+ );
210
+
211
+ return presets;
212
+ }
213
+
214
+ export { PresetError };
@@ -38,14 +38,22 @@ class PromptBuilderError extends Error {
38
38
  * Why absolute path: Ensures templates are found regardless of cwd (cross-platform)
39
39
  *
40
40
  * @param {string} templateName - Name of template file
41
- * @param {string} baseDir - Base directory (default: .claude)
41
+ * @param {string} baseDir - Base directory (default: try .claude, fallback to templates)
42
42
  * @returns {Promise<string>} Template content
43
43
  * @throws {PromptBuilderError} If template not found
44
44
  */
45
45
  const loadTemplate = async (templateName, baseDir = '.claude') => {
46
46
  // Why: Use repo root for absolute path (works on Windows/PowerShell/Git Bash)
47
47
  const repoRoot = getRepoRoot();
48
- const templatePath = path.join(repoRoot, baseDir, templateName);
48
+ let templatePath = path.join(repoRoot, baseDir, templateName);
49
+
50
+ // Try .claude first, fallback to templates/ if not found
51
+ try {
52
+ await fs.access(templatePath);
53
+ } catch {
54
+ // Try templates/ directory as fallback
55
+ templatePath = path.join(repoRoot, 'templates', templateName);
56
+ }
49
57
 
50
58
  logger.debug(
51
59
  'prompt-builder - loadTemplate',
@@ -104,6 +112,57 @@ const replaceTemplate = (template, variables) => {
104
112
  }, template);
105
113
  };
106
114
 
115
+ /**
116
+ * Loads a prompt template and replaces variables
117
+ * Why: High-level interface that combines loading and variable replacement
118
+ *
119
+ * @param {string} templateName - Name of template file
120
+ * @param {Object} variables - Variables to replace in template
121
+ * @param {string} baseDir - Base directory (default: .claude)
122
+ * @returns {Promise<string>} Prompt with replaced variables
123
+ * @throws {PromptBuilderError} If template not found
124
+ *
125
+ * Example:
126
+ * const prompt = await loadPrompt('COMMIT_MESSAGE.md', {
127
+ * FILE_LIST: 'file1.js\nfile2.js',
128
+ * FILE_COUNT: 2,
129
+ * INSERTIONS: 10,
130
+ * DELETIONS: 5
131
+ * });
132
+ */
133
+ const loadPrompt = async (templateName, variables = {}, baseDir = '.claude') => {
134
+ logger.debug(
135
+ 'prompt-builder - loadPrompt',
136
+ 'Loading prompt with variables',
137
+ { templateName, variableCount: Object.keys(variables).length }
138
+ );
139
+
140
+ try {
141
+ // Load template
142
+ const template = await loadTemplate(templateName, baseDir);
143
+
144
+ // Replace variables
145
+ const prompt = replaceTemplate(template, variables);
146
+
147
+ logger.debug(
148
+ 'prompt-builder - loadPrompt',
149
+ 'Prompt loaded successfully',
150
+ { templateName, promptLength: prompt.length }
151
+ );
152
+
153
+ return prompt;
154
+
155
+ } catch (error) {
156
+ logger.error(
157
+ 'prompt-builder - loadPrompt',
158
+ `Failed to load prompt: ${templateName}`,
159
+ error
160
+ );
161
+
162
+ throw error;
163
+ }
164
+ };
165
+
107
166
  /**
108
167
  * Formats file information for prompt
109
168
  * Why: Structures file data in readable format for Claude
@@ -144,6 +203,7 @@ const formatFileSection = ({ path, diff, content, isNew }) => {
144
203
  * @param {string} options.guidelinesName - Guidelines filename
145
204
  * @param {Array<Object>} options.files - Array of file data objects
146
205
  * @param {Object} options.metadata - Additional metadata (repo name, branch, etc.)
206
+ * @param {Object} options.subagentConfig - Subagent configuration (optional)
147
207
  * @returns {Promise<string>} Complete analysis prompt
148
208
  *
149
209
  * File data object structure:
@@ -158,12 +218,13 @@ const buildAnalysisPrompt = async ({
158
218
  templateName = 'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
159
219
  guidelinesName = 'CLAUDE_PRE_COMMIT_SONAR.md',
160
220
  files = [],
161
- metadata = {}
221
+ metadata = {},
222
+ subagentConfig = null
162
223
  } = {}) => {
163
224
  logger.debug(
164
225
  'prompt-builder - buildAnalysisPrompt',
165
226
  'Building analysis prompt',
166
- { templateName, guidelinesName, fileCount: files.length }
227
+ { templateName, guidelinesName, fileCount: files.length, subagentsEnabled: subagentConfig?.enabled }
167
228
  );
168
229
 
169
230
  try {
@@ -176,6 +237,22 @@ const buildAnalysisPrompt = async ({
176
237
  // Start with template
177
238
  let prompt = template;
178
239
 
240
+ // Add subagent instruction if enabled and 3+ files
241
+ if (subagentConfig?.enabled && files.length >= 3) {
242
+ try {
243
+ const subagentInstruction = await loadTemplate('SUBAGENT_INSTRUCTION.md');
244
+ const subagentVariables = {
245
+ BATCH_SIZE: subagentConfig.batchSize || 3,
246
+ MODEL: subagentConfig.model || 'haiku'
247
+ };
248
+ prompt += '\n\n' + replaceTemplate(subagentInstruction, subagentVariables) + '\n';
249
+
250
+ logger.info(`🚀 Batch optimization enabled: ${files.length} files, ${subagentVariables.BATCH_SIZE} per batch, ${subagentVariables.MODEL} model`);
251
+ } catch (error) {
252
+ logger.warning('Subagent instruction template not found, proceeding without parallel analysis');
253
+ }
254
+ }
255
+
179
256
  // Add guidelines section
180
257
  prompt += '\n\n=== EVALUATION GUIDELINES ===\n';
181
258
  prompt += guidelines;
@@ -212,72 +289,11 @@ const buildAnalysisPrompt = async ({
212
289
  }
213
290
  };
214
291
 
215
- /**
216
- * Builds commit message generation prompt
217
- * Why: Used by prepare-commit-msg hook
218
- *
219
- * @param {Object} options - Build options
220
- * @param {Array<Object>} options.files - Array of changed files with diffs
221
- * @param {Object} options.stats - Staging statistics
222
- * @returns {Promise<string>} Complete commit message prompt
223
- *
224
- * Stats object structure:
225
- * {
226
- * totalFiles: number,
227
- * addedFiles: number,
228
- * modifiedFiles: number,
229
- * deletedFiles: number,
230
- * insertions: number,
231
- * deletions: number
232
- * }
233
- */
234
- const buildCommitMessagePrompt = async ({ files = [], stats = {} } = {}) => {
235
- logger.debug(
236
- 'prompt-builder - buildCommitMessagePrompt',
237
- 'Building commit message prompt',
238
- { fileCount: files.length, stats }
239
- );
240
-
241
- let prompt = `Generate a conventional commit message for the following changes.\n\n`;
242
-
243
- // Add statistics
244
- prompt += `=== STATISTICS ===\n`;
245
- prompt += `Files changed: ${stats.totalFiles || files.length}\n`;
246
- prompt += `Added: ${stats.addedFiles || 0}, Modified: ${stats.modifiedFiles || 0}, Deleted: ${stats.deletedFiles || 0}\n`;
247
- prompt += `Insertions: ${stats.insertions || 0}, Deletions: ${stats.deletions || 0}\n\n`;
248
-
249
- // Add file changes
250
- prompt += `=== CHANGED FILES ===\n`;
251
- files.forEach(({ path, diff }) => {
252
- prompt += `\n--- ${path} ---\n`;
253
- if (diff) {
254
- prompt += `${diff}\n`;
255
- }
256
- });
257
-
258
- // Add instructions
259
- prompt += `\n\n=== INSTRUCTIONS ===\n`;
260
- prompt += `Generate a concise commit message following Conventional Commits format:\n`;
261
- prompt += `- type: feat, fix, docs, style, refactor, test, chore\n`;
262
- prompt += `- Format: "type: description" or "type(scope): description"\n`;
263
- prompt += `- Description: Clear, imperative mood, no period at end\n`;
264
- prompt += `- Optional body: Detailed explanation if needed\n\n`;
265
- prompt += `Respond with ONLY the commit message, no explanations.\n`;
266
-
267
- logger.debug(
268
- 'prompt-builder - buildCommitMessagePrompt',
269
- 'Commit message prompt built',
270
- { promptLength: prompt.length }
271
- );
272
-
273
- return prompt;
274
- };
275
-
276
292
  export {
277
293
  PromptBuilderError,
278
294
  loadTemplate,
279
295
  replaceTemplate,
296
+ loadPrompt,
280
297
  formatFileSection,
281
- buildAnalysisPrompt,
282
- buildCommitMessagePrompt
298
+ buildAnalysisPrompt
283
299
  };
@@ -179,14 +179,20 @@ ${content}
179
179
  const generateResolutionPrompt = async (
180
180
  analysisResult,
181
181
  {
182
- outputPath = 'claude_resolution_prompt.md',
182
+ outputPath = null,
183
183
  templatePath = '.claude/CLAUDE_RESOLUTION_PROMPT.md',
184
184
  fileCount = 0
185
185
  } = {}
186
186
  ) => {
187
187
  // Why: Use repo root for absolute paths (works on Windows/PowerShell/Git Bash)
188
188
  const repoRoot = getRepoRoot();
189
- const absoluteOutputPath = path.join(repoRoot, outputPath);
189
+
190
+ // Load config to get default output path
191
+ const { getConfig } = await import('../config.js');
192
+ const config = await getConfig();
193
+ const finalOutputPath = outputPath || config.output.resolutionFile;
194
+
195
+ const absoluteOutputPath = path.join(repoRoot, finalOutputPath);
190
196
  const absoluteTemplatePath = path.join(repoRoot, templatePath);
191
197
 
192
198
  logger.debug(
@@ -226,6 +232,10 @@ const generateResolutionPrompt = async (
226
232
  .replace(/{{BLOCKING_ISSUES}}/g, issuesFormatted)
227
233
  .replace(/{{FILE_CONTENTS}}/g, fileContentsFormatted);
228
234
 
235
+ // Ensure output directory exists
236
+ const outputDir = path.dirname(absoluteOutputPath);
237
+ await fs.mkdir(outputDir, { recursive: true });
238
+
229
239
  // Write resolution prompt file
230
240
  await fs.writeFile(absoluteOutputPath, prompt, 'utf8');
231
241