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 CHANGED
@@ -5,6 +5,55 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
5
5
  El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.45.0] - 2026-05-13
9
+
10
+ ### ✨ Added
11
+ - Added centralized config-registry module with local defaults and remote override support (SUE-138)
12
+ - Added lib/defaults.json as the single source of truth for all package default values (SUE-138)
13
+
14
+ ### 🔧 Changed
15
+ - Replaced inline hardcoded constants across modules with config-registry lookups — judge, orchestrator, Linear connector, model aliases, linter tools, and PR analysis categories now read from defaults.json with remote override capability (SUE-138)
16
+ - Updated config merge priority to HARDCODED < remote settings.json < defaults < preset < user overrides (SUE-138)
17
+ - Added config-registry mocks to unit tests for modules consuming centralized defaults (SUE-138)
18
+
19
+ ### 🐛 Fixed
20
+ - Fixed Linear ticket fetch issue (#155)
21
+
22
+
23
+ ## [2.44.1] - 2026-05-13
24
+
25
+ ### ✨ Added
26
+ - Added unit tests for linear-connector module covering ticket extraction, parsing, token loading, connection testing, and ticket fetching (SUE-154)
27
+
28
+ ### 🐛 Fixed
29
+ - Fixed help command to cap book reads at 5 in Pass 2, preventing oversized prompts when the LLM requests too many library books (SUE-154)
30
+
31
+ ### 🗑️ Removed
32
+ - Removed CLAUDE-MIGRATION.md — migration map no longer needed after library stabilization
33
+
34
+
35
+ ## [2.44.0] - 2026-05-04
36
+
37
+ ### ✨ Added
38
+ - New `HELP_NAVIGATE.md` prompt template for the first-pass catalog navigation (SUE-152)
39
+ - Auto-discovery of `.library/` catalog files (by-code, by-domain, by-task-type shelves) for AI help context
40
+
41
+ ### 🔧 Changed
42
+ - Rewrote AI help command to use a two-pass librarian approach navigating local `.library/` instead of single-pass CLAUDE.md lookup (SUE-152)
43
+ - Pass 1 sends project catalog to Claude; if deeper detail is needed, Pass 2 reads specific book files from local disk — no GitHub API calls
44
+ - Updated `HELP_QUERY.md` template to accept catalog + book content instead of flat documentation
45
+ - Replaced `NEED_MORE_CONTEXT: file1, file2` protocol with structured `ANSWER:` / `BOOKS:` response format
46
+ - Updated `report-issue` flow to use internal `_readPackageFile()` helper instead of the removed `readClaudeMd()`
47
+ - Updated unit tests to cover two-pass librarian flow, catalog mocking, and book-read scenarios
48
+
49
+ ### 🔒 Security
50
+ - Added path traversal protection when reading LLM-requested book paths — resolved paths are verified against the package root before disk access
51
+
52
+ ### 🗑️ Removed
53
+ - Removed GitHub API dependency (`fetchFileContent`, `parseGitHubRepo`) from AI help flow — all context is now read from local disk
54
+ - Removed `readClaudeMd()` single-file reader in favor of `readLibraryCatalog()` multi-file catalog builder
55
+
56
+
8
57
  ## [2.43.0] - 2026-04-30
9
58
 
10
59
  ### ✨ Added
package/README.md CHANGED
@@ -295,7 +295,7 @@ claude-hooks telemetry clear # Clear data
295
295
  ### Help & Issue Reporting
296
296
 
297
297
  ```bash
298
- claude-hooks help "how do presets work?" # AI-powered help (uses CLAUDE.md)
298
+ claude-hooks help "how do presets work?" # AI-powered help (navigates .library/)
299
299
  claude-hooks help --report-issue # Interactive GitHub issue creation
300
300
  claude-hooks --help # Static command reference
301
301
  ```
@@ -355,7 +355,7 @@ claude-hooks update # Update to latest version
355
355
  | `debug.js` | **Debug toggle** - enable/disable verbose logging | `runSetDebug()` |
356
356
  | `telemetry-cmd.js` | **Telemetry commands** - show/clear statistics | `runShowTelemetry()`, `runClearTelemetry()` |
357
357
  | `diff-batch-info.js` | **Batch info** - orchestration config + per-model speed telemetry | `runDiffBatchInfo()` |
358
- | `help.js` | **Help, AI help, report-issue** | `runShowHelp()`, `showStaticHelp()`, `runShowVersion()` |
358
+ | `help.js` | **Help, AI librarian, report-issue** | `runShowHelp()`, `showStaticHelp()`, `runShowVersion()` |
359
359
 
360
360
  ### Utility Modules (`lib/utils/`)
361
361
 
@@ -33,6 +33,7 @@ import { promptConfirmation, promptMenu } from '../utils/interactive-ui.js';
33
33
  import { CostTracker } from '../utils/cost-tracker.js';
34
34
  import { colors, error, fatal, info, warning, checkGitRepo } from './helpers.js';
35
35
  import logger from '../utils/logger.js';
36
+ import { resolveSection } from '../utils/config-registry.js';
36
37
  import path from 'path';
37
38
 
38
39
  // ─── JSON Error Helper ────────────────────────────────────────────────────
@@ -380,23 +381,14 @@ export async function runAnalyzePr(args) {
380
381
  warning(`Could not load preset "${presetName}" guidelines, using defaults`);
381
382
  }
382
383
 
383
- // Step 7: Build category strings from config
384
- const inlineCategories = config.prAnalysis?.inlineCategories || [
385
- 'bug',
386
- 'security',
387
- 'performance',
388
- 'hotspot'
389
- ];
390
- const generalCategories = config.prAnalysis?.generalCategories || [
391
- 'ticket-alignment',
392
- 'scope',
393
- 'style',
394
- 'good-practice',
395
- 'extensibility',
396
- 'observability',
397
- 'documentation',
398
- 'testing'
399
- ];
384
+ // Step 7: Build category strings from config (remote categories.json > local defaults)
385
+ // Categories are guaranteed by defaults.json loaded at import time via config-registry.
386
+ // The || [] fallback guards against unexpected config corruption or a missing section.
387
+ const resolvedPrAnalysis = await resolveSection('prAnalysis');
388
+ const inlineCategories = resolvedPrAnalysis?.inlineCategories ||
389
+ config.prAnalysis?.inlineCategories || [];
390
+ const generalCategories = resolvedPrAnalysis?.generalCategories ||
391
+ config.prAnalysis?.generalCategories || [];
400
392
  const allCategories = [...inlineCategories, ...generalCategories];
401
393
 
402
394
  const inlineCategoriesStr = inlineCategories.map((c) => `- \`${c}\``).join('\n');
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Features:
6
6
  * - Dynamic help from command registry (no args, --help, -h)
7
- * - AI help: routes questions to Claude with CLAUDE.md as knowledge base
7
+ * - AI help: two-pass librarian navigating .library/ for answers
8
8
  * - Report issue: interactive issue creation guided by Claude
9
9
  */
10
10
 
@@ -160,59 +160,137 @@ function printAiResponse(response, source) {
160
160
  console.log('');
161
161
  }
162
162
 
163
+ /** Package root directory — resolved once from __dirname */
164
+ const _packageRoot = path.join(__dirname, '..', '..');
165
+
166
+ /**
167
+ * Maximum number of books to read in Pass 2.
168
+ * Why: Matches the "do not request more than 5" instruction in HELP_NAVIGATE.md.
169
+ * Caps LLM-sourced lists to prevent oversized prompts that would fail the second call.
170
+ */
171
+ const MAX_BOOKS = 5;
172
+
163
173
  /**
164
- * Read CLAUDE.md from package root
165
- * Why: CLAUDE.md is the primary knowledge source shipped with the npm package
174
+ * Catalog directories within .library/ to auto-discover
175
+ * Why: These contain index and shelf files that form the navigational catalog.
176
+ * books/ is excluded — those are fetched on-demand in Pass 2.
177
+ */
178
+ const _CATALOG_DIRS = ['by-code', 'by-domain', 'by-task-type'];
179
+
180
+ /**
181
+ * Read a single file from package root, returning null on failure
166
182
  *
167
- * @returns {Promise<string|null>} CLAUDE.md content or null
183
+ * @param {string} relativePath - Path relative to package root
184
+ * @returns {Promise<string|null>} File content or null
168
185
  */
169
- const readClaudeMd = async () => {
170
- const claudeMdPath = path.join(__dirname, '..', '..', 'CLAUDE.md');
186
+ const _readPackageFile = async (relativePath) => {
171
187
  try {
172
- const content = await fs.readFile(claudeMdPath, 'utf8');
173
- logger.debug('help - readClaudeMd', 'CLAUDE.md loaded', { length: content.length });
174
- return content;
175
- } catch (error) {
176
- logger.debug('help - readClaudeMd', 'CLAUDE.md not found', {
177
- path: claudeMdPath,
178
- error: error.message
179
- });
188
+ return await fs.readFile(path.join(_packageRoot, relativePath), 'utf8');
189
+ } catch {
190
+ return null;
191
+ }
192
+ };
193
+
194
+ /**
195
+ * Read the project catalog from .library/ and CLAUDE.md
196
+ * Why: The catalog provides navigational context for the AI librarian (Pass 1).
197
+ * Auto-discovers index/shelf files so the catalog stays current as .library/ grows.
198
+ *
199
+ * Reads:
200
+ * - CLAUDE.md (global rules)
201
+ * - .library/index.md (repo identity, capabilities)
202
+ * - .library/conventions.md (coding standards)
203
+ * - .library/by-code/*.md (source-path shelves)
204
+ * - .library/by-domain/*.md (workflow reading lists)
205
+ * - .library/by-task-type/*.md (task guidance)
206
+ *
207
+ * @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
208
+ */
209
+ const readLibraryCatalog = async () => {
210
+ const sections = [];
211
+
212
+ // Fixed files: CLAUDE.md + core library files
213
+ const fixedFiles = [
214
+ { label: 'CLAUDE.md', path: 'CLAUDE.md' },
215
+ { label: '.library/index.md', path: '.library/index.md' },
216
+ { label: '.library/conventions.md', path: '.library/conventions.md' }
217
+ ];
218
+
219
+ for (const file of fixedFiles) {
220
+ const content = await _readPackageFile(file.path);
221
+ if (content) {
222
+ sections.push(`--- ${file.label} ---\n${content}`);
223
+ }
224
+ }
225
+
226
+ // Auto-discover .md files in each catalog directory
227
+ for (const dir of _CATALOG_DIRS) {
228
+ const dirPath = path.join(_packageRoot, '.library', dir);
229
+ let entries;
230
+ try {
231
+ entries = await fs.readdir(dirPath);
232
+ } catch {
233
+ logger.debug('help - readLibraryCatalog', `Could not read .library/${dir}/`);
234
+ continue;
235
+ }
236
+
237
+ const mdFiles = entries.filter((f) => f.endsWith('.md')).sort();
238
+ for (const file of mdFiles) {
239
+ const relativePath = `.library/${dir}/${file}`;
240
+ const content = await _readPackageFile(relativePath);
241
+ if (content) {
242
+ sections.push(`--- ${relativePath} ---\n${content}`);
243
+ }
244
+ }
245
+ }
246
+
247
+ if (sections.length === 0) {
248
+ logger.debug('help - readLibraryCatalog', 'No catalog files found');
180
249
  return null;
181
250
  }
251
+
252
+ const catalog = sections.join('\n\n');
253
+ logger.debug('help - readLibraryCatalog', 'Catalog loaded', {
254
+ sections: sections.length,
255
+ length: catalog.length
256
+ });
257
+ return catalog;
182
258
  };
183
259
 
184
260
  /**
185
- * AI-powered help: routes question to Claude with CLAUDE.md context
186
- * Why: Provides intelligent answers about claude-hooks based on documentation
261
+ * AI-powered help: two-pass librarian navigating .library/
262
+ * Why: Provides intelligent answers by reading the project catalog, then
263
+ * fetching specific books from local disk only when deeper detail is needed.
187
264
  *
188
265
  * Flow:
189
- * 1. Read CLAUDE.md from package root
190
- * 2. Load HELP_QUERY.md template
191
- * 3. Call Claude with 30s timeout
192
- * 4. If NEED_MORE_CONTEXT: fetch referenced files from GitHub, re-query
193
- * 5. Display response
194
- * 6. On any error: fall back to static help
266
+ * 1. Read catalog from .library/ (auto-discovered indexes + shelves)
267
+ * 2. Pass 1: Send catalog to Claude via HELP_NAVIGATE.md
268
+ * 3. If ANSWER: display directly (1 Claude call)
269
+ * 4. If BOOKS: read requested books from local disk, send to HELP_QUERY.md (2 Claude calls)
270
+ * 5. On any error: fall back to static help
195
271
  *
196
272
  * @param {string} question - User's natural language question
197
273
  */
198
274
  async function runAiHelp(question) {
199
275
  try {
200
- const claudeMdContent = await readClaudeMd();
201
- if (!claudeMdContent) {
276
+ const catalog = await readLibraryCatalog();
277
+ if (!catalog) {
202
278
  logger.debug(
203
279
  'help - runAiHelp',
204
- 'CLAUDE.md not available, falling back to static help'
280
+ 'Catalog not available, falling back to static help'
205
281
  );
206
282
  showStaticHelp();
207
283
  return;
208
284
  }
209
285
 
210
- const prompt = await loadPrompt('HELP_QUERY.md', {
211
- DOCUMENTATION: claudeMdContent,
286
+ const prompt = await loadPrompt('HELP_NAVIGATE.md', {
287
+ CATALOG: catalog,
212
288
  QUESTION: question
213
289
  });
214
290
 
215
- logger.debug('help - runAiHelp', 'Querying Claude', { questionLength: question.length });
291
+ logger.debug('help - runAiHelp', 'Querying Claude (Pass 1)', {
292
+ questionLength: question.length
293
+ });
216
294
  console.log('\nSearching documentation...\n');
217
295
 
218
296
  const pkg = getPackageJson();
@@ -221,95 +299,106 @@ async function runAiHelp(question) {
221
299
  const response = await executeClaudeWithRetry(prompt, { timeout: 60000 });
222
300
  const trimmedResponse = response.trim();
223
301
 
224
- // Check for NEED_MORE_CONTEXT second pass
225
- const needMoreLine = trimmedResponse
226
- .split('\n')
227
- .find((l) => l.includes('NEED_MORE_CONTEXT'));
228
- if (needMoreLine) {
229
- const enrichedResponse = await handleNeedMoreContext(
230
- needMoreLine,
302
+ // Check for BOOKS: second pass
303
+ const booksLine = trimmedResponse.split('\n').find((l) => l.startsWith('BOOKS:'));
304
+ if (booksLine) {
305
+ const enrichedAnswer = await readAndAnswerWithBooks(
306
+ booksLine,
231
307
  question,
232
- claudeMdContent
308
+ catalog
233
309
  );
234
- if (enrichedResponse) {
235
- printAiResponse(enrichedResponse, `${localVersion} + source`);
236
- return;
237
- }
238
- // Enrichment failed: show first-pass answer with marker line stripped
239
- const cleanResponse = trimmedResponse
240
- .split('\n')
241
- .filter((l) => !l.includes('NEED_MORE_CONTEXT'))
242
- .join('\n')
243
- .trim();
244
- if (cleanResponse) {
245
- printAiResponse(cleanResponse, localVersion);
310
+ if (enrichedAnswer) {
311
+ printAiResponse(enrichedAnswer, `${localVersion} + source`);
246
312
  return;
247
313
  }
314
+ // Books not readable — avoid surfacing raw BOOKS: line to the user
315
+ showStaticHelp();
316
+ return;
248
317
  }
249
318
 
250
- printAiResponse(trimmedResponse, localVersion);
251
- } catch (error) {
319
+ // ANSWER: response or fallback — strip the "ANSWER:" prefix if present
320
+ const answerLine = trimmedResponse.indexOf('ANSWER:');
321
+ const answer =
322
+ answerLine !== -1
323
+ ? trimmedResponse.substring(answerLine + 'ANSWER:'.length).trim()
324
+ : trimmedResponse;
325
+
326
+ printAiResponse(answer, localVersion);
327
+ } catch (err) {
252
328
  logger.debug('help - runAiHelp', 'AI help failed, falling back to static help', {
253
- error: error.message
329
+ error: err.message
254
330
  });
255
331
  showStaticHelp();
256
332
  }
257
333
  }
258
334
 
259
335
  /**
260
- * Handle NEED_MORE_CONTEXT response by fetching referenced files from GitHub
336
+ * Pass 2: Read requested books from local disk and answer with enriched context
261
337
  *
262
- * @param {string} needMoreLine - The line containing NEED_MORE_CONTEXT marker
338
+ * @param {string} booksLine - The line starting with "BOOKS:" containing comma-separated paths
263
339
  * @param {string} question - Original user question
264
- * @param {string} claudeMdContent - Original CLAUDE.md content
265
- * @returns {Promise<string|null>} Enriched response or null on failure
340
+ * @param {string} catalog - The catalog content from Pass 1
341
+ * @returns {Promise<string|null>} Enriched answer or null on failure
266
342
  */
267
- async function handleNeedMoreContext(needMoreLine, question, claudeMdContent) {
343
+ async function readAndAnswerWithBooks(booksLine, question, catalog) {
268
344
  try {
269
- // Parse file paths from line: "NEED_MORE_CONTEXT: file1.js, file2.js"
270
- const pathsPart = needMoreLine.replace('NEED_MORE_CONTEXT', '').replace(':', '').trim();
271
- const filePaths = pathsPart
345
+ const pathsPart = booksLine.replace('BOOKS:', '').trim();
346
+ const bookPaths = pathsPart
272
347
  .split(',')
273
348
  .map((p) => p.trim())
274
349
  .filter(Boolean);
275
350
 
276
- if (filePaths.length === 0) {
277
- logger.debug('help - handleNeedMoreContext', 'No file paths in NEED_MORE_CONTEXT');
351
+ if (bookPaths.length === 0) {
352
+ logger.debug('help - readAndAnswerWithBooks', 'No book paths in BOOKS line');
278
353
  return null;
279
354
  }
280
355
 
281
- logger.debug('help - handleNeedMoreContext', 'Fetching additional files', { filePaths });
282
-
283
- const { owner, repo } = getSourceRepo();
356
+ if (bookPaths.length > MAX_BOOKS) {
357
+ logger.debug('help - readAndAnswerWithBooks', 'Capping book list', {
358
+ requested: bookPaths.length,
359
+ max: MAX_BOOKS
360
+ });
361
+ bookPaths.length = MAX_BOOKS;
362
+ }
284
363
 
285
- // Fetch files in parallel (no ref = default branch, avoids 404 on unreleased tags)
286
- const fetchResults = await Promise.all(
287
- filePaths.map(async (filePath) => {
288
- const content = await fetchFileContent(owner, repo, filePath);
289
- return { filePath, content };
364
+ logger.debug('help - readAndAnswerWithBooks', 'Reading books from disk', { bookPaths });
365
+
366
+ // Read books from local filesystem in parallel
367
+ // Contain LLM-sourced paths to the package root to prevent traversal
368
+ const readResults = await Promise.all(
369
+ bookPaths.map(async (bookPath) => {
370
+ const resolvedPath = path.resolve(_packageRoot, bookPath);
371
+ if (!resolvedPath.startsWith(_packageRoot + path.sep)) {
372
+ logger.debug('help - readAndAnswerWithBooks', 'Path traversal blocked', { bookPath });
373
+ return { bookPath, content: null };
374
+ }
375
+ const content = await _readPackageFile(bookPath);
376
+ return { bookPath, content };
290
377
  })
291
378
  );
292
379
 
293
- // Build enriched documentation
294
- const additionalContent = fetchResults
380
+ const bookContent = readResults
295
381
  .filter((r) => r.content !== null)
296
- .map((r) => `\n--- Source: ${r.filePath} ---\n${r.content}`)
297
- .join('\n');
382
+ .map((r) => `--- ${r.bookPath} ---\n${r.content}`)
383
+ .join('\n\n');
298
384
 
299
- if (!additionalContent) {
300
- logger.debug('help - handleNeedMoreContext', 'No additional content fetched');
385
+ if (!bookContent) {
386
+ logger.debug('help - readAndAnswerWithBooks', 'No book content read from disk');
301
387
  return null;
302
388
  }
303
389
 
304
390
  const enrichedPrompt = await loadPrompt('HELP_QUERY.md', {
305
- DOCUMENTATION: `${claudeMdContent}\n\n=== ADDITIONAL SOURCE FILES ===\n${additionalContent}`,
391
+ CATALOG: catalog,
392
+ BOOKS: bookContent,
306
393
  QUESTION: question
307
394
  });
308
395
 
309
396
  const enrichedResponse = await executeClaudeWithRetry(enrichedPrompt, { timeout: 60000 });
310
397
  return enrichedResponse.trim();
311
- } catch (error) {
312
- logger.debug('help - handleNeedMoreContext', 'Enrichment failed', { error: error.message });
398
+ } catch (err) {
399
+ logger.debug('help - readAndAnswerWithBooks', 'Book enrichment failed', {
400
+ error: err.message
401
+ });
313
402
  return null;
314
403
  }
315
404
  }
@@ -371,8 +460,8 @@ async function runReportIssue() {
371
460
  return;
372
461
  }
373
462
 
374
- // Step 5: Read CLAUDE.md for project context
375
- const claudeMdContent = await readClaudeMd();
463
+ // Step 5: Read CLAUDE.md for project context (report-issue only needs basic context)
464
+ const claudeMdContent = await _readPackageFile('CLAUDE.md');
376
465
 
377
466
  // Step 6: Generate questions from template using Claude
378
467
  const questionsPrompt = await loadPrompt('HELP_REPORT_ISSUE.md', {
package/lib/config.js CHANGED
@@ -24,104 +24,23 @@
24
24
  import fs from 'fs';
25
25
  import path from 'path';
26
26
  import logger from './utils/logger.js';
27
+ import { getDefaults, resolveSection } from './utils/config-registry.js';
27
28
 
28
29
  /**
29
- * Hardcoded defaults (v3.0.0)
30
- * These are NOT user-configurable - sensible defaults that work for everyone
30
+ * Package defaults loaded from lib/defaults.json via config-registry.
31
+ * These are NOT user-configurable sensible defaults that work for everyone.
32
+ * See lib/defaults.json for the full structure and values.
31
33
  */
32
- const HARDCODED = {
33
- analysis: {
34
- maxFileSize: 1000000, // 1MB - sufficient for most files
35
- maxFiles: 30, // Reasonable limit per commit
36
- timeout: 360000, // 6 minutes - adequate for Claude API
37
- contextLines: 3, // Git default
38
- ignoreExtensions: [] // Can be set in advanced config only
39
- },
40
- commitMessage: {
41
- autoKeyword: 'auto', // Standard keyword
42
- timeout: 300000, // Use same timeout as analysis
43
- taskIdPattern: '([A-Z]{1,3}[-\\s]\\d{3,5})' // Jira/GitHub/Linear pattern
44
- },
45
- subagents: {
46
- enabled: true // Enable by default (faster analysis via orchestration)
47
- },
48
- templates: {
49
- baseDir: '.claude/prompts',
50
- analysis: 'CLAUDE_ANALYSIS_PROMPT.md',
51
- guidelines: 'CLAUDE_PRE_COMMIT.md',
52
- commitMessage: 'COMMIT_MESSAGE.md',
53
- analyzeDiff: 'ANALYZE_DIFF.md',
54
- resolution: 'CLAUDE_RESOLUTION_PROMPT.md',
55
- createGithubPR: 'CREATE_GITHUB_PR.md'
56
- },
57
- output: {
58
- outputDir: '.claude/out',
59
- debugFile: '.claude/out/debug-claude-response.json',
60
- resolutionFile: '.claude/out/claude_resolution_prompt.md',
61
- prAnalysisFile: '.claude/out/pr-analysis.json'
62
- },
63
- system: {
64
- debug: false, // Controlled by --debug flag
65
- wslCheckTimeout: 15000 // System behavior
66
- },
67
- git: {
68
- diffFilter: 'ACM' // Standard: Added, Copied, Modified
69
- },
70
- github: {
71
- enabled: true // Always enabled
72
- },
73
- prAnalysis: {
74
- model: 'sonnet',
75
- timeout: 300000, // 5 minutes
76
- inlineCategories: ['bug', 'security', 'performance', 'hotspot'],
77
- generalCategories: [
78
- 'ticket-alignment',
79
- 'scope',
80
- 'style',
81
- 'good-practice',
82
- 'extensibility',
83
- 'observability',
84
- 'documentation',
85
- 'testing'
86
- ]
87
- },
88
- linting: {
89
- enabled: true, // Run linters before Claude analysis
90
- autoFix: true, // Auto-fix and re-stage
91
- failOnError: true, // Block commit on linting errors
92
- failOnWarning: false, // Do not block on warnings
93
- timeout: 30000 // 30s per linter
94
- },
95
- claude: {
96
- defaultModel: 'sonnet' // Fallback model for SDK headless mode
97
- }
98
- };
34
+ const HARDCODED = getDefaults();
99
35
 
100
36
  /**
101
37
  * Default user-configurable values (v2.8.0)
38
+ * Derived from the github.pr section of HARDCODED.
102
39
  * Only these can be overridden in .claude/config.json
103
40
  */
104
41
  const defaults = {
105
- // GitHub PR configuration (user-specific)
106
42
  github: {
107
- pr: {
108
- defaultBase: 'develop', // Project default branch
109
- reviewers: [], // Project reviewers
110
- labelRules: {
111
- // Labels by preset
112
- backend: ['backend', 'java'],
113
- frontend: ['frontend', 'react'],
114
- fullstack: ['fullstack'],
115
- database: ['database', 'sql'],
116
- ai: ['ai', 'tooling'],
117
- default: []
118
- },
119
- // Auto-push configuration (v2.11.0)
120
- autoPush: true, // Auto-push unpublished branches
121
- pushConfirm: true, // Prompt for confirmation before push
122
- verifyRemote: true, // Verify remote exists before push
123
- showCommits: true // Show commit preview before push
124
- }
43
+ pr: structuredClone(HARDCODED.github.pr)
125
44
  }
126
45
  };
127
46
 
@@ -132,7 +51,7 @@ const defaults = {
132
51
  * - v2.8.0: { version: "2.8.0", preset: "...", overrides: {...} }
133
52
  * - Legacy: { preset: "...", analysis: {...}, ... } (auto-migrates with warning)
134
53
  *
135
- * Merge priority: HARDCODED < defaults < preset config < user overrides
54
+ * Merge priority: HARDCODED < remote settings.json < defaults < preset config < user overrides
136
55
  *
137
56
  * @param {string} baseDir - Base directory to search for config (default: cwd)
138
57
  * @returns {Promise<Object>} Merged configuration
@@ -194,8 +113,17 @@ const loadUserConfig = async (baseDir = process.cwd()) => {
194
113
  }
195
114
  }
196
115
 
197
- // Merge priority: HARDCODED < defaults < preset < user overrides
198
- const baseConfig = deepMerge(HARDCODED, defaults);
116
+ // Fetch remote settings.json overrides for team-policy sections
117
+ const remoteSettings = {};
118
+ const settingsSections = ['analysis', 'commitMessage', 'linting'];
119
+ const sectionResults = await Promise.all(settingsSections.map((s) => resolveSection(s)));
120
+ settingsSections.forEach((section, i) => {
121
+ if (sectionResults[i]) remoteSettings[section] = sectionResults[i];
122
+ });
123
+
124
+ // Merge priority: HARDCODED < remote settings.json < defaults < preset < user overrides
125
+ const withRemote = deepMerge(HARDCODED, remoteSettings);
126
+ const baseConfig = deepMerge(withRemote, defaults);
199
127
  const withPreset = deepMerge(baseConfig, presetConfig);
200
128
  const final = deepMerge(withPreset, userOverrides);
201
129