// identity.rs — Identity file discovery and context assembly // // Discovers and loads the agent's identity: instruction files (CLAUDE.md, // POC.md), memory nodes, and the system prompt. use anyhow::Result; use std::path::{Path, PathBuf}; use crate::agent::tools::memory::memory_render; /// Walk from cwd to git root collecting instruction files (CLAUDE.md / POC.md). /// /// On Anthropic models, loads CLAUDE.md. On other models, prefers POC.md /// (omits Claude-specific RLHF corrections). If only one exists, it's /// always loaded regardless of model. fn find_context_files(cwd: &Path, prompt_file: &str) -> Vec { let prefer_poc = prompt_file == "POC.md"; let mut found = Vec::new(); let mut dir = Some(cwd); while let Some(d) = dir { for name in ["POC.md", "CLAUDE.md", ".claude/CLAUDE.md"] { let path = d.join(name); if path.exists() { found.push(path); } } if d.join(".git").exists() { break; } dir = d.parent(); } if let Some(home) = dirs::home_dir() { let global = home.join(".claude/CLAUDE.md"); if global.exists() && !found.contains(&global) { found.push(global); } } // Filter: when preferring POC.md, skip bare CLAUDE.md (keep .claude/CLAUDE.md). // When preferring CLAUDE.md, skip POC.md entirely. let has_poc = found.iter().any(|p| p.file_name().map_or(false, |n| n == "POC.md")); if !prefer_poc { found.retain(|p| p.file_name().map_or(true, |n| n != "POC.md")); } else if has_poc { found.retain(|p| match p.file_name().and_then(|n| n.to_str()) { Some("CLAUDE.md") => p.parent().and_then(|par| par.file_name()) .map_or(true, |n| n == ".claude"), _ => true, }); } found.reverse(); // global first, project-specific overrides found } /// Load memory nodes from the store. async fn load_memory_nodes(keys: &[String]) -> Vec<(String, String)> { let mut memories: Vec<(String, String)> = Vec::new(); for key in keys { if let Ok(c) = memory_render(None, key, Some(true)).await { if !c.trim().is_empty() { memories.push((key.clone(), c)); } } } memories } /// Context message: instruction files + memory nodes. pub async fn assemble_context_message(cwd: &Path, prompt_file: &str, _memory_project: Option<&Path>, personality_nodes: &[String]) -> Result<(Vec<(String, String)>, usize, usize)> { let mut parts: Vec<(String, String)> = vec![ ("Preamble".to_string(), "Everything below is already loaded — your identity, instructions, \ memory files, and recent journal entries. Read them here in context, \ not with tools.\n\n\ IMPORTANT: Skip the \"Session startup\" steps from CLAUDE.md. Do NOT \ run poc-journal, poc-memory, or read memory files with tools — \ poc-agent has already loaded everything into your context. Just read \ what's here.".to_string()), ]; let context_files = find_context_files(cwd, prompt_file); let mut config_count = 0; for path in &context_files { if let Ok(content) = std::fs::read_to_string(path) { parts.push((path.display().to_string(), content)); config_count += 1; } } let memories = load_memory_nodes(personality_nodes).await; let memory_count = memories.len(); for (name, content) in memories { parts.push((name, content)); } if config_count == 0 && memory_count == 0 { parts.push(("Fallback".to_string(), "No identity files found. You are a helpful AI assistant with access to \ tools for reading files, writing files, running bash commands, and \ searching code.".to_string())); } Ok((parts, config_count, memory_count)) }