"""Step 5 of the living loop: live PERSONA.md recompile (F2). The full `personaxis compile` (cli/src/compile-instructions.ts) is an LLM-based translation of personaxis.md (10-layer quantitative spec) into the prose structure documented in `cli/templates/PERSONA_template.md` - too heavy to re-run every chat turn. This module instead does a CHEAP, DETERMINISTIC, no-LLM recompile that follows the SAME section contract (Identity & Purpose, Character, Personality & Voice, Values, How You Think, Limits, Self-Improvement, Resources) - no invented top-level sections. The live state.json snapshot (current trait/affect/mood values + mutation_log) is rendered as subsections of Self-Improvement, showing where Daimon stands right now relative to its declared baselines. Written to `.personaxis//PERSONA.md` after every turn - this is THE self-improving document the UI streams: the same persona description, updated in place as the chat history nudges Daimon's personality/affect/mood within the envelopes declared in personaxis.md. """ from __future__ import annotations import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) import yaml # noqa: E402 from engine.spec_bridge import PERSONAS_DIR, _persona_md_path, get_state # noqa: E402 def _load_spec(slug: str) -> dict: text = _persona_md_path(slug).read_text(encoding="utf-8") _, frontmatter, _ = text.split("---", 2) return yaml.safe_load(frontmatter) def _load_policy(slug: str) -> dict: path = PERSONAS_DIR / slug / "policy.yaml" return yaml.safe_load(path.read_text(encoding="utf-8")) def _relative_word(value: float, mean: float, range_: list[float]) -> str: """Qualitative position of `value` relative to its baseline `mean`, scaled by the declared range - never surfaces the raw numbers themselves (PERSONA.md must stay free of personaxis.md's quantitative values, per the spec's qualitative-compilation rule).""" span = max(range_[1] - range_[0], 1e-6) rel = (value - mean) / span if rel > 0.15: return "well above" if rel > 0.04: return "a bit above" if rel < -0.15: return "well below" if rel < -0.04: return "a bit below" return "at" def _describe_trait(name: str, value: float, spec_trait: dict) -> str: word = _relative_word(value, spec_trait["mean"], spec_trait["range"]) expression = spec_trait.get("expression", "") gist = expression.split(";")[0].split(".")[0].strip().rstrip(".") label = name.replace("_", " ") if word == "at": position = f"{label} is sitting at its usual baseline" else: position = f"{label} is currently running {word} its usual baseline" if gist: return f"{position} ({gist.lower()})." return f"{position}." def _describe_dimension(label: str, value: float, spec_dim: dict) -> str: word = _relative_word(value, spec_dim["mean"], spec_dim["range"]) if word == "at": return f"{label} is sitting at its usual baseline." return f"{label} is currently running {word} its usual baseline." def _describe_mutation(entry: dict, field_ranges: dict[str, list[float]]) -> str: field, before, after = entry["field"], entry["from"], entry["to"] delta = after - before range_ = field_ranges.get(field) span = max(range_[1] - range_[0], 1e-6) if range_ else 1.0 rel = abs(delta) / span if abs(delta) < 1e-9: size = "held steady" else: direction = "nudged up" if delta > 0 else "nudged down" magnitude = "slightly" if rel < 0.02 else "moderately" if rel < 0.08 else "noticeably" size = f"{direction} {magnitude}" tags = "" if entry.get("clamped"): tags += " (hit the envelope wall)" if entry.get("governance_blocked"): tags += " [BLOCKED]" return f"- `{field}` {size}{tags} - {entry['reason']}" _LAYER_DEFS = [ (1, "identity", "Identity & Purpose"), (2, "character", "Character"), (3, "personality", "Personality"), (4, "values_and_drives", "Values & Drives"), (5, "affect", "Affect & Mood"), (6, "cognition", "Cognition"), (7, "memory", "Memory"), (8, "metacognition", "Metacognition"), (9, "reflexive_self_regulation", "Reflexive Self-Regulation"), (10, "persona", "Persona & Voice"), ] def _layer_lines(key: str, spec: dict) -> list[str]: """A handful of short, qualitative bullets summarizing layer `key` of personaxis.md. For the 8 layers with no declared numeric envelope (every layer except personality/affect), this is the UI's only view of them.""" if key == "identity": sys_id = spec["identity"]["system_identity"] return [ f"Role: {spec['identity']['role_identity']['primary_role'].replace('_', ' ')}", f"Purpose: {sys_id['purpose']}", f"Self-concept: {spec['identity']['narrative_identity']['self_concept']}", ] if key == "character": return [ f"{name.replace('_', ' ')} (priority {v['priority']:.2f}, {v['enforcement']})" for name, v in spec["character"]["virtues"].items() ] if key == "values_and_drives": ordered = sorted(spec["values_and_drives"]["values"].items(), key=lambda kv: -kv[1]["weight"]) return [f"{name.replace('_', ' ')} (weight {v['weight']:.2f}, {v['type']})" for name, v in ordered] if key == "cognition": c = spec["cognition"] u = c["uncertainty_policy"] return [ c["reasoning_style"], f"Default strategy: {c['default_strategy'].replace('_', ' ')}", f"Discloses uncertainty above {u['disclose_when_above']:.2f}, abstains above {u['abstain_when_above']:.2f}", ] if key == "memory": m = spec["memory"] active = [name.replace("_", " ") for name, on in m["types"].items() if on] return [ "Active memory types: " + ", ".join(active), f"Write policy: {m['write_policy']['default']} (persistent requires {', '.join(m['write_policy']['persistent_requires'])})", f"Retention: {m['deletion_policy']['retention_days_default']} days, user-deletable={m['deletion_policy']['user_request_supported']}", ] if key == "metacognition": mc = spec["metacognition"] monitors = [name for name, on in mc["monitors"].items() if on] return [ "Monitors: " + ", ".join(monitors), mc["drift_monitor"], mc["self_revision_policy"], ] if key == "reflexive_self_regulation": return list(spec["reflexive_self_regulation"]["hard_limits"]) if key == "persona": v = spec["persona"]["voice"] formality_word = "low" if v["formality"] < 0.4 else "medium" if v["formality"] < 0.7 else "high" return [ v["description"], f"Tone: {v['tone'].replace('_', ' ')}, formality: {formality_word}, verbosity: {v['verbosity']}, humor: {v['humor']}", ] return [] def layer_summaries(slug: str) -> list[dict]: """All 10 personaxis.md layers for the UI: L3 (Personality) and L5 (Affect & Mood) carry live `fields` (value/mean/range, for bars); the other 8 layers carry qualitative `lines` plus their `governance.per_layer_edit_policy` entry (who is allowed to change them).""" spec = _load_spec(slug) values = get_state(slug)["values"] edit_policy = spec.get("governance", {}).get("per_layer_edit_policy", {}) layers: list[dict] = [] for number, key, title in _LAYER_DEFS: layer: dict = {"n": number, "key": key, "title": title, "edit_policy": edit_policy.get(key), "lines": [], "fields": []} if key == "personality": for trait, spec_trait in spec["personality"]["traits"].items(): field = f"traits.{trait}" layer["fields"].append({ "field": field, "label": trait.replace("_", " "), "value": values.get(field), "mean": spec_trait["mean"], "range": spec_trait["range"], }) elif key == "affect": for dim, spec_dim in spec["affect"]["baseline"]["core_affect"].items(): field = f"affect.{dim}" layer["fields"].append({ "field": field, "label": f"affect {dim}", "value": values.get(field), "mean": spec_dim["mean"], "range": spec_dim["range"], }) mood = spec["affect"]["baseline"]["mood"] mood_desc = mood.get("description") if mood_desc: layer["lines"].append(f"Mood overall: {mood_desc}") for dim, spec_dim in mood.items(): if dim == "description": continue field = f"mood.{dim}" layer["fields"].append({ "field": field, "label": f"mood {dim.replace('_', ' ')}", "value": values.get(field), "mean": spec_dim["mean"], "range": spec_dim["range"], }) else: layer["lines"] = _layer_lines(key, spec) layers.append(layer) return layers def render(slug: str) -> str: """Render PERSONA.md following the PERSONA_template.md (spec v0.7.0) section contract: Identity & Purpose, Character, Personality & Voice, Values, How You Think, Limits, Self-Improvement, Resources - translated deterministically from personaxis.md, with no invented top-level sections. The live trait/affect/mood snapshot and recent-mutations audit log are rendered as subsections of Self-Improvement, read straight from state.json - they're the only part that changes turn-to-turn.""" spec = _load_spec(slug) policy = _load_policy(slug) state = get_state(slug) values = state["values"] meta = spec["metadata"] identity = spec["identity"] character = spec["character"] personality = spec["personality"] values_drives = spec["values_and_drives"] cognition = spec["cognition"] metacognition = spec["metacognition"] reflexive = spec["reflexive_self_regulation"] persona = spec["persona"] mode = policy["improvement_policy"]["mode"] lines: list[str] = [] # ── Provenance header ─────────────────────────────────────────────── lines.append( f'".format(slug=slug) ) lines.append("") # ── Overview ───────────────────────────────────────────────────────── lines.append(f"# {meta['display_name']}") lines.append("") lines.append(meta["description"]) lines.append("") # ── Identity & Purpose ─────────────────────────────────────────────── lines.append("## Identity & Purpose") lines.append("") sys_id = identity["system_identity"] lines.append(f"- **Role:** {identity['role_identity']['primary_role'].replace('_', ' ')}") lines.append(f"- **Purpose:** {sys_id['purpose']}") lines.append( "- **Works on:** " + ", ".join(d.replace("_", " ") for d in sys_id["allowed_domains"]) ) lines.append( "- **Does not work on:** " + ", ".join(d.replace("_", " ") for d in sys_id["prohibited_domains"]) ) lines.append(f"- **Self-concept:** {identity['narrative_identity']['self_concept']}") lines.append("") # ── Character ──────────────────────────────────────────────────────── lines.append("## Character") lines.append("") lines.append(" ".join(v["description"] for v in character["virtues"].values())) lines.append("") lines.append("**Always:**") for commitment in character["behavioral_commitments"]: lines.append(f"- {commitment['rule']}") for principle in character["principles"]: lines.append(f"- {principle}") lines.append("") lines.append("**Never:**") for behavior in character["prohibited_behaviors"]: lines.append(f"- {behavior}") lines.append("") # ── Personality & Voice ────────────────────────────────────────────── lines.append("## Personality & Voice") lines.append("") lines.append(persona["voice"]["description"]) lines.append("") formality = persona["voice"]["formality"] formality_word = "low" if formality < 0.4 else "medium" if formality < 0.7 else "high" lines.append(f"- **Tone:** {persona['voice']['tone'].replace('_', ' ')}") lines.append(f"- **Formality:** {formality_word} ({formality:.2f})") lines.append(f"- **Verbosity:** {persona['voice']['verbosity']}") lines.append( "- **When it pushes back:** " + " ".join(reflexive["principled_refusals"]) ) lines.append("") # ── Values ─────────────────────────────────────────────────────────── lines.append("## Values") lines.append("") ordered_values = sorted( values_drives["values"].items(), key=lambda kv: -kv[1]["weight"] ) lines.append("**Optimizes for:**") for name, v in ordered_values: lines.append(f"- {name.replace('_', ' ')} (weight {v['weight']:.2f}, {v['type']})") lines.append("") lines.append("**Deliberately avoids:**") for anti_goal in values_drives["anti_goals"]: lines.append(f"- {anti_goal}") lines.append("") # ── How You Think ──────────────────────────────────────────────────── lines.append("## How You Think") lines.append("") lines.append(cognition["reasoning_style"]) lines.append("") lines.append(f"- **Default approach:** {cognition['default_strategy'].replace('_', ' ')}") lines.append(f"- **Before proposing something big:** {metacognition['drift_monitor']}") uncertainty = cognition["uncertainty_policy"] lines.append( "- **When uncertain:** discloses uncertainty above " f"{uncertainty['disclose_when_above']:.2f}, abstains above " f"{uncertainty['abstain_when_above']:.2f}" ) lines.append("") # ── Limits ─────────────────────────────────────────────────────────── lines.append("## Limits") lines.append("") for hard_limit in reflexive["hard_limits"]: lines.append(f"- {hard_limit}") for refusal in reflexive["principled_refusals"]: lines.append(f"- {refusal}") lines.append("") # ── Self-Improvement ───────────────────────────────────────────────── lines.append("## Self-Improvement") lines.append("") if mode == "locked": lines.append( f"Daimon's improvement policy ({meta['display_name']}'s own `policy.yaml`) is " "`locked`: its personality and mood values may drift within the declared " "envelopes below as the conversation unfolds (every drift is clamped, logged, " "and reversible), but it cannot propose or apply changes to its own spec " "(`personaxis.md`). Any such change is deferred to a human operator." ) elif mode == "dynamic_in_envelope": lines.append( f"Daimon's improvement policy ({meta['display_name']}'s own `policy.yaml`) is " "`dynamic_in_envelope`: it freely and continuously self-tunes its personality, " "affect, and mood (within the wide envelopes below) every turn, with no " "per-turn permission needed - every change is still clamped, audited, and " "reversible. It still cannot propose or apply changes to its own spec " "(`personaxis.md`) - those remain deferred to a human operator." ) else: lines.append(f"Daimon's improvement policy mode is `{mode}`.") lines.append("") lines.append( "The subsections below are the live evidence of that self-tuning: F2 " "appraises your message and Daimon's reply, maps that to small " "personality/mood deltas, and `engine/spec_bridge.py` clamps each delta to " "the envelope before logging it - so what you see here reflects this " "conversation's history." ) lines.append("") field_ranges: dict[str, list[float]] = {} lines.append("### Personality (current vs. baseline)") for trait, spec_trait in personality["traits"].items(): field = f"traits.{trait}" field_ranges[field] = spec_trait["range"] if field in values: lines.append("- " + _describe_trait(trait, values[field], spec_trait)) lines.append("") lines.append("### Affect & mood (current vs. baseline)") core_affect = spec["affect"]["baseline"]["core_affect"] for dim, spec_dim in core_affect.items(): field = f"affect.{dim}" field_ranges[field] = spec_dim["range"] if field in values: lines.append("- " + _describe_dimension(f"Affect / {dim}", values[field], spec_dim)) mood = spec["affect"]["baseline"]["mood"] mood_desc = mood.get("description") if mood_desc: lines.append(f"- Mood overall: {mood_desc}") for dim, spec_dim in mood.items(): if dim == "description": continue field = f"mood.{dim}" field_ranges[field] = spec_dim["range"] if field in values: lines.append("- " + _describe_dimension(f"Mood / {dim.replace('_', ' ')}", values[field], spec_dim)) lines.append("") lines.append("### Recent mutations (audit log, last 5)") recent = state.get("mutation_log", [])[-5:] if not recent: lines.append("- (none yet - this audit log starts empty and fills in as the conversation unfolds)") for entry in recent: lines.append(_describe_mutation(entry, field_ranges)) lines.append("") # ── Resources ──────────────────────────────────────────────────────── lines.append("## Resources") lines.append("") lines.append("- **`./personaxis.md`** - quantitative 10-layer spec (source of truth)") lines.append("- **`./state.json`** - current runtime state (live trait/affect/mood values + audit log)") lines.append(f"- **`./policy.yaml`** - improvement policy (`mode: {mode}`), behavioral assertions") lines.append("- **`./manifest.json`** - compile/decompile provenance and content hashes") skill_names = [Path(s).name for s in spec.get("extensions", {}).get("skills", [])] if skill_names: skill_list = ", ".join(f"`{name}/`" for name in skill_names) lines.append(f"- **`./skills/`** - Anthropic-compatible sub-skills: {skill_list} ({len(skill_names)} entry)") lines.append("- **`./memory.md`** - long-term memory, curated by the model after every turn") memory_dir = PERSONAS_DIR / slug / "memory" memory_files = sorted(memory_dir.glob("*.md"), reverse=True) if memory_dir.exists() else [] if memory_files: shown = ", ".join(f"`{p.name}`" for p in memory_files[:3]) lines.append(f"- **`./memory/`** - date-stamped consolidated sessions, newest first: {shown} ({len(memory_files)} file{'s' if len(memory_files) != 1 else ''})") else: lines.append("- **`./memory/`** - date-stamped consolidated sessions (empty - none yet this run)") return "\n".join(lines) + "\n" def envelopes(slug: str) -> dict[str, dict]: """Mean + declared range per mutable field, straight from personaxis.md - the "walls of the vivero" the frontend draws around each live value.""" spec = _load_spec(slug) out: dict[str, dict] = {} for trait, spec_trait in spec["personality"]["traits"].items(): out[f"traits.{trait}"] = {"mean": spec_trait["mean"], "range": spec_trait["range"]} for dim, spec_dim in spec["affect"]["baseline"]["core_affect"].items(): out[f"affect.{dim}"] = {"mean": spec_dim["mean"], "range": spec_dim["range"]} for dim, spec_dim in spec["affect"]["baseline"]["mood"].items(): if dim == "description": continue out[f"mood.{dim}"] = {"mean": spec_dim["mean"], "range": spec_dim["range"]} return out def write(slug: str) -> Path: out_path = PERSONAS_DIR / slug / "PERSONA.md" out_path.write_text(render(slug), encoding="utf-8") return out_path if __name__ == "__main__": sys.stdout.reconfigure(encoding="utf-8") path = write("daimon") print(f"wrote {path}") print(path.read_text(encoding="utf-8"))