From 07bcb6b7671715def0c24bc1c51f7359a73a5db8 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 24 Dec 2025 22:05:50 +0100 Subject: [PATCH 1/6] feat: add auto-load CLAUDE.md functionality - Introduced a new setting to enable automatic loading of CLAUDE.md files from project-specific directories. - Updated relevant services and components to support the new setting, including the AgentService and AutoModeService. - Added UI controls for managing the auto-load setting in the settings view. - Enhanced SDK options to incorporate settingSources for CLAUDE.md loading. - Updated global and project settings interfaces to include autoLoadClaudeMd property. --- .gitignore | 3 +- apps/server/src/index.ts | 7 +- apps/server/src/lib/sdk-options.ts | 104 +++++++++++++++++- apps/server/src/providers/claude-provider.ts | 2 + apps/server/src/providers/types.ts | 3 +- apps/server/src/services/agent-service.ts | 44 +++++++- apps/server/src/services/auto-mode-service.ts | 43 +++++++- .../ui/src/components/views/settings-view.tsx | 7 ++ .../claude/claude-md-settings.tsx | 82 ++++++++++++++ apps/ui/src/hooks/use-settings-migration.ts | 1 + apps/ui/src/store/app-store.ts | 16 +++ libs/types/src/settings.ts | 9 ++ 12 files changed, 306 insertions(+), 15 deletions(-) create mode 100644 apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx diff --git a/.gitignore b/.gitignore index c752c12e..5efd9e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,5 @@ blob-report/ # Misc *.pem -docker-compose.override.yml \ No newline at end of file +docker-compose.override.yml +.claude/ \ No newline at end of file diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 548f4629..37460af9 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -112,10 +112,11 @@ app.use(express.json({ limit: '50mb' })); const events: EventEmitter = createEventEmitter(); // Create services -const agentService = new AgentService(DATA_DIR, events); -const featureLoader = new FeatureLoader(); -const autoModeService = new AutoModeService(events); +// Note: settingsService is created first so it can be injected into other services const settingsService = new SettingsService(DATA_DIR); +const agentService = new AgentService(DATA_DIR, events, settingsService); +const featureLoader = new FeatureLoader(); +const autoModeService = new AutoModeService(events, settingsService); const claudeUsageService = new ClaudeUsageService(); // Initialize services diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index 7853fbd2..85dc3eb3 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -136,6 +136,71 @@ function getBaseOptions(): Partial { }; } +/** + * Build system prompt configuration based on autoLoadClaudeMd setting. + * When autoLoadClaudeMd is true: + * - Uses preset mode with 'claude_code' to enable CLAUDE.md auto-loading + * - If there's a custom systemPrompt, appends it to the preset + * - Sets settingSources to ['project'] for SDK to load CLAUDE.md files + * + * @param config - The SDK options config + * @returns Object with systemPrompt and settingSources for SDK options + */ +function buildClaudeMdOptions(config: CreateSdkOptionsConfig): { + systemPrompt?: string | SystemPromptConfig; + settingSources?: Array<'user' | 'project' | 'local'>; +} { + if (!config.autoLoadClaudeMd) { + // Standard mode - just pass through the system prompt as-is + return config.systemPrompt ? { systemPrompt: config.systemPrompt } : {}; + } + + // Auto-load CLAUDE.md mode - use preset with settingSources + const result: { + systemPrompt: SystemPromptConfig; + settingSources: Array<'user' | 'project' | 'local'>; + } = { + systemPrompt: { + type: 'preset', + preset: 'claude_code', + }, + // Load both user (~/.claude/CLAUDE.md) and project (.claude/CLAUDE.md) settings + settingSources: ['user', 'project'], + }; + + // If there's a custom system prompt, append it to the preset + if (config.systemPrompt) { + result.systemPrompt.append = config.systemPrompt; + } + + console.log( + '[SDK Options] CLAUDE.md auto-loading enabled:', + JSON.stringify( + { + systemPrompt: result.systemPrompt, + settingSources: result.settingSources, + }, + null, + 2 + ) + ); + + return result; +} + +/** + * System prompt configuration for SDK options + * When using preset mode with claude_code, CLAUDE.md files are automatically loaded + */ +export interface SystemPromptConfig { + /** Use preset mode with claude_code to enable CLAUDE.md auto-loading */ + type: 'preset'; + /** The preset to use - 'claude_code' enables CLAUDE.md loading */ + preset: 'claude_code'; + /** Optional additional prompt to append to the preset */ + append?: string; +} + /** * Options configuration for creating SDK options */ @@ -160,6 +225,9 @@ export interface CreateSdkOptionsConfig { type: 'json_schema'; schema: Record; }; + + /** Enable auto-loading of CLAUDE.md files via SDK's settingSources */ + autoLoadClaudeMd?: boolean; } /** @@ -169,11 +237,15 @@ export interface CreateSdkOptionsConfig { * - Uses read-only tools for codebase analysis * - Extended turns for thorough exploration * - Opus model by default (can be overridden) + * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading */ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Options { // Validate working directory before creating options validateWorkingDirectory(config.cwd); + // Build CLAUDE.md auto-loading options if enabled + const claudeMdOptions = buildClaudeMdOptions(config); + return { ...getBaseOptions(), // Override permissionMode - spec generation only needs read-only tools @@ -184,7 +256,7 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt maxTurns: MAX_TURNS.maximum, cwd: config.cwd, allowedTools: [...TOOL_PRESETS.specGeneration], - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), ...(config.outputFormat && { outputFormat: config.outputFormat }), }; @@ -197,11 +269,15 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt * - Uses read-only tools (just needs to read the spec) * - Quick turns since it's mostly JSON generation * - Sonnet model by default for speed + * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading */ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig): Options { // Validate working directory before creating options validateWorkingDirectory(config.cwd); + // Build CLAUDE.md auto-loading options if enabled + const claudeMdOptions = buildClaudeMdOptions(config); + return { ...getBaseOptions(), // Override permissionMode - feature generation only needs read-only tools @@ -210,7 +286,7 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig): maxTurns: MAX_TURNS.quick, cwd: config.cwd, allowedTools: [...TOOL_PRESETS.readOnly], - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), }; } @@ -222,18 +298,22 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig): * - Uses read-only tools for analysis * - Standard turns to allow thorough codebase exploration and structured output generation * - Opus model by default for thorough analysis + * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading */ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Options { // Validate working directory before creating options validateWorkingDirectory(config.cwd); + // Build CLAUDE.md auto-loading options if enabled + const claudeMdOptions = buildClaudeMdOptions(config); + return { ...getBaseOptions(), model: getModelForUseCase('suggestions', config.model), maxTurns: MAX_TURNS.extended, cwd: config.cwd, allowedTools: [...TOOL_PRESETS.readOnly], - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), ...(config.outputFormat && { outputFormat: config.outputFormat }), }; @@ -247,6 +327,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option * - Standard turns for interactive sessions * - Model priority: explicit model > session model > chat default * - Sandbox enabled for bash safety + * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading */ export function createChatOptions(config: CreateSdkOptionsConfig): Options { // Validate working directory before creating options @@ -255,6 +336,9 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options { // Model priority: explicit model > session model > chat default const effectiveModel = config.model || config.sessionModel; + // Build CLAUDE.md auto-loading options if enabled + const claudeMdOptions = buildClaudeMdOptions(config); + return { ...getBaseOptions(), model: getModelForUseCase('chat', effectiveModel), @@ -265,7 +349,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options { enabled: true, autoAllowBashIfSandboxed: true, }, - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), }; } @@ -278,11 +362,15 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options { * - Extended turns for thorough feature implementation * - Uses default model (can be overridden) * - Sandbox enabled for bash safety + * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading */ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options { // Validate working directory before creating options validateWorkingDirectory(config.cwd); + // Build CLAUDE.md auto-loading options if enabled + const claudeMdOptions = buildClaudeMdOptions(config); + return { ...getBaseOptions(), model: getModelForUseCase('auto', config.model), @@ -293,7 +381,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options { enabled: true, autoAllowBashIfSandboxed: true, }, - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), }; } @@ -302,6 +390,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options { * Create custom SDK options with explicit configuration * * Use this when the preset options don't fit your use case. + * When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading */ export function createCustomOptions( config: CreateSdkOptionsConfig & { @@ -313,6 +402,9 @@ export function createCustomOptions( // Validate working directory before creating options validateWorkingDirectory(config.cwd); + // Build CLAUDE.md auto-loading options if enabled + const claudeMdOptions = buildClaudeMdOptions(config); + return { ...getBaseOptions(), model: getModelForUseCase('default', config.model), @@ -320,7 +412,7 @@ export function createCustomOptions( cwd: config.cwd, allowedTools: config.allowedTools ? [...config.allowedTools] : [...TOOL_PRESETS.readOnly], ...(config.sandbox && { sandbox: config.sandbox }), - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), }; } diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index 2ed2728d..9237cdf6 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -55,6 +55,8 @@ export class ClaudeProvider extends BaseProvider { ...(sdkSessionId && conversationHistory && conversationHistory.length > 0 ? { resume: sdkSessionId } : {}), + // Forward settingSources for CLAUDE.md file loading + ...(options.settingSources && { settingSources: options.settingSources }), }; // Build prompt payload diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts index f3aa22d5..5a594361 100644 --- a/apps/server/src/providers/types.ts +++ b/apps/server/src/providers/types.ts @@ -26,13 +26,14 @@ export interface ExecuteOptions { prompt: string | Array<{ type: string; text?: string; source?: object }>; model: string; cwd: string; - systemPrompt?: string; + systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string }; maxTurns?: number; allowedTools?: string[]; mcpServers?: Record; abortController?: AbortController; conversationHistory?: ConversationMessage[]; // Previous messages for context sdkSessionId?: string; // Claude SDK session ID for resuming conversations + settingSources?: Array<'user' | 'project' | 'local'>; // Claude filesystem settings to load } /** diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 93df5566..e25efebe 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -16,6 +16,7 @@ import { import { ProviderFactory } from '../providers/provider-factory.js'; import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; import { PathNotAllowedError } from '@automaker/platform'; +import type { SettingsService } from './settings-service.js'; interface Message { id: string; @@ -57,11 +58,13 @@ export class AgentService { private stateDir: string; private metadataFile: string; private events: EventEmitter; + private settingsService: SettingsService | null = null; - constructor(dataDir: string, events: EventEmitter) { + constructor(dataDir: string, events: EventEmitter, settingsService?: SettingsService) { this.stateDir = path.join(dataDir, 'agent-sessions'); this.metadataFile = path.join(dataDir, 'sessions-metadata.json'); this.events = events; + this.settingsService = settingsService ?? null; } async initialize(): Promise { @@ -186,7 +189,11 @@ export class AgentService { // Determine the effective working directory for context loading const effectiveWorkDir = workingDirectory || session.workingDirectory; + // Load autoLoadClaudeMd setting (project setting takes precedence over global) + const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(effectiveWorkDir); + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) + // Note: When autoLoadClaudeMd is enabled, SDK handles CLAUDE.md loading via settingSources const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ projectPath: effectiveWorkDir, fsModule: secureFs as Parameters[0]['fsModule'], @@ -205,6 +212,7 @@ export class AgentService { sessionModel: session.model, systemPrompt: combinedSystemPrompt, abortController: session.abortController!, + autoLoadClaudeMd, }); // Extract model, maxTurns, and allowedTools from SDK options @@ -224,11 +232,12 @@ export class AgentService { prompt: '', // Will be set below based on images model: effectiveModel, cwd: effectiveWorkDir, - systemPrompt: combinedSystemPrompt, + systemPrompt: sdkOptions.systemPrompt, maxTurns: maxTurns, allowedTools: allowedTools, abortController: session.abortController!, conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, + settingSources: sdkOptions.settingSources, sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming }; @@ -595,6 +604,37 @@ You have full access to the codebase and can: - Execute tests and builds`; } + /** + * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. + * Returns false if settings service is not available. + */ + private async getAutoLoadClaudeMdSetting(projectPath: string): Promise { + if (!this.settingsService) { + console.log('[AgentService] SettingsService not available, autoLoadClaudeMd disabled'); + return false; + } + + try { + // Check project settings first (takes precedence) + const projectSettings = await this.settingsService.getProjectSettings(projectPath); + if (projectSettings.autoLoadClaudeMd !== undefined) { + console.log( + `[AgentService] autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}` + ); + return projectSettings.autoLoadClaudeMd; + } + + // Fall back to global settings + const globalSettings = await this.settingsService.getGlobalSettings(); + const result = globalSettings.autoLoadClaudeMd ?? false; + console.log(`[AgentService] autoLoadClaudeMd from global settings: ${result}`); + return result; + } catch (error) { + console.error('[AgentService] Failed to load autoLoadClaudeMd setting:', error); + return false; + } + } + private generateId(): string { return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 1da65e35..a7b24fef 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -27,6 +27,7 @@ import * as secureFs from '../lib/secure-fs.js'; import type { EventEmitter } from '../lib/events.js'; import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; import { FeatureLoader } from './feature-loader.js'; +import type { SettingsService } from './settings-service.js'; const execAsync = promisify(exec); @@ -341,9 +342,11 @@ export class AutoModeService { private autoLoopAbortController: AbortController | null = null; private config: AutoModeConfig | null = null; private pendingApprovals = new Map(); + private settingsService: SettingsService | null = null; - constructor(events: EventEmitter) { + constructor(events: EventEmitter, settingsService?: SettingsService) { this.events = events; + this.settingsService = settingsService ?? null; } /** @@ -1780,11 +1783,15 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. return; } + // Load autoLoadClaudeMd setting (project setting takes precedence over global) + const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(finalProjectPath); + // Build SDK options using centralized configuration for feature implementation const sdkOptions = createAutoModeOptions({ cwd: workDir, model: model, abortController, + autoLoadClaudeMd, }); // Extract model, maxTurns, and allowedTools from SDK options @@ -1823,7 +1830,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. cwd: workDir, allowedTools: allowedTools, abortController, - systemPrompt: options?.systemPrompt, + systemPrompt: sdkOptions.systemPrompt, + settingSources: sdkOptions.settingSources, }; // Execute via provider @@ -2494,4 +2502,35 @@ Begin implementing task ${task.id} now.`; } }); } + + /** + * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. + * Returns false if settings service is not available. + */ + private async getAutoLoadClaudeMdSetting(projectPath: string): Promise { + if (!this.settingsService) { + console.log('[AutoMode] SettingsService not available, autoLoadClaudeMd disabled'); + return false; + } + + try { + // Check project settings first (takes precedence) + const projectSettings = await this.settingsService.getProjectSettings(projectPath); + if (projectSettings.autoLoadClaudeMd !== undefined) { + console.log( + `[AutoMode] autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}` + ); + return projectSettings.autoLoadClaudeMd; + } + + // Fall back to global settings + const globalSettings = await this.settingsService.getGlobalSettings(); + const result = globalSettings.autoLoadClaudeMd ?? false; + console.log(`[AutoMode] autoLoadClaudeMd from global settings: ${result}`); + return result; + } catch (error) { + console.error('[AutoMode] Failed to load autoLoadClaudeMd setting:', error); + return false; + } + } } diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index f57735bf..b888c9b6 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -10,6 +10,7 @@ import { SettingsNavigation } from './settings-view/components/settings-navigati import { ApiKeysSection } from './settings-view/api-keys/api-keys-section'; import { ClaudeUsageSection } from './settings-view/api-keys/claude-usage-section'; import { ClaudeCliStatus } from './settings-view/cli-status/claude-cli-status'; +import { ClaudeMdSettings } from './settings-view/claude/claude-md-settings'; import { AIEnhancementSection } from './settings-view/ai-enhancement'; import { AppearanceSection } from './settings-view/appearance/appearance-section'; import { TerminalSection } from './settings-view/terminal/terminal-section'; @@ -47,6 +48,8 @@ export function SettingsView() { apiKeys, validationModel, setValidationModel, + autoLoadClaudeMd, + setAutoLoadClaudeMd, } = useAppStore(); // Hide usage tracking when using API key (only show for Claude Code CLI users) @@ -102,6 +105,10 @@ export function SettingsView() { isChecking={isCheckingClaudeCli} onRefresh={handleRefreshClaudeCli} /> + {showUsageTracking && } ); diff --git a/apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx b/apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx new file mode 100644 index 00000000..920984be --- /dev/null +++ b/apps/ui/src/components/views/settings-view/claude/claude-md-settings.tsx @@ -0,0 +1,82 @@ +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { FileCode } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface ClaudeMdSettingsProps { + autoLoadClaudeMd: boolean; + onAutoLoadClaudeMdChange: (enabled: boolean) => void; +} + +/** + * ClaudeMdSettings Component + * + * UI control for the autoLoadClaudeMd setting which enables automatic loading + * of project instructions from .claude/CLAUDE.md files via the Claude Agent SDK. + * + * Usage: + * ```tsx + * + * ``` + */ +export function ClaudeMdSettings({ + autoLoadClaudeMd, + onAutoLoadClaudeMdChange, +}: ClaudeMdSettingsProps) { + return ( +
+
+
+
+ +
+

+ CLAUDE.md Integration +

+
+

+ Configure automatic loading of project-specific instructions. +

+
+
+
+ onAutoLoadClaudeMdChange(checked === true)} + className="mt-1" + data-testid="auto-load-claude-md-checkbox" + /> +
+ +

+ Automatically load project instructions from{' '} + + .claude/CLAUDE.md + {' '} + files. When enabled, Claude will read and follow conventions specified in your + project's CLAUDE.md file. Project settings override global settings. +

+
+
+
+
+ ); +} diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index df5d85a5..2bca750b 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -223,6 +223,7 @@ export async function syncSettingsToServer(): Promise { muteDoneSound: state.muteDoneSound, enhancementModel: state.enhancementModel, validationModel: state.validationModel, + autoLoadClaudeMd: state.autoLoadClaudeMd, keyboardShortcuts: state.keyboardShortcuts, aiProfiles: state.aiProfiles, projects: state.projects, diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 978d67cc..874e1a6d 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -478,6 +478,9 @@ export interface AppState { // Validation Model Settings validationModel: AgentModel; // Model used for GitHub issue validation (default: opus) + // Claude Agent SDK Settings + autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -751,6 +754,9 @@ export interface AppActions { // Validation Model actions setValidationModel: (model: AgentModel) => void; + // Claude Agent SDK Settings actions + setAutoLoadClaudeMd: (enabled: boolean) => Promise; + // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; @@ -922,6 +928,7 @@ const initialState: AppState = { muteDoneSound: false, // Default to sound enabled (not muted) enhancementModel: 'sonnet', // Default to sonnet for feature enhancement validationModel: 'opus', // Default to opus for GitHub issue validation + autoLoadClaudeMd: false, // Default to disabled (user must opt-in) aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, @@ -1547,6 +1554,14 @@ export const useAppStore = create()( // Validation Model actions setValidationModel: (model) => set({ validationModel: model }), + // Claude Agent SDK Settings actions + setAutoLoadClaudeMd: async (enabled) => { + set({ autoLoadClaudeMd: enabled }); + // Sync to server settings file + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + await syncSettingsToServer(); + }, + // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; @@ -2690,6 +2705,7 @@ export const useAppStore = create()( muteDoneSound: state.muteDoneSound, enhancementModel: state.enhancementModel, validationModel: state.validationModel, + autoLoadClaudeMd: state.autoLoadClaudeMd, // Profiles and sessions aiProfiles: state.aiProfiles, chatSessions: state.chatSessions, diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index e18c2987..e73e7269 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -297,6 +297,10 @@ export interface GlobalSettings { // Window State (Electron only) /** Persisted window bounds for restoring position/size across sessions */ windowBounds?: WindowBounds; + + // Claude Agent SDK Settings + /** Auto-load CLAUDE.md files using SDK's settingSources option */ + autoLoadClaudeMd?: boolean; } /** @@ -392,6 +396,10 @@ export interface ProjectSettings { // Session Tracking /** Last chat session selected in this project */ lastSelectedSessionId?: string; + + // Claude Agent SDK Settings + /** Auto-load CLAUDE.md files using SDK's settingSources option (project override) */ + autoLoadClaudeMd?: boolean; } /** @@ -450,6 +458,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { recentFolders: [], worktreePanelCollapsed: false, lastSelectedSessionByProject: {}, + autoLoadClaudeMd: false, }; /** Default credentials (empty strings - user must provide API keys) */ From 3154121840ec1a1a5415e4e9c2d4852dad179ba3 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 24 Dec 2025 22:34:22 +0100 Subject: [PATCH 2/6] feat: integrate settings service for auto-load CLAUDE.md functionality - Updated API routes to accept an optional settings service for loading the autoLoadClaudeMd setting. - Introduced a new settings helper utility for retrieving project-specific settings. - Enhanced feature generation and spec generation processes to utilize the autoLoadClaudeMd setting. - Refactored relevant route handlers to support the new settings integration across various endpoints. --- apps/server/src/index.ts | 8 ++-- apps/server/src/lib/settings-helpers.ts | 45 +++++++++++++++++++ .../app-spec/generate-features-from-spec.ts | 13 +++++- .../src/routes/app-spec/generate-spec.ts | 21 ++++++++- apps/server/src/routes/app-spec/index.ts | 10 +++-- .../app-spec/routes/generate-features.ts | 8 +++- .../src/routes/app-spec/routes/generate.ts | 6 ++- apps/server/src/routes/context/index.ts | 8 ++-- .../routes/context/routes/describe-file.ts | 15 ++++++- .../routes/context/routes/describe-image.ts | 15 ++++++- apps/server/src/routes/github/index.ts | 8 +++- .../routes/github/routes/validate-issue.ts | 21 +++++++-- .../suggestions/generate-suggestions.ts | 13 +++++- apps/server/src/routes/suggestions/index.ts | 12 ++++- .../src/routes/suggestions/routes/generate.ts | 5 ++- apps/server/src/services/auto-mode-service.ts | 31 +++++++++---- libs/types/src/provider.ts | 12 ++++- 17 files changed, 212 insertions(+), 39 deletions(-) create mode 100644 apps/server/src/lib/settings-helpers.ts diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 37460af9..bd0fd4a0 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -149,17 +149,17 @@ app.use('/api/enhance-prompt', createEnhancePromptRoutes()); app.use('/api/worktree', createWorktreeRoutes()); app.use('/api/git', createGitRoutes()); app.use('/api/setup', createSetupRoutes()); -app.use('/api/suggestions', createSuggestionsRoutes(events)); +app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService)); app.use('/api/models', createModelsRoutes()); -app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events)); +app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService)); app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService)); app.use('/api/workspace', createWorkspaceRoutes()); app.use('/api/templates', createTemplatesRoutes()); app.use('/api/terminal', createTerminalRoutes()); app.use('/api/settings', createSettingsRoutes(settingsService)); app.use('/api/claude', createClaudeRoutes(claudeUsageService)); -app.use('/api/github', createGitHubRoutes(events)); -app.use('/api/context', createContextRoutes()); +app.use('/api/github', createGitHubRoutes(events, settingsService)); +app.use('/api/context', createContextRoutes(settingsService)); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts new file mode 100644 index 00000000..9bf48361 --- /dev/null +++ b/apps/server/src/lib/settings-helpers.ts @@ -0,0 +1,45 @@ +/** + * Helper utilities for loading settings across different parts of the server + */ + +import type { SettingsService } from '../services/settings-service.js'; + +/** + * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. + * Returns false 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, + logPrefix = '[SettingsHelper]' +): Promise { + if (!settingsService) { + console.log(`${logPrefix} SettingsService not available, autoLoadClaudeMd disabled`); + return false; + } + + try { + // Check project settings first (takes precedence) + const projectSettings = await settingsService.getProjectSettings(projectPath); + if (projectSettings.autoLoadClaudeMd !== undefined) { + console.log( + `${logPrefix} autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}` + ); + return projectSettings.autoLoadClaudeMd; + } + + // Fall back to global settings + const globalSettings = await settingsService.getGlobalSettings(); + const result = globalSettings.autoLoadClaudeMd ?? false; + console.log(`${logPrefix} autoLoadClaudeMd from global settings: ${result}`); + return result; + } catch (error) { + console.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error); + return false; + } +} diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 17a83078..e2b7124d 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -10,6 +10,8 @@ import { createFeatureGenerationOptions } from '../../lib/sdk-options.js'; import { logAuthStatus } from './common.js'; import { parseAndCreateFeatures } from './parse-and-create-features.js'; import { getAppSpecPath } from '@automaker/platform'; +import type { SettingsService } from '../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; const logger = createLogger('SpecRegeneration'); @@ -19,7 +21,8 @@ export async function generateFeaturesFromSpec( projectPath: string, events: EventEmitter, abortController: AbortController, - maxFeatures?: number + maxFeatures?: number, + settingsService?: SettingsService ): Promise { const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES; logger.debug('========== generateFeaturesFromSpec() started =========='); @@ -91,9 +94,17 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge projectPath: projectPath, }); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[FeatureGeneration]' + ); + const options = createFeatureGenerationOptions({ cwd: projectPath, abortController, + autoLoadClaudeMd, }); logger.debug('SDK Options:', JSON.stringify(options, null, 2)); diff --git a/apps/server/src/routes/app-spec/generate-spec.ts b/apps/server/src/routes/app-spec/generate-spec.ts index 4b6a6426..0762bb90 100644 --- a/apps/server/src/routes/app-spec/generate-spec.ts +++ b/apps/server/src/routes/app-spec/generate-spec.ts @@ -17,6 +17,8 @@ import { createSpecGenerationOptions } from '../../lib/sdk-options.js'; import { logAuthStatus } from './common.js'; import { generateFeaturesFromSpec } from './generate-features-from-spec.js'; import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform'; +import type { SettingsService } from '../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; const logger = createLogger('SpecRegeneration'); @@ -27,7 +29,8 @@ export async function generateSpec( abortController: AbortController, generateFeatures?: boolean, analyzeProject?: boolean, - maxFeatures?: number + maxFeatures?: number, + settingsService?: SettingsService ): Promise { logger.info('========== generateSpec() started =========='); logger.info('projectPath:', projectPath); @@ -83,9 +86,17 @@ ${getStructuredSpecPromptInstruction()}`; content: 'Starting spec generation...\n', }); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[SpecRegeneration]' + ); + const options = createSpecGenerationOptions({ cwd: projectPath, abortController, + autoLoadClaudeMd, outputFormat: { type: 'json_schema', schema: specOutputSchema, @@ -269,7 +280,13 @@ ${getStructuredSpecPromptInstruction()}`; // Create a new abort controller for feature generation const featureAbortController = new AbortController(); try { - await generateFeaturesFromSpec(projectPath, events, featureAbortController, maxFeatures); + await generateFeaturesFromSpec( + projectPath, + events, + featureAbortController, + maxFeatures, + settingsService + ); // Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures } catch (featureError) { logger.error('Feature generation failed:', featureError); diff --git a/apps/server/src/routes/app-spec/index.ts b/apps/server/src/routes/app-spec/index.ts index 47950cd3..342aecd7 100644 --- a/apps/server/src/routes/app-spec/index.ts +++ b/apps/server/src/routes/app-spec/index.ts @@ -9,13 +9,17 @@ import { createGenerateHandler } from './routes/generate.js'; import { createGenerateFeaturesHandler } from './routes/generate-features.js'; import { createStopHandler } from './routes/stop.js'; import { createStatusHandler } from './routes/status.js'; +import type { SettingsService } from '../../services/settings-service.js'; -export function createSpecRegenerationRoutes(events: EventEmitter): Router { +export function createSpecRegenerationRoutes( + events: EventEmitter, + settingsService?: SettingsService +): Router { const router = Router(); router.post('/create', createCreateHandler(events)); - router.post('/generate', createGenerateHandler(events)); - router.post('/generate-features', createGenerateFeaturesHandler(events)); + router.post('/generate', createGenerateHandler(events, settingsService)); + router.post('/generate-features', createGenerateFeaturesHandler(events, settingsService)); router.post('/stop', createStopHandler()); router.get('/status', createStatusHandler()); diff --git a/apps/server/src/routes/app-spec/routes/generate-features.ts b/apps/server/src/routes/app-spec/routes/generate-features.ts index a2e6143a..0c80a9b6 100644 --- a/apps/server/src/routes/app-spec/routes/generate-features.ts +++ b/apps/server/src/routes/app-spec/routes/generate-features.ts @@ -13,10 +13,14 @@ import { getErrorMessage, } from '../common.js'; import { generateFeaturesFromSpec } from '../generate-features-from-spec.js'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('SpecRegeneration'); -export function createGenerateFeaturesHandler(events: EventEmitter) { +export function createGenerateFeaturesHandler( + events: EventEmitter, + settingsService?: SettingsService +) { return async (req: Request, res: Response): Promise => { logger.info('========== /generate-features endpoint called =========='); logger.debug('Request body:', JSON.stringify(req.body, null, 2)); @@ -49,7 +53,7 @@ export function createGenerateFeaturesHandler(events: EventEmitter) { setRunningState(true, abortController); logger.info('Starting background feature generation task...'); - generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures) + generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService) .catch((error) => { logError(error, 'Feature generation failed with error'); events.emit('spec-regeneration:event', { diff --git a/apps/server/src/routes/app-spec/routes/generate.ts b/apps/server/src/routes/app-spec/routes/generate.ts index 341d634d..a03dacb7 100644 --- a/apps/server/src/routes/app-spec/routes/generate.ts +++ b/apps/server/src/routes/app-spec/routes/generate.ts @@ -13,10 +13,11 @@ import { getErrorMessage, } from '../common.js'; import { generateSpec } from '../generate-spec.js'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('SpecRegeneration'); -export function createGenerateHandler(events: EventEmitter) { +export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) { return async (req: Request, res: Response): Promise => { logger.info('========== /generate endpoint called =========='); logger.debug('Request body:', JSON.stringify(req.body, null, 2)); @@ -67,7 +68,8 @@ export function createGenerateHandler(events: EventEmitter) { abortController, generateFeatures, analyzeProject, - maxFeatures + maxFeatures, + settingsService ) .catch((error) => { logError(error, 'Generation failed with error'); diff --git a/apps/server/src/routes/context/index.ts b/apps/server/src/routes/context/index.ts index 37e447bf..3f49f1c1 100644 --- a/apps/server/src/routes/context/index.ts +++ b/apps/server/src/routes/context/index.ts @@ -8,17 +8,19 @@ import { Router } from 'express'; import { createDescribeImageHandler } from './routes/describe-image.js'; import { createDescribeFileHandler } from './routes/describe-file.js'; +import type { SettingsService } from '../../services/settings-service.js'; /** * Create the context router * + * @param settingsService - Optional settings service for loading autoLoadClaudeMd setting * @returns Express router with context endpoints */ -export function createContextRoutes(): Router { +export function createContextRoutes(settingsService?: SettingsService): Router { const router = Router(); - router.post('/describe-image', createDescribeImageHandler()); - router.post('/describe-file', createDescribeFileHandler()); + router.post('/describe-image', createDescribeImageHandler(settingsService)); + router.post('/describe-file', createDescribeFileHandler(settingsService)); return router; } diff --git a/apps/server/src/routes/context/routes/describe-file.ts b/apps/server/src/routes/context/routes/describe-file.ts index 0e680b65..472cbb76 100644 --- a/apps/server/src/routes/context/routes/describe-file.ts +++ b/apps/server/src/routes/context/routes/describe-file.ts @@ -17,6 +17,8 @@ import { PathNotAllowedError } from '@automaker/platform'; import { createCustomOptions } from '../../../lib/sdk-options.js'; import * as secureFs from '../../../lib/secure-fs.js'; import * as path from 'path'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; const logger = createLogger('DescribeFile'); @@ -72,9 +74,12 @@ async function extractTextFromStream( /** * Create the describe-file request handler * + * @param settingsService - Optional settings service for loading autoLoadClaudeMd setting * @returns Express request handler for file description */ -export function createDescribeFileHandler(): (req: Request, res: Response) => Promise { +export function createDescribeFileHandler( + settingsService?: SettingsService +): (req: Request, res: Response) => Promise { return async (req: Request, res: Response): Promise => { try { const { filePath } = req.body as DescribeFileRequestBody; @@ -165,6 +170,13 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`; // Use the file's directory as the working directory const cwd = path.dirname(resolvedPath); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + cwd, + settingsService, + '[DescribeFile]' + ); + // Use centralized SDK options with proper cwd validation // No tools needed since we're passing file content directly const sdkOptions = createCustomOptions({ @@ -172,6 +184,7 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`; model: CLAUDE_MODEL_MAP.haiku, maxTurns: 1, allowedTools: [], + autoLoadClaudeMd, sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, }); diff --git a/apps/server/src/routes/context/routes/describe-image.ts b/apps/server/src/routes/context/routes/describe-image.ts index 64ddfa0f..e4821b4a 100644 --- a/apps/server/src/routes/context/routes/describe-image.ts +++ b/apps/server/src/routes/context/routes/describe-image.ts @@ -17,6 +17,8 @@ import { CLAUDE_MODEL_MAP } from '@automaker/types'; import { createCustomOptions } from '../../../lib/sdk-options.js'; import * as fs from 'fs'; import * as path from 'path'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; const logger = createLogger('DescribeImage'); @@ -226,9 +228,12 @@ async function extractTextFromStream( * Uses Claude SDK query with multi-part content blocks to include the image (base64), * matching the agent runner behavior. * + * @param settingsService - Optional settings service for loading autoLoadClaudeMd setting * @returns Express request handler for image description */ -export function createDescribeImageHandler(): (req: Request, res: Response) => Promise { +export function createDescribeImageHandler( + settingsService?: SettingsService +): (req: Request, res: Response) => Promise { return async (req: Request, res: Response): Promise => { const requestId = `describe-image-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const startedAt = Date.now(); @@ -325,12 +330,20 @@ export function createDescribeImageHandler(): (req: Request, res: Response) => P const cwd = path.dirname(actualPath); logger.info(`[${requestId}] Using cwd=${cwd}`); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + cwd, + settingsService, + '[DescribeImage]' + ); + // Use the same centralized option builder used across the server (validates cwd) const sdkOptions = createCustomOptions({ cwd, model: CLAUDE_MODEL_MAP.haiku, maxTurns: 1, allowedTools: [], + autoLoadClaudeMd, sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, }); diff --git a/apps/server/src/routes/github/index.ts b/apps/server/src/routes/github/index.ts index e8088159..1a2f12ae 100644 --- a/apps/server/src/routes/github/index.ts +++ b/apps/server/src/routes/github/index.ts @@ -16,8 +16,12 @@ import { createDeleteValidationHandler, createMarkViewedHandler, } from './routes/validation-endpoints.js'; +import type { SettingsService } from '../../services/settings-service.js'; -export function createGitHubRoutes(events: EventEmitter): Router { +export function createGitHubRoutes( + events: EventEmitter, + settingsService?: SettingsService +): Router { const router = Router(); router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler()); @@ -26,7 +30,7 @@ export function createGitHubRoutes(events: EventEmitter): Router { router.post( '/validate-issue', validatePathParams('projectPath'), - createValidateIssueHandler(events) + createValidateIssueHandler(events, settingsService) ); // Validation management endpoints diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index 3e75098e..c987453a 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -23,6 +23,8 @@ import { logError, logger, } from './validation-common.js'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; /** Valid model values for validation */ const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const; @@ -54,7 +56,8 @@ async function runValidation( issueLabels: string[] | undefined, model: AgentModel, events: EventEmitter, - abortController: AbortController + abortController: AbortController, + settingsService?: SettingsService ): Promise { // Emit start event const startEvent: IssueValidationEvent = { @@ -76,12 +79,20 @@ async function runValidation( // Build the prompt const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels); + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[ValidateIssue]' + ); + // Create SDK options with structured output and abort controller const options = createSuggestionsOptions({ cwd: projectPath, model, systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT, abortController, + autoLoadClaudeMd, outputFormat: { type: 'json_schema', schema: issueValidationSchema as Record, @@ -190,7 +201,10 @@ async function runValidation( * - System prompt guiding the validation process * - Async execution with event emission */ -export function createValidateIssueHandler(events: EventEmitter) { +export function createValidateIssueHandler( + events: EventEmitter, + settingsService?: SettingsService +) { return async (req: Request, res: Response): Promise => { try { const { @@ -256,7 +270,8 @@ export function createValidateIssueHandler(events: EventEmitter) { issueLabels, model, events, - abortController + abortController, + settingsService ) .catch((error) => { // Error is already handled inside runValidation (event emitted) diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index e4d6aaed..2af01a42 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -9,6 +9,8 @@ import { createSuggestionsOptions } from '../../lib/sdk-options.js'; import { FeatureLoader } from '../../services/feature-loader.js'; import { getAppSpecPath } from '@automaker/platform'; import * as secureFs from '../../lib/secure-fs.js'; +import type { SettingsService } from '../../services/settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; const logger = createLogger('Suggestions'); @@ -125,7 +127,8 @@ export async function generateSuggestions( projectPath: string, suggestionType: string, events: EventEmitter, - abortController: AbortController + abortController: AbortController, + settingsService?: SettingsService ): Promise { const typePrompts: Record = { features: 'Analyze this project and suggest new features that would add value.', @@ -154,9 +157,17 @@ The response will be automatically formatted as structured JSON.`; // Don't send initial message - let the agent output speak for itself // The first agent message will be captured as an info entry + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + settingsService, + '[Suggestions]' + ); + const options = createSuggestionsOptions({ cwd: projectPath, abortController, + autoLoadClaudeMd, outputFormat: { type: 'json_schema', schema: suggestionsSchema, diff --git a/apps/server/src/routes/suggestions/index.ts b/apps/server/src/routes/suggestions/index.ts index 2ea6f9ae..01e22879 100644 --- a/apps/server/src/routes/suggestions/index.ts +++ b/apps/server/src/routes/suggestions/index.ts @@ -8,11 +8,19 @@ import { validatePathParams } from '../../middleware/validate-paths.js'; import { createGenerateHandler } from './routes/generate.js'; import { createStopHandler } from './routes/stop.js'; import { createStatusHandler } from './routes/status.js'; +import type { SettingsService } from '../../services/settings-service.js'; -export function createSuggestionsRoutes(events: EventEmitter): Router { +export function createSuggestionsRoutes( + events: EventEmitter, + settingsService?: SettingsService +): Router { const router = Router(); - router.post('/generate', validatePathParams('projectPath'), createGenerateHandler(events)); + router.post( + '/generate', + validatePathParams('projectPath'), + createGenerateHandler(events, settingsService) + ); router.post('/stop', createStopHandler()); router.get('/status', createStatusHandler()); diff --git a/apps/server/src/routes/suggestions/routes/generate.ts b/apps/server/src/routes/suggestions/routes/generate.ts index 939e0cde..da57ed76 100644 --- a/apps/server/src/routes/suggestions/routes/generate.ts +++ b/apps/server/src/routes/suggestions/routes/generate.ts @@ -7,10 +7,11 @@ import type { EventEmitter } from '../../../lib/events.js'; import { createLogger } from '@automaker/utils'; import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js'; import { generateSuggestions } from '../generate-suggestions.js'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('Suggestions'); -export function createGenerateHandler(events: EventEmitter) { +export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) { return async (req: Request, res: Response): Promise => { try { const { projectPath, suggestionType = 'features' } = req.body as { @@ -37,7 +38,7 @@ export function createGenerateHandler(events: EventEmitter) { setRunningState(true, abortController); // Start generation in background - generateSuggestions(projectPath, suggestionType, events, abortController) + generateSuggestions(projectPath, suggestionType, events, abortController, settingsService) .catch((error) => { logError(error, 'Generate suggestions failed (background)'); events.emit('suggestions:event', { diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index a7b24fef..af9ef746 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -25,7 +25,11 @@ import { promisify } from 'util'; import path from 'path'; import * as secureFs from '../lib/secure-fs.js'; import type { EventEmitter } from '../lib/events.js'; -import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; +import { + createAutoModeOptions, + createCustomOptions, + validateWorkingDirectory, +} from '../lib/sdk-options.js'; import { FeatureLoader } from './feature-loader.js'; import type { SettingsService } from './settings-service.js'; @@ -1068,11 +1072,6 @@ Address the follow-up instructions above. Review the previous work and make the * Analyze project to gather context */ async analyzeProject(projectPath: string): Promise { - // Validate project path before proceeding - // This is called here because analyzeProject builds ExecuteOptions directly - // without using a factory function from sdk-options.ts - validateWorkingDirectory(projectPath); - const abortController = new AbortController(); const analysisFeatureId = `analysis-${Date.now()}`; @@ -1100,13 +1099,27 @@ Format your response as a structured markdown document.`; const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude); const provider = ProviderFactory.getProviderForModel(analysisModel); - const options: ExecuteOptions = { - prompt, + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(projectPath); + + // Use createCustomOptions for centralized SDK configuration with CLAUDE.md support + const sdkOptions = createCustomOptions({ + cwd: projectPath, model: analysisModel, maxTurns: 5, - cwd: projectPath, allowedTools: ['Read', 'Glob', 'Grep'], abortController, + autoLoadClaudeMd, + }); + + const options: ExecuteOptions = { + prompt, + model: sdkOptions.model ?? analysisModel, + cwd: sdkOptions.cwd ?? projectPath, + maxTurns: sdkOptions.maxTurns, + allowedTools: sdkOptions.allowedTools as string[], + abortController, + settingSources: sdkOptions.settingSources, }; const stream = provider.executeQuery(options); diff --git a/libs/types/src/provider.ts b/libs/types/src/provider.ts index f3aa22d5..53c92717 100644 --- a/libs/types/src/provider.ts +++ b/libs/types/src/provider.ts @@ -19,6 +19,15 @@ export interface ConversationMessage { content: string | Array<{ type: string; text?: string; source?: object }>; } +/** + * System prompt preset configuration for CLAUDE.md auto-loading + */ +export interface SystemPromptPreset { + type: 'preset'; + preset: 'claude_code'; + append?: string; +} + /** * Options for executing a query via a provider */ @@ -26,13 +35,14 @@ export interface ExecuteOptions { prompt: string | Array<{ type: string; text?: string; source?: object }>; model: string; cwd: string; - systemPrompt?: string; + systemPrompt?: string | SystemPromptPreset; maxTurns?: number; allowedTools?: string[]; mcpServers?: Record; abortController?: AbortController; conversationHistory?: ConversationMessage[]; // Previous messages for context sdkSessionId?: string; // Claude SDK session ID for resuming conversations + settingSources?: Array<'user' | 'project' | 'local'>; // Sources for CLAUDE.md loading } /** From 99a19cb2a26498a26d160fc8c801b476327eafdd Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 24 Dec 2025 22:48:02 +0100 Subject: [PATCH 3/6] refactor: streamline auto-load CLAUDE.md setting retrieval - Removed the private method for getting the autoLoadClaudeMd setting from AgentService and AutoModeService. - Updated both services to utilize the new settings helper for retrieving the autoLoadClaudeMd setting, improving code reusability and clarity. - Adjusted error handling in the settings helper to throw errors instead of returning false when the settings service is unavailable. --- apps/server/src/lib/sdk-options.ts | 12 ----- apps/server/src/lib/settings-helpers.ts | 4 +- apps/server/src/services/agent-service.ts | 38 +++------------- apps/server/src/services/auto-mode-service.ts | 44 +++++-------------- 4 files changed, 19 insertions(+), 79 deletions(-) diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index 85dc3eb3..e7fc3578 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -173,18 +173,6 @@ function buildClaudeMdOptions(config: CreateSdkOptionsConfig): { result.systemPrompt.append = config.systemPrompt; } - console.log( - '[SDK Options] CLAUDE.md auto-loading enabled:', - JSON.stringify( - { - systemPrompt: result.systemPrompt, - settingSources: result.settingSources, - }, - null, - 2 - ) - ); - return result; } diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts index 9bf48361..477c81ec 100644 --- a/apps/server/src/lib/settings-helpers.ts +++ b/apps/server/src/lib/settings-helpers.ts @@ -15,7 +15,7 @@ import type { SettingsService } from '../services/settings-service.js'; */ export async function getAutoLoadClaudeMdSetting( projectPath: string, - settingsService?: SettingsService, + settingsService?: SettingsService | null, logPrefix = '[SettingsHelper]' ): Promise { if (!settingsService) { @@ -40,6 +40,6 @@ export async function getAutoLoadClaudeMdSetting( return result; } catch (error) { console.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error); - return false; + throw error; } } diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index e25efebe..e5402424 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -17,6 +17,7 @@ import { ProviderFactory } from '../providers/provider-factory.js'; import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; import { PathNotAllowedError } from '@automaker/platform'; import type { SettingsService } from './settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../lib/settings-helpers.js'; interface Message { id: string; @@ -190,7 +191,11 @@ export class AgentService { const effectiveWorkDir = workingDirectory || session.workingDirectory; // Load autoLoadClaudeMd setting (project setting takes precedence over global) - const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(effectiveWorkDir); + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + effectiveWorkDir, + this.settingsService, + '[AgentService]' + ); // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) // Note: When autoLoadClaudeMd is enabled, SDK handles CLAUDE.md loading via settingSources @@ -604,37 +609,6 @@ You have full access to the codebase and can: - Execute tests and builds`; } - /** - * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. - * Returns false if settings service is not available. - */ - private async getAutoLoadClaudeMdSetting(projectPath: string): Promise { - if (!this.settingsService) { - console.log('[AgentService] SettingsService not available, autoLoadClaudeMd disabled'); - return false; - } - - try { - // Check project settings first (takes precedence) - const projectSettings = await this.settingsService.getProjectSettings(projectPath); - if (projectSettings.autoLoadClaudeMd !== undefined) { - console.log( - `[AgentService] autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}` - ); - return projectSettings.autoLoadClaudeMd; - } - - // Fall back to global settings - const globalSettings = await this.settingsService.getGlobalSettings(); - const result = globalSettings.autoLoadClaudeMd ?? false; - console.log(`[AgentService] autoLoadClaudeMd from global settings: ${result}`); - return result; - } catch (error) { - console.error('[AgentService] Failed to load autoLoadClaudeMd setting:', error); - return false; - } - } - private generateId(): string { return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index af9ef746..83fb3feb 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -32,6 +32,7 @@ import { } from '../lib/sdk-options.js'; import { FeatureLoader } from './feature-loader.js'; import type { SettingsService } from './settings-service.js'; +import { getAutoLoadClaudeMdSetting } from '../lib/settings-helpers.js'; const execAsync = promisify(exec); @@ -1100,7 +1101,11 @@ Format your response as a structured markdown document.`; const provider = ProviderFactory.getProviderForModel(analysisModel); // Load autoLoadClaudeMd setting - const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(projectPath); + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); // Use createCustomOptions for centralized SDK configuration with CLAUDE.md support const sdkOptions = createCustomOptions({ @@ -1797,7 +1802,11 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. } // Load autoLoadClaudeMd setting (project setting takes precedence over global) - const autoLoadClaudeMd = await this.getAutoLoadClaudeMdSetting(finalProjectPath); + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + finalProjectPath, + this.settingsService, + '[AutoMode]' + ); // Build SDK options using centralized configuration for feature implementation const sdkOptions = createAutoModeOptions({ @@ -2515,35 +2524,4 @@ Begin implementing task ${task.id} now.`; } }); } - - /** - * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. - * Returns false if settings service is not available. - */ - private async getAutoLoadClaudeMdSetting(projectPath: string): Promise { - if (!this.settingsService) { - console.log('[AutoMode] SettingsService not available, autoLoadClaudeMd disabled'); - return false; - } - - try { - // Check project settings first (takes precedence) - const projectSettings = await this.settingsService.getProjectSettings(projectPath); - if (projectSettings.autoLoadClaudeMd !== undefined) { - console.log( - `[AutoMode] autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}` - ); - return projectSettings.autoLoadClaudeMd; - } - - // Fall back to global settings - const globalSettings = await this.settingsService.getGlobalSettings(); - const result = globalSettings.autoLoadClaudeMd ?? false; - console.log(`[AutoMode] autoLoadClaudeMd from global settings: ${result}`); - return result; - } catch (error) { - console.error('[AutoMode] Failed to load autoLoadClaudeMd setting:', error); - return false; - } - } } From 077dd31b4f4b9162babf4bee5b18618ce2b59da1 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 24 Dec 2025 22:59:57 +0100 Subject: [PATCH 4/6] refactor: enhance context loading strategy in AgentService and AutoModeService - Updated both services to conditionally load CLAUDE.md based on the autoLoadClaudeMd setting, preventing duplication. - Improved clarity in comments regarding the loading process of context files. - Ensured consistent retrieval of the autoLoadClaudeMd setting across different methods. --- apps/server/src/services/agent-service.ts | 13 +++-- apps/server/src/services/auto-mode-service.ts | 51 ++++++++++++++----- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index e5402424..fe1cc357 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -198,11 +198,14 @@ export class AgentService { ); // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - // Note: When autoLoadClaudeMd is enabled, SDK handles CLAUDE.md loading via settingSources - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ - projectPath: effectiveWorkDir, - fsModule: secureFs as Parameters[0]['fsModule'], - }); + // Note: When autoLoadClaudeMd is enabled, skip loading CLAUDE.md here since SDK handles it + // to avoid duplication. The SDK's settingSources will load CLAUDE.md from standard locations. + const { formattedPrompt: contextFilesPrompt } = autoLoadClaudeMd + ? { formattedPrompt: '' } + : await loadContextFiles({ + projectPath: effectiveWorkDir, + fsModule: secureFs as Parameters[0]['fsModule'], + }); // Build combined system prompt with base prompt and context files const baseSystemPrompt = this.getSystemPrompt(); diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 83fb3feb..66532031 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -559,13 +559,24 @@ export class AutoModeService { // Update feature status to in_progress await this.updateFeatureStatus(projectPath, featureId, 'in_progress'); + // Load autoLoadClaudeMd setting to determine context loading strategy + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + // Build the prompt - use continuation prompt if provided (for recovery after plan approval) let prompt: string; // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ - projectPath, - fsModule: secureFs as Parameters[0]['fsModule'], - }); + // Note: When autoLoadClaudeMd is enabled, skip loading CLAUDE.md here since SDK handles it + // to avoid duplication. The SDK's settingSources will load CLAUDE.md from standard locations. + const { formattedPrompt: contextFilesPrompt } = autoLoadClaudeMd + ? { formattedPrompt: '' } + : await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); if (options?.continuationPrompt) { // Continuation prompt is used when recovering from a plan approval @@ -612,6 +623,7 @@ export class AutoModeService { planningMode: feature.planningMode, requirePlanApproval: feature.requirePlanApproval, systemPrompt: contextFilesPrompt || undefined, + autoLoadClaudeMd, } ); @@ -754,11 +766,22 @@ export class AutoModeService { // No previous context } - // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + // Load autoLoadClaudeMd setting to determine context loading strategy + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( projectPath, - fsModule: secureFs as Parameters[0]['fsModule'], - }); + this.settingsService, + '[AutoMode]' + ); + + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt + // Note: When autoLoadClaudeMd is enabled, skip loading CLAUDE.md here since SDK handles it + // to avoid duplication. The SDK's settingSources will load CLAUDE.md from standard locations. + const { formattedPrompt: contextFilesPrompt } = autoLoadClaudeMd + ? { formattedPrompt: '' } + : await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); // Build complete prompt with feature info, previous context, and follow-up instructions let fullPrompt = `## Follow-up on Feature Implementation @@ -887,6 +910,7 @@ Address the follow-up instructions above. Review the previous work and make the planningMode: 'skip', // Follow-ups don't require approval previousContent: previousContext || undefined, systemPrompt: contextFilesPrompt || undefined, + autoLoadClaudeMd, } ); @@ -1729,6 +1753,7 @@ This helps parse your summary correctly in the output logs.`; requirePlanApproval?: boolean; previousContent?: string; systemPrompt?: string; + autoLoadClaudeMd?: boolean; } ): Promise { const finalProjectPath = options?.projectPath || projectPath; @@ -1802,11 +1827,11 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. } // Load autoLoadClaudeMd setting (project setting takes precedence over global) - const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( - finalProjectPath, - this.settingsService, - '[AutoMode]' - ); + // Use provided value if available, otherwise load from settings + const autoLoadClaudeMd = + options?.autoLoadClaudeMd !== undefined + ? options.autoLoadClaudeMd + : await getAutoLoadClaudeMdSetting(finalProjectPath, this.settingsService, '[AutoMode]'); // Build SDK options using centralized configuration for feature implementation const sdkOptions = createAutoModeOptions({ From 387bb15a3df3fd7b893b51765a9a8d6db496b45d Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 24 Dec 2025 23:07:00 +0100 Subject: [PATCH 5/6] refactor: enhance context file handling in AgentService and AutoModeService - Updated both services to conditionally load context files while excluding CLAUDE.md when autoLoadClaudeMd is enabled, preventing duplication. - Improved the structure and clarity of the context files prompt, emphasizing the importance of following project-specific rules and conventions. - Ensured consistent handling of context file loading across different methods in both services. --- apps/server/src/services/agent-service.ts | 52 +++++++-- apps/server/src/services/auto-mode-service.ts | 104 +++++++++++++++--- 2 files changed, 135 insertions(+), 21 deletions(-) diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index fe1cc357..62811ba3 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -198,15 +198,53 @@ export class AgentService { ); // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - // Note: When autoLoadClaudeMd is enabled, skip loading CLAUDE.md here since SDK handles it - // to avoid duplication. The SDK's settingSources will load CLAUDE.md from standard locations. - const { formattedPrompt: contextFilesPrompt } = autoLoadClaudeMd - ? { formattedPrompt: '' } - : await loadContextFiles({ - projectPath: effectiveWorkDir, - fsModule: secureFs as Parameters[0]['fsModule'], + const contextResult = await loadContextFiles({ + projectPath: effectiveWorkDir, + fsModule: secureFs as Parameters[0]['fsModule'], + }); + + // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication + // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md + let contextFilesPrompt = contextResult.formattedPrompt; + if (autoLoadClaudeMd && contextResult.files.length > 0) { + const nonClaudeFiles = contextResult.files.filter( + (f) => f.name.toLowerCase() !== 'claude.md' + ); + + // Rebuild prompt without CLAUDE.md + if (nonClaudeFiles.length > 0) { + const formattedFiles = nonClaudeFiles.map((file) => { + 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}`; }); + contextFilesPrompt = `# 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. +`; + } else { + // All files were CLAUDE.md, so no context to add + contextFilesPrompt = ''; + } + } + // Build combined system prompt with base prompt and context files const baseSystemPrompt = this.getSystemPrompt(); const combinedSystemPrompt = contextFilesPrompt diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 66532031..38da279e 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -569,15 +569,53 @@ export class AutoModeService { // Build the prompt - use continuation prompt if provided (for recovery after plan approval) let prompt: string; // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - // Note: When autoLoadClaudeMd is enabled, skip loading CLAUDE.md here since SDK handles it - // to avoid duplication. The SDK's settingSources will load CLAUDE.md from standard locations. - const { formattedPrompt: contextFilesPrompt } = autoLoadClaudeMd - ? { formattedPrompt: '' } - : await loadContextFiles({ - projectPath, - fsModule: secureFs as Parameters[0]['fsModule'], + const contextResult = await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); + + // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication + // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md + let contextFilesPrompt = contextResult.formattedPrompt; + if (autoLoadClaudeMd && contextResult.files.length > 0) { + const nonClaudeFiles = contextResult.files.filter( + (f) => f.name.toLowerCase() !== 'claude.md' + ); + + // Rebuild prompt without CLAUDE.md + if (nonClaudeFiles.length > 0) { + const formattedFiles = nonClaudeFiles.map((file) => { + 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}`; }); + contextFilesPrompt = `# 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. +`; + } else { + // All files were CLAUDE.md, so no context to add + contextFilesPrompt = ''; + } + } + if (options?.continuationPrompt) { // Continuation prompt is used when recovering from a plan approval // The plan was already approved, so skip the planning phase @@ -774,15 +812,53 @@ export class AutoModeService { ); // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - // Note: When autoLoadClaudeMd is enabled, skip loading CLAUDE.md here since SDK handles it - // to avoid duplication. The SDK's settingSources will load CLAUDE.md from standard locations. - const { formattedPrompt: contextFilesPrompt } = autoLoadClaudeMd - ? { formattedPrompt: '' } - : await loadContextFiles({ - projectPath, - fsModule: secureFs as Parameters[0]['fsModule'], + const contextResult = await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); + + // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication + // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md + let contextFilesPrompt = contextResult.formattedPrompt; + if (autoLoadClaudeMd && contextResult.files.length > 0) { + const nonClaudeFiles = contextResult.files.filter( + (f) => f.name.toLowerCase() !== 'claude.md' + ); + + // Rebuild prompt without CLAUDE.md + if (nonClaudeFiles.length > 0) { + const formattedFiles = nonClaudeFiles.map((file) => { + 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}`; }); + contextFilesPrompt = `# 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. +`; + } else { + // All files were CLAUDE.md, so no context to add + contextFilesPrompt = ''; + } + } + // Build complete prompt with feature info, previous context, and follow-up instructions let fullPrompt = `## Follow-up on Feature Implementation From 072ad72f14dba497c86cde9ad37db97fd56c749b Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 24 Dec 2025 23:17:20 +0100 Subject: [PATCH 6/6] refactor: implement filterClaudeMdFromContext utility for context file handling - Introduced a new utility function to filter out CLAUDE.md from context files when autoLoadClaudeMd is enabled, enhancing clarity and preventing duplication. - Updated AgentService and AutoModeService to utilize the new filtering function, streamlining context file management. - Improved documentation for the new utility, detailing its purpose and usage in context file handling. --- apps/server/src/lib/settings-helpers.ts | 67 ++++++++++++++- apps/server/src/services/agent-service.ts | 42 +--------- apps/server/src/services/auto-mode-service.ts | 82 +------------------ 3 files changed, 71 insertions(+), 120 deletions(-) diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts index 477c81ec..9c4456ff 100644 --- a/apps/server/src/lib/settings-helpers.ts +++ b/apps/server/src/lib/settings-helpers.ts @@ -1,8 +1,9 @@ /** - * Helper utilities for loading settings across different parts of the server + * 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'; /** * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. @@ -43,3 +44,67 @@ export async function getAutoLoadClaudeMdSetting( throw error; } } + +/** + * 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}`; +} diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 62811ba3..323c23c8 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -17,7 +17,7 @@ import { ProviderFactory } from '../providers/provider-factory.js'; import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; import { PathNotAllowedError } from '@automaker/platform'; import type { SettingsService } from './settings-service.js'; -import { getAutoLoadClaudeMdSetting } from '../lib/settings-helpers.js'; +import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js'; interface Message { id: string; @@ -205,45 +205,7 @@ export class AgentService { // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md - let contextFilesPrompt = contextResult.formattedPrompt; - if (autoLoadClaudeMd && contextResult.files.length > 0) { - const nonClaudeFiles = contextResult.files.filter( - (f) => f.name.toLowerCase() !== 'claude.md' - ); - - // Rebuild prompt without CLAUDE.md - if (nonClaudeFiles.length > 0) { - const formattedFiles = nonClaudeFiles.map((file) => { - 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}`; - }); - - contextFilesPrompt = `# 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. -`; - } else { - // All files were CLAUDE.md, so no context to add - contextFilesPrompt = ''; - } - } + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); // Build combined system prompt with base prompt and context files const baseSystemPrompt = this.getSystemPrompt(); diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 38da279e..bcdb92a8 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -32,7 +32,7 @@ import { } from '../lib/sdk-options.js'; import { FeatureLoader } from './feature-loader.js'; import type { SettingsService } from './settings-service.js'; -import { getAutoLoadClaudeMdSetting } from '../lib/settings-helpers.js'; +import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js'; const execAsync = promisify(exec); @@ -576,45 +576,7 @@ export class AutoModeService { // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md - let contextFilesPrompt = contextResult.formattedPrompt; - if (autoLoadClaudeMd && contextResult.files.length > 0) { - const nonClaudeFiles = contextResult.files.filter( - (f) => f.name.toLowerCase() !== 'claude.md' - ); - - // Rebuild prompt without CLAUDE.md - if (nonClaudeFiles.length > 0) { - const formattedFiles = nonClaudeFiles.map((file) => { - 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}`; - }); - - contextFilesPrompt = `# 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. -`; - } else { - // All files were CLAUDE.md, so no context to add - contextFilesPrompt = ''; - } - } + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); if (options?.continuationPrompt) { // Continuation prompt is used when recovering from a plan approval @@ -819,45 +781,7 @@ ${formattedFiles.join('\n\n---\n\n')} // When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication // (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md - let contextFilesPrompt = contextResult.formattedPrompt; - if (autoLoadClaudeMd && contextResult.files.length > 0) { - const nonClaudeFiles = contextResult.files.filter( - (f) => f.name.toLowerCase() !== 'claude.md' - ); - - // Rebuild prompt without CLAUDE.md - if (nonClaudeFiles.length > 0) { - const formattedFiles = nonClaudeFiles.map((file) => { - 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}`; - }); - - contextFilesPrompt = `# 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. -`; - } else { - // All files were CLAUDE.md, so no context to add - contextFilesPrompt = ''; - } - } + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); // Build complete prompt with feature info, previous context, and follow-up instructions let fullPrompt = `## Follow-up on Feature Implementation