claude-git-hooks 2.43.0 → 2.45.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.
@@ -0,0 +1,130 @@
1
+ {
2
+ "analysis": {
3
+ "maxFileSize": 1000000,
4
+ "maxFiles": 30,
5
+ "timeout": 360000,
6
+ "contextLines": 3,
7
+ "ignoreExtensions": []
8
+ },
9
+ "commitMessage": {
10
+ "autoKeyword": "auto",
11
+ "timeout": 300000,
12
+ "taskIdPattern": "([A-Z]{1,3}[-\\s]\\d{3,5})"
13
+ },
14
+ "subagents": {
15
+ "enabled": true
16
+ },
17
+ "templates": {
18
+ "baseDir": ".claude/prompts",
19
+ "analysis": "CLAUDE_ANALYSIS_PROMPT.md",
20
+ "guidelines": "CLAUDE_PRE_COMMIT.md",
21
+ "commitMessage": "COMMIT_MESSAGE.md",
22
+ "analyzeDiff": "ANALYZE_DIFF.md",
23
+ "resolution": "CLAUDE_RESOLUTION_PROMPT.md",
24
+ "createGithubPR": "CREATE_GITHUB_PR.md"
25
+ },
26
+ "output": {
27
+ "outputDir": ".claude/out",
28
+ "debugFile": ".claude/out/debug-claude-response.json",
29
+ "resolutionFile": ".claude/out/claude_resolution_prompt.md",
30
+ "prAnalysisFile": ".claude/out/pr-analysis.json"
31
+ },
32
+ "system": {
33
+ "debug": false,
34
+ "wslCheckTimeout": 15000
35
+ },
36
+ "git": {
37
+ "diffFilter": "ACM"
38
+ },
39
+ "github": {
40
+ "enabled": true,
41
+ "pr": {
42
+ "defaultBase": "develop",
43
+ "reviewers": [],
44
+ "labelRules": {
45
+ "backend": ["backend", "java"],
46
+ "frontend": ["frontend", "react"],
47
+ "fullstack": ["fullstack"],
48
+ "database": ["database", "sql"],
49
+ "ai": ["ai", "tooling"],
50
+ "default": []
51
+ },
52
+ "autoPush": true,
53
+ "pushConfirm": true,
54
+ "verifyRemote": true,
55
+ "showCommits": true
56
+ }
57
+ },
58
+ "prAnalysis": {
59
+ "model": "sonnet",
60
+ "timeout": 300000,
61
+ "inlineCategories": ["bug", "security", "performance", "hotspot"],
62
+ "generalCategories": [
63
+ "ticket-alignment",
64
+ "scope",
65
+ "style",
66
+ "good-practice",
67
+ "extensibility",
68
+ "observability",
69
+ "documentation",
70
+ "testing"
71
+ ]
72
+ },
73
+ "linting": {
74
+ "enabled": true,
75
+ "autoFix": true,
76
+ "failOnError": true,
77
+ "failOnWarning": false,
78
+ "timeout": 30000
79
+ },
80
+ "claude": {
81
+ "defaultModel": "sonnet"
82
+ },
83
+ "models": {
84
+ "modelMap": {
85
+ "haiku": "claude-haiku-4-5-20251001",
86
+ "sonnet": "claude-sonnet-4-6",
87
+ "opus": "claude-opus-4-6"
88
+ },
89
+ "defaults": {
90
+ "analysis": "sonnet",
91
+ "orchestrator": "opus",
92
+ "judge": "sonnet",
93
+ "prAnalysis": "sonnet",
94
+ "commitMessage": "sonnet"
95
+ }
96
+ },
97
+ "tools": {
98
+ "prettier": { "enabled": true, "timeout": 30000 },
99
+ "eslint": { "enabled": true, "timeout": 30000 },
100
+ "spotless": { "enabled": true, "timeout": 120000, "perFile": true },
101
+ "sqlfluff": { "enabled": true, "timeout": 30000 }
102
+ },
103
+ "presetLinters": {
104
+ "frontend": ["prettier", "eslint"],
105
+ "backend": ["spotless"],
106
+ "fullstack": ["prettier", "eslint", "spotless"],
107
+ "database": ["sqlfluff"],
108
+ "ai": ["prettier", "eslint"],
109
+ "default": ["prettier", "eslint"]
110
+ },
111
+ "judge": {
112
+ "model": "sonnet",
113
+ "timeout": 360000
114
+ },
115
+ "orchestrator": {
116
+ "model": "opus",
117
+ "timeout": 60000,
118
+ "threshold": 3
119
+ },
120
+ "prMetadata": {
121
+ "timeout": 180000,
122
+ "maxDiffSize": 50000
123
+ },
124
+ "linear": {
125
+ "hostname": "api.linear.app",
126
+ "path": "/graphql",
127
+ "timeout": 10000,
128
+ "maxRetries": 2
129
+ }
130
+ }
@@ -35,6 +35,7 @@ import { orchestrateBatches } from './diff-analysis-orchestrator.js';
35
35
  import { buildAnalysisPrompt } from './prompt-builder.js';
36
36
  import logger from './logger.js';
37
37
  import { recordMetric } from './metrics.js';
38
+ import { getDefaultSection, resolveSection } from './config-registry.js';
38
39
 
39
40
  /**
40
41
  * Standard file data schema used throughout the analysis pipeline
@@ -360,7 +361,7 @@ export const displayResults = (result, { silent = false } = {}) => {
360
361
  };
361
362
 
362
363
  // Minimum file count to trigger Opus orchestration instead of sequential analysis
363
- const ORCHESTRATOR_THRESHOLD = 3;
364
+ const ORCHESTRATOR_THRESHOLD = getDefaultSection('orchestrator').threshold;
364
365
 
365
366
  /**
366
367
  * Runs code analysis on files
@@ -385,12 +386,15 @@ export const runAnalysis = async (filesData, config, options = {}) => {
385
386
  return createEmptyResult();
386
387
  }
387
388
 
388
- const useOrchestrator = filesData.length >= ORCHESTRATOR_THRESHOLD;
389
+ // Resolve orchestrator threshold: remote settings.json > local defaults.json
390
+ const orchConfig = await resolveSection('orchestrator');
391
+ const threshold = orchConfig?.threshold || ORCHESTRATOR_THRESHOLD;
392
+ const useOrchestrator = filesData.length >= threshold;
389
393
 
390
394
  logger.debug('analysis-engine - runAnalysis', 'Starting analysis', {
391
395
  fileCount: filesData.length,
392
396
  useOrchestrator,
393
- threshold: ORCHESTRATOR_THRESHOLD
397
+ threshold
394
398
  });
395
399
 
396
400
  let result;
@@ -21,6 +21,7 @@ import path from 'path';
21
21
  import os from 'os';
22
22
  import logger from './logger.js';
23
23
  import config from '../config.js';
24
+ import { getDefaultSection } from './config-registry.js';
24
25
  import {
25
26
  detectClaudeError,
26
27
  formatClaudeError,
@@ -47,14 +48,17 @@ class ClaudeClientError extends Error {
47
48
 
48
49
  /**
49
50
  * Model alias map — resolves shorthand names to full SDK model IDs.
51
+ * Loaded from lib/defaults.json via config-registry, with env var overrides.
50
52
  * Env var overrides match Lumiere contract (CLAUDE_MODEL_HAIKU, CLAUDE_MODEL_SONNET, CLAUDE_MODEL_OPUS).
51
53
  * Full model IDs (e.g., 'claude-sonnet-4-6') pass through unchanged.
52
54
  */
53
- const MODEL_ALIASES = {
54
- haiku: process.env.CLAUDE_MODEL_HAIKU || 'claude-haiku-4-5-20251001',
55
- sonnet: process.env.CLAUDE_MODEL_SONNET || 'claude-sonnet-4-6',
56
- opus: process.env.CLAUDE_MODEL_OPUS || 'claude-opus-4-6'
57
- };
55
+ const _localModelMap = getDefaultSection('models').modelMap;
56
+ const MODEL_ALIASES = Object.fromEntries(
57
+ Object.entries(_localModelMap).map(([alias, fullId]) => [
58
+ alias,
59
+ process.env[`CLAUDE_MODEL_${alias.toUpperCase()}`] || fullId
60
+ ])
61
+ );
58
62
 
59
63
  /**
60
64
  * Resolves a model alias to a full model ID.
@@ -0,0 +1,257 @@
1
+ /**
2
+ * File: config-registry.js
3
+ * Purpose: Unified configuration reader with local defaults + remote overrides
4
+ *
5
+ * Design:
6
+ * - Reads lib/defaults.json synchronously at import time (ships with the package)
7
+ * - Fetches remote overrides from git-hooks-config via remote-config.js (async, cached)
8
+ * - Merge priority: local defaults < remote overrides
9
+ * - Consumers higher in the chain (presets, user .claude/config.json) merge on top separately
10
+ *
11
+ * Section-to-remote-file mapping:
12
+ * models → models.json (root)
13
+ * tools → tools.json (.tools key)
14
+ * prAnalysis → categories.json (.inlineCategories, .generalCategories → merged into prAnalysis)
15
+ * presetLinters → formatters.json (.presetTools)
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { fileURLToPath } from 'url';
21
+ import logger from './logger.js';
22
+ import { fetchRemoteConfig } from './remote-config.js';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+
27
+ /** Path to the package defaults file */
28
+ const DEFAULTS_PATH = path.resolve(__dirname, '..', 'defaults.json');
29
+
30
+ /**
31
+ * Helper: create a mapping entry that reads a section key from settings.json.
32
+ * @param {string} sectionKey - Key within settings.json (e.g., 'analysis', 'judge')
33
+ * @returns {{ file: string, extract: (remote: Object) => Object }}
34
+ */
35
+ const _settingsEntry = (sectionKey) => ({
36
+ file: 'settings.json',
37
+ extract: (remote) => remote?.[sectionKey] || {}
38
+ });
39
+
40
+ /**
41
+ * Maps defaults.json section names to remote config files and their extraction logic.
42
+ * Each entry defines:
43
+ * - file: the remote JSON filename to fetch
44
+ * - extract: function that transforms the remote JSON into the shape expected for deep-merge
45
+ * @type {Object<string, { file: string, extract: (remote: Object) => Object }>}
46
+ */
47
+ const SECTION_REMOTE_MAP = {
48
+ models: {
49
+ file: 'models.json',
50
+ extract: (remote) => remote
51
+ },
52
+ tools: {
53
+ file: 'tools.json',
54
+ extract: (remote) => remote?.tools || {}
55
+ },
56
+ prAnalysis: {
57
+ file: 'categories.json',
58
+ extract: (remote) => {
59
+ const result = {};
60
+ if (remote?.inlineCategories) result.inlineCategories = remote.inlineCategories;
61
+ if (remote?.generalCategories) result.generalCategories = remote.generalCategories;
62
+ return result;
63
+ }
64
+ },
65
+ presetLinters: {
66
+ file: 'formatters.json',
67
+ extract: (remote) => remote?.presetTools || {}
68
+ },
69
+ // Team-policy sections — all read from settings.json
70
+ analysis: _settingsEntry('analysis'),
71
+ commitMessage: _settingsEntry('commitMessage'),
72
+ judge: _settingsEntry('judge'),
73
+ orchestrator: _settingsEntry('orchestrator'),
74
+ prMetadata: _settingsEntry('prMetadata'),
75
+ linting: _settingsEntry('linting')
76
+ };
77
+
78
+ // ── Sync: local defaults (read once at import time) ────────────────────────
79
+
80
+ let _defaults = null;
81
+
82
+ /**
83
+ * Load defaults.json synchronously. Called once at module init.
84
+ * @returns {Object} Parsed defaults
85
+ * @throws {Error} If defaults.json is missing or invalid (fatal — package is broken)
86
+ */
87
+ function _loadDefaults() {
88
+ if (_defaults !== null) return _defaults;
89
+
90
+ try {
91
+ const raw = fs.readFileSync(DEFAULTS_PATH, 'utf8');
92
+ _defaults = JSON.parse(raw);
93
+ logger.debug('config-registry', 'Loaded defaults.json', {
94
+ sections: Object.keys(_defaults)
95
+ });
96
+ return _defaults;
97
+ } catch (err) {
98
+ throw new Error(
99
+ `Failed to load package defaults from ${DEFAULTS_PATH}: ${err.message}. ` +
100
+ 'This indicates a broken installation — reinstall claude-git-hooks.'
101
+ );
102
+ }
103
+ }
104
+
105
+ // Load immediately on import
106
+ _loadDefaults();
107
+
108
+ /**
109
+ * Returns the full defaults object (sync, no I/O after init).
110
+ * @returns {Object} Complete defaults from lib/defaults.json
111
+ */
112
+ export function getDefaults() {
113
+ return _defaults;
114
+ }
115
+
116
+ /**
117
+ * Returns a single section from defaults (sync).
118
+ * @param {string} sectionName - Top-level key in defaults.json (e.g., 'models', 'tools', 'judge')
119
+ * @returns {Object|undefined} The section value, or undefined if not found
120
+ */
121
+ export function getDefaultSection(sectionName) {
122
+ return _defaults?.[sectionName];
123
+ }
124
+
125
+ // ── Async: remote overrides (fetched on demand, cached) ────────────────────
126
+
127
+ /**
128
+ * In-memory cache for resolved sections (local + remote merged).
129
+ * @type {Map<string, Object>}
130
+ */
131
+ const _resolvedCache = new Map();
132
+
133
+ /**
134
+ * Deep-merge two objects (source overrides target).
135
+ * Arrays are replaced wholesale (not concatenated).
136
+ * @param {Object} target - Base object
137
+ * @param {Object} source - Override object
138
+ * @returns {Object} Merged result
139
+ */
140
+ function _deepMerge(target, source) {
141
+ const result = { ...target };
142
+ for (const key in source) {
143
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
144
+ result[key] = _deepMerge(target[key] || {}, source[key]);
145
+ } else {
146
+ result[key] = source[key];
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Resolve a config section by merging local defaults with remote overrides.
154
+ *
155
+ * If the section has a mapping in SECTION_REMOTE_MAP, the corresponding remote
156
+ * file is fetched and merged (remote > local). If fetch fails, local defaults
157
+ * are returned. Results are cached per process.
158
+ *
159
+ * @param {string} sectionName - Top-level key in defaults.json
160
+ * @returns {Promise<Object>} Merged section (local defaults + remote overrides)
161
+ */
162
+ export async function resolveSection(sectionName) {
163
+ if (_resolvedCache.has(sectionName)) {
164
+ return _resolvedCache.get(sectionName);
165
+ }
166
+
167
+ const localSection = getDefaultSection(sectionName);
168
+ if (localSection === undefined) {
169
+ logger.debug('config-registry - resolveSection', 'Section not found in defaults', {
170
+ sectionName
171
+ });
172
+ return undefined;
173
+ }
174
+
175
+ const mapping = SECTION_REMOTE_MAP[sectionName];
176
+ if (!mapping) {
177
+ // No remote file for this section — return local as-is
178
+ _resolvedCache.set(sectionName, localSection);
179
+ return localSection;
180
+ }
181
+
182
+ const remoteData = await fetchRemoteConfig(mapping.file);
183
+ if (!remoteData) {
184
+ logger.debug('config-registry - resolveSection', 'Remote unavailable, using local defaults', {
185
+ sectionName,
186
+ remoteFile: mapping.file
187
+ });
188
+ _resolvedCache.set(sectionName, localSection);
189
+ return localSection;
190
+ }
191
+
192
+ const remoteOverrides = mapping.extract(remoteData);
193
+ const merged = _deepMerge(localSection, remoteOverrides);
194
+
195
+ logger.debug('config-registry - resolveSection', 'Merged local + remote', {
196
+ sectionName,
197
+ remoteFile: mapping.file,
198
+ localKeys: Object.keys(localSection),
199
+ remoteKeys: Object.keys(remoteOverrides)
200
+ });
201
+
202
+ _resolvedCache.set(sectionName, merged);
203
+ return merged;
204
+ }
205
+
206
+ // ── Convenience resolvers ──────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Resolve a model alias to a full model ID, with remote override support.
210
+ * Priority: env var > remote models.json > local defaults.json
211
+ *
212
+ * @param {string} alias - Model alias ('sonnet', 'opus', 'haiku') or full ID
213
+ * @returns {Promise<string>} Full model ID
214
+ */
215
+ export async function resolveModelAlias(alias) {
216
+ const modelsConfig = await resolveSection('models');
217
+ const modelMap = modelsConfig?.modelMap || {};
218
+
219
+ // Env var override (highest priority)
220
+ const envKey = `CLAUDE_MODEL_${alias.toUpperCase()}`;
221
+ if (process.env[envKey]) {
222
+ return process.env[envKey];
223
+ }
224
+
225
+ return modelMap[alias] || alias;
226
+ }
227
+
228
+ /**
229
+ * Resolve configuration for a specific linter tool, with remote override support.
230
+ * Merges local defaults.tools[toolName] with remote tools.json tools[toolName].
231
+ *
232
+ * @param {string} toolName - Tool name ('prettier', 'eslint', 'spotless', 'sqlfluff')
233
+ * @returns {Promise<Object|undefined>} Tool config (enabled, timeout, perFile) or undefined
234
+ */
235
+ export async function resolveToolConfig(toolName) {
236
+ const toolsConfig = await resolveSection('tools');
237
+ return toolsConfig?.[toolName];
238
+ }
239
+
240
+ // ── Test helpers ───────────────────────────────────────────────────────────
241
+
242
+ /**
243
+ * Reset all caches. Test helper — not intended for production use.
244
+ */
245
+ export function _resetRegistry() {
246
+ _resolvedCache.clear();
247
+ _defaults = null;
248
+ _loadDefaults();
249
+ }
250
+
251
+ /**
252
+ * Reset resolved cache only (keeps defaults loaded).
253
+ * Useful for testing remote merge behavior without re-reading defaults.json.
254
+ */
255
+ export function _resetResolvedCache() {
256
+ _resolvedCache.clear();
257
+ }
@@ -23,10 +23,11 @@ import path from 'path';
23
23
  import { executeClaudeWithRetry, extractJSON } from './claude-client.js';
24
24
  import { loadPrompt } from './prompt-builder.js';
25
25
  import logger from './logger.js';
26
+ import { getDefaultSection, resolveSection } from './config-registry.js';
26
27
 
27
- // Orchestration model Opus for semantic grouping quality, not user-configurable
28
- const ORCHESTRATOR_MODEL = 'opus';
29
- const ORCHESTRATOR_TIMEOUT = 60000; // 60s — lightweight call, just file overview
28
+ // Orchestration config from lib/defaults.json (sync fallback)
29
+ const { model: ORCHESTRATOR_MODEL, timeout: ORCHESTRATOR_TIMEOUT } =
30
+ getDefaultSection('orchestrator');
30
31
 
31
32
  /**
32
33
  * Counts added/removed lines in a unified diff string
@@ -184,6 +185,10 @@ const _buildCommonContext = (filesData, batchGroups) => {
184
185
  * @returns {Promise<{batches: Array<{files: FileData[], rationale: string, model: string}>, commonContext: string}>}
185
186
  */
186
187
  export const orchestrateBatches = async (filesData, { headless = false } = {}) => {
188
+ // Resolve orchestrator config: remote settings.json > local defaults.json
189
+ const orchConfig = await resolveSection('orchestrator');
190
+ const orchModel = orchConfig?.model || ORCHESTRATOR_MODEL;
191
+ const orchTimeout = orchConfig?.timeout || ORCHESTRATOR_TIMEOUT;
187
192
  logger.debug('diff-analysis-orchestrator - orchestrateBatches', 'Building file overview', {
188
193
  fileCount: filesData.length
189
194
  });
@@ -214,20 +219,20 @@ export const orchestrateBatches = async (filesData, { headless = false } = {}) =
214
219
  }
215
220
 
216
221
  logger.debug('diff-analysis-orchestrator - orchestrateBatches', 'Calling Opus orchestrator', {
217
- model: ORCHESTRATOR_MODEL,
218
- timeout: ORCHESTRATOR_TIMEOUT
222
+ model: orchModel,
223
+ timeout: orchTimeout
219
224
  });
220
225
 
221
226
  let rawResponse;
222
227
  try {
223
228
  rawResponse = await executeClaudeWithRetry(prompt, {
224
- model: ORCHESTRATOR_MODEL,
225
- timeout: ORCHESTRATOR_TIMEOUT,
229
+ model: orchModel,
230
+ timeout: orchTimeout,
226
231
  headless,
227
232
  telemetryContext: {
228
233
  hook: 'orchestrator',
229
234
  fileCount: filesData.length,
230
- model: ORCHESTRATOR_MODEL
235
+ model: orchModel
231
236
  }
232
237
  });
233
238
  } catch (err) {
@@ -26,9 +26,9 @@ import { formatBlockingIssues, getAffectedFiles, formatFileContents } from './re
26
26
  import { getRepoRoot, getRepoName, getCurrentBranch } from './git-operations.js';
27
27
  import logger from './logger.js';
28
28
  import { recordMetric } from './metrics.js';
29
+ import { getDefaultSection, resolveSection } from './config-registry.js';
29
30
 
30
- const JUDGE_DEFAULT_MODEL = 'sonnet';
31
- const JUDGE_TIMEOUT = 360000;
31
+ const { model: JUDGE_DEFAULT_MODEL, timeout: JUDGE_TIMEOUT } = getDefaultSection('judge');
32
32
 
33
33
  /**
34
34
  * Applies a single search/replace fix to a file and stages it
@@ -103,7 +103,10 @@ const applyFix = async (fix, repoRoot) => {
103
103
  * @returns {Promise<{fixedCount: number, falsePositiveCount: number, remainingIssues: Array, verdicts: Array}>}
104
104
  */
105
105
  const judgeAndFix = async (analysisResult, filesData, config, { headless = false } = {}) => {
106
- const model = config.judge?.model || JUDGE_DEFAULT_MODEL;
106
+ // Resolve judge config: remote settings.json > local defaults.json
107
+ const judgeConfig = await resolveSection('judge');
108
+ const model = config.judge?.model ?? judgeConfig?.model ?? JUDGE_DEFAULT_MODEL;
109
+ const judgeTimeout = judgeConfig?.timeout ?? JUDGE_TIMEOUT;
107
110
  const repoRoot = getRepoRoot();
108
111
 
109
112
  // Get all issues: prefer details (all severities), fallback to blockingIssues
@@ -135,7 +138,7 @@ const judgeAndFix = async (analysisResult, filesData, config, { headless = false
135
138
  // Call LLM
136
139
  const response = await executeClaudeWithRetry(prompt, {
137
140
  model,
138
- timeout: JUDGE_TIMEOUT,
141
+ timeout: judgeTimeout,
139
142
  headless
140
143
  });
141
144
 
@@ -19,11 +19,13 @@
19
19
  import https from 'https';
20
20
  import logger from './logger.js';
21
21
  import { loadToken } from './token-store.js';
22
+ import { getDefaultSection } from './config-registry.js';
22
23
 
23
- const LINEAR_HOSTNAME = 'api.linear.app';
24
- const LINEAR_PATH = '/graphql';
25
- const LINEAR_TIMEOUT = 10000;
26
- const MAX_RETRIES = 2;
24
+ const linearDefaults = getDefaultSection('linear') || {};
25
+ const LINEAR_HOSTNAME = linearDefaults.hostname || 'api.linear.app';
26
+ const LINEAR_PATH = linearDefaults.path || '/graphql';
27
+ const LINEAR_TIMEOUT = linearDefaults.timeout || 10000;
28
+ const MAX_RETRIES = linearDefaults.maxRetries || 2;
27
29
 
28
30
  /**
29
31
  * Custom error for Linear connector failures
@@ -27,6 +27,7 @@ import {
27
27
  import { getRepoRoot } from './git-operations.js';
28
28
  import { which } from './which-command.js';
29
29
  import { fetchRemoteConfig } from './remote-config.js';
30
+ import { resolveSection } from './config-registry.js';
30
31
  import logger from './logger.js';
31
32
 
32
33
  /**
@@ -422,6 +423,9 @@ export async function runLinters(files, config, presetName = 'default') {
422
423
  const timeout = lintConfig.timeout || 30000;
423
424
 
424
425
  const tools = await getLinterToolsForPreset(presetName);
426
+
427
+ // Merge remote tools.json overrides (timeout, perFile, enabled) into tool definitions
428
+ const remoteToolsConfig = await resolveSection('tools');
425
429
  const results = [];
426
430
 
427
431
  logger.debug('linter-runner - runLinters', 'Starting linters', {
@@ -431,7 +435,18 @@ export async function runLinters(files, config, presetName = 'default') {
431
435
  autoFix
432
436
  });
433
437
 
434
- for (const toolDef of tools) {
438
+ for (let toolDef of tools) {
439
+ // Apply remote tool config overrides (remote > local)
440
+ const remoteToolOverride = remoteToolsConfig?.[toolDef.name];
441
+ if (remoteToolOverride) {
442
+ toolDef = { ...toolDef };
443
+ if (remoteToolOverride.timeout !== undefined) toolDef.timeout = remoteToolOverride.timeout;
444
+ if (remoteToolOverride.perFile !== undefined) toolDef.perFile = remoteToolOverride.perFile;
445
+ if (remoteToolOverride.enabled === false) {
446
+ logger.info(`Skipping ${toolDef.name} (disabled by remote config)`);
447
+ continue;
448
+ }
449
+ }
435
450
  // Filter files by tool's extensions
436
451
  const matchingFiles = filterFilesByTool(files, toolDef);
437
452
 
@@ -32,6 +32,7 @@ import { executeClaudeWithRetry, extractJSON } from './claude-client.js';
32
32
  import { loadPrompt } from './prompt-builder.js';
33
33
  import { getConfig } from '../config.js';
34
34
  import logger from './logger.js';
35
+ import { getDefaultSection, resolveSection } from './config-registry.js';
35
36
 
36
37
  /**
37
38
  * @typedef {Object} BranchContext
@@ -58,13 +59,11 @@ import logger from './logger.js';
58
59
  */
59
60
 
60
61
  /**
61
- * Internal defaults (not documented in config.example.json)
62
+ * Internal defaults loaded from lib/defaults.json
62
63
  * Why: Convention over configuration - sensible defaults for 95% of cases
64
+ * Fallbacks: timeout=180000 (3 min), maxDiffSize=51200 (50KB)
63
65
  */
64
- const DEFAULTS = {
65
- timeout: 180000, // 3 minutes
66
- maxDiffSize: 50000 // 50KB
67
- };
66
+ const DEFAULTS = getDefaultSection('prMetadata') || { timeout: 180000, maxDiffSize: 50000 };
68
67
 
69
68
  /**
70
69
  * Builds diff payload with tiered reduction strategy
@@ -310,12 +309,14 @@ export const getBranchContext = async (targetBranch) => {
310
309
 
311
310
  logger.debug('pr-metadata-engine - getBranchContext', 'Commits retrieved');
312
311
 
313
- // Build diff payload with tiered reduction
312
+ // Build diff payload with tiered reduction (remote settings.json > local defaults)
313
+ const resolvedPrMeta = await resolveSection('prMetadata');
314
+ const maxDiffSize = resolvedPrMeta?.maxDiffSize || DEFAULTS.maxDiffSize;
314
315
  const {
315
316
  payload: diff,
316
317
  isTruncated,
317
318
  truncationDetails
318
- } = buildDiffPayload(baseBranch, 'HEAD', files, DEFAULTS.maxDiffSize);
319
+ } = buildDiffPayload(baseBranch, 'HEAD', files, maxDiffSize);
319
320
 
320
321
  logger.debug('pr-metadata-engine - getBranchContext', 'Diff payload built', {
321
322
  diffSize: diff.length,
@@ -371,7 +372,9 @@ export const getBranchContext = async (targetBranch) => {
371
372
  export const generatePRMetadata = async (context, options = {}) => {
372
373
  const config = await getConfig();
373
374
  const configTimeout = config.analysis?.timeout;
374
- const { timeout = configTimeout || DEFAULTS.timeout, hook = 'pr-metadata', headless = false, costTracker = null } = options;
375
+ const resolvedPrMetaGen = await resolveSection('prMetadata');
376
+ const defaultTimeout = resolvedPrMetaGen?.timeout || DEFAULTS.timeout;
377
+ const { timeout = configTimeout || defaultTimeout, hook = 'pr-metadata', headless = false, costTracker = null } = options;
375
378
 
376
379
  logger.debug('pr-metadata-engine - generatePRMetadata', 'Generating PR metadata', {
377
380
  filesCount: context.filesCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.43.0",
3
+ "version": "2.45.0",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {