Skip to content

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

  1. Hierarchical Configuration: How to layer rules from global to local
  2. Rule Discovery: Automatically finding instruction files in projects
  3. Priority Merging: Resolving conflicts between rule sources
  4. Dynamic Prompts: Building system prompts at runtime
  5. 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.md files
  • Cursor: .cursorrules files
  • Aider: .aider.md files

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

npm run lesson:12

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:

const merger = new RuleMerger({ strategy: 'latest' });
// Later sources override earlier ones

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:

You are helping with the {{projectName}} project.

Current date: {{date}}
User: {{userName}}

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:

## Constraints
- Never commit API keys or secrets
- Always validate user input before processing

Avoid:

## Constraints
- Be careful with security stuff
- Make sure things work

Gitignore Local Files

# Local instruction overrides
*.local.md
CLAUDE.local.md

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!