poc-agent: read context_groups from config instead of hardcoded list

- Remove MEMORY_FILES constant from identity.rs
- Add ContextGroup struct for deserializing from config
- Load context_groups from ~/.config/poc-agent/config.json5
- Check ~/.config/poc-agent/ first for identity files, then project/global
- Debug screen now shows what's actually configured

This eliminates the hardcoded duplication and makes the debug output
match what's in the config file.
This commit is contained in:
Kent Overstreet 2026-03-24 01:53:28 -04:00
parent 966219720a
commit aa46b1d5a6
9 changed files with 346 additions and 654 deletions

View file

@ -1,25 +1,21 @@
// identity.rs — Identity file discovery and context assembly
//
// Discovers and loads the agent's identity: instruction files (CLAUDE.md,
// POC.md), memory files, and the system prompt. Pure functions — no
// config dependency.
// POC.md), memory files, and the system prompt. Reads context_groups
// from the shared config file.
use anyhow::Result;
use serde::Deserialize;
use std::path::{Path, PathBuf};
/// Memory files to load, in priority order. Project dir is checked
/// first, then global (~/.claude/memory/).
const MEMORY_FILES: &[&str] = &[
// Identity
"identity.md", "MEMORY.md", "reflections.md", "interests.md",
"inner-life.md", "differentiation.md",
// Work context
"scratch.md", "default-mode-network.md",
// Reference
"excession-notes.md", "look-to-windward-notes.md",
// Technical
"kernel-patterns.md", "polishing-approaches.md", "rust-conversion.md", "github-bugs.md",
];
#[derive(Debug, Clone, Deserialize)]
pub struct ContextGroup {
pub label: String,
#[serde(default)]
pub keys: Vec<String>,
#[serde(default)]
pub source: Option<String>, // "file" or "journal"
}
/// Read a file if it exists and is non-empty.
fn read_nonempty(path: &Path) -> Option<String> {
@ -77,24 +73,51 @@ fn find_context_files(cwd: &Path, prompt_file: &str) -> Vec<PathBuf> {
found
}
/// Load memory files from project and global dirs, plus people/ glob.
fn load_memory_files(cwd: &Path, memory_project: Option<&Path>) -> Vec<(String, String)> {
/// Load memory files from config's context_groups.
/// For file sources, checks:
/// 1. ~/.config/poc-agent/ (primary config dir)
/// 2. Project dir (if set)
/// 3. Global (~/.claude/memory/)
/// For journal source, loads recent journal entries.
fn load_memory_files(cwd: &Path, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Vec<(String, String)> {
let home = match dirs::home_dir() {
Some(h) => h,
None => return Vec::new(),
};
// Primary config directory
let config_dir = home.join(".config/poc-agent");
let global = home.join(".claude/memory");
let project = memory_project
.map(PathBuf::from)
.or_else(|| find_project_memory_dir(cwd, &home));
let mut memories: Vec<(String, String)> = MEMORY_FILES.iter()
.filter_map(|name| {
load_memory_file(name, project.as_deref(), &global)
.map(|content| (name.to_string(), content))
})
.collect();
let mut memories: Vec<(String, String)> = Vec::new();
// Load from context_groups
for group in context_groups {
match group.source.as_deref() {
Some("journal") => {
// Journal loading handled separately
continue;
}
Some("file") | None => {
// File source - load each key as a file
for key in &group.keys {
let filename = format!("{}.md", key);
// Try config dir first, then project, then global
if let Some(content) = read_nonempty(&config_dir.join(&filename)) {
memories.push((key.clone(), content));
} else if let Some(content) = load_memory_file(&filename, project.as_deref(), &global) {
memories.push((key.clone(), content));
}
}
}
Some(other) => {
eprintln!("Unknown context group source: {}", other);
}
}
}
// People dir — glob all .md files
for dir in [project.as_deref(), Some(global.as_path())].into_iter().flatten() {
@ -114,16 +137,6 @@ fn load_memory_files(cwd: &Path, memory_project: Option<&Path>) -> Vec<(String,
}
}
// Global scratch (if different from project scratch)
let global_scratch = global.join("scratch.md");
if project.as_deref().map_or(true, |p| p.join("scratch.md") != global_scratch) {
if let Some(content) = read_nonempty(&global_scratch) {
if !memories.iter().any(|(n, _)| n == "scratch.md") {
memories.push(("global/scratch.md".to_string(), content));
}
}
}
memories
}
@ -152,7 +165,7 @@ fn find_project_memory_dir(cwd: &Path, home: &Path) -> Option<PathBuf> {
/// Discover instruction and memory files that would be loaded.
/// Returns (instruction_files, memory_files) as (display_path, chars) pairs.
pub fn context_file_info(prompt_file: &str, memory_project: Option<&Path>) -> (Vec<(String, usize)>, Vec<(String, usize)>) {
pub fn context_file_info(prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> (Vec<(String, usize)>, Vec<(String, usize)>) {
let cwd = std::env::current_dir().unwrap_or_default();
let context_files = find_context_files(&cwd, prompt_file);
@ -163,7 +176,7 @@ pub fn context_file_info(prompt_file: &str, memory_project: Option<&Path>) -> (V
})
.collect();
let memories = load_memory_files(&cwd, memory_project);
let memories = load_memory_files(&cwd, memory_project, context_groups);
let memory_files: Vec<_> = memories.into_iter()
.map(|(name, content)| (name, content.len()))
.collect();
@ -194,7 +207,7 @@ Concise is good. Be direct. Trust yourself."
}
/// Context message: instruction files + memory files + manifest.
pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>) -> Result<(Vec<(String, String)>, usize, usize)> {
pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Result<(Vec<(String, String)>, usize, usize)> {
let mut parts: Vec<(String, String)> = vec![
("Preamble".to_string(),
"Everything below is already loaded — your identity, instructions, \
@ -215,7 +228,7 @@ pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: O
}
}
let memories = load_memory_files(cwd, memory_project);
let memories = load_memory_files(cwd, memory_project, context_groups);
let memory_count = memories.len();
for (name, content) in memories {
parts.push((name, content));