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..bd0fd4a0 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 @@ -148,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/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index 7853fbd2..e7fc3578 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -136,6 +136,59 @@ 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; + } + + 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 +213,9 @@ export interface CreateSdkOptionsConfig { type: 'json_schema'; schema: Record; }; + + /** Enable auto-loading of CLAUDE.md files via SDK's settingSources */ + autoLoadClaudeMd?: boolean; } /** @@ -169,11 +225,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 +244,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 +257,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 +274,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 +286,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 +315,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 +324,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 +337,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options { enabled: true, autoAllowBashIfSandboxed: true, }, - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), }; } @@ -278,11 +350,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 +369,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options { enabled: true, autoAllowBashIfSandboxed: true, }, - ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...claudeMdOptions, ...(config.abortController && { abortController: config.abortController }), }; } @@ -302,6 +378,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 +390,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 +400,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/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts new file mode 100644 index 00000000..9c4456ff --- /dev/null +++ b/apps/server/src/lib/settings-helpers.ts @@ -0,0 +1,110 @@ +/** + * 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. + * 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 | null, + 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); + 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/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/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/agent-service.ts b/apps/server/src/services/agent-service.ts index 93df5566..323c23c8 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -16,6 +16,8 @@ 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'; +import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js'; interface Message { id: string; @@ -57,11 +59,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,12 +190,23 @@ 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 getAutoLoadClaudeMdSetting( + effectiveWorkDir, + this.settingsService, + '[AgentService]' + ); + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + 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 + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); + // Build combined system prompt with base prompt and context files const baseSystemPrompt = this.getSystemPrompt(); const combinedSystemPrompt = contextFilesPrompt @@ -205,6 +220,7 @@ export class AgentService { sessionModel: session.model, systemPrompt: combinedSystemPrompt, abortController: session.abortController!, + autoLoadClaudeMd, }); // Extract model, maxTurns, and allowedTools from SDK options @@ -224,11 +240,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 }; diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 1da65e35..bcdb92a8 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -25,8 +25,14 @@ 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'; +import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js'; const execAsync = promisify(exec); @@ -341,9 +347,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; } /** @@ -551,14 +559,25 @@ 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({ + 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 + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); + if (options?.continuationPrompt) { // Continuation prompt is used when recovering from a plan approval // The plan was already approved, so skip the planning phase @@ -604,6 +623,7 @@ export class AutoModeService { planningMode: feature.planningMode, requirePlanApproval: feature.requirePlanApproval, systemPrompt: contextFilesPrompt || undefined, + autoLoadClaudeMd, } ); @@ -746,12 +766,23 @@ export class AutoModeService { // No previous context } + // Load autoLoadClaudeMd setting to determine context loading strategy + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + 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 + const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); + // Build complete prompt with feature info, previous context, and follow-up instructions let fullPrompt = `## Follow-up on Feature Implementation @@ -879,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, } ); @@ -1065,11 +1097,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()}`; @@ -1097,13 +1124,31 @@ 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 getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + + // 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); @@ -1708,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; @@ -1780,11 +1826,19 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. return; } + // Load autoLoadClaudeMd setting (project setting takes precedence over global) + // 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({ cwd: workDir, model: model, abortController, + autoLoadClaudeMd, }); // Extract model, maxTurns, and allowedTools from SDK options @@ -1823,7 +1877,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 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/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 } /** 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) */