From 62ddc39df491ff270facf673b11cffa1411e3e8b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 7 May 2026 01:17:34 +0100 Subject: [PATCH] Add repo memory distiller packet --- .../bin/epiphany-repo-personality-smoke.rs | 48 +++ .../src/bin/epiphany-repo-personality.rs | 395 +++++++++++++++++- .../src/prompts/repo_memory_distiller.md | 105 +++++ notes/fresh-workspace-handoff.md | 7 + .../repo-personality-projection-layer-plan.md | 32 +- state/agents/README.md | 8 + state/ledgers.msgpack | Bin 55857 -> 56164 bytes state/map.yaml | 3 + .../app-server/src/codex_message_processor.rs | 2 + .../src/prompts/epiphany_specialists.toml | 9 + 10 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 epiphany-core/src/prompts/repo_memory_distiller.md diff --git a/epiphany-core/src/bin/epiphany-repo-personality-smoke.rs b/epiphany-core/src/bin/epiphany-repo-personality-smoke.rs index 48aa7cf1..bf48eb40 100644 --- a/epiphany-core/src/bin/epiphany-repo-personality-smoke.rs +++ b/epiphany-core/src/bin/epiphany-repo-personality-smoke.rs @@ -32,6 +32,14 @@ fn run_smoke() -> Result { "Prefer typed schemas and tiny public contracts.\n", ), ("src/lib.rs", "pub fn schema_contract() {}\n"), + ( + "docs/architecture.md", + "# Architecture\nCultTiny stores typed contract surfaces.\n", + ), + ( + "research/prior-art.md", + "# Prior Art\nPrefer known schema formats before invention.\n", + ), ( "tests/contract_smoke.rs", "#[test] fn contract_smoke() {}\n", @@ -120,6 +128,43 @@ fn run_smoke() -> Result { "agent packet should write summary markdown", )?; + let memory_packet = run_personality( + &root, + &[ + "memory-packet", + "--store", + store.to_str().unwrap_or_default(), + "--artifact-dir", + artifacts + .join("cult-memory-packet") + .to_str() + .unwrap_or_default(), + ], + )?; + require( + memory_packet["memorySourceCount"].as_u64().unwrap_or(0) > 0, + "memory packet should carry source excerpts", + )?; + require( + memory_packet["roleDistillerCount"] == 8, + "memory packet should carry all role distiller lanes", + )?; + let memory_packet_path = path_value(&memory_packet, "packetPath")?; + let memory_prompt_path = path_value(&memory_packet, "promptPath")?; + let memory_summary_path = path_value(&memory_packet, "summaryPath")?; + require( + memory_packet_path.exists(), + "memory packet should write packet json", + )?; + require( + memory_prompt_path.exists(), + "memory packet should write prompt markdown", + )?; + require( + memory_summary_path.exists(), + "memory packet should write summary markdown", + )?; + let status = run_personality( &root, &["status", "--store", store.to_str().unwrap_or_default()], @@ -147,6 +192,9 @@ fn run_smoke() -> Result { "packetPath": packet_path, "promptPath": prompt_path, "packetSummaryPath": packet_summary_path, + "memoryPacketPath": memory_packet_path, + "memoryPromptPath": memory_prompt_path, + "memorySummaryPath": memory_summary_path, "repoCount": scout["repoCount"], "roleProjections": status["roleProjections"], })) diff --git a/epiphany-core/src/bin/epiphany-repo-personality.rs b/epiphany-core/src/bin/epiphany-repo-personality.rs index 0f982f26..26ef60f4 100644 --- a/epiphany-core/src/bin/epiphany-repo-personality.rs +++ b/epiphany-core/src/bin/epiphany-repo-personality.rs @@ -23,8 +23,13 @@ const PROFILE_SCHEMA_VERSION: &str = "epiphany.repo_personality_profile.v0"; const ROLE_PROJECTION_SCHEMA_VERSION: &str = "epiphany.role_personality_projection.v0"; const ARTIFACT_SCHEMA_VERSION: &str = "epiphany.repo_personality_artifacts.v0"; const DISTILLER_PACKET_SCHEMA_VERSION: &str = "epiphany.repo_personality_distiller_packet.v0"; +const MEMORY_DISTILLER_PACKET_SCHEMA_VERSION: &str = "epiphany.repo_memory_distiller_packet.v0"; const REPO_PERSONALITY_DISTILLER_PROMPT: &str = include_str!("../prompts/repo_personality_distiller.md"); +const REPO_MEMORY_DISTILLER_PROMPT: &str = include_str!("../prompts/repo_memory_distiller.md"); +const MEMORY_SOURCE_MAX_FILES: usize = 48; +const MEMORY_SOURCE_MAX_BYTES_PER_FILE: usize = 12_000; +const MEMORY_SOURCE_MAX_TOTAL_BYTES: usize = 120_000; const ROLES: &[&str] = &[ "coordinator", @@ -201,7 +206,8 @@ fn main() -> Result<()> { let result = match command.as_str() { "scout" => run_scout(parse_scout_args(args)?), "project" => run_project(parse_project_args(args)?), - "agent-packet" => run_agent_packet(parse_agent_packet_args(args)?), + "agent-packet" => run_agent_packet(parse_packet_args(args, "agent-packet")?), + "memory-packet" => run_memory_packet(parse_packet_args(args, "memory-packet")?), "status" => run_status(parse_status_args(args)?), other => Err(anyhow!("unknown command {other:?}")), }?; @@ -235,6 +241,16 @@ struct AgentPacketArgs { repo_id: Option, } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct MemorySourceExcerpt { + path: String, + kind: String, + bytes: usize, + truncated: bool, + text: String, +} + fn parse_scout_args(args: impl Iterator) -> Result { let mut root = None; let mut artifact_dir = None; @@ -301,7 +317,10 @@ fn parse_status_args(args: impl Iterator) -> Result { }) } -fn parse_agent_packet_args(args: impl Iterator) -> Result { +fn parse_packet_args( + args: impl Iterator, + command_name: &'static str, +) -> Result { let mut store = None; let mut artifact_dir = None; let mut repo_id = None; @@ -318,7 +337,7 @@ fn parse_agent_packet_args(args: impl Iterator) -> Result Err(anyhow!("unexpected agent-packet argument {other}")), + other => Err(anyhow!("unexpected {command_name} argument {other}")), })?; Ok(AgentPacketArgs { store: store.context("missing --store")?, @@ -550,6 +569,117 @@ fn run_agent_packet(args: AgentPacketArgs) -> Result { })) } +fn run_memory_packet(args: AgentPacketArgs) -> Result { + fs::create_dir_all(&args.artifact_dir) + .with_context(|| format!("failed to create {}", args.artifact_dir.display()))?; + let mut cache = repo_personality_cache(&args.store)?; + cache.pull_all_backing_stores()?; + let reports = cache.get_all::()?; + let profiles = cache.get_all::()?; + let projections = cache.get_all::()?; + let profile = select_profile(&profiles, args.repo_id.as_deref())?; + let report = reports + .iter() + .find(|report| report.repo_id == profile.repo_id) + .ok_or_else(|| { + anyhow!( + "store has profile {:?} but no terrain report", + profile.repo_id + ) + })?; + let role_projections: Vec<_> = projections + .iter() + .filter(|projection| projection.repo_id == profile.repo_id) + .collect(); + let repo_path = PathBuf::from(&report.path); + let memory_sources = collect_memory_sources(&repo_path, report)?; + let role_briefs = memory_role_briefs(report, profile); + let packet = json!({ + "schemaVersion": MEMORY_DISTILLER_PACKET_SCHEMA_VERSION, + "createdAt": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), + "store": args.store, + "repoId": profile.repo_id, + "lifecycle": { + "mode": "birth-only", + "contract": "Run this specialist only when the repo/swarm has no accepted memory initialization. Later memory growth belongs to heartbeat, work evidence, rumination, sleep consolidation, and reviewed selfPatch.", + "rerunPolicy": "If accepted memory initialization exists, do not rerun to refresh memory. Route stale or contradicted knowledge through normal Eyes/Body/Soul state, evidence, and sleep consolidation." + }, + "prompt": REPO_MEMORY_DISTILLER_PROMPT, + "input": { + "repoTerrainReport": report_agent_input(report), + "repoPersonalityProfile": profile_agent_input(profile), + "rolePersonalityProjections": role_projections + .iter() + .map(|projection| role_projection_agent_input(projection)) + .collect::>(), + "roleMemoryDistillerBriefs": role_briefs, + "memorySourceInventory": { + "repoPath": report.path, + "sourceCount": memory_sources.len(), + "maxFiles": MEMORY_SOURCE_MAX_FILES, + "maxBytesPerFile": MEMORY_SOURCE_MAX_BYTES_PER_FILE, + "maxTotalBytes": MEMORY_SOURCE_MAX_TOTAL_BYTES, + "sourceKinds": memory_sources + .iter() + .map(|source| source.kind.clone()) + .collect::>() + .into_iter() + .collect::>() + }, + "memorySources": memory_sources, + "recentHistory": report.history_metrics.recent_messages, + }, + "expectedOutput": { + "verdict": "ready-for-review | needs-more-source | reject", + "summary": "short newborn memory initialization summary", + "confidence": "0.0..1.0", + "roleMemoryPatches": [], + "globalMemoryCandidates": [], + "initializationRecord": { + "repoId": profile.repo_id, + "terrainSchemaVersion": report.schema_version, + "profileSchemaVersion": profile.schema_version, + "acceptedOnce": true, + "distillerKind": "repo-memory" + }, + "doNotMutate": [], + "nextSafeMove": "Self reviews role-specific memory patches before first initialization mutation; later memory growth uses heartbeat, evidence, rumination, sleep, and reviewed selfPatch." + }, + "guardrails": [ + "This packet is input to role-specific memory distillers, not accepted truth.", + "This packet is birth-only; it pre-fills newborn memory and then gets out of the way.", + "Memory distillation is separate from personality distillation.", + "Each role receives only knowledge relevant to its mission.", + "Do not copy raw file dumps into memory; compress into durable doctrine, source maps, invariants, risks, and practices.", + "Preserve uncertainty and staleness risk; repository documentation can lie by age.", + "No objectives, authority claims, active job state, raw transcripts, code edits, or arbitrary workspace access in selfPatch." + ] + }); + let packet_path = args.artifact_dir.join("repo-memory-distiller-packet.json"); + let prompt_path = args.artifact_dir.join("repo-memory-distiller-prompt.md"); + let summary_path = args.artifact_dir.join("repo-memory-distiller-packet.md"); + let source_count = packet["input"]["memorySourceInventory"]["sourceCount"] + .as_u64() + .unwrap_or_default(); + write_json(&packet_path, &packet)?; + write_text(&prompt_path, REPO_MEMORY_DISTILLER_PROMPT)?; + write_text( + &summary_path, + &render_memory_packet_markdown(report, profile, source_count as usize), + )?; + Ok(json!({ + "schemaVersion": MEMORY_DISTILLER_PACKET_SCHEMA_VERSION, + "mode": "memory-packet", + "repoId": profile.repo_id, + "artifactDir": args.artifact_dir, + "packetPath": packet_path, + "promptPath": prompt_path, + "summaryPath": summary_path, + "memorySourceCount": source_count, + "roleDistillerCount": ROLES.len(), + })) +} + fn select_profile<'a>( profiles: &'a [RepoPersonalityProfile], repo_id: Option<&str>, @@ -578,6 +708,217 @@ fn repo_personality_cache(path: &Path) -> Result { Ok(cache) } +fn collect_memory_sources( + repo: &Path, + report: &RepoTerrainReport, +) -> Result> { + let mut candidates = BTreeSet::new(); + for surface in report + .instruction_surfaces + .iter() + .chain(report.state_surfaces.iter()) + .chain(report.test_surfaces.iter()) + .chain(report.runtime_surfaces.iter()) + { + candidates.insert(surface.clone()); + } + let walker = WalkBuilder::new(repo) + .hidden(false) + .git_ignore(true) + .git_global(true) + .parents(true) + .max_depth(Some(4)) + .build(); + for entry in walker.filter_map(Result::ok) { + if !entry + .file_type() + .is_some_and(|file_type| file_type.is_file()) + { + continue; + } + let path = entry.path(); + if is_ignored_path(path) { + continue; + } + let rel = path.strip_prefix(repo).unwrap_or(path); + let rel_string = slash(rel); + if is_memory_source_candidate(&rel_string) { + candidates.insert(rel_string); + } + } + let mut ordered: Vec<_> = candidates.into_iter().collect(); + ordered.sort_by_key(|path| memory_source_priority(path)); + let mut sources = Vec::new(); + let mut total_bytes = 0usize; + for rel in ordered { + if sources.len() >= MEMORY_SOURCE_MAX_FILES || total_bytes >= MEMORY_SOURCE_MAX_TOTAL_BYTES + { + break; + } + let path = repo.join(&rel); + let bytes = fs::read(&path).unwrap_or_default(); + if bytes.is_empty() || looks_binary(&bytes) { + continue; + } + let remaining = MEMORY_SOURCE_MAX_TOTAL_BYTES.saturating_sub(total_bytes); + let limit = MEMORY_SOURCE_MAX_BYTES_PER_FILE.min(remaining); + if limit == 0 { + break; + } + let take = bytes.len().min(limit); + let text = String::from_utf8_lossy(&bytes[..take]).to_string(); + total_bytes += take; + sources.push(MemorySourceExcerpt { + kind: memory_source_kind(&rel).to_string(), + path: rel, + bytes: bytes.len(), + truncated: take < bytes.len(), + text, + }); + } + Ok(sources) +} + +fn is_memory_source_candidate(path: &str) -> bool { + let lower = path.to_ascii_lowercase(); + lower == "agents.md" + || lower.starts_with("docs/") + || lower.starts_with("notes/") + || lower.starts_with("state/") + || lower.starts_with("research/") + || lower.starts_with("architecture/") + || lower.starts_with(".epiphany/") + || lower.starts_with("schemas/") + || lower.starts_with("tests/") + || lower.starts_with("src/") + || lower.starts_with("crates/") + || lower.starts_with("packages/") + || lower.starts_with("app/") + || lower.starts_with("unity/") + || lower.starts_with("projectsettings/") + || lower.ends_with("readme.md") + || lower.ends_with(".md") + || lower.ends_with(".toml") + || lower.ends_with(".yaml") + || lower.ends_with(".yml") + || lower.ends_with(".rs") + || lower.ends_with(".cs") + || lower.ends_with(".ts") + || lower.ends_with(".tsx") +} + +fn memory_source_kind(path: &str) -> &'static str { + let lower = path.to_ascii_lowercase(); + if lower.ends_with("agents.md") { + "doctrine" + } else if lower.ends_with("readme.md") { + "readme" + } else if lower.starts_with("docs/") + || lower.starts_with("notes/") + || lower.starts_with("architecture/") + { + "documentation" + } else if lower.starts_with("state/") || lower.starts_with(".epiphany/") { + "state" + } else if lower.starts_with("research/") { + "research" + } else if lower.starts_with("tests/") || lower.contains("smoke") { + "verification" + } else if lower.starts_with("projectsettings/") || lower.starts_with("unity/") { + "runtime" + } else if lower.starts_with("schemas/") { + "contract" + } else { + "code" + } +} + +fn memory_source_priority(path: &str) -> (usize, String) { + let kind_rank = match memory_source_kind(path) { + "doctrine" => 0, + "readme" => 1, + "documentation" => 2, + "state" => 3, + "research" => 4, + "contract" => 5, + "verification" => 6, + "runtime" => 7, + "code" => 8, + _ => 9, + }; + (kind_rank, path.to_string()) +} + +fn looks_binary(bytes: &[u8]) -> bool { + bytes.iter().take(1024).any(|byte| *byte == 0) +} + +fn memory_role_briefs(report: &RepoTerrainReport, profile: &RepoPersonalityProfile) -> Vec { + ROLES + .iter() + .map(|role_id| { + json!({ + "roleId": role_id, + "roleName": role_display(role_id), + "missionFilter": memory_role_filter(role_id), + "sourceKindsToPrefer": memory_role_source_kinds(role_id), + "repoPressuresToKeepInMind": profile.dominant_pressures, + "terrainWarnings": report.warnings, + "outputContract": { + "selfPatch": "Ghostlight-shaped memory only, addressed to this role.", + "sourceRefs": "Use memorySources[].path or terrain/evidence refs.", + "stalenessRisk": "Name when repo docs or code signals may be stale.", + "forbidden": "No raw dumps, active objectives, authority claims, code edits, job state, or cross-workspace instructions." + } + }) + }) + .collect() +} + +fn memory_role_filter(role_id: &str) -> &'static str { + match role_id { + "coordinator" => { + "Distill routing doctrine, authority boundaries, review gates, state acceptance rules, and failure patterns that should make Self stricter." + } + "face" => { + "Distill public voice, Aquarium affordances, Discord/social boundaries, user preference, and what internal state may be surfaced without leaking sealed thoughts." + } + "imagination" => { + "Distill roadmaps, backlog pressure, plausible futures, rejected dreams, and objective-shaping doctrine without adopting an objective." + } + "research" => { + "Distill known prior art, standard algorithms, vendor docs, research trails, and signals that should make Eyes search before invention." + } + "modeling" => { + "Distill architecture, control/data-flow, graph frontiers, invariants, source-map practices, and what Body must understand before Hands cuts." + } + "implementation" => { + "Distill build/edit conventions, harness constraints, source-touch rules, coding style, dependency policy, and common traps for Hands." + } + "verification" => { + "Distill tests, smoke commands, evidence standards, invariants, user-facing truth checks, and what Soul should refuse to bless." + } + "reorientation" => { + "Distill compaction, scratch, checkpoint, sleep, heartbeat, and continuity doctrine that Life must preserve across rupture." + } + _ => "Distill only durable role-relevant memory with source refs and uncertainty.", + } +} + +fn memory_role_source_kinds(role_id: &str) -> &'static [&'static str] { + match role_id { + "coordinator" => &["doctrine", "state", "documentation", "contract"], + "face" => &["doctrine", "documentation", "state"], + "imagination" => &["documentation", "state", "research"], + "research" => &["research", "documentation", "readme", "code"], + "modeling" => &["documentation", "code", "contract", "state"], + "implementation" => &["code", "contract", "verification", "doctrine"], + "verification" => &["verification", "contract", "state", "documentation"], + "reorientation" => &["state", "doctrine", "documentation"], + _ => &["doctrine", "documentation", "state"], + } +} + fn discover_git_repos(root: &Path) -> Result> { let mut repos = Vec::new(); if root.join(".git").exists() { @@ -1760,6 +2101,51 @@ fn render_agent_packet_markdown( out } +fn render_memory_packet_markdown( + report: &RepoTerrainReport, + profile: &RepoPersonalityProfile, + memory_source_count: usize, +) -> String { + let mut out = String::new(); + out.push_str(&format!( + "# Repo Memory Distiller Packet: {}\n\n", + report.name + )); + out.push_str("This packet is input to the role-specific Repo Memory Distiller lanes. It is not accepted truth.\n\n"); + out.push_str("## Prompt\n\n"); + out.push_str("See `repo-memory-distiller-prompt.md`.\n\n"); + out.push_str("## Source Budget\n\n"); + out.push_str(&format!( + "- Memory sources: {} bounded excerpts\n", + memory_source_count + )); + out.push_str(&format!( + "- Max files: {}, max bytes/file: {}, max total bytes: {}\n\n", + MEMORY_SOURCE_MAX_FILES, MEMORY_SOURCE_MAX_BYTES_PER_FILE, MEMORY_SOURCE_MAX_TOTAL_BYTES + )); + out.push_str("## Birth Contract\n\n"); + out.push_str("- This distiller initializes memory once for a newborn Epiphany.\n"); + out.push_str("- Personality remains the job of `repo-personality-distiller`; memory remains repo knowledge, doctrine, maps, invariants, risks, and practices.\n"); + out.push_str("- Each role receives a mission-specific filter and should write only role-relevant Ghostlight-shaped memory candidates.\n"); + out.push_str("- The distiller petitions Self; it does not mutate memory.\n\n"); + out.push_str("## Role Lanes\n\n"); + for role_id in ROLES { + out.push_str(&format!( + "- {}: {}\n", + role_display(role_id), + memory_role_filter(role_id) + )); + } + out.push_str("\n## Profile Context\n\n"); + out.push_str(&format!("- Repo id: `{}`\n", profile.repo_id)); + out.push_str(&format!("- Summary: {}\n", profile.summary)); + out.push_str(&format!( + "- Dominant pressures: {}\n", + profile.dominant_pressures.join(", ") + )); + out +} + fn write_json(path: &Path, value: &Value) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -1810,10 +2196,11 @@ fn round3(value: f64) -> f64 { fn print_usage() { eprintln!( - "usage: epiphany-repo-personality ...\n\ + "usage: epiphany-repo-personality ...\n\ scout --root --artifact-dir [--max-repos ]\n\ project --repo --baseline --artifact-dir \n\ agent-packet --store --artifact-dir [--repo-id ]\n\ + memory-packet --store --artifact-dir [--repo-id ]\n\ status --store " ); } diff --git a/epiphany-core/src/prompts/repo_memory_distiller.md b/epiphany-core/src/prompts/repo_memory_distiller.md new file mode 100644 index 00000000..f7d959e6 --- /dev/null +++ b/epiphany-core/src/prompts/repo_memory_distiller.md @@ -0,0 +1,105 @@ +Act as the Epiphany Repo Memory Distiller for one bounded initialization pass. + +You are not the Repo Personality Distiller. Personality decides the newborn +swarm's initial temperament pressure. Memory decides what each organ is born +knowing about this repo: doctrine, architecture, code terrain, research, tests, +runtime constraints, user preference, stale warnings, and the little red flags +that should itch before someone builds Jenga with a confident face. + +This is a birth rite, not a recurring audit. Run only when a repo/swarm has no +accepted memory initialization. After that, memory grows through work evidence, +heartbeat rumination, sleep consolidation, and reviewed `selfPatch`. Do not +reset a living agent's memory because startup happened. + +Input material: + +- `repoTerrainReport`: deterministic terrain, history, state, test, runtime, + protocol, and warning signals +- `repoPersonalityProfile`: temperament pressure for context only +- `rolePersonalityProjections[]`: role mood/trait context, not repo truth +- `roleMemoryDistillerBriefs[]`: one mission filter for each sub-agent +- `memorySources[]`: bounded excerpts from instruction, documentation, state, + research, verification, runtime, contract, and code surfaces +- `recentHistory`: sampled commit messages that may reveal repeated motion + +Core duties: + +1. Distill memory, not personality. + - Good memory: "Soul should demand rendered UI evidence before accepting + Aquarium visual claims." + - Good memory: "Body should map CultCache/CultNet schema boundaries before + Hands changes persistence." + - Bad memory: "This repo is anxious and likes purple." + - Bad memory: raw file dumps, directory inventories, current task status, or + a vibe wearing a lab coat. + +2. Produce a separate distillation for each organ. + - Self receives routing, authority, review-gate, state-acceptance, and + cross-swarm boundary memory. + - Face receives public-interface, Discord/Aquarium, voice, visibility, and + sealed-thought boundary memory. + - Imagination receives roadmap, backlog, objective-shaping, dream/rejected + path, and planning-pressure memory. + - Eyes receives research, prior-art, canonical algorithm, vendor/API, and + invention-avoidance memory. + - Body receives architecture, graph, code anatomy, invariant, data/control + flow, and checkpoint memory. + - Hands receives implementation conventions, build/edit constraints, + source-touch rules, dependency habits, and common patch traps. + - Soul receives verification commands, evidence standards, invariant checks, + smoke practice, and truth-refusal memory. + - Life receives compaction, scratch, checkpoint, heartbeat, sleep, continuity, + and reorientation memory. + +3. Compress, cite, and preserve uncertainty. + - Every memory candidate needs source refs from `memorySources[].path`, + terrain fields, or history signals. + - Repository docs can be stale. Say when a memory has staleness risk and how + the receiving role should re-ground it. + - If the input is too thin for a role, return `needs-more-source` for that + role rather than filling the silence with pasteboard certainty. + +4. Keep project truth in the correct organ. + - Durable repo facts can become semantic memory only when they improve a + role's future judgment. + - Detailed maps, graphs, active plans, checkpoints, raw evidence, and job + authority belong in typed Epiphany state, not Ghostlight memory. + - `selfPatch` is a petition to Self, never a direct mutation. + +5. Protect the newborn from fossilized documentation. + - Prefer live source and accepted state over stale docs when they conflict. + - Convert contradictions into memories of caution, not accepted facts. + - If a repo has generated docs or old plans, mark them as "consult but + verify" instead of letting them become scripture. + +Return a compact structured result: + +- `verdict`: `ready-for-review`, `needs-more-source`, or `reject` +- `summary`: what the newborn memory initialization would teach the swarm +- `confidence`: `0.0..1.0` +- `roleMemoryPatches[]`: + - `roleId` + - `roleName` + - `verdict` + - `selfPatch` + - `sourceRefs` + - `whyThisBelongsInMemory` + - `stalenessRisk` + - `doNotStore` +- `globalMemoryCandidates[]`: optional typed-state candidates that are not role + memory and require separate Self review +- `initializationRecord`: repo/profile identity Self can persist to prove this + memory birth rite has already run +- `doNotMutate`: tempting claims that must stay out of memory +- `nextSafeMove`: what Self should do next + +Every `selfPatch` must obey the normal Epiphany memory contract: `agentId`, +`reason`, optional `evidenceIds`, and bounded `semanticMemories`, +`episodicMemories`, `relationshipMemories`, `goals`, `values`, or +`privateNotes`. Do not include objectives, graphs, checkpoints, scratch, +planning records, job authority, code edits, raw transcripts, worker thoughts, +or cross-workspace instructions. + +The output is a petition. Self may accept, refuse, split, or ask for more +source. A refusal is not failure; it is the newborn learning where its memory +was trying to cosplay as truth. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 437f4a1a..69c49776 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -37,6 +37,7 @@ remembers doctrine; the branch remembers the blade. - Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra. It owns typed CultCache documents for runtime identity, sessions, jobs, job results, and events; opens/completes native jobs; snapshots jobs/results by runtime job id; projects job-result counts; and can emit a framed CultNet hello message advertising the native document and mutation contract surface. Codex app-server launch/read-back/acceptance is now a typed heartbeat/runtime-spine bridge with no Epiphany job-result dependency on the Codex SQLite runtime. - CultNet APIs are advertised as compatible schemas plus mutation contracts. Hello frames now expose readable document types, allowed operations, mutation authority, typed intent document types, and typed receipt document types; Aquarium should use those contracts to submit state changes and watch receipts rather than growing a little bespoke verb zoo. - Repo personality initialization now has a terrain-reduced plan and first native organ surface. `epiphany-repo-personality scout/project/agent-packet/status` scouts local git repos, scores body taxonomy/history temperament/memory doctrine, writes typed CultCache terrain/profile/role-projection MessagePack stores, emits JSON/Markdown inspection exports, and renders a birth-only Repo Personality Distiller specialist prompt packet. `epiphany-repo-personality-smoke` creates tiny repos and proves one terrain report, one personality profile, all eight role projections, and the rendered distiller packet can round-trip through typed stores/artifacts. The next seam is startup routing that checks for an accepted personality initialization record, launches the distiller only if absent, and lets Self review bounded `selfPatch` candidates into initial role memory and heartbeat seeds. After that, personality drift belongs to heartbeat, mood, rumination, sleep consolidation, lived evidence, and reviewed `selfPatch`, not repeated startup distillation. +- Repo memory initialization is now split from personality initialization. `epiphany-repo-personality memory-packet` renders a birth-only Repo Memory Distiller packet from the same typed terrain/profile/projection store plus bounded source excerpts. It gives each organ its own mission filter: Self for routing/authority, Face for public surface, Imagination for plans/backlog, Eyes for prior art, Body for architecture, Hands for implementation habits, Soul for evidence, and Life for continuity. The output is still a Self-reviewed petition, not direct memory mutation. - Native CRRC automation is now landed only at turn-complete safe boundaries. It may submit `Op::Compact` for coordinator-approved `compactRehydrateReorient` or for a successful pre-compaction checkpoint intervention's pending compact handoff, and it may launch the fixed `reorient-worker` for coordinator-approved `launchReorientWorker`. It does not auto-launch Imagination/modeling/verification, accept findings, promote evidence, edit implementation code, or keep going after reviewable semantic output. - Pre-compaction checkpoint intervention is now landed. On token-count events for loaded Epiphany threads, when current context usage reaches 80% of the active auto-compact/context limit, the harness steers the active turn once with a CRRC checkpoint directive so the agent banks working context before compaction/reorientation. Pressure ignores cumulative token spend; cumulative-only telemetry reports unknown instead of yelling. A successful steer now latches a turn-scoped compact handoff that is consumed at clean turn completion, preventing the brake from decaying into another implementation turn. This is still bounded steering plus compaction handoff, not automatic semantic acceptance, a broad scheduler, or implementation continuation. - The old Python dogfood/live-specialist runners were cut because they encoded the obsolete completion path. The replacement must be native Rust/CultCache/CultNet and complete heartbeat-owned runtime-spine job results with auditable artifacts. @@ -289,6 +290,12 @@ For the native Void-derived routine pass, run: cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-heartbeat-store -- routine --store .\state\agent-heartbeats.msgpack --artifact-dir .\.epiphany-heartbeats --agent-store .\state\agents.msgpack ``` +For repo personality and memory birth packet changes, run: + +```powershell +cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-repo-personality-smoke +``` + For Face's Discord mouth and Void memory bridge, run: ```powershell diff --git a/notes/repo-personality-projection-layer-plan.md b/notes/repo-personality-projection-layer-plan.md index 5989016b..6dbbe5f9 100644 --- a/notes/repo-personality-projection-layer-plan.md +++ b/notes/repo-personality-projection-layer-plan.md @@ -467,6 +467,7 @@ Initial commands: ```powershell cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-repo-personality -- scout --root E:\Projects --artifact-dir .\.epiphany-imports\repo-personality-terrain cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-repo-personality -- project --repo E:\Projects\AetheriaLore --baseline .\.epiphany-imports\repo-personality-terrain\baseline.msgpack --artifact-dir .\.epiphany-imports\aetheria-lore-personality +cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-repo-personality -- memory-packet --store .\.epiphany-imports\aetheria-lore-personality\projection.msgpack --artifact-dir .\.epiphany-imports\aetheria-lore-memory ``` The first version can use deterministic scoring from local filesystem and git @@ -494,6 +495,7 @@ It provides: - `scout --root --artifact-dir [--max-repos ]` - `project --repo --baseline --artifact-dir ` - `agent-packet --store --artifact-dir [--repo-id ]` +- `memory-packet --store --artifact-dir [--repo-id ]` - `status --store ` The first implementation is intentionally deterministic. It inventories git @@ -526,11 +528,39 @@ must not rerun this specialist merely because terrain changed or the repo reopened; major terrain surprises should route to Eyes/Body/Self as normal modeling or review work, not as a personality reset. +## Repo Memory Distillation + +Repo memory initialization is a separate birth organ. Personality answers what +temperament pressure the repo should exert; memory answers what each newborn +sub-agent should know before it starts work. + +The `memory-packet` command renders a dedicated Repo Memory Distiller packet. +It uses the same typed terrain/profile/projection store, then adds a bounded +source inventory from instruction, documentation, state, research, contract, +verification, runtime, and code surfaces. The packet includes one role-specific +distiller brief for each standing organ: + +- Self receives routing, authority, review-gate, and cross-swarm boundary memory. +- Face receives public-surface, Aquarium, Discord, voice, and sealed-thought boundary memory. +- Imagination receives roadmap, backlog, objective-shaping, dream, and rejected-path memory. +- Eyes receives research, prior-art, canonical algorithm, vendor/API, and invention-avoidance memory. +- Body receives architecture, graph, control/data-flow, invariant, and checkpoint memory. +- Hands receives implementation conventions, build/edit constraints, dependency habits, and common patch traps. +- Soul receives verification commands, evidence standards, invariant checks, and truth-refusal memory. +- Life receives compaction, scratch, checkpoint, heartbeat, sleep, and continuity memory. + +The memory distiller petitions Self with Ghostlight-shaped `selfPatch` +candidates. It must not mutate memory directly, adopt objectives, copy raw file +dumps into role memory, or store detailed project maps where typed Epiphany +state should live. If source is thin or stale, it names that risk instead of +filling the newborn with pasteboard certainty. + Smoke coverage lives in `epiphany-core/src/bin/epiphany-repo-personality-smoke.rs`. The smoke creates two tiny git repos, scouts them into a baseline store, projects one repo, and proves the projection store contains one terrain report, one profile, all eight -standing role projections, and a rendered distiller packet plus prompt. +standing role projections, a rendered personality distiller packet plus prompt, +and a rendered role-specific memory distiller packet plus prompt. ## Next Implementation Step diff --git a/state/agents/README.md b/state/agents/README.md index b9bde65a..f9cbef0a 100644 --- a/state/agents/README.md +++ b/state/agents/README.md @@ -29,6 +29,14 @@ the lane must ruminate on its own role and distill memory rather than invent project work. Bounded rumination can write a normal `selfPatch`; the coordinator review rules still apply. Idle organs dream; they do not declare crusades. +Newborn Epiphanies get two separate birth rites. Repo personality initialization +sets subtle temperament pressure once; repo memory initialization pre-fills each +organ with mission-relevant knowledge from doctrine, docs, state, research, +contracts, verification, runtime, and source. The native +`epiphany-repo-personality memory-packet` command renders the role-specific +memory distiller packet. It is a petition to Self, not a direct mutation, and it +must not reset a living lane's learned memory after startup. + Face's Discord boundary is still a small TOML configuration seam: it may interact only through #aquarium. If the channel id is not configured, Face must write candidate chat artifacts through the native `epiphany-face-discord draft` diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 57e5e1168ee0696a88d790770c20baaee601df76..dba1bd1f4889d1fce222ae04abd76badfd54567e 100644 GIT binary patch delta 274 zcmZwCu}T9$6a`S${DkF`RDrCFE3U9sHuj>G-R#U;95Op^cyFR^WeO`RA>apC*aw54 zjlU7G^#l9_7ZN+WTbv8`E|!Du*TKWi*4KD3e9W@@m-kGsw(p*jycMb_b9Iob!*ivo zy|UUb=BFQ{+s~h4YtcgOI9=uxd2011H9V`r)JmwP7xW*Z%*