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.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -2
- package/lib/commands/analyze-pr.js +9 -17
- package/lib/commands/help.js +169 -80
- package/lib/config.js +19 -91
- package/lib/defaults.json +130 -0
- package/lib/utils/analysis-engine.js +7 -3
- package/lib/utils/claude-client.js +9 -5
- package/lib/utils/config-registry.js +257 -0
- package/lib/utils/diff-analysis-orchestrator.js +13 -8
- package/lib/utils/judge.js +7 -4
- package/lib/utils/linear-connector.js +6 -4
- package/lib/utils/linter-runner.js +16 -1
- package/lib/utils/pr-metadata-engine.js +11 -8
- package/package.json +1 -1
- package/templates/HELP_NAVIGATE.md +41 -0
- package/templates/HELP_QUERY.md +7 -11
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
28
|
-
const ORCHESTRATOR_MODEL =
|
|
29
|
-
|
|
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:
|
|
218
|
-
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:
|
|
225
|
-
timeout:
|
|
229
|
+
model: orchModel,
|
|
230
|
+
timeout: orchTimeout,
|
|
226
231
|
headless,
|
|
227
232
|
telemetryContext: {
|
|
228
233
|
hook: 'orchestrator',
|
|
229
234
|
fileCount: filesData.length,
|
|
230
|
-
model:
|
|
235
|
+
model: orchModel
|
|
231
236
|
}
|
|
232
237
|
});
|
|
233
238
|
} catch (err) {
|
package/lib/utils/judge.js
CHANGED
|
@@ -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 = '
|
|
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
|
-
|
|
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:
|
|
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
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
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 (
|
|
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
|
|
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,
|
|
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
|
|
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,
|