feat: implement context file loading system for agent prompts

- Introduced a new utility function `loadContextFiles` to load project-specific context files from the `.automaker/context/` directory, enhancing agent prompts with project rules and guidelines.
- Updated `AgentService` and `AutoModeService` to utilize the new context loading functionality, combining context prompts with existing system prompts for improved agent performance.
- Added comprehensive documentation on the context files system, including usage examples and metadata structure, to facilitate better understanding and implementation.
- Removed redundant context loading logic from `AutoModeService`, streamlining the codebase.

These changes aim to improve the agent's contextual awareness and adherence to project-specific conventions.
This commit is contained in:
Test User
2025-12-22 00:09:57 -05:00
parent e2718b37e3
commit 398f7b8fdd
5 changed files with 462 additions and 81 deletions

View File

@@ -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<typeof loadContextFiles>[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!,

View File

@@ -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<typeof loadContextFiles>[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<typeof loadContextFiles>[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<string> {
// 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
*/

View File

@@ -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<typeof loadContextFiles>[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<typeof loadContextFiles>[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`

View File

@@ -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<string, { description: string }>;
}
/**
* 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<void>;
readdir: (path: string) => Promise<string[]>;
readFile: (path: string, encoding: string) => Promise<string>;
};
}
/**
* 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<ContextMetadata> {
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<ContextFilesResult> {
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<Array<{ name: string; path: string; description?: string }>> {
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 [];
}
}

View File

@@ -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';