Source Code
The runnable TypeScript source for this lesson is in
lessons/12-rules-system/
Lesson 12: Rules & Instructions System¶
Building dynamic system prompts from hierarchical configuration sources
What You'll Learn¶
- Hierarchical Configuration: How to layer rules from global to local
- Rule Discovery: Automatically finding instruction files in projects
- Priority Merging: Resolving conflicts between rule sources
- Dynamic Prompts: Building system prompts at runtime
- Template Expansion: Injecting variables into prompts
Why This Matters¶
Modern AI coding assistants support configuration files that let users customize behavior. Examples:
- Claude Code:
CLAUDE.mdfiles - Cursor:
.cursorrulesfiles - Aider:
.aider.mdfiles
A rules system enables: - Per-project coding standards - Personal preferences that apply everywhere - Directory-specific overrides - Dynamic prompt construction
Key Concepts¶
Scope Hierarchy¶
Rules come from multiple scopes, with more specific scopes overriding general ones:
+-----------------------------------------------+
| Global | ~/.claude/CLAUDE.md
| +---------------------------------------+ |
| | User | | ~/CLAUDE.md
| | +-------------------------------+ | |
| | | Project | | | /project/CLAUDE.md
| | | +-------------------------+ | | |
| | | | Directory | | | | /project/src/CLAUDE.local.md
| | | | +-------------------+ | | | |
| | | | | Session | | | | | Runtime overrides
| | | | +-------------------+ | | | |
| | | +-------------------------+ | | |
| | +-------------------------------+ | |
| +---------------------------------------+ |
+-----------------------------------------------+
Priority: session > directory > project > user > global
Instruction File Format¶
---
scope: project
priority: 300
tags: [typescript, web]
---
# Project Instructions
## Context
This is a TypeScript web application.
## Constraints
- Never commit .env files
- Always use strict mode
## Preferences
- Use functional components
- Prefer async/await over callbacks
Rule Types¶
| Type | Purpose | Example |
|---|---|---|
persona |
Agent identity/role | "You are a TypeScript expert" |
context |
Background information | "This is a React application" |
instruction |
What to do | "Explain your reasoning" |
constraint |
What NOT to do | "Never expose API keys" |
preference |
What to prefer | "Use functional programming" |
format |
Output formatting | "Use markdown for responses" |
tool-config |
Tool-specific settings | "Prefer Edit over Write" |
Files in This Lesson¶
| File | Purpose |
|---|---|
types.ts |
Type definitions for rules and sources |
rule-loader.ts |
File discovery and parsing |
rule-merger.ts |
Combining rules with priority |
prompt-builder.ts |
Constructing system prompts |
main.ts |
Demonstration of all concepts |
Running This Lesson¶
Code Examples¶
Discovering Rules¶
import { RuleLoader } from './rule-loader.js';
const loader = new RuleLoader({
baseDir: process.cwd(),
filePatterns: ['CLAUDE.md', 'AGENTS.md'],
});
// Find all instruction files
const sources = await loader.discover();
// Load and parse each one
for (const source of sources) {
const result = await loader.load(source);
if (result.success) {
const parsed = loader.parseFile(result.content!, source.location);
console.log(`Found ${parsed.sections.length} sections`);
}
}
Merging Rules¶
import { RuleMerger } from './rule-merger.js';
const merger = new RuleMerger({
strategy: 'combine', // Include all rules
deduplicate: true, // Remove duplicates
});
// Merge rules from multiple files
const ruleSet = merger.merge([
{ source: globalSource, file: globalFile },
{ source: projectSource, file: projectFile },
]);
console.log(`Merged ${ruleSet.metadata.mergedRules} rules`);
Building Prompts¶
import { PromptBuilder } from './prompt-builder.js';
const builder = new PromptBuilder({
includeSectionHeaders: true,
maxLength: 10000,
});
const systemPrompt = builder.build(ruleSet);
// Or build with variables
const prompt = builder.buildWithVariables(ruleSet, {
projectName: 'My Project',
date: new Date().toISOString(),
});
Direct Section Building¶
import { buildFromSections } from './prompt-builder.js';
const prompt = buildFromSections({
persona: 'You are a helpful assistant.',
instructions: 'Always explain your reasoning.',
constraints: 'Never execute dangerous commands.',
});
Merge Strategies¶
combine (default)¶
Include all rules, sorted by priority:
const merger = new RuleMerger({ strategy: 'combine' });
// All rules included, sorted by scope then priority
priority¶
Higher priority rules override lower ones:
const merger = new RuleMerger({ strategy: 'priority' });
// Only highest priority rule of each type kept
latest¶
Most recently loaded rules win:
Per-Type Strategies¶
const merger = new RuleMerger({
strategy: 'combine',
typeStrategies: {
constraint: 'combine', // Keep all constraints
preference: 'priority', // Highest priority wins
persona: 'latest', // Most specific wins
},
});
Template Variables¶
Prompts can include template variables:
Expanded at runtime:
const prompt = builder.buildWithVariables(ruleSet, {
projectName: 'First Principles Agent',
date: '2024-01-15',
userName: 'Developer',
});
Frontmatter Options¶
---
# Scope override
scope: directory
# Priority within scope (lower = higher priority)
priority: 200
# Tags for filtering
tags: [typescript, testing]
# Condition for inclusion
condition:
directories: ['src/**']
fileTypes: ['*.ts', '*.tsx']
# Enable/disable
enabled: true
---
Best Practices¶
File Organization¶
~/.claude/
CLAUDE.md # Global preferences
/my-project/
CLAUDE.md # Project standards
.claude/
CLAUDE.md # Additional project rules
instructions.md # Alternative location
/src/
CLAUDE.local.md # Directory-specific (gitignored)
Rule Writing¶
Good rules:
Avoid:
Gitignore Local Files¶
Integration Example¶
import { RuleLoader, RuleMerger, PromptBuilder } from './12-rules-system';
async function buildAgentPrompt(workingDir: string): Promise<string> {
const loader = new RuleLoader({ baseDir: workingDir });
const merger = new RuleMerger();
const builder = new PromptBuilder();
// Discover and load all instruction files
const sources = await loader.discover();
const files = [];
for (const source of sources) {
const result = await loader.load(source);
if (result.success) {
const file = loader.parseFile(result.content!, source.location);
files.push({ source, file });
}
}
// Merge and build
const ruleSet = merger.merge(files);
return builder.build(ruleSet);
}
// Use in agent
const systemPrompt = await buildAgentPrompt(process.cwd());
const agent = new Agent({
systemPrompt,
// ...
});
Advanced: .agentignore Support¶
The production agent implements .agentignore - AI-specific file exclusion patterns separate from .gitignore.
Why Separate from .gitignore?¶
Files to hide from AI but keep in git:
- Large data files (agent doesn't need to analyze)
- Generated documentation
- Test fixtures with large payloads
Files in .gitignore the AI might need:
- Local config files (for understanding setup)
- Build outputs (for debugging)
Priority Order¶
1. .agentignore (highest - AI-specific)
2. .gitignore (medium - inherit git patterns)
3. ~/.agent/ignore (global defaults)
4. Built-in patterns (always applied)
Pattern Syntax¶
Uses gitignore-style patterns:
# .agentignore - AI-specific file exclusion
# Directories
data/
docs/generated/
# Glob patterns
*.csv
*.json.bak
# Negation (include despite previous patterns)
!important-data.csv
# Directory-only (trailing /)
scratch/
# Root-anchored (leading /)
/config/secrets/
Built-in Patterns¶
Always ignored regardless of configuration:
const BUILTIN_PATTERNS = [
// Version control
'.git', '.svn', '.hg',
// Dependencies
'node_modules', '__pycache__', '.venv', 'venv',
// IDE/Editor
'.idea', '.vscode', '*.swp', '*~',
// OS files
'.DS_Store', 'Thumbs.db',
// Build outputs
'dist', 'build', 'out',
// Sensitive files
'.env', '.env.local', '*.pem', '*.key',
'credentials.json', 'secrets.json',
];
Usage¶
import { createIgnoreManager } from './ignore.js';
// Create and load patterns
const ignore = createIgnoreManager({
includeGitignore: true, // Include .gitignore patterns
includeGlobal: true, // Include ~/.agent/ignore
});
await ignore.load(process.cwd());
// Check single path
if (ignore.shouldIgnore('data/large-file.csv')) {
console.log('Skipping file');
}
// Filter path list
const visibleFiles = ignore.filterPaths(allFiles);
// Filter with directory detection
const filtered = await ignore.filterPathsWithStats(paths);
Sample .agentignore¶
# .agentignore - AI-specific file exclusion
# Files listed here will be hidden from the AI agent
# but remain visible to git and other tools
# Large data files that don't need AI analysis
data/
*.csv
*.json.bak
# Generated documentation
docs/api/
docs/generated/
# Test fixtures with large data
tests/fixtures/large/
# Temporary development files
scratch/
notes.md
# Sensitive configuration not needed for code changes
.env.production
config/secrets/
# Build artifacts already in .gitignore but ensure agent ignores
coverage/
.nyc_output/
Events¶
ignore.subscribe((event) => {
switch (event.type) {
case 'ignore.loaded':
console.log(`Loaded ${event.patternCount} patterns from ${event.source}`);
break;
case 'ignore.matched':
console.log(`Ignoring ${event.path} (matched: ${event.pattern})`);
break;
}
});
Integration with File Tools¶
// In file listing tool
async function listFiles(dir: string, ignoreManager: IgnoreManager) {
const allFiles = await glob('**/*', { cwd: dir });
return ignoreManager.filterPaths(allFiles);
}
// In file reading tool
async function readFile(path: string, ignoreManager: IgnoreManager) {
if (ignoreManager.shouldIgnore(path)) {
return { error: `File ${path} is excluded by .agentignore` };
}
return { content: await fs.readFile(path, 'utf-8') };
}
Next Steps¶
In Lesson 13: Client/Server Separation, we'll build a server API that exposes the agent functionality, allowing multiple clients (CLI, web, IDE) to connect.
The rules system integrates naturally - clients can send rule updates, and the server rebuilds prompts dynamically!