20 min
architecture
February 8, 2026

Implementation Sub-Plan: Section 14 — Configuration

Implementation Sub-Plan: Section 14 — Configuration

System: Forge (Agentic SDLC Orchestrator) Section: 14 - Configuration Status: Draft for Implementation Last Updated: 2026-02-07


1. Overview

The configuration system is the control plane for Forge. It determines how agents behave, what models they use, what safety limits they respect, and how the entire pipeline operates. This plan covers the complete implementation of the configuration system from discovery to validation to runtime resolution.

1.1 Design Goals

  1. Developer-friendly — TypeScript-first with full type safety and autocomplete
  2. Sensible defaults — Works out of the box with minimal configuration
  3. Flexible overrides — CLI flags > env vars > config file > defaults
  4. Secure by default — Secrets never in config files, always from environment
  5. Validatable — Runtime validation catches misconfigurations at startup
  6. Observable — Config snapshots saved with every run for reproducibility

1.2 Non-Goals for MVP

  • Hot reload (restart required for config changes)
  • Config schema migration (breaking changes = manual update)
  • Remote config fetching (file-based only)
  • Config inheritance chains (single config + env overrides only)

2. forge.config.ts Format

2.1 The defineConfig() API

typescript
// ─── forge.config.ts (user's project root) ──────────────── import { defineConfig } from '@forge/core'; export default defineConfig({ // Project metadata name: 'my-app', language: 'typescript', // LLM configuration llm: { provider: 'anthropic', model: 'claude-sonnet-4-5-20250929', fastModel: 'claude-haiku-4-5-20251001', // API key from env var (required) apiKey: process.env.ANTHROPIC_API_KEY, // Optional overrides baseUrl: 'https://api.anthropic.com', temperature: 0.7, maxTokens: 4096, }, // Project tooling tools: { testCommand: 'bun test', lintCommand: 'bun run lint', buildCommand: 'bun run build', typecheckCommand: 'bun run typecheck', }, // Safety controls safety: { costPerRun: 50, costPerDay: 200, automationLevel: 1, // 0-4 automation ladder maxIterations: { planning: 20, implementation: 50, review: 10, testing: 5, deployment: 3, }, timeouts: { planning: 30 * 60_000, implementation: 60 * 60_000, review: 30 * 60_000, testing: 20 * 60_000, deployment: 15 * 60_000, }, }, // GitHub integration github: { owner: 'myorg', repo: 'my-app', reviewOnPR: true, postComments: true, token: process.env.GITHUB_TOKEN, }, // Memory system memory: { dbPath: '.forge/memory.db', consolidateInterval: 86400000, // 1 day maxMemories: 10_000, embeddingModel: 'local', }, // Per-agent overrides (optional) agents: { planner: { model: 'claude-opus-4-6', // Use stronger model for planning systemPrompt: 'additional context here...', }, reviewer: { model: 'claude-haiku-4-5-20251001', // Fast, cheap review maxIterations: 5, }, }, // Per-phase overrides (optional) phases: { review: { skip: false, minRiskForHumanGate: 'high', // low | medium | high | critical maxBounces: 3, }, deployment: { skip: false, requireHumanApproval: true, // Always for production }, }, });

2.2 defineConfig() Helper Implementation

typescript
// ─── src/core/config.ts ─────────────────────────────────── import type { ForgeConfig } from './types'; /** * Type-safe config definition helper. * Provides autocomplete and type checking for config files. */ export function defineConfig(config: ForgeConfig): ForgeConfig { return config; } /** * Alternative: defineConfig with validation at definition time. * This can catch errors earlier but requires the schema to be loaded. */ export function defineConfigWithValidation(config: ForgeConfig): ForgeConfig { const result = ForgeConfigSchema.safeParse(config); if (!result.success) { throw new Error( `Invalid config:\n${result.error.issues.map(i => ` - ${i.path.join('.')}: ${i.message}`).join('\n')}` ); } return result.data; }

3. ForgeConfig Type Definition

3.1 Complete TypeScript Type

typescript
// ─── src/core/types.ts ──────────────────────────────────── export type ForgeConfig = { // ─── Project ─────────────────────────────────────────── name: string; language: 'typescript' | 'javascript' | 'python' | 'go' | 'rust'; root?: string; // Auto-detected if not provided // ─── LLM Provider ────────────────────────────────────── llm: { provider: 'anthropic' | 'openai' | 'ollama' | 'custom'; model: string; // Primary model for complex tasks fastModel?: string; // Cheaper model for simple tasks (defaults to model) apiKey?: string; // From env var, never hardcoded baseUrl?: string; // Custom API endpoint temperature?: number; // 0-1, default 0.7 maxTokens?: number; // Default 4096 timeout?: number; // Request timeout in ms, default 60000 }; // ─── Tools ───────────────────────────────────────────── tools: { testCommand?: string; // Default: auto-detect (bun test, npm test, etc) lintCommand?: string; // Default: auto-detect buildCommand?: string; // Default: auto-detect typecheckCommand?: string; // Default: auto-detect custom?: Record<string, string>; // User-defined tools }; // ─── Safety Controls ─────────────────────────────────── safety: { costPerRun?: number; // USD, default 50 costPerDay?: number; // USD, default 200 automationLevel?: 0 | 1 | 2 | 3 | 4; // Default 1 maxIterations?: { planning?: number; // Default 20 implementation?: number; // Default 50 review?: number; // Default 10 testing?: number; // Default 5 deployment?: number; // Default 3 }; timeouts?: { planning?: number; // ms, default 30 min implementation?: number; // ms, default 1 hour review?: number; // ms, default 30 min testing?: number; // ms, default 20 min deployment?: number; // ms, default 15 min totalPipeline?: number; // ms, default 2 hours }; stagnationThreshold?: number; // Iterations without progress, default 3 errorRateThresholds?: { warning?: number; // Default 0.10 critical?: number; // Default 0.25 catastrophic?: number; // Default 0.50 }; }; // ─── GitHub Integration ──────────────────────────────── github?: { owner: string; repo: string; reviewOnPR?: boolean; // Auto-review new PRs, default true postComments?: boolean; // Post findings as comments, default true token?: string; // From env var webhookSecret?: string; // For webhook verification }; // ─── Memory System ───────────────────────────────────── memory?: { dbPath?: string; // Default .forge/memory.db consolidateInterval?: number; // ms, default 1 day maxMemories?: number; // Default 10_000 embeddingModel?: 'local' | 'openai' | 'voyage'; // Default local confidenceThresholds?: { store?: number; // Min confidence to store, default 0.5 apply?: number; // Min confidence to apply, default 0.8 prune?: number; // Below this = archive, default 0.2 }; }; // ─── Per-Agent Overrides ─────────────────────────────── agents?: { planner?: AgentOverride; implementer?: AgentOverride; reviewer?: AgentOverride; tester?: AgentOverride; deployer?: AgentOverride; }; // ─── Per-Phase Overrides ─────────────────────────────── phases?: { planning?: PhaseOverride; implementation?: PhaseOverride; review?: PhaseOverride; testing?: PhaseOverride; deployment?: PhaseOverride; }; }; export type AgentOverride = { model?: string; // Override LLM model for this agent fastModel?: string; temperature?: number; maxTokens?: number; maxIterations?: number; systemPrompt?: string; // Additional context appended to base prompt tools?: string[]; // Restrict available tools }; export type PhaseOverride = { skip?: boolean; // Skip this phase entirely requireHumanApproval?: boolean; minRiskForHumanGate?: 'low' | 'medium' | 'high' | 'critical'; maxBounces?: number; // Max review → fix → review cycles timeout?: number; // Override default timeout };

4. Config Schema (Zod)

4.1 Runtime Validation Schema

typescript
// ─── src/core/schema.ts ─────────────────────────────────── import { z } from 'zod'; // ─── Helper Schemas ──────────────────────────────────────── const EnvVarRefSchema = z.string().regex( /^process\.env\.[A-Z_][A-Z0-9_]*$/, 'Must be process.env.VARIABLE_NAME' ); const PositiveNumberSchema = z.number().positive('Must be positive'); const PercentSchema = z.number().min(0).max(1, 'Must be between 0 and 1'); const MillisecondsSchema = z.number().positive('Must be positive milliseconds'); // ─── Main Schema ─────────────────────────────────────────── export const ForgeConfigSchema = z.object({ // Project name: z.string().min(1, 'Project name is required'), language: z.enum(['typescript', 'javascript', 'python', 'go', 'rust']), root: z.string().optional(), // LLM llm: z.object({ provider: z.enum(['anthropic', 'openai', 'ollama', 'custom']), model: z.string().min(1, 'Model name is required'), fastModel: z.string().optional(), apiKey: z.string().optional(), // Validated at runtime, not here baseUrl: z.string().url().optional(), temperature: PercentSchema.optional(), maxTokens: PositiveNumberSchema.optional(), timeout: MillisecondsSchema.optional(), }), // Tools tools: z.object({ testCommand: z.string().optional(), lintCommand: z.string().optional(), buildCommand: z.string().optional(), typecheckCommand: z.string().optional(), custom: z.record(z.string()).optional(), }), // Safety safety: z.object({ costPerRun: PositiveNumberSchema.optional(), costPerDay: PositiveNumberSchema.optional(), automationLevel: z.union([ z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4), ]).optional(), maxIterations: z.object({ planning: PositiveNumberSchema.optional(), implementation: PositiveNumberSchema.optional(), review: PositiveNumberSchema.optional(), testing: PositiveNumberSchema.optional(), deployment: PositiveNumberSchema.optional(), }).optional(), timeouts: z.object({ planning: MillisecondsSchema.optional(), implementation: MillisecondsSchema.optional(), review: MillisecondsSchema.optional(), testing: MillisecondsSchema.optional(), deployment: MillisecondsSchema.optional(), totalPipeline: MillisecondsSchema.optional(), }).optional(), stagnationThreshold: PositiveNumberSchema.optional(), errorRateThresholds: z.object({ warning: PercentSchema.optional(), critical: PercentSchema.optional(), catastrophic: PercentSchema.optional(), }).optional(), }), // GitHub github: z.object({ owner: z.string().min(1), repo: z.string().min(1), reviewOnPR: z.boolean().optional(), postComments: z.boolean().optional(), token: z.string().optional(), webhookSecret: z.string().optional(), }).optional(), // Memory memory: z.object({ dbPath: z.string().optional(), consolidateInterval: MillisecondsSchema.optional(), maxMemories: PositiveNumberSchema.optional(), embeddingModel: z.enum(['local', 'openai', 'voyage']).optional(), confidenceThresholds: z.object({ store: PercentSchema.optional(), apply: PercentSchema.optional(), prune: PercentSchema.optional(), }).optional(), }).optional(), // Per-agent overrides agents: z.object({ planner: AgentOverrideSchema.optional(), implementer: AgentOverrideSchema.optional(), reviewer: AgentOverrideSchema.optional(), tester: AgentOverrideSchema.optional(), deployer: AgentOverrideSchema.optional(), }).optional(), // Per-phase overrides phases: z.object({ planning: PhaseOverrideSchema.optional(), implementation: PhaseOverrideSchema.optional(), review: PhaseOverrideSchema.optional(), testing: PhaseOverrideSchema.optional(), deployment: PhaseOverrideSchema.optional(), }).optional(), }); const AgentOverrideSchema = z.object({ model: z.string().optional(), fastModel: z.string().optional(), temperature: PercentSchema.optional(), maxTokens: PositiveNumberSchema.optional(), maxIterations: PositiveNumberSchema.optional(), systemPrompt: z.string().optional(), tools: z.array(z.string()).optional(), }); const PhaseOverrideSchema = z.object({ skip: z.boolean().optional(), requireHumanApproval: z.boolean().optional(), minRiskForHumanGate: z.enum(['low', 'medium', 'high', 'critical']).optional(), maxBounces: PositiveNumberSchema.optional(), timeout: MillisecondsSchema.optional(), }); export type ForgeConfig = z.infer<typeof ForgeConfigSchema>;

4.2 Custom Validation Rules

Beyond Zod schema validation, additional runtime checks:

typescript
// ─── src/core/validation.ts ─────────────────────────────── export class ConfigValidator { async validate(config: ForgeConfig): Promise<ValidationResult> { const errors: string[] = []; const warnings: string[] = []; // ─── Check required secrets are present ──────────────── if (config.llm.provider === 'anthropic' || config.llm.provider === 'openai') { if (!config.llm.apiKey) { errors.push( `LLM API key is required for ${config.llm.provider}. ` + `Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable.` ); } } if (config.github && !config.github.token) { errors.push( 'GitHub token is required for GitHub integration. ' + 'Set GITHUB_TOKEN environment variable.' ); } // ─── Check model names are valid ─────────────────────── const knownModels = await this.getKnownModels(config.llm.provider); if (!knownModels.includes(config.llm.model)) { warnings.push( `Model "${config.llm.model}" is not a known ${config.llm.provider} model. ` + `This may cause runtime errors.` ); } // ─── Check database path is writable ─────────────────── if (config.memory?.dbPath) { const dbDir = path.dirname(config.memory.dbPath); try { await fs.access(dbDir, fs.constants.W_OK); } catch { errors.push( `Database path "${config.memory.dbPath}" is not writable. ` + `Ensure directory ${dbDir} exists and has write permissions.` ); } } // ─── Check cost budgets are reasonable ───────────────── if (config.safety.costPerRun && config.safety.costPerDay) { if (config.safety.costPerRun > config.safety.costPerDay) { errors.push( `costPerRun ($${config.safety.costPerRun}) cannot exceed ` + `costPerDay ($${config.safety.costPerDay})` ); } } // ─── Check timeout hierarchy ─────────────────────────── if (config.safety.timeouts?.totalPipeline) { const phaseSum = Object.values(config.safety.timeouts) .filter(t => typeof t === 'number' && t !== config.safety.timeouts!.totalPipeline) .reduce((a, b) => a + b, 0); if (phaseSum > config.safety.timeouts.totalPipeline) { warnings.push( `Sum of phase timeouts (${phaseSum}ms) exceeds totalPipeline timeout ` + `(${config.safety.timeouts.totalPipeline}ms). Phase timeouts will be capped.` ); } } // ─── Check automation level vs safety ────────────────── if (config.safety.automationLevel >= 3 && config.safety.costPerRun > 100) { warnings.push( `High automation level (${config.safety.automationLevel}) with high cost limit ` + `($${config.safety.costPerRun}) may result in expensive autonomous actions.` ); } return { valid: errors.length === 0, errors, warnings, }; } private async getKnownModels(provider: string): Promise<string[]> { // In real implementation, fetch from provider or use static list const models: Record<string, string[]> = { anthropic: [ 'claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001', ], openai: [ 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', ], ollama: [], // Any model name valid }; return models[provider] || []; } }

5. Config Loading

5.1 Discovery Algorithm

typescript
// ─── src/core/loader.ts ─────────────────────────────────── import { findUp } from 'find-up'; import { pathToFileURL } from 'node:url'; export class ConfigLoader { private static CONFIG_FILENAMES = [ 'forge.config.ts', 'forge.config.js', 'forge.config.mjs', ]; /** * Discover config file by walking up from cwd to git root. */ async discover(): Promise<string | null> { // Start from current directory let searchDir = process.cwd(); // Find git root (if exists) const gitRoot = await this.findGitRoot(searchDir); // Search for config file const configPath = await findUp( ConfigLoader.CONFIG_FILENAMES, { cwd: searchDir, stopAt: gitRoot || '/' } ); return configPath || null; } /** * Load and parse config file. */ async load(configPath: string): Promise<ForgeConfig> { try { // Dynamic import to support TypeScript via bun const configModule = await import(pathToFileURL(configPath).href); const config = configModule.default || configModule; if (!config || typeof config !== 'object') { throw new Error( `Config file at ${configPath} must export a default object` ); } return config as ForgeConfig; } catch (error) { throw new Error( `Failed to load config from ${configPath}: ${error.message}` ); } } private async findGitRoot(cwd: string): Promise<string | null> { const gitDir = await findUp('.git', { cwd, type: 'directory' }); return gitDir ? path.dirname(gitDir) : null; } }

5.2 Environment Variable Overrides

typescript
// ─── src/core/env-overrides.ts ──────────────────────────── export class EnvOverrides { /** * Apply environment variable overrides to config. * Env vars take precedence over config file values. */ apply(config: ForgeConfig): ForgeConfig { const overrides: Partial<ForgeConfig> = {}; // LLM overrides if (process.env.FORGE_LLM_PROVIDER) { overrides.llm = { ...config.llm, provider: process.env.FORGE_LLM_PROVIDER as any, }; } if (process.env.FORGE_MODEL) { overrides.llm = { ...config.llm, ...overrides.llm, model: process.env.FORGE_MODEL, }; } if (process.env.FORGE_FAST_MODEL) { overrides.llm = { ...config.llm, ...overrides.llm, fastModel: process.env.FORGE_FAST_MODEL, }; } // API keys (always from env) if (process.env.ANTHROPIC_API_KEY) { overrides.llm = { ...config.llm, ...overrides.llm, apiKey: process.env.ANTHROPIC_API_KEY, }; } if (process.env.OPENAI_API_KEY) { overrides.llm = { ...config.llm, ...overrides.llm, apiKey: process.env.OPENAI_API_KEY, }; } // Safety overrides if (process.env.FORGE_COST_PER_RUN) { overrides.safety = { ...config.safety, costPerRun: parseFloat(process.env.FORGE_COST_PER_RUN), }; } if (process.env.FORGE_COST_PER_DAY) { overrides.safety = { ...config.safety, ...overrides.safety, costPerDay: parseFloat(process.env.FORGE_COST_PER_DAY), }; } if (process.env.FORGE_AUTOMATION_LEVEL) { overrides.safety = { ...config.safety, ...overrides.safety, automationLevel: parseInt(process.env.FORGE_AUTOMATION_LEVEL) as any, }; } // GitHub overrides if (process.env.GITHUB_TOKEN) { overrides.github = { ...config.github!, token: process.env.GITHUB_TOKEN, }; } // Memory overrides if (process.env.FORGE_DB_PATH) { overrides.memory = { ...config.memory, dbPath: process.env.FORGE_DB_PATH, }; } return { ...config, ...overrides }; } /** * Supported environment variables. */ static readonly SUPPORTED_ENV_VARS = [ 'FORGE_LLM_PROVIDER', 'FORGE_MODEL', 'FORGE_FAST_MODEL', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'FORGE_COST_PER_RUN', 'FORGE_COST_PER_DAY', 'FORGE_AUTOMATION_LEVEL', 'GITHUB_TOKEN', 'FORGE_DB_PATH', ] as const; }

5.3 CLI Flag Overrides

typescript
// ─── src/cli/config-overrides.ts ────────────────────────── export interface CLIFlags { model?: string; fastModel?: string; costLimit?: number; automationLevel?: number; skipPhase?: string[]; // ['review', 'deployment'] requireApproval?: boolean; } export class CLIOverrides { /** * Apply CLI flags to config. * CLI flags take precedence over env vars and config file. */ apply(config: ForgeConfig, flags: CLIFlags): ForgeConfig { const overrides: Partial<ForgeConfig> = {}; // Model overrides if (flags.model) { overrides.llm = { ...config.llm, model: flags.model, }; } if (flags.fastModel) { overrides.llm = { ...config.llm, ...overrides.llm, fastModel: flags.fastModel, }; } // Cost overrides if (flags.costLimit !== undefined) { overrides.safety = { ...config.safety, costPerRun: flags.costLimit, }; } // Automation level override if (flags.automationLevel !== undefined) { overrides.safety = { ...config.safety, ...overrides.safety, automationLevel: flags.automationLevel as any, }; } // Phase skipping if (flags.skipPhase && flags.skipPhase.length > 0) { overrides.phases = { ...config.phases }; for (const phase of flags.skipPhase) { overrides.phases[phase] = { ...config.phases?.[phase], skip: true, }; } } // Human approval override if (flags.requireApproval !== undefined) { overrides.phases = { ...config.phases }; const allPhases = ['planning', 'implementation', 'review', 'testing', 'deployment']; for (const phase of allPhases) { overrides.phases[phase] = { ...config.phases?.[phase], requireHumanApproval: flags.requireApproval, }; } } return { ...config, ...overrides }; } }

5.4 Complete Resolution Pipeline

typescript
// ─── src/core/config-resolver.ts ────────────────────────── export class ConfigResolver { private loader = new ConfigLoader(); private envOverrides = new EnvOverrides(); private cliOverrides = new CLIOverrides(); private validator = new ConfigValidator(); /** * Complete config resolution pipeline: * 1. Load defaults * 2. Discover and load config file * 3. Apply env var overrides * 4. Apply CLI flag overrides * 5. Validate final config */ async resolve(flags: CLIFlags = {}): Promise<ResolvedConfig> { // 1. Start with defaults let config = this.getDefaults(); // 2. Load config file if exists const configPath = await this.loader.discover(); if (configPath) { const fileConfig = await this.loader.load(configPath); config = this.merge(config, fileConfig); } // 3. Apply env overrides config = this.envOverrides.apply(config); // 4. Apply CLI overrides config = this.cliOverrides.apply(config, flags); // 5. Validate const validation = await this.validator.validate(config); if (!validation.valid) { throw new ConfigValidationError(validation.errors); } // Log warnings but don't fail if (validation.warnings.length > 0) { console.warn('Config warnings:'); for (const warning of validation.warnings) { console.warn(`${warning}`); } } return { config, source: configPath || 'defaults', overrides: { env: this.getAppliedEnvVars(), cli: flags, }, validation, }; } /** * Deep merge strategy for nested objects. * Arrays are replaced, not merged. */ private merge(base: ForgeConfig, override: Partial<ForgeConfig>): ForgeConfig { const merged = { ...base }; for (const key in override) { const value = override[key]; if (value === undefined) continue; if (typeof value === 'object' && !Array.isArray(value) && value !== null) { // Deep merge objects merged[key] = { ...(merged[key] || {}), ...value, }; } else { // Replace primitives and arrays merged[key] = value; } } return merged; } private getAppliedEnvVars(): Record<string, string> { const applied: Record<string, string> = {}; for (const envVar of EnvOverrides.SUPPORTED_ENV_VARS) { if (process.env[envVar]) { applied[envVar] = process.env[envVar]!; } } return applied; } private getDefaults(): ForgeConfig { return DEFAULT_CONFIG; // Defined below } } export interface ResolvedConfig { config: ForgeConfig; source: string; overrides: { env: Record<string, string>; cli: CLIFlags; }; validation: ValidationResult; }

6. Sensible Defaults

typescript
// ─── src/core/defaults.ts ───────────────────────────────── export const DEFAULT_CONFIG: ForgeConfig = { // Project name: 'unknown-project', language: 'typescript', // LLM llm: { provider: 'anthropic', model: 'claude-sonnet-4-5-20250929', fastModel: 'claude-haiku-4-5-20251001', temperature: 0.7, maxTokens: 4096, timeout: 60_000, // 1 minute }, // Tools (auto-detected at runtime) tools: {}, // Safety safety: { costPerRun: 50, // USD costPerDay: 200, automationLevel: 1, // Conservative default maxIterations: { planning: 20, implementation: 50, review: 10, testing: 5, deployment: 3, }, timeouts: { planning: 30 * 60_000, // 30 min implementation: 60 * 60_000, // 1 hour review: 30 * 60_000, testing: 20 * 60_000, deployment: 15 * 60_000, totalPipeline: 120 * 60_000, // 2 hours }, stagnationThreshold: 3, errorRateThresholds: { warning: 0.10, critical: 0.25, catastrophic: 0.50, }, }, // Memory memory: { dbPath: '.forge/memory.db', consolidateInterval: 86400000, // 1 day maxMemories: 10_000, embeddingModel: 'local', confidenceThresholds: { store: 0.5, apply: 0.8, prune: 0.2, }, }, // GitHub (optional, no defaults) github: undefined, // Agent overrides (none by default) agents: {}, // Phase overrides phases: { deployment: { requireHumanApproval: true, // Always for safety }, }, };

7. Secrets Management

7.1 Never Store Secrets in Config Files

typescript
// ❌ NEVER DO THIS export default defineConfig({ llm: { apiKey: 'sk-ant-1234567890', // NEVER HARDCODE }, }); // ✅ ALWAYS DO THIS export default defineConfig({ llm: { apiKey: process.env.ANTHROPIC_API_KEY, }, });

7.2 .env File Support

typescript
// ─── src/core/env-loader.ts ─────────────────────────────── import { config } from 'dotenv'; import { expand } from 'dotenv-expand'; /** * Load .env file if exists. * Looks for .env, .env.local, .env.[NODE_ENV] */ export function loadEnvFile(): void { const env = process.env.NODE_ENV || 'development'; const files = [ `.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ]; for (const file of files) { const result = config({ path: file }); if (result.parsed) { expand(result); // Support ${VAR} expansion console.log(`Loaded environment from ${file}`); break; } } }

7.3 Secrets Validation

typescript
// ─── src/core/secrets-validator.ts ──────────────────────── export class SecretsValidator { /** * Validate that required secrets are present. */ validate(config: ForgeConfig): string[] { const missing: string[] = []; // LLM API key if (config.llm.provider === 'anthropic' && !config.llm.apiKey) { missing.push('ANTHROPIC_API_KEY'); } if (config.llm.provider === 'openai' && !config.llm.apiKey) { missing.push('OPENAI_API_KEY'); } // GitHub token (if integration enabled) if (config.github && !config.github.token) { missing.push('GITHUB_TOKEN'); } return missing; } /** * Check if a value looks like a hardcoded secret (security risk). */ detectHardcodedSecrets(config: ForgeConfig): string[] { const suspicious: string[] = []; // Pattern: starts with sk-, looks like API key const apiKeyPattern = /^sk-[a-zA-Z0-9]{20,}$/; if (config.llm.apiKey && apiKeyPattern.test(config.llm.apiKey)) { suspicious.push('llm.apiKey appears to be hardcoded (starts with sk-)'); } if (config.github?.token && /^gh[ps]_[a-zA-Z0-9]{36,}$/.test(config.github.token)) { suspicious.push('github.token appears to be hardcoded'); } return suspicious; } }

8. Config Hot Reload (Deferred for MVP)

Decision for MVP: No hot reload support. Configuration changes require restarting the Forge process.

Rationale:

  1. Simpler implementation
  2. Clearer behavior (no mid-run config changes)
  3. Safer (avoid race conditions during config updates)
  4. Config is captured in run metadata for reproducibility

Post-MVP Consideration:

  • Watch forge.config.ts for changes
  • Reload config between pipeline runs (not during)
  • Emit warning if config changes during active run
typescript
// ─── Future: Hot reload implementation ──────────────────── export class ConfigWatcher { private watcher?: FSWatcher; watch(configPath: string, onReload: (config: ForgeConfig) => void): void { this.watcher = fs.watch(configPath, async (event) => { if (event === 'change') { const newConfig = await this.loader.load(configPath); const validation = await this.validator.validate(newConfig); if (validation.valid) { console.log('Config reloaded successfully'); onReload(newConfig); } else { console.error('Config validation failed, keeping old config'); } } }); } stop(): void { this.watcher?.close(); } }

9. Config Documentation Generation

9.1 Auto-Generated Reference from Schema

typescript
// ─── src/cli/generate-config-docs.ts ────────────────────── import { zodToJsonSchema } from 'zod-to-json-schema'; export function generateConfigDocs(): string { const jsonSchema = zodToJsonSchema(ForgeConfigSchema, 'ForgeConfig'); let markdown = '# Forge Configuration Reference\n\n'; markdown += '> Auto-generated from Zod schema\n\n'; markdown += '## Full Configuration\n\n'; markdown += '```typescript\n'; markdown += JSON.stringify(jsonSchema, null, 2); markdown += '\n```\n\n'; // Add human-readable descriptions markdown += '## Configuration Sections\n\n'; markdown += '### llm\n'; markdown += 'Configure the LLM provider and model settings.\n\n'; markdown += '**Required fields:** `provider`, `model`\n\n'; // ... continue for each section return markdown; }

9.2 Example Configs

typescript
// ─── examples/minimal.config.ts ─────────────────────────── import { defineConfig } from '@forge/core'; export default defineConfig({ name: 'my-app', language: 'typescript', llm: { provider: 'anthropic', model: 'claude-sonnet-4-5-20250929', }, });
typescript
// ─── examples/full.config.ts ────────────────────────────── import { defineConfig } from '@forge/core'; export default defineConfig({ name: 'production-app', language: 'typescript', llm: { provider: 'anthropic', model: 'claude-opus-4-6', // Strongest model fastModel: 'claude-haiku-4-5-20251001', temperature: 0.5, // More deterministic maxTokens: 8192, }, tools: { testCommand: 'bun test --coverage', lintCommand: 'bun run lint --fix', buildCommand: 'bun run build --production', typecheckCommand: 'tsc --noEmit', }, safety: { costPerRun: 100, costPerDay: 500, automationLevel: 2, // More autonomous maxIterations: { planning: 30, implementation: 100, }, }, github: { owner: 'myorg', repo: 'production-app', reviewOnPR: true, postComments: true, }, memory: { dbPath: '/var/forge/memory.db', consolidateInterval: 12 * 60 * 60 * 1000, // 12 hours maxMemories: 50_000, }, agents: { planner: { model: 'claude-opus-4-6', // Best model for planning }, reviewer: { model: 'claude-haiku-4-5-20251001', // Fast review maxIterations: 3, }, }, phases: { review: { minRiskForHumanGate: 'high', maxBounces: 5, }, deployment: { requireHumanApproval: true, }, }, });
typescript
// ─── examples/ci.config.ts ──────────────────────────────── import { defineConfig } from '@forge/core'; export default defineConfig({ name: 'ci-runner', language: 'typescript', llm: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001', // Fastest, cheapest }, safety: { costPerRun: 10, // Strict limit for CI automationLevel: 0, // No automation, analysis only timeouts: { totalPipeline: 10 * 60_000, // 10 min max }, }, memory: { dbPath: ':memory:', // In-memory for CI }, phases: { planning: { skip: true }, // Skip planning in CI deployment: { skip: true }, // No deployment from CI }, });

10. Per-Environment Config

10.1 Config Extension Pattern

typescript
// ─── forge.config.ts (base) ─────────────────────────────── import { defineConfig } from '@forge/core'; const baseConfig = defineConfig({ name: 'my-app', language: 'typescript', llm: { provider: 'anthropic', model: 'claude-sonnet-4-5-20250929', }, }); export default baseConfig;
typescript
// ─── forge.config.production.ts ─────────────────────────── import { defineConfig } from '@forge/core'; import baseConfig from './forge.config'; export default defineConfig({ ...baseConfig, safety: { ...baseConfig.safety, automationLevel: 3, // Higher automation in prod costPerRun: 200, }, phases: { deployment: { requireHumanApproval: true, // Always for production }, }, });
typescript
// ─── forge.config.ci.ts ─────────────────────────────────── import { defineConfig } from '@forge/core'; import baseConfig from './forge.config'; export default defineConfig({ ...baseConfig, llm: { ...baseConfig.llm, model: 'claude-haiku-4-5-20251001', // Faster for CI }, safety: { ...baseConfig.safety, costPerRun: 10, timeouts: { totalPipeline: 10 * 60_000, // 10 min }, }, memory: { dbPath: ':memory:', }, phases: { planning: { skip: true }, deployment: { skip: true }, }, });

10.2 Environment-Based Config Selection

typescript
// ─── src/core/env-config-loader.ts ──────────────────────── export class EnvConfigLoader { /** * Load config based on NODE_ENV or FORGE_ENV. * Looks for forge.config.[env].ts first, falls back to forge.config.ts */ async load(): Promise<string | null> { const env = process.env.FORGE_ENV || process.env.NODE_ENV || 'development'; const candidates = [ `forge.config.${env}.ts`, `forge.config.${env}.js`, 'forge.config.ts', 'forge.config.js', ]; for (const filename of candidates) { const configPath = await findUp(filename); if (configPath) { console.log(`Using config: ${filename} (env=${env})`); return configPath; } } return null; } }

11. Implementation Checklist

Phase 1: Core Config System (Week 1)

  • Define ForgeConfig TypeScript type
  • Implement Zod validation schema
  • Create defineConfig() helper
  • Implement config file discovery (walk up to git root)
  • Implement config loader (TypeScript file import)
  • Define default configuration
  • Write unit tests for config loading

Phase 2: Override System (Week 1)

  • Implement environment variable overrides
  • Implement CLI flag overrides
  • Implement deep merge strategy
  • Create config resolution pipeline
  • Test override priority (CLI > env > file > defaults)

Phase 3: Validation & Secrets (Week 2)

  • Implement custom validation rules
  • Add secrets detection (hardcoded API keys)
  • Implement required secrets check
  • Add .env file loading support
  • Test validation error messages

Phase 4: Documentation (Week 2)

  • Generate config reference from Zod schema
  • Create example configs (minimal, full, CI)
  • Write configuration guide
  • Document all environment variables
  • Document CLI flags

Phase 5: Testing & Polish (Week 3)

  • Unit tests for all config sections
  • Integration tests for resolution pipeline
  • Test error cases (invalid config, missing secrets)
  • Performance testing (config load time)
  • Edge case testing (malformed files, circular refs)

12. Testing Strategy

12.1 Unit Tests

typescript
// ─── src/core/__tests__/config.test.ts ──────────────────── describe('ConfigLoader', () => { it('should discover config in current directory', async () => { const loader = new ConfigLoader(); const path = await loader.discover(); expect(path).toContain('forge.config.ts'); }); it('should walk up to git root', async () => { // Test from nested directory process.chdir('./src/nested'); const loader = new ConfigLoader(); const path = await loader.discover(); expect(path).toContain('forge.config.ts'); }); it('should load TypeScript config file', async () => { const loader = new ConfigLoader(); const config = await loader.load('./fixtures/valid.config.ts'); expect(config.name).toBe('test-project'); }); it('should throw on invalid config file', async () => { const loader = new ConfigLoader(); await expect(loader.load('./fixtures/invalid.config.ts')) .rejects.toThrow('must export a default object'); }); }); describe('ConfigResolver', () => { it('should apply overrides in correct priority', async () => { process.env.FORGE_MODEL = 'env-model'; const flags = { model: 'cli-model' }; const resolver = new ConfigResolver(); const { config } = await resolver.resolve(flags); expect(config.llm.model).toBe('cli-model'); // CLI wins }); it('should deep merge nested objects', async () => { const base = { safety: { costPerRun: 50, costPerDay: 200 }, }; const override = { safety: { costPerRun: 100 }, }; const resolver = new ConfigResolver(); const merged = resolver['merge'](base, override); expect(merged.safety.costPerRun).toBe(100); expect(merged.safety.costPerDay).toBe(200); // Preserved }); }); describe('ConfigValidator', () => { it('should require API key for cloud providers', async () => { const config = { llm: { provider: 'anthropic', model: 'test' }, }; const validator = new ConfigValidator(); const result = await validator.validate(config); expect(result.valid).toBe(false); expect(result.errors).toContain(/API key is required/); }); it('should detect hardcoded secrets', async () => { const config = { llm: { apiKey: 'sk-ant-1234567890' }, }; const validator = new SecretsValidator(); const suspicious = validator.detectHardcodedSecrets(config); expect(suspicious).toHaveLength(1); expect(suspicious[0]).toContain('hardcoded'); }); });

12.2 Integration Tests

typescript
// ─── src/core/__tests__/integration.test.ts ─────────────── describe('Config Integration', () => { it('should load, override, and validate complete config', async () => { // Setup process.env.ANTHROPIC_API_KEY = 'test-key'; process.env.FORGE_COST_PER_RUN = '75'; // Execute const resolver = new ConfigResolver(); const { config, validation } = await resolver.resolve({ model: 'claude-opus-4-6', }); // Assert expect(validation.valid).toBe(true); expect(config.llm.model).toBe('claude-opus-4-6'); // CLI override expect(config.safety.costPerRun).toBe(75); // Env override expect(config.llm.apiKey).toBe('test-key'); // Env secret }); });

13. Error Messages

Good error messages are critical for developer experience.

typescript
// ─── Examples of good error messages ────────────────────── // Missing config file throw new Error( `No forge.config.ts found in current directory or any parent directory up to git root.\n` + `Run "forge init" to create one, or create it manually:\n\n` + ` import { defineConfig } from '@forge/core';\n` + ` export default defineConfig({ name: 'my-app', language: 'typescript' });\n` ); // Missing API key throw new Error( `Anthropic API key is required.\n` + `Set the ANTHROPIC_API_KEY environment variable:\n\n` + ` export ANTHROPIC_API_KEY=sk-ant-...\n\n` + `Or add to .env file:\n` + ` ANTHROPIC_API_KEY=sk-ant-...\n\n` + `Get your API key at: https://console.anthropic.com/\n` ); // Invalid model throw new Error( `Model "gpt-5" is not a known OpenAI model.\n` + `Valid models: ${knownModels.join(', ')}\n\n` + `If this is a custom/new model, you can ignore this warning.\n` ); // Cost budget exceeded throw new Error( `costPerRun ($100) cannot exceed costPerDay ($50).\n` + `Either increase costPerDay or decrease costPerRun in your config.\n` );

14. Config Snapshot for Reproducibility

Every pipeline run should save the exact config used.

typescript
// ─── src/orchestrator/run-metadata.ts ───────────────────── export async function saveRunMetadata( runId: string, config: ForgeConfig, overrides: ConfigOverrides ): Promise<void> { const snapshot = { runId, timestamp: new Date().toISOString(), config: JSON.stringify(config, null, 2), overrides: { env: overrides.env, cli: overrides.cli, }, system: { nodeVersion: process.version, platform: process.platform, arch: process.arch, }, }; await db.insert(runMetadata).values(snapshot); } /** * Reproduce a previous run with exact same config. */ export async function reproduceRun(runId: string): Promise<ForgeConfig> { const metadata = await db .select() .from(runMetadata) .where(eq(runMetadata.runId, runId)) .get(); if (!metadata) { throw new Error(`Run ${runId} not found`); } return JSON.parse(metadata.config); }

15. Summary

What We Built

  1. Type-safe config — Full TypeScript types with autocomplete
  2. Flexible overrides — CLI > env > file > defaults
  3. Secure secrets — Never hardcoded, always from environment
  4. Runtime validation — Zod schema + custom checks
  5. Good defaults — Works with minimal configuration
  6. Extensible — Per-agent and per-phase overrides
  7. Reproducible — Config snapshots saved with runs

Key Files

  • src/core/types.ts — ForgeConfig type definition
  • src/core/schema.ts — Zod validation schema
  • src/core/loader.ts — Config file discovery and loading
  • src/core/env-overrides.ts — Environment variable overrides
  • src/cli/config-overrides.ts — CLI flag overrides
  • src/core/config-resolver.ts — Complete resolution pipeline
  • src/core/defaults.ts — Default configuration
  • src/core/validation.ts — Custom validation rules

Implementation Order

  1. Week 1: Core type, schema, loader, defaults
  2. Week 2: Override system (env + CLI), validation
  3. Week 3: Testing, documentation, examples

Post-MVP Enhancements

  • Config hot reload (watch for changes)
  • Config migration tool (for breaking changes)
  • Config templates for common setups
  • Interactive config builder CLI (forge init --interactive)

End of Implementation Sub-Plan: Configuration

This plan provides everything needed to implement a production-ready configuration system for Forge. All code examples are complete and ready to be adapted for the actual implementation.