File size: 35,166 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
/**
 * Helper utilities for loading settings and context file handling across different parts of the server
 */

import type { SettingsService } from '../services/settings-service.js';
import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils';
import { createLogger } from '@automaker/utils';
import type {
  MCPServerConfig,
  McpServerConfig,
  PromptCustomization,
  ClaudeApiProfile,
  ClaudeCompatibleProvider,
  PhaseModelKey,
  PhaseModelEntry,
  Credentials,
} from '@automaker/types';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import {
  mergeAutoModePrompts,
  mergeAgentPrompts,
  mergeBacklogPlanPrompts,
  mergeEnhancementPrompts,
  mergeCommitMessagePrompts,
  mergeTitleGenerationPrompts,
  mergeIssueValidationPrompts,
  mergeIdeationPrompts,
  mergeAppSpecPrompts,
  mergeContextDescriptionPrompts,
  mergeSuggestionsPrompts,
  mergeTaskExecutionPrompts,
} from '@automaker/prompts';

const logger = createLogger('SettingsHelper');

/** Default number of agent turns used when no value is configured. */
export const DEFAULT_MAX_TURNS = 10000;

/** Upper bound for the max-turns clamp; values above this are capped here. */
export const MAX_ALLOWED_TURNS = 10000;

/**
 * Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
 * Falls back to global settings and defaults to true when unset.
 * Returns true if settings service is not available.
 *
 * @param projectPath - Path to the project
 * @param settingsService - Optional settings service instance
 * @param logPrefix - Prefix for log messages (e.g., '[DescribeImage]')
 * @returns Promise resolving to the autoLoadClaudeMd setting value
 */
export async function getAutoLoadClaudeMdSetting(
  projectPath: string,
  settingsService?: SettingsService | null,
  logPrefix = '[SettingsHelper]'
): Promise<boolean> {
  if (!settingsService) {
    logger.info(`${logPrefix} SettingsService not available, autoLoadClaudeMd defaulting to true`);
    return true;
  }

  try {
    // Check project settings first (takes precedence)
    const projectSettings = await settingsService.getProjectSettings(projectPath);
    if (projectSettings.autoLoadClaudeMd !== undefined) {
      logger.info(
        `${logPrefix} autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}`
      );
      return projectSettings.autoLoadClaudeMd;
    }

    // Fall back to global settings
    const globalSettings = await settingsService.getGlobalSettings();
    const result = globalSettings.autoLoadClaudeMd ?? true;
    logger.info(`${logPrefix} autoLoadClaudeMd from global settings: ${result}`);
    return result;
  } catch (error) {
    logger.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error);
    throw error;
  }
}

/**
 * Get the useClaudeCodeSystemPrompt setting, with project settings taking precedence over global.
 * Falls back to global settings and defaults to true when unset.
 * Returns true if settings service is not available.
 *
 * @param projectPath - Path to the project
 * @param settingsService - Optional settings service instance
 * @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
 * @returns Promise resolving to the useClaudeCodeSystemPrompt setting value
 */
export async function getUseClaudeCodeSystemPromptSetting(
  projectPath: string,
  settingsService?: SettingsService | null,
  logPrefix = '[SettingsHelper]'
): Promise<boolean> {
  if (!settingsService) {
    logger.info(
      `${logPrefix} SettingsService not available, useClaudeCodeSystemPrompt defaulting to true`
    );
    return true;
  }

  try {
    // Check project settings first (takes precedence)
    const projectSettings = await settingsService.getProjectSettings(projectPath);
    if (projectSettings.useClaudeCodeSystemPrompt !== undefined) {
      logger.info(
        `${logPrefix} useClaudeCodeSystemPrompt from project settings: ${projectSettings.useClaudeCodeSystemPrompt}`
      );
      return projectSettings.useClaudeCodeSystemPrompt;
    }

    // Fall back to global settings
    const globalSettings = await settingsService.getGlobalSettings();
    const result = globalSettings.useClaudeCodeSystemPrompt ?? true;
    logger.info(`${logPrefix} useClaudeCodeSystemPrompt from global settings: ${result}`);
    return result;
  } catch (error) {
    logger.error(`${logPrefix} Failed to load useClaudeCodeSystemPrompt setting:`, error);
    throw error;
  }
}

/**
 * Get the default max turns setting from global settings.
 *
 * Reads the user's configured `defaultMaxTurns` setting, which controls the maximum
 * number of agent turns (tool-call round-trips) for feature execution.
 *
 * @param settingsService - Settings service instance (may be null)
 * @param logPrefix - Logging prefix for debugging
 * @returns The user's configured max turns, or {@link DEFAULT_MAX_TURNS} as default
 */
export async function getDefaultMaxTurnsSetting(
  settingsService?: SettingsService | null,
  logPrefix = '[SettingsHelper]'
): Promise<number> {
  if (!settingsService) {
    logger.info(
      `${logPrefix} SettingsService not available, using default maxTurns=${DEFAULT_MAX_TURNS}`
    );
    return DEFAULT_MAX_TURNS;
  }

  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const raw = globalSettings.defaultMaxTurns;
    const result = Number.isFinite(raw) ? (raw as number) : DEFAULT_MAX_TURNS;
    // Clamp to valid range
    const clamped = Math.max(1, Math.min(MAX_ALLOWED_TURNS, Math.floor(result)));
    logger.debug(`${logPrefix} defaultMaxTurns from global settings: ${clamped}`);
    return clamped;
  } catch (error) {
    logger.error(`${logPrefix} Failed to load defaultMaxTurns setting:`, error);
    return DEFAULT_MAX_TURNS;
  }
}

/**
 * Filters out CLAUDE.md from context files when autoLoadClaudeMd is enabled
 * and rebuilds the formatted prompt without it.
 *
 * When autoLoadClaudeMd is true, the SDK handles CLAUDE.md loading via settingSources,
 * so we need to exclude it from the manual context loading to avoid duplication.
 * Other context files (CODE_QUALITY.md, CONVENTIONS.md, etc.) are preserved.
 *
 * @param contextResult - Result from loadContextFiles
 * @param autoLoadClaudeMd - Whether SDK auto-loading is enabled
 * @returns Filtered context prompt (empty string if no non-CLAUDE.md files)
 */
export function filterClaudeMdFromContext(
  contextResult: ContextFilesResult,
  autoLoadClaudeMd: boolean
): string {
  // If autoLoadClaudeMd is disabled, return the original prompt unchanged
  if (!autoLoadClaudeMd || contextResult.files.length === 0) {
    return contextResult.formattedPrompt;
  }

  // Filter out CLAUDE.md (case-insensitive)
  const nonClaudeFiles = contextResult.files.filter((f) => f.name.toLowerCase() !== 'claude.md');

  // If all files were CLAUDE.md, return empty string
  if (nonClaudeFiles.length === 0) {
    return '';
  }

  // Rebuild prompt without CLAUDE.md using the same format as loadContextFiles
  const formattedFiles = nonClaudeFiles.map((file) => formatContextFileEntry(file));

  return `# Project Context Files

The following context files provide project-specific rules, conventions, and guidelines.
Each file serves a specific purpose - use the description to understand when to reference it.
If you need more details about a context file, you can read the full file at the path provided.

**IMPORTANT**: You MUST follow the rules and conventions specified in these files.
- Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`)
- Follow ALL coding conventions, commit message formats, and architectural patterns specified
- Reference these rules before running ANY shell commands or making commits

---

${formattedFiles.join('\n\n---\n\n')}

---

**REMINDER**: Before taking any action, verify you are following the conventions specified above.
`;
}

/**
 * Format a single context file entry for the prompt
 * (Matches the format used in @automaker/utils/context-loader.ts)
 */
function formatContextFileEntry(file: ContextFileInfo): string {
  const header = `## ${file.name}`;
  const pathInfo = `**Path:** \`${file.path}\``;
  const descriptionInfo = file.description ? `\n**Purpose:** ${file.description}` : '';
  return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`;
}

/**
 * Get enabled MCP servers from global settings, converted to SDK format.
 * Returns an empty object if settings service is not available or no servers are configured.
 *
 * @param settingsService - Optional settings service instance
 * @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
 * @returns Promise resolving to MCP servers in SDK format (keyed by name)
 */
export async function getMCPServersFromSettings(
  settingsService?: SettingsService | null,
  logPrefix = '[SettingsHelper]'
): Promise<Record<string, McpServerConfig>> {
  if (!settingsService) {
    return {};
  }

  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const mcpServers = globalSettings.mcpServers || [];

    // Filter to only enabled servers and convert to SDK format
    const enabledServers = mcpServers.filter((s) => s.enabled !== false);

    if (enabledServers.length === 0) {
      return {};
    }

    // Convert settings format to SDK format (keyed by name)
    const sdkServers: Record<string, McpServerConfig> = {};
    for (const server of enabledServers) {
      sdkServers[server.name] = convertToSdkFormat(server);
    }

    logger.info(
      `${logPrefix} Loaded ${enabledServers.length} MCP server(s): ${enabledServers.map((s) => s.name).join(', ')}`
    );

    return sdkServers;
  } catch (error) {
    logger.error(`${logPrefix} Failed to load MCP servers setting:`, error);
    return {};
  }
}

/**
 * Convert a settings MCPServerConfig to SDK McpServerConfig format.
 * Validates required fields and throws informative errors if missing.
 */
function convertToSdkFormat(server: MCPServerConfig): McpServerConfig {
  if (server.type === 'sse') {
    if (!server.url) {
      throw new Error(`SSE MCP server "${server.name}" is missing a URL.`);
    }
    return {
      type: 'sse',
      url: server.url,
      headers: server.headers,
    };
  }

  if (server.type === 'http') {
    if (!server.url) {
      throw new Error(`HTTP MCP server "${server.name}" is missing a URL.`);
    }
    return {
      type: 'http',
      url: server.url,
      headers: server.headers,
    };
  }

  // Default to stdio
  if (!server.command) {
    throw new Error(`Stdio MCP server "${server.name}" is missing a command.`);
  }
  return {
    type: 'stdio',
    command: server.command,
    args: server.args,
    env: server.env,
  };
}

/**
 * Get prompt customization from global settings and merge with defaults.
 * Returns prompts merged with built-in defaults - custom prompts override defaults.
 *
 * @param settingsService - Optional settings service instance
 * @param logPrefix - Prefix for log messages
 * @returns Promise resolving to merged prompts for all categories
 */
export async function getPromptCustomization(
  settingsService?: SettingsService | null,
  logPrefix = '[PromptHelper]'
): Promise<{
  autoMode: ReturnType<typeof mergeAutoModePrompts>;
  agent: ReturnType<typeof mergeAgentPrompts>;
  backlogPlan: ReturnType<typeof mergeBacklogPlanPrompts>;
  enhancement: ReturnType<typeof mergeEnhancementPrompts>;
  commitMessage: ReturnType<typeof mergeCommitMessagePrompts>;
  titleGeneration: ReturnType<typeof mergeTitleGenerationPrompts>;
  issueValidation: ReturnType<typeof mergeIssueValidationPrompts>;
  ideation: ReturnType<typeof mergeIdeationPrompts>;
  appSpec: ReturnType<typeof mergeAppSpecPrompts>;
  contextDescription: ReturnType<typeof mergeContextDescriptionPrompts>;
  suggestions: ReturnType<typeof mergeSuggestionsPrompts>;
  taskExecution: ReturnType<typeof mergeTaskExecutionPrompts>;
}> {
  let customization: PromptCustomization = {};

  if (settingsService) {
    try {
      const globalSettings = await settingsService.getGlobalSettings();
      customization = globalSettings.promptCustomization || {};
      logger.info(`${logPrefix} Loaded prompt customization from settings`);
    } catch (error) {
      logger.error(`${logPrefix} Failed to load prompt customization:`, error);
      // Fall through to use empty customization (all defaults)
    }
  } else {
    logger.info(`${logPrefix} SettingsService not available, using default prompts`);
  }

  return {
    autoMode: mergeAutoModePrompts(customization.autoMode),
    agent: mergeAgentPrompts(customization.agent),
    backlogPlan: mergeBacklogPlanPrompts(customization.backlogPlan),
    enhancement: mergeEnhancementPrompts(customization.enhancement),
    commitMessage: mergeCommitMessagePrompts(customization.commitMessage),
    titleGeneration: mergeTitleGenerationPrompts(customization.titleGeneration),
    issueValidation: mergeIssueValidationPrompts(customization.issueValidation),
    ideation: mergeIdeationPrompts(customization.ideation),
    appSpec: mergeAppSpecPrompts(customization.appSpec),
    contextDescription: mergeContextDescriptionPrompts(customization.contextDescription),
    suggestions: mergeSuggestionsPrompts(customization.suggestions),
    taskExecution: mergeTaskExecutionPrompts(customization.taskExecution),
  };
}

/**
 * Get Skills configuration from settings.
 * Returns configuration for enabling skills and which sources to load from.
 *
 * @param settingsService - Settings service instance
 * @returns Skills configuration with enabled state, sources, and tool inclusion flag
 */
export async function getSkillsConfiguration(settingsService: SettingsService): Promise<{
  enabled: boolean;
  sources: Array<'user' | 'project'>;
  shouldIncludeInTools: boolean;
}> {
  const settings = await settingsService.getGlobalSettings();
  const enabled = settings.enableSkills ?? true; // Default enabled
  const sources = settings.skillsSources ?? ['user', 'project']; // Default both sources

  return {
    enabled,
    sources,
    shouldIncludeInTools: enabled && sources.length > 0,
  };
}

/**
 * Get Subagents configuration from settings.
 * Returns configuration for enabling subagents and which sources to load from.
 *
 * @param settingsService - Settings service instance
 * @returns Subagents configuration with enabled state, sources, and tool inclusion flag
 */
export async function getSubagentsConfiguration(settingsService: SettingsService): Promise<{
  enabled: boolean;
  sources: Array<'user' | 'project'>;
  shouldIncludeInTools: boolean;
}> {
  const settings = await settingsService.getGlobalSettings();
  const enabled = settings.enableSubagents ?? true; // Default enabled
  const sources = settings.subagentsSources ?? ['user', 'project']; // Default both sources

  return {
    enabled,
    sources,
    shouldIncludeInTools: enabled && sources.length > 0,
  };
}

/**
 * Get custom subagents from settings, merging global and project-level definitions.
 * Project-level subagents take precedence over global ones with the same name.
 *
 * @param settingsService - Settings service instance
 * @param projectPath - Path to the project for loading project-specific subagents
 * @returns Record of agent names to definitions, or undefined if none configured
 */
export async function getCustomSubagents(
  settingsService: SettingsService,
  projectPath?: string
): Promise<Record<string, import('@automaker/types').AgentDefinition> | undefined> {
  // Get global subagents
  const globalSettings = await settingsService.getGlobalSettings();
  const globalSubagents = globalSettings.customSubagents || {};

  // If no project path, return only global subagents
  if (!projectPath) {
    return Object.keys(globalSubagents).length > 0 ? globalSubagents : undefined;
  }

  // Get project-specific subagents
  const projectSettings = await settingsService.getProjectSettings(projectPath);
  const projectSubagents = projectSettings.customSubagents || {};

  // Merge: project-level takes precedence
  const merged = {
    ...globalSubagents,
    ...projectSubagents,
  };

  return Object.keys(merged).length > 0 ? merged : undefined;
}

/** Result from getActiveClaudeApiProfile */
export interface ActiveClaudeApiProfileResult {
  /** The active profile, or undefined if using direct Anthropic API */
  profile: ClaudeApiProfile | undefined;
  /** Credentials for resolving 'credentials' apiKeySource */
  credentials: import('@automaker/types').Credentials | undefined;
}

/**
 * Get the active Claude API profile and credentials from settings.
 * Checks project settings first for per-project overrides, then falls back to global settings.
 * Returns both the profile and credentials for resolving 'credentials' apiKeySource.
 *
 * @deprecated Use getProviderById and getPhaseModelWithOverrides instead for the new provider system.
 * This function is kept for backward compatibility during migration.
 *
 * @param settingsService - Optional settings service instance
 * @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
 * @param projectPath - Optional project path for per-project override
 * @returns Promise resolving to object with profile and credentials
 */
export async function getActiveClaudeApiProfile(
  settingsService?: SettingsService | null,
  logPrefix = '[SettingsHelper]',
  projectPath?: string
): Promise<ActiveClaudeApiProfileResult> {
  if (!settingsService) {
    return { profile: undefined, credentials: undefined };
  }

  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const credentials = await settingsService.getCredentials();
    const profiles = globalSettings.claudeApiProfiles || [];

    // Check for project-level override first
    let activeProfileId: string | null | undefined;
    let isProjectOverride = false;

    if (projectPath) {
      const projectSettings = await settingsService.getProjectSettings(projectPath);
      // undefined = use global, null = explicit no profile, string = specific profile
      if (projectSettings.activeClaudeApiProfileId !== undefined) {
        activeProfileId = projectSettings.activeClaudeApiProfileId;
        isProjectOverride = true;
      }
    }

    // Fall back to global if project doesn't specify
    if (activeProfileId === undefined && !isProjectOverride) {
      activeProfileId = globalSettings.activeClaudeApiProfileId;
    }

    // No active profile selected - use direct Anthropic API
    if (!activeProfileId) {
      if (isProjectOverride && activeProfileId === null) {
        logger.info(`${logPrefix} Project explicitly using Direct Anthropic API`);
      }
      return { profile: undefined, credentials };
    }

    // Find the active profile by ID
    const activeProfile = profiles.find((p) => p.id === activeProfileId);

    if (activeProfile) {
      const overrideSuffix = isProjectOverride ? ' (project override)' : '';
      logger.info(`${logPrefix} Using Claude API profile: ${activeProfile.name}${overrideSuffix}`);
      return { profile: activeProfile, credentials };
    } else {
      logger.warn(
        `${logPrefix} Active profile ID "${activeProfileId}" not found, falling back to direct Anthropic API`
      );
      return { profile: undefined, credentials };
    }
  } catch (error) {
    logger.error(`${logPrefix} Failed to load Claude API profile:`, error);
    return { profile: undefined, credentials: undefined };
  }
}

// ============================================================================
// New Provider System Helpers
// ============================================================================

/** Result from getProviderById */
export interface ProviderByIdResult {
  /** The provider, or undefined if not found */
  provider: ClaudeCompatibleProvider | undefined;
  /** Credentials for resolving 'credentials' apiKeySource */
  credentials: Credentials | undefined;
}

/**
 * Get a ClaudeCompatibleProvider by its ID.
 * Returns the provider configuration and credentials for API key resolution.
 *
 * @param providerId - The provider ID to look up
 * @param settingsService - Settings service instance
 * @param logPrefix - Prefix for log messages
 * @returns Promise resolving to object with provider and credentials
 */
export async function getProviderById(
  providerId: string,
  settingsService: SettingsService,
  logPrefix = '[SettingsHelper]'
): Promise<ProviderByIdResult> {
  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const credentials = await settingsService.getCredentials();
    const providers = globalSettings.claudeCompatibleProviders || [];

    const provider = providers.find((p) => p.id === providerId);

    if (provider) {
      if (provider.enabled === false) {
        logger.warn(`${logPrefix} Provider "${provider.name}" (${providerId}) is disabled`);
      } else {
        logger.debug(`${logPrefix} Found provider: ${provider.name}`);
      }
      return { provider, credentials };
    } else {
      logger.warn(`${logPrefix} Provider not found: ${providerId}`);
      return { provider: undefined, credentials };
    }
  } catch (error) {
    logger.error(`${logPrefix} Failed to load provider by ID:`, error);
    return { provider: undefined, credentials: undefined };
  }
}

/** Result from getPhaseModelWithOverrides */
export interface PhaseModelWithOverridesResult {
  /** The resolved phase model entry */
  phaseModel: PhaseModelEntry;
  /** Whether a project override was applied */
  isProjectOverride: boolean;
  /** The provider if providerId is set and found */
  provider: ClaudeCompatibleProvider | undefined;
  /** Credentials for API key resolution */
  credentials: Credentials | undefined;
}

/**
 * Get the phase model configuration for a specific phase, applying project overrides if available.
 * Also resolves the provider if the phase model has a providerId.
 *
 * @param phase - The phase key (e.g., 'enhancementModel', 'specGenerationModel')
 * @param settingsService - Optional settings service instance (returns defaults if undefined)
 * @param projectPath - Optional project path for checking overrides
 * @param logPrefix - Prefix for log messages
 * @returns Promise resolving to phase model with provider info
 */
export async function getPhaseModelWithOverrides(
  phase: PhaseModelKey,
  settingsService?: SettingsService | null,
  projectPath?: string,
  logPrefix = '[SettingsHelper]'
): Promise<PhaseModelWithOverridesResult> {
  // Handle undefined settingsService gracefully
  if (!settingsService) {
    logger.info(`${logPrefix} SettingsService not available, using default for ${phase}`);
    return {
      phaseModel: DEFAULT_PHASE_MODELS[phase] || { model: 'sonnet' },
      isProjectOverride: false,
      provider: undefined,
      credentials: undefined,
    };
  }

  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const credentials = await settingsService.getCredentials();
    const globalPhaseModels = globalSettings.phaseModels || {};

    // Start with global phase model
    let phaseModel = globalPhaseModels[phase];
    let isProjectOverride = false;

    // Check for project override
    if (projectPath) {
      const projectSettings = await settingsService.getProjectSettings(projectPath);
      const projectOverrides = projectSettings.phaseModelOverrides || {};

      if (projectOverrides[phase]) {
        phaseModel = projectOverrides[phase];
        isProjectOverride = true;
        logger.debug(`${logPrefix} Using project override for ${phase}`);
      }
    }

    // If no phase model found, use per-phase default
    if (!phaseModel) {
      phaseModel = DEFAULT_PHASE_MODELS[phase] || { model: 'sonnet' };
      logger.debug(`${logPrefix} No ${phase} configured, using default: ${phaseModel.model}`);
    }

    // Resolve provider if providerId is set
    let provider: ClaudeCompatibleProvider | undefined;
    if (phaseModel.providerId) {
      const providers = globalSettings.claudeCompatibleProviders || [];
      provider = providers.find((p) => p.id === phaseModel.providerId);

      if (provider) {
        if (provider.enabled === false) {
          logger.warn(
            `${logPrefix} Provider "${provider.name}" for ${phase} is disabled, falling back to direct API`
          );
          provider = undefined;
        } else {
          logger.debug(`${logPrefix} Using provider "${provider.name}" for ${phase}`);
        }
      } else {
        logger.warn(
          `${logPrefix} Provider ${phaseModel.providerId} not found for ${phase}, falling back to direct API`
        );
      }
    }

    return {
      phaseModel,
      isProjectOverride,
      provider,
      credentials,
    };
  } catch (error) {
    logger.error(`${logPrefix} Failed to get phase model with overrides:`, error);
    // Return a safe default
    return {
      phaseModel: { model: 'sonnet' },
      isProjectOverride: false,
      provider: undefined,
      credentials: undefined,
    };
  }
}

/** Result from getProviderByModelId */
export interface ProviderByModelIdResult {
  /** The provider that contains this model, or undefined if not found */
  provider: ClaudeCompatibleProvider | undefined;
  /** The model configuration if found */
  modelConfig: import('@automaker/types').ProviderModel | undefined;
  /** Credentials for API key resolution */
  credentials: Credentials | undefined;
  /** The resolved Claude model ID to use for API calls (from mapsToClaudeModel) */
  resolvedModel: string | undefined;
}

/** Result from resolveProviderContext */
export interface ProviderContextResult {
  /** The provider configuration */
  provider: ClaudeCompatibleProvider | undefined;
  /** Credentials for API key resolution */
  credentials: Credentials | undefined;
  /** The resolved Claude model ID for SDK configuration */
  resolvedModel: string | undefined;
  /** The original model config from the provider if found */
  modelConfig: import('@automaker/types').ProviderModel | undefined;
}

/**
 * Checks if a provider is enabled.
 * Providers with enabled: undefined are treated as enabled (default state).
 * Only explicitly set enabled: false means the provider is disabled.
 */
function isProviderEnabled(provider: ClaudeCompatibleProvider): boolean {
  return provider.enabled !== false;
}

/**
 * Finds a model config in a provider's models array by ID (case-insensitive).
 */
function findModelInProvider(
  provider: ClaudeCompatibleProvider,
  modelId: string
): import('@automaker/types').ProviderModel | undefined {
  return provider.models?.find(
    (m) => m.id === modelId || m.id.toLowerCase() === modelId.toLowerCase()
  );
}

/**
 * Resolves the provider and Claude-compatible model configuration.
 *
 * This is the central logic for resolving provider context, supporting:
 * 1. Explicit lookup by providerId (most reliable for persistence)
 * 2. Fallback lookup by modelId across all enabled providers
 * 3. Resolution of mapsToClaudeModel for SDK configuration
 *
 * @param settingsService - Settings service instance
 * @param modelId - The model ID to resolve
 * @param providerId - Optional explicit provider ID
 * @param logPrefix - Prefix for log messages
 * @returns Promise resolving to the provider context
 */
export async function resolveProviderContext(
  settingsService: SettingsService,
  modelId: string,
  providerId?: string,
  logPrefix = '[SettingsHelper]'
): Promise<ProviderContextResult> {
  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const credentials = await settingsService.getCredentials();
    const providers = globalSettings.claudeCompatibleProviders || [];

    logger.debug(
      `${logPrefix} Resolving provider context: modelId="${modelId}", providerId="${providerId ?? 'none'}", providers count=${providers.length}`
    );

    let provider: ClaudeCompatibleProvider | undefined;
    let modelConfig: import('@automaker/types').ProviderModel | undefined;

    // 1. Try resolving by explicit providerId first (most reliable)
    if (providerId) {
      provider = providers.find((p) => p.id === providerId);
      if (provider) {
        if (!isProviderEnabled(provider)) {
          logger.warn(
            `${logPrefix} Explicitly requested provider "${provider.name}" (${providerId}) is disabled (enabled=${provider.enabled})`
          );
        } else {
          logger.debug(
            `${logPrefix} Found provider "${provider.name}" (${providerId}), enabled=${provider.enabled ?? 'undefined (treated as enabled)'}`
          );
          // Find the model config within this provider to check for mappings
          modelConfig = findModelInProvider(provider, modelId);
          if (!modelConfig && provider.models && provider.models.length > 0) {
            logger.debug(
              `${logPrefix} Model "${modelId}" not found in provider "${provider.name}". Available models: ${provider.models.map((m) => m.id).join(', ')}`
            );
          }
        }
      } else {
        logger.warn(
          `${logPrefix} Explicitly requested provider "${providerId}" not found. Available providers: ${providers.map((p) => p.id).join(', ')}`
        );
      }
    }

    // 2. Fallback to model-based lookup across all providers if modelConfig not found
    // Note: We still search even if provider was found, to get the modelConfig for mapping
    if (!modelConfig) {
      for (const p of providers) {
        if (!isProviderEnabled(p) || p.id === providerId) continue; // Skip disabled or already checked

        const config = findModelInProvider(p, modelId);

        if (config) {
          // Only override provider if we didn't find one by explicit ID
          if (!provider) {
            provider = p;
          }
          modelConfig = config;
          logger.debug(`${logPrefix} Found model "${modelId}" in provider "${p.name}" (fallback)`);
          break;
        }
      }
    }

    // 3. Resolve the mapped Claude model if specified
    let resolvedModel: string | undefined;
    if (modelConfig?.mapsToClaudeModel) {
      const { resolveModelString } = await import('@automaker/model-resolver');
      resolvedModel = resolveModelString(modelConfig.mapsToClaudeModel);
      logger.debug(
        `${logPrefix} Model "${modelId}" maps to Claude model "${modelConfig.mapsToClaudeModel}" -> "${resolvedModel}"`
      );
    }

    // Log final result for debugging
    logger.debug(
      `${logPrefix} Provider context resolved: provider=${provider?.name ?? 'none'}, modelConfig=${modelConfig ? 'found' : 'not found'}, resolvedModel=${resolvedModel ?? modelId}`
    );

    return { provider, credentials, resolvedModel, modelConfig };
  } catch (error) {
    logger.error(`${logPrefix} Failed to resolve provider context:`, error);
    return {
      provider: undefined,
      credentials: undefined,
      resolvedModel: undefined,
      modelConfig: undefined,
    };
  }
}

/**
 * Find a ClaudeCompatibleProvider by one of its model IDs.
 * Searches through all enabled providers to find one that contains the specified model.
 * This is useful when you have a model string from the UI but need the provider config.
 *
 * Also resolves the `mapsToClaudeModel` field to get the actual Claude model ID to use
 * when calling the API (e.g., "GLM-4.5-Air" -> "claude-haiku-4-5").
 *
 * @param modelId - The model ID to search for (e.g., "GLM-4.7", "MiniMax-M2.1")
 * @param settingsService - Settings service instance
 * @param logPrefix - Prefix for log messages
 * @returns Promise resolving to object with provider, model config, credentials, and resolved model
 */
export async function getProviderByModelId(
  modelId: string,
  settingsService: SettingsService,
  logPrefix = '[SettingsHelper]'
): Promise<ProviderByModelIdResult> {
  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const credentials = await settingsService.getCredentials();
    const providers = globalSettings.claudeCompatibleProviders || [];

    // Search through all enabled providers for this model
    for (const provider of providers) {
      // Skip disabled providers
      if (provider.enabled === false) {
        continue;
      }

      // Check if this provider has the model
      const modelConfig = provider.models?.find(
        (m) => m.id === modelId || m.id.toLowerCase() === modelId.toLowerCase()
      );

      if (modelConfig) {
        logger.info(`${logPrefix} Found model "${modelId}" in provider "${provider.name}"`);

        // Resolve the mapped Claude model if specified
        let resolvedModel: string | undefined;
        if (modelConfig.mapsToClaudeModel) {
          // Import resolveModelString to convert alias to full model ID
          const { resolveModelString } = await import('@automaker/model-resolver');
          resolvedModel = resolveModelString(modelConfig.mapsToClaudeModel);
          logger.info(
            `${logPrefix} Model "${modelId}" maps to Claude model "${modelConfig.mapsToClaudeModel}" -> "${resolvedModel}"`
          );
        }

        return { provider, modelConfig, credentials, resolvedModel };
      }
    }

    // Model not found in any provider
    logger.debug(`${logPrefix} Model "${modelId}" not found in any provider`);
    return {
      provider: undefined,
      modelConfig: undefined,
      credentials: undefined,
      resolvedModel: undefined,
    };
  } catch (error) {
    logger.error(`${logPrefix} Failed to find provider by model ID:`, error);
    return {
      provider: undefined,
      modelConfig: undefined,
      credentials: undefined,
      resolvedModel: undefined,
    };
  }
}

/**
 * Get all enabled provider models for use in model dropdowns.
 * Returns models from all enabled ClaudeCompatibleProviders.
 *
 * @param settingsService - Settings service instance
 * @param logPrefix - Prefix for log messages
 * @returns Promise resolving to array of provider models with their provider info
 */
export async function getAllProviderModels(
  settingsService: SettingsService,
  logPrefix = '[SettingsHelper]'
): Promise<
  Array<{
    providerId: string;
    providerName: string;
    model: import('@automaker/types').ProviderModel;
  }>
> {
  try {
    const globalSettings = await settingsService.getGlobalSettings();
    const providers = globalSettings.claudeCompatibleProviders || [];

    const allModels: Array<{
      providerId: string;
      providerName: string;
      model: import('@automaker/types').ProviderModel;
    }> = [];

    for (const provider of providers) {
      // Skip disabled providers
      if (provider.enabled === false) {
        continue;
      }

      for (const model of provider.models || []) {
        allModels.push({
          providerId: provider.id,
          providerName: provider.name,
          model,
        });
      }
    }

    logger.debug(
      `${logPrefix} Found ${allModels.length} models from ${providers.length} providers`
    );
    return allModels;
  } catch (error) {
    logger.error(`${logPrefix} Failed to get all provider models:`, error);
    return [];
  }
}