diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 996a4a38..93df5566 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -7,7 +7,12 @@ import path from 'path'; import * as secureFs from '../lib/secure-fs.js'; import type { EventEmitter } from '../lib/events.js'; import type { ExecuteOptions } from '@automaker/types'; -import { readImageAsBase64, buildPromptWithImages, isAbortError } from '@automaker/utils'; +import { + readImageAsBase64, + buildPromptWithImages, + isAbortError, + loadContextFiles, +} from '@automaker/utils'; import { ProviderFactory } from '../providers/provider-factory.js'; import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js'; import { PathNotAllowedError } from '@automaker/platform'; @@ -178,12 +183,27 @@ export class AgentService { await this.saveSession(sessionId, session.messages); try { + // Determine the effective working directory for context loading + const effectiveWorkDir = workingDirectory || session.workingDirectory; + + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) + const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + projectPath: effectiveWorkDir, + fsModule: secureFs as Parameters[0]['fsModule'], + }); + + // Build combined system prompt with base prompt and context files + const baseSystemPrompt = this.getSystemPrompt(); + const combinedSystemPrompt = contextFilesPrompt + ? `${contextFilesPrompt}\n\n${baseSystemPrompt}` + : baseSystemPrompt; + // Build SDK options using centralized configuration const sdkOptions = createChatOptions({ - cwd: workingDirectory || session.workingDirectory, + cwd: effectiveWorkDir, model: model, sessionModel: session.model, - systemPrompt: this.getSystemPrompt(), + systemPrompt: combinedSystemPrompt, abortController: session.abortController!, }); @@ -203,8 +223,8 @@ export class AgentService { const options: ExecuteOptions = { prompt: '', // Will be set below based on images model: effectiveModel, - cwd: workingDirectory || session.workingDirectory, - systemPrompt: this.getSystemPrompt(), + cwd: effectiveWorkDir, + systemPrompt: combinedSystemPrompt, maxTurns: maxTurns, allowedTools: allowedTools, abortController: session.abortController!, diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index da48308e..0ab3d54d 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -11,10 +11,15 @@ import { ProviderFactory } from '../providers/provider-factory.js'; import type { ExecuteOptions, Feature } from '@automaker/types'; -import { buildPromptWithImages, isAbortError, classifyError } from '@automaker/utils'; +import { + buildPromptWithImages, + isAbortError, + classifyError, + loadContextFiles, +} from '@automaker/utils'; import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver'; import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver'; -import { getFeatureDir, getAutomakerDir, getFeaturesDir, getContextDir } from '@automaker/platform'; +import { getFeatureDir, getAutomakerDir, getFeaturesDir } from '@automaker/platform'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; @@ -549,7 +554,10 @@ 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 - const contextFiles = await this.loadContextFiles(projectPath); + const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); if (options?.continuationPrompt) { // Continuation prompt is used when recovering from a plan approval @@ -595,7 +603,7 @@ export class AutoModeService { projectPath, planningMode: feature.planningMode, requirePlanApproval: feature.requirePlanApproval, - systemPrompt: contextFiles || undefined, + systemPrompt: contextFilesPrompt || undefined, } ); @@ -736,7 +744,10 @@ export class AutoModeService { } // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt - const contextFiles = await this.loadContextFiles(projectPath); + const { formattedPrompt: contextFilesPrompt } = 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 @@ -864,7 +875,7 @@ Address the follow-up instructions above. Review the previous work and make the projectPath, planningMode: 'skip', // Follow-ups don't require approval previousContent: previousContext || undefined, - systemPrompt: contextFiles || undefined, + systemPrompt: contextFilesPrompt || undefined, } ); @@ -1044,63 +1055,6 @@ Address the follow-up instructions above. Review the previous work and make the } } - /** - * Load context files from .automaker/context/ directory - * These are user-defined context files (CLAUDE.md, CODE_QUALITY.md, etc.) - * that provide project-specific rules and guidelines for the agent. - */ - private async loadContextFiles(projectPath: string): Promise { - // Use path.resolve for cross-platform absolute path handling - const contextDir = path.resolve(getContextDir(projectPath)); - - try { - // Check if directory exists first - await secureFs.access(contextDir); - - const files = await secureFs.readdir(contextDir); - // Filter for text-based context files (case-insensitive for Windows) - const textFiles = files.filter((f) => { - const lower = f.toLowerCase(); - return lower.endsWith('.md') || lower.endsWith('.txt'); - }); - - if (textFiles.length === 0) return ''; - - const contents: string[] = []; - for (const file of textFiles) { - // Use path.join for cross-platform path construction - const filePath = path.join(contextDir, file); - const content = (await secureFs.readFile(filePath, 'utf-8')) as string; - contents.push(`## ${file}\n\n${content}`); - } - - console.log(`[AutoMode] Loaded ${textFiles.length} context file(s): ${textFiles.join(', ')}`); - - return `# ⚠️ CRITICAL: Project Context Files - READ AND FOLLOW STRICTLY - -**IMPORTANT**: The following context files contain MANDATORY project-specific rules and conventions. You MUST: -1. Read these rules carefully before taking any action -2. Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`) -3. Follow ALL coding conventions, commit message formats, and architectural patterns specified -4. Reference these rules before running ANY shell commands or making commits - -Failure to follow these rules will result in broken builds, failed CI, and rejected commits. - -${contents.join('\n\n---\n\n')} - ---- - -**REMINDER**: Before running any command, verify you are using the correct package manager and following the conventions above. - ---- - -`; - } catch { - // Context directory doesn't exist or is empty - this is fine - return ''; - } - } - /** * Analyze project to gather context */ diff --git a/docs/context-files-pattern.md b/docs/context-files-pattern.md new file mode 100644 index 00000000..c35bfe53 --- /dev/null +++ b/docs/context-files-pattern.md @@ -0,0 +1,170 @@ +# Context Files System + +This document describes how context files work in Automaker and how to use them in agent prompts. + +## Overview + +Context files are user-defined documents stored in `.automaker/context/` that provide project-specific rules, conventions, and guidelines for AI agents. They are automatically loaded and prepended to agent prompts. + +## Directory Structure + +``` +{projectPath}/.automaker/context/ +├── CLAUDE.md # Project rules and conventions +├── CODE_QUALITY.md # Code quality guidelines +├── context-metadata.json # File descriptions +└── ... (any .md or .txt files) +``` + +## Metadata + +File descriptions are stored in `context-metadata.json`: + +```json +{ + "files": { + "CLAUDE.md": { + "description": "Project-specific rules including package manager, commit conventions, and architectural patterns" + }, + "CODE_QUALITY.md": { + "description": "Code quality standards, testing requirements, and linting rules" + } + } +} +``` + +## Shared Utility + +The `loadContextFiles` function from `@automaker/utils` provides a unified way to load context files: + +```typescript +import { loadContextFiles } from '@automaker/utils'; + +// Load context files from a project +const { formattedPrompt, files } = await loadContextFiles({ + projectPath: '/path/to/project', + // Optional: inject custom fs module for secure operations + fsModule: secureFs, +}); + +// formattedPrompt contains the formatted system prompt +// files contains metadata about each loaded file +``` + +### Return Value + +```typescript +interface ContextFilesResult { + files: ContextFileInfo[]; // Individual file info + formattedPrompt: string; // Formatted prompt ready to use +} + +interface ContextFileInfo { + name: string; // File name (e.g., "CLAUDE.md") + path: string; // Full path to file + content: string; // File contents + description?: string; // From metadata (explains when/why to use) +} +``` + +## Usage in Services + +### Auto-Mode Service (Feature Execution) + +```typescript +import { loadContextFiles } from '@automaker/utils'; +import * as secureFs from '../lib/secure-fs.js'; + +// In executeFeature() or followUpFeature() +const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], +}); + +// Pass as system prompt +await this.runAgent(workDir, featureId, prompt, abortController, projectPath, imagePaths, model, { + projectPath, + systemPrompt: contextFilesPrompt || undefined, +}); +``` + +### Agent Service (Chat Sessions) + +```typescript +import { loadContextFiles } from '@automaker/utils'; +import * as secureFs from '../lib/secure-fs.js'; + +// In sendMessage() +const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({ + projectPath: effectiveWorkDir, + fsModule: secureFs as Parameters[0]['fsModule'], +}); + +// Combine with base system prompt +const combinedSystemPrompt = contextFilesPrompt + ? `${contextFilesPrompt}\n\n${baseSystemPrompt}` + : baseSystemPrompt; +``` + +## Formatted Prompt Structure + +The formatted prompt includes: + +1. **Header** - Emphasizes that these are project-specific rules +2. **File Entries** - Each file with: + - File name + - Full path (for agents to read more if needed) + - Purpose/description (from metadata) + - Full file content +3. **Reminder** - Reinforces that agents must follow the conventions + +Example output: + +```markdown +# 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 + +--- + +## CLAUDE.md + +**Path:** `/path/to/project/.automaker/context/CLAUDE.md` +**Purpose:** Project-specific rules including package manager, commit conventions, and architectural patterns + +[File content here] + +--- + +## CODE_QUALITY.md + +**Path:** `/path/to/project/.automaker/context/CODE_QUALITY.md` +**Purpose:** Code quality standards, testing requirements, and linting rules + +[File content here] + +--- + +**REMINDER**: Before taking any action, verify you are following the conventions specified above. +``` + +## Best Practices + +1. **Add descriptions** - Always add descriptions to `context-metadata.json` so agents understand when to reference each file +2. **Be specific** - Context files should contain concrete rules, not general guidelines +3. **Include examples** - Show correct command usage, commit formats, etc. +4. **Keep focused** - Each file should have a single purpose + +## File Locations + +- **Shared Utility**: `libs/utils/src/context-loader.ts` +- **Auto-Mode Service**: `apps/server/src/services/auto-mode-service.ts` +- **Agent Service**: `apps/server/src/services/agent-service.ts` diff --git a/libs/utils/src/context-loader.ts b/libs/utils/src/context-loader.ts new file mode 100644 index 00000000..0e94092b --- /dev/null +++ b/libs/utils/src/context-loader.ts @@ -0,0 +1,238 @@ +/** + * Context Loader - Loads project context files for agent prompts + * + * Provides a shared utility to load context files from .automaker/context/ + * and format them as system prompt content. Used by both auto-mode-service + * and agent-service to ensure all agents are aware of project context. + * + * Context files contain project-specific rules, conventions, and guidelines + * that agents must follow when working on the project. + */ + +import path from 'path'; +import fs from 'fs/promises'; + +/** + * Metadata structure for context files + * Stored in {projectPath}/.automaker/context/context-metadata.json + */ +export interface ContextMetadata { + files: Record; +} + +/** + * Individual context file with metadata + */ +export interface ContextFileInfo { + name: string; + path: string; + content: string; + description?: string; +} + +/** + * Result of loading context files + */ +export interface ContextFilesResult { + files: ContextFileInfo[]; + formattedPrompt: string; +} + +/** + * Options for loading context files + */ +export interface LoadContextFilesOptions { + /** Project path to load context from */ + projectPath: string; + /** Optional custom secure fs module (for dependency injection) */ + fsModule?: { + access: (path: string) => Promise; + readdir: (path: string) => Promise; + readFile: (path: string, encoding: string) => Promise; + }; +} + +/** + * Get the context directory path for a project + */ +function getContextDir(projectPath: string): string { + return path.join(projectPath, '.automaker', 'context'); +} + +/** + * Load context metadata from the metadata file + */ +async function loadContextMetadata( + contextDir: string, + fsModule: typeof fs +): Promise { + const metadataPath = path.join(contextDir, 'context-metadata.json'); + try { + const content = await fsModule.readFile(metadataPath, 'utf-8'); + return JSON.parse(content); + } catch { + // Metadata file doesn't exist yet - that's fine + return { files: {} }; + } +} + +/** + * Format a single context file entry for the prompt + */ +function formatContextFileEntry(file: ContextFileInfo): string { + const header = `## ${file.name}`; + const pathInfo = `**Path:** \`${file.path}\``; + + let descriptionInfo = ''; + if (file.description) { + descriptionInfo = `\n**Purpose:** ${file.description}`; + } + + return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`; +} + +/** + * Build the formatted system prompt from context files + */ +function buildContextPrompt(files: ContextFileInfo[]): string { + if (files.length === 0) { + return ''; + } + + const formattedFiles = files.map(formatContextFileEntry); + + 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. +`; +} + +/** + * Load context files from a project's .automaker/context/ directory + * + * This function loads all .md and .txt files from the context directory, + * along with their metadata (descriptions), and formats them into a + * system prompt that can be prepended to agent prompts. + * + * @param options - Configuration options + * @returns Promise resolving to context files and formatted prompt + * + * @example + * ```typescript + * const { formattedPrompt, files } = await loadContextFiles({ + * projectPath: '/path/to/project' + * }); + * + * // Use as system prompt + * const executeOptions = { + * prompt: userPrompt, + * systemPrompt: formattedPrompt, + * }; + * ``` + */ +export async function loadContextFiles( + options: LoadContextFilesOptions +): Promise { + const { projectPath, fsModule = fs } = options; + const contextDir = path.resolve(getContextDir(projectPath)); + + try { + // Check if directory exists + await fsModule.access(contextDir); + + // Read directory contents + const allFiles = await fsModule.readdir(contextDir); + + // Filter for text-based context files (case-insensitive for cross-platform) + const textFiles = allFiles.filter((f) => { + const lower = f.toLowerCase(); + return (lower.endsWith('.md') || lower.endsWith('.txt')) && f !== 'context-metadata.json'; + }); + + if (textFiles.length === 0) { + return { files: [], formattedPrompt: '' }; + } + + // Load metadata for descriptions + const metadata = await loadContextMetadata(contextDir, fsModule as typeof fs); + + // Load each file with its content and metadata + const files: ContextFileInfo[] = []; + for (const fileName of textFiles) { + const filePath = path.join(contextDir, fileName); + try { + const content = await fsModule.readFile(filePath, 'utf-8'); + files.push({ + name: fileName, + path: filePath, + content, + description: metadata.files[fileName]?.description, + }); + } catch (error) { + console.warn(`[ContextLoader] Failed to read context file ${fileName}:`, error); + } + } + + const formattedPrompt = buildContextPrompt(files); + + console.log( + `[ContextLoader] Loaded ${files.length} context file(s): ${files.map((f) => f.name).join(', ')}` + ); + + return { files, formattedPrompt }; + } catch { + // Context directory doesn't exist or is inaccessible - this is fine + return { files: [], formattedPrompt: '' }; + } +} + +/** + * Get a summary of available context files (names and descriptions only) + * Useful for informing the agent about what context is available without + * loading full content. + */ +export async function getContextFilesSummary( + options: LoadContextFilesOptions +): Promise> { + const { projectPath, fsModule = fs } = options; + const contextDir = path.resolve(getContextDir(projectPath)); + + try { + await fsModule.access(contextDir); + const allFiles = await fsModule.readdir(contextDir); + + const textFiles = allFiles.filter((f) => { + const lower = f.toLowerCase(); + return (lower.endsWith('.md') || lower.endsWith('.txt')) && f !== 'context-metadata.json'; + }); + + if (textFiles.length === 0) { + return []; + } + + const metadata = await loadContextMetadata(contextDir, fsModule as typeof fs); + + return textFiles.map((fileName) => ({ + name: fileName, + path: path.join(contextDir, fileName), + description: metadata.files[fileName]?.description, + })); + } catch { + return []; + } +} diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts index ef2187f3..c3e39b33 100644 --- a/libs/utils/src/index.ts +++ b/libs/utils/src/index.ts @@ -37,21 +37,20 @@ export { } from './prompt-builder.js'; // Logger -export { - createLogger, - getLogLevel, - setLogLevel, - LogLevel, -} from './logger.js'; +export { createLogger, getLogLevel, setLogLevel, LogLevel } from './logger.js'; // File system utilities -export { - mkdirSafe, - existsSafe, -} from './fs-utils.js'; +export { mkdirSafe, existsSafe } from './fs-utils.js'; // Path utilities +export { normalizePath, pathsEqual } from './path-utils.js'; + +// Context file loading export { - normalizePath, - pathsEqual, -} from './path-utils.js'; + loadContextFiles, + getContextFilesSummary, + type ContextMetadata, + type ContextFileInfo, + type ContextFilesResult, + type LoadContextFilesOptions, +} from './context-loader.js';