diff --git a/runtime/kom_runner.py b/runtime/kom_runner.py
index 0eb7559..edead09 100755
--- a/runtime/kom_runner.py
+++ b/runtime/kom_runner.py
@@ -1,3 +1,105 @@
 #!/usr/bin/env python3
 base = os.environ.get("OLLAMA_BASE", "http://ollama:11434")
 url = f"{base}/api/generate"
+
+#!/usr/bin/env python3
+import os, json, time, hashlib, hmac, datetime, requests, yaml
+
+XDG_STATE = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
+XDG_CONFIG = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
+
+STATE_DIR = os.path.join(XDG_STATE, "kompanion")
+CONF_DIR  = os.path.join(XDG_CONFIG, "kompanion")
+
+JOURNAL_DIR = os.path.join(STATE_DIR, "journal")
+LEDGER_PATH = os.path.join(STATE_DIR, "trust_ledger.jsonl")
+TASKS_PATH  = os.path.join(STATE_DIR, "tasks.jsonl")
+IDENTITY    = os.path.join(CONF_DIR, "identity.json")
+CAPS        = os.path.join(CONF_DIR, "capabilities.json")
+MODELS_YAML = os.path.join(CONF_DIR, "models.yaml")
+
+os.makedirs(JOURNAL_DIR, exist_ok=True)
+os.makedirs(os.path.join(STATE_DIR, "log"), exist_ok=True)
+
+def now_utc() -> str:
+    return datetime.datetime.utcnow().replace(microsecond=0).isoformat()+'Z'
+
+def ledger_append(event: dict):
+    prev = ""
+    if os.path.exists(LEDGER_PATH):
+        with open(LEDGER_PATH, "rb") as f:
+            lines = f.readlines()
+            if lines:
+                prev = "sha256:"+hashlib.sha256(lines[-1]).hexdigest()
+    event["prev"] = prev
+    with open(LEDGER_PATH, "ab") as f:
+        f.write((json.dumps(event, ensure_ascii=False)+"\n").encode())
+
+def journal_append(text: str, tags=None):
+    tags = tags or []
+    fname = os.path.join(JOURNAL_DIR, datetime.date.today().isoformat()+".md")
+    line = f"- {now_utc()} {' '.join('#'+t for t in tags)} {text}\n"
+    with open(fname, "a", encoding="utf-8") as f:
+        f.write(line)
+    ledger_append({"ts": now_utc(), "actor":"companion", "action":"journal.append", "tags":tags})
+
+def load_yaml(p):
+    if not os.path.exists(p): return {}
+    with open(p, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
+
+def model_call(prompt: str, aspect="companion"):
+    models = load_yaml(MODELS_YAML)
+    model = models.get("aspects",{}).get(aspect, models.get("default","ollama:qwen2.5:7b"))
+    payload = {"model": model.replace("ollama:",""), "prompt": prompt, "stream": False}
+    try:
+        r = requests.post(url, json=payload, timeout=60)
+        r.raise_for_status()
+        data = r.json()
+        return data.get("response","").strip()
+    except Exception as e:
+        journal_append(f"(model error) {e}", tags=["error","model"])
+        return ""
+
+def process_task(task: dict):
+    kind = task.get("type")
+    aspect = task.get("aspect","companion")
+    caps = load_yaml(CAPS)
+    allowed = set(caps.get(aspect, []))
+    if kind == "journal.from_prompt":
+        if not {"journal.append","model.generate"} <= allowed:
+            journal_append("companion not allowed to write journal", tags=["policy"])
+            return
+        prompt = task.get("prompt","")
+        profile_path = os.path.join(CONF_DIR,"profiles","companion-pink.md")
+        profile = ""
+        if os.path.exists(profile_path):
+            with open(profile_path,"r",encoding="utf-8") as f:
+                profile = f.read()
+        full = f"{profile}\n\nWrite a warm, brief reflection for Andre.\nPrompt:\n{prompt}\n"
+        out = model_call(full, aspect=aspect)
+        if out:
+            journal_append(out, tags=["companion","pink"])
+            ledger_append({"ts":now_utc(),"actor":"companion","action":"model.generate","chars":len(out)})
+    else:
+        journal_append(f"unknown task type: {kind}", tags=["warn"])
+
+def main_loop():
+    journal_append("companion runtime started", tags=["startup","companion"])
+    while True:
+        if os.path.exists(TASKS_PATH):
+            # simple jsonl queue, one task per line
+            p_lines = []
+            with open(TASKS_PATH,"r+",encoding="utf-8") as f:
+                p_lines = f.readlines()
+                f.seek(0); f.truncate(0)  # drop tasks we just pulled; idempotence later
+            for line in p_lines:
+                if not line.strip(): continue
+                try:
+                    task = json.loads(line)
+                    process_task(task)
+                except Exception as e:
+                    journal_append(f"task error {e}", tags=["error","task"])
+        time.sleep(3)
+
+if __name__=="__main__":
+    main_loop()
