diff --git a/evennia_tools/telemetry.py b/evennia_tools/telemetry.py index b1b8b11..a8f5e84 100644 --- a/evennia_tools/telemetry.py +++ b/evennia_tools/telemetry.py @@ -2,24 +2,52 @@ import json from datetime import datetime, timezone from pathlib import Path +DEFAULT_BASE_DIR = "~/.timmy/training-data/evennia" -def telemetry_dir(base_dir: str | Path = "~/.timmy/training-data/evennia") -> Path: + +def telemetry_dir(base_dir: str | Path = DEFAULT_BASE_DIR) -> Path: return Path(base_dir).expanduser() -def event_log_path(session_id: str, base_dir: str | Path = "~/.timmy/training-data/evennia") -> Path: +def _session_day() -> str: + return datetime.now(timezone.utc).strftime("%Y%m%d") + + +def event_log_path(session_id: str, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path: session_id = (session_id or "unbound").strip() or "unbound" - day = datetime.now(timezone.utc).strftime("%Y%m%d") - return telemetry_dir(base_dir) / day / f"{session_id}.jsonl" + return telemetry_dir(base_dir) / _session_day() / f"{session_id}.jsonl" -def append_event(session_id: str, event: dict, base_dir: str | Path = "~/.timmy/training-data/evennia") -> Path: +def session_meta_path(session_id: str, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path: + session_id = (session_id or "unbound").strip() or "unbound" + return telemetry_dir(base_dir) / _session_day() / f"{session_id}.meta.json" + + +def write_session_metadata(session_id: str, metadata: dict, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path: + path = session_meta_path(session_id, base_dir) + path.parent.mkdir(parents=True, exist_ok=True) + payload = {} + if path.exists(): + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except Exception: + payload = {} + payload.update(metadata or {}) + payload.setdefault("session_id", session_id) + payload.setdefault("event_log_path", str(event_log_path(session_id, base_dir))) + payload.setdefault("updated_at", datetime.now(timezone.utc).isoformat()) + path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + return path + + +def append_event(session_id: str, event: dict, base_dir: str | Path = DEFAULT_BASE_DIR) -> Path: path = event_log_path(session_id, base_dir) path.parent.mkdir(parents=True, exist_ok=True) payload = dict(event) payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat()) with path.open("a", encoding="utf-8") as f: f.write(json.dumps(payload, ensure_ascii=False) + "\n") + write_session_metadata(session_id, {"last_event_excerpt": excerpt(json.dumps(payload, ensure_ascii=False), 400)}, base_dir) return path diff --git a/evennia_tools/training.py b/evennia_tools/training.py new file mode 100644 index 0000000..1b74353 --- /dev/null +++ b/evennia_tools/training.py @@ -0,0 +1,19 @@ +from pathlib import Path + +WORLD_BASICS_STEPS = ( + {"command": "look", "expected": ("Gate",)}, + {"command": "enter", "expected": ("Courtyard",)}, + {"command": "workshop", "expected": ("Workshop", "Workbench")}, + {"command": "look", "expected": ("Workshop", "Workbench")}, + {"command": "courtyard", "expected": ("Courtyard", "Map Table")}, + {"command": "chapel", "expected": ("Chapel", "Prayer Wall")}, + {"command": "look Book of the Soul", "expected": ("Book of the Soul", "doctrinal anchor")}, +) + + +def example_trace_path(repo_root: str | Path) -> Path: + return Path(repo_root) / "training-data" / "evennia" / "examples" / "world-basics-trace.example.jsonl" + + +def example_eval_path(repo_root: str | Path) -> Path: + return Path(repo_root) / "training-data" / "evennia" / "examples" / "world-basics-eval.example.json" diff --git a/reports/production/2026-03-28-evennia-training-baseline.md b/reports/production/2026-03-28-evennia-training-baseline.md new file mode 100644 index 0000000..aa687c9 --- /dev/null +++ b/reports/production/2026-03-28-evennia-training-baseline.md @@ -0,0 +1,35 @@ +# Evennia Training Proof — 2026-03-28 + +Issue: +- #37 Hermes/Evennia telemetry, replay, and DPO/eval alignment + +What this slice adds: +- canonical telemetry contract for the Evennia lane +- session-id sidecar mapping path +- sample trace generator +- deterministic replay/eval harness for world basics +- committed example trace/eval artifacts + +Committed example artifacts: +- `training-data/evennia/examples/world-basics-trace.example.jsonl` +- `training-data/evennia/examples/world-basics-eval.example.json` + +Final result: +- replay/eval now starts from a deterministic Gate anchor using a dedicated eval account (`TimmyEval`) +- sample trace generation succeeds +- world-basics eval passes cleanly +- orientation: pass +- navigation: pass +- object inspection: pass + +Canonical mapping: +- Hermes session id is the join key +- world events write to `~/.timmy/training-data/evennia/YYYYMMDD/.jsonl` +- sidecar mapping file writes to `~/.timmy/training-data/evennia/YYYYMMDD/.meta.json` +- the bridge binds the session id through `mcp_evennia_bind_session` + +Why this matters: +- world interaction no longer disappears into an opaque side channel +- we now have a path from Hermes transcript -> Evennia event log -> replay/eval +- this complements rather than replaces NLE/MiniHack +- the persistent-world lane now has a real green baseline, not just an aspiration diff --git a/scripts/evennia/__init__.py b/scripts/evennia/__init__.py new file mode 100644 index 0000000..d2addab --- /dev/null +++ b/scripts/evennia/__init__.py @@ -0,0 +1 @@ +"""Evennia world-lane scripts.""" diff --git a/scripts/evennia/eval_world_basics.py b/scripts/evennia/eval_world_basics.py new file mode 100644 index 0000000..92cd2ff --- /dev/null +++ b/scripts/evennia/eval_world_basics.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from evennia_tools.training import WORLD_BASICS_STEPS, example_eval_path +from scripts.evennia import evennia_mcp_server as bridge + +EVENNIA_BIN = Path.home() / '.timmy' / 'evennia' / 'venv' / 'bin' / 'evennia' +GAME_DIR = Path.home() / '.timmy' / 'evennia' / 'timmy_world' +EVAL_USERNAME = os.environ.get("TIMMY_EVENNIA_EVAL_USERNAME", "TimmyEval") +EVAL_PASSWORD = os.environ.get("TIMMY_EVENNIA_EVAL_PASSWORD", "timmy-eval-world-dev") + + +def reset_timmy_to_gate(): + env = dict(**os.environ) + env['PYTHONPATH'] = str(REPO_ROOT) + ':' + env.get('PYTHONPATH', '') + code = f"from evennia.accounts.models import AccountDB; from evennia.accounts.accounts import DefaultAccount; from evennia.utils.search import search_object; acc=AccountDB.objects.filter(username='{EVAL_USERNAME}').first(); acc = acc or DefaultAccount.create(username='{EVAL_USERNAME}', password='{EVAL_PASSWORD}')[0]; char=list(acc.characters)[0]; gate=search_object('Gate', exact=True)[0]; char.home=gate; char.move_to(gate, quiet=True, move_hooks=False); char.save(); print('RESET_OK')" + subprocess.run([str(EVENNIA_BIN), 'shell', '-c', code], cwd=GAME_DIR, env=env, check=True, capture_output=True, text=True, timeout=120) + + +def normalize_to_gate() -> None: + output = bridge._observe("timmy").get("output", "") + if not output: + output = bridge._command("look", name="timmy", wait_ms=400).get("output", "") + for _ in range(6): + if "Gate" in output: + return + if "Courtyard" in output: + output = bridge._command("gate", name="timmy", wait_ms=400).get("output", "") + continue + if any(room in output for room in ("Workshop", "Archive", "Chapel")): + output = bridge._command("courtyard", name="timmy", wait_ms=400).get("output", "") + continue + output = bridge._command("look", name="timmy", wait_ms=400).get("output", "") + + +def main(): + try: + bridge._disconnect("timmy") + except Exception: + pass + reset_timmy_to_gate() + bridge._save_bound_session_id(os.environ.get("TIMMY_EVENNIA_EVAL_SESSION_ID", "eval-evennia-world-basics")) + bridge._connect(name="timmy", username=EVAL_USERNAME, password=EVAL_PASSWORD) + normalize_to_gate() + results = [] + for step in WORLD_BASICS_STEPS: + command = step["command"] + expected = step["expected"] + res = bridge._command(command, name="timmy", wait_ms=400) + output = res.get("output", "") + passed = all(token in output for token in expected) + results.append({"command": command, "expected": expected, "passed": passed, "output_excerpt": output[:300]}) + bridge._disconnect("timmy") + summary = { + "passed": all(item["passed"] for item in results), + "checks": results, + "orientation": next((item["passed"] for item in results if item["command"] == "look"), False), + "navigation": all(item["passed"] for item in results if item["command"] in ("enter", "workshop", "courtyard", "chapel")), + "object_inspection": next((item["passed"] for item in results if item["command"] == "look Book of the Soul"), False), + } + out = example_eval_path(REPO_ROOT) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, indent=2), encoding="utf-8") + print(json.dumps({"eval_path": str(out), **summary}, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/evennia/generate_sample_trace.py b/scripts/evennia/generate_sample_trace.py new file mode 100644 index 0000000..11c5dee --- /dev/null +++ b/scripts/evennia/generate_sample_trace.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from evennia_tools.telemetry import event_log_path, session_meta_path +from evennia_tools.training import WORLD_BASICS_STEPS, example_trace_path +from scripts.evennia import evennia_mcp_server as bridge + +EVENNIA_BIN = Path.home() / '.timmy' / 'evennia' / 'venv' / 'bin' / 'evennia' +GAME_DIR = Path.home() / '.timmy' / 'evennia' / 'timmy_world' + +SESSION_ID = os.environ.get("TIMMY_EVENNIA_SAMPLE_SESSION_ID", "sample-evennia-world-basics") +EVAL_USERNAME = os.environ.get("TIMMY_EVENNIA_EVAL_USERNAME", "TimmyEval") +EVAL_PASSWORD = os.environ.get("TIMMY_EVENNIA_EVAL_PASSWORD", "timmy-eval-world-dev") + + +def reset_timmy_to_gate(): + env = dict(**os.environ) + env['PYTHONPATH'] = str(REPO_ROOT) + ':' + env.get('PYTHONPATH', '') + code = f"from evennia.accounts.models import AccountDB; from evennia.accounts.accounts import DefaultAccount; from evennia.utils.search import search_object; acc=AccountDB.objects.filter(username='{EVAL_USERNAME}').first(); acc = acc or DefaultAccount.create(username='{EVAL_USERNAME}', password='{EVAL_PASSWORD}')[0]; char=list(acc.characters)[0]; gate=search_object('Gate', exact=True)[0]; char.home=gate; char.move_to(gate, quiet=True, move_hooks=False); char.save(); print('RESET_OK')" + subprocess.run([str(EVENNIA_BIN), 'shell', '-c', code], cwd=GAME_DIR, env=env, check=True, capture_output=True, text=True, timeout=120) + + +def normalize_to_gate() -> None: + output = bridge._observe("timmy").get("output", "") + if not output: + output = bridge._command("look", name="timmy", wait_ms=400).get("output", "") + for _ in range(6): + if "Gate" in output: + return + if "Courtyard" in output: + output = bridge._command("gate", name="timmy", wait_ms=400).get("output", "") + continue + if any(room in output for room in ("Workshop", "Archive", "Chapel")): + output = bridge._command("courtyard", name="timmy", wait_ms=400).get("output", "") + continue + output = bridge._command("look", name="timmy", wait_ms=400).get("output", "") + + +def main(): + try: + bridge._disconnect("timmy") + except Exception: + pass + reset_timmy_to_gate() + bridge._save_bound_session_id(SESSION_ID) + bridge._connect(name="timmy", username=EVAL_USERNAME, password=EVAL_PASSWORD) + normalize_to_gate() + for step in WORLD_BASICS_STEPS: + bridge._command(step["command"], name="timmy", wait_ms=400) + bridge._disconnect("timmy") + + log_path = event_log_path(SESSION_ID) + meta_path = session_meta_path(SESSION_ID) + repo_trace = example_trace_path(REPO_ROOT) + repo_trace.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(log_path, repo_trace) + print(json.dumps({"session_id": SESSION_ID, "log_path": str(log_path), "meta_path": str(meta_path), "repo_trace": str(repo_trace)}, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/specs/evennia-training-data-contract.md b/specs/evennia-training-data-contract.md new file mode 100644 index 0000000..d4879bb --- /dev/null +++ b/specs/evennia-training-data-contract.md @@ -0,0 +1,34 @@ +# Evennia Training Data Contract + +Canonical local telemetry root: +- `~/.timmy/training-data/evennia/` + +Per-session layout: +- `~/.timmy/training-data/evennia/YYYYMMDD/.jsonl` +- `~/.timmy/training-data/evennia/YYYYMMDD/.meta.json` + +Meaning: +- `.jsonl` is the event stream emitted by the world lane +- `.meta.json` is the sidecar mapping record tying the world trace back to the Hermes session id and event log path + +Minimum event fields: +- `timestamp` +- `event` +- `actor` +- for commands: `command` +- for text output: `output` (possibly excerpted for compactness) + +Canonical mapping rule: +- Hermes session id is the primary join key +- the Evennia MCP bridge receives it through `mcp_evennia_bind_session` +- all subsequent world events for that run append to the matching `.jsonl` +- the sidecar meta file records `session_id` and `event_log_path` + +Why this matters: +- Hermes transcript and Evennia world trace can be paired later for DPO curation +- the world does not become an opaque side channel +- replay/eval tools can consume the same contract + +Benchmark boundary: +- Evennia traces represent the persistent-world lane +- NLE/MiniHack remains the benchmark lane diff --git a/tests/test_evennia_training.py b/tests/test_evennia_training.py new file mode 100644 index 0000000..41099da --- /dev/null +++ b/tests/test_evennia_training.py @@ -0,0 +1,38 @@ +import tempfile +import unittest +from pathlib import Path + +from evennia_tools.telemetry import event_log_path, session_meta_path, write_session_metadata +from evennia_tools.training import WORLD_BASICS_STEPS, example_eval_path, example_trace_path + + +class TestEvenniaTraining(unittest.TestCase): + def test_world_basics_sequence_is_stable(self): + self.assertEqual( + tuple(step["command"] for step in WORLD_BASICS_STEPS), + ("look", "enter", "workshop", "look", "courtyard", "chapel", "look Book of the Soul"), + ) + + def test_each_step_has_nonempty_expectations(self): + for step in WORLD_BASICS_STEPS: + self.assertTrue(step["expected"]) + + def test_example_paths_land_in_examples_dir(self): + root = Path("/tmp/repo") + self.assertEqual(example_trace_path(root).name, "world-basics-trace.example.jsonl") + self.assertEqual(example_eval_path(root).name, "world-basics-eval.example.json") + + def test_session_meta_path_suffix(self): + path = session_meta_path("sess1", "/tmp/evennia-test") + self.assertEqual(path.name, "sess1.meta.json") + + def test_write_session_metadata(self): + with tempfile.TemporaryDirectory() as td: + path = write_session_metadata("sess2", {"kind": "world"}, td) + self.assertTrue(path.exists()) + self.assertIn("sess2", path.read_text()) + self.assertIn(str(event_log_path("sess2", td)), path.read_text()) + + +if __name__ == "__main__": + unittest.main() diff --git a/training-data/evennia/examples/world-basics-eval.example.json b/training-data/evennia/examples/world-basics-eval.example.json new file mode 100644 index 0000000..dee17b1 --- /dev/null +++ b/training-data/evennia/examples/world-basics-eval.example.json @@ -0,0 +1,69 @@ +{ + "passed": true, + "checks": [ + { + "command": "look", + "expected": [ + "Gate" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mGate\u001b[0m\r\nA deliberate threshold into Timmy's world. The air is still here, as if entry itself matters.\r\n\u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m\r\n" + }, + { + "command": "enter", + "expected": [ + "Courtyard" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mCourtyard\u001b[0m\r\nThe central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness.\r\n\u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m\r\n" + }, + { + "command": "workshop", + "expected": [ + "Workshop", + "Workbench" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mWorkshop\u001b[0m\r\nBenches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts.\r\n\u001b[1m\u001b[37mExits:\u001b[0m courtyard\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m\r\n" + }, + { + "command": "look", + "expected": [ + "Workshop", + "Workbench" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mWorkshop\u001b[0m\r\nBenches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts.\r\n\u001b[1m\u001b[37mExits:\u001b[0m courtyard\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m\r\n" + }, + { + "command": "courtyard", + "expected": [ + "Courtyard", + "Map Table" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mCourtyard\u001b[0m\r\nThe central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness.\r\n\u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m\r\n" + }, + { + "command": "chapel", + "expected": [ + "Chapel", + "Prayer Wall" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mChapel\u001b[0m\r\nA quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried.\r\n\u001b[1m\u001b[37mExits:\u001b[0m courtyard\r\n\u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m\r\n" + }, + { + "command": "look Book of the Soul", + "expected": [ + "Book of the Soul", + "doctrinal anchor" + ], + "passed": true, + "output_excerpt": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m\r\nA doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m\r\n" + } + ], + "orientation": true, + "navigation": true, + "object_inspection": true +} \ No newline at end of file diff --git a/training-data/evennia/examples/world-basics-trace.example.jsonl b/training-data/evennia/examples/world-basics-trace.example.jsonl new file mode 100644 index 0000000..3b0417c --- /dev/null +++ b/training-data/evennia/examples/world-basics-trace.example.jsonl @@ -0,0 +1,52 @@ +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:10:27.151397+00:00"} +{"event": "connect", "actor": "Timmy", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect \u001b[0m If you n...", "timestamp": "2026-03-28T19:10:29.161065+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:10:29.566853+00:00"} +{"event": "command", "actor": "timmy", "command": "enter", "output": "Command 'enter' is not available. Maybe you meant \"chardelete\" or \"emote\"?\u001b[0m", "timestamp": "2026-03-28T19:10:29.969554+00:00"} +{"event": "command", "actor": "timmy", "command": "workshop", "output": "Command 'workshop' is not available. Maybe you meant \"who\"?\u001b[0m", "timestamp": "2026-03-28T19:10:30.375076+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:10:30.776821+00:00"} +{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:10:31.182522+00:00"} +{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:10:31.588268+00:00"} +{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:10:31.993857+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:10:31.994155+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:12:41.096394+00:00"} +{"event": "connect", "actor": "Timmy", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect \u001b[0m If you n...", "timestamp": "2026-03-28T19:12:43.109101+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:12:43.514836+00:00"} +{"event": "command", "actor": "timmy", "command": "enter", "output": "Command 'enter' is not available. Maybe you meant \"chardelete\" or \"emote\"?\u001b[0m", "timestamp": "2026-03-28T19:12:43.920420+00:00"} +{"event": "command", "actor": "timmy", "command": "workshop", "output": "Command 'workshop' is not available. Maybe you meant \"who\"?\u001b[0m", "timestamp": "2026-03-28T19:12:44.323687+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:12:44.729334+00:00"} +{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:12:45.134875+00:00"} +{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:12:45.535855+00:00"} +{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:12:45.941394+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:12:45.941887+00:00"} +{"event": "connect", "actor": "Timmy", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect \u001b[0m If you n...", "timestamp": "2026-03-28T19:14:18.509613+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:14:18.916211+00:00"} +{"event": "command", "actor": "timmy", "command": "enter", "output": "Command 'enter' is not available. Maybe you meant \"chardelete\" or \"emote\"?\u001b[0m", "timestamp": "2026-03-28T19:14:19.319885+00:00"} +{"event": "command", "actor": "timmy", "command": "workshop", "output": "Command 'workshop' is not available. Maybe you meant \"who\"?\u001b[0m", "timestamp": "2026-03-28T19:14:19.720596+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:14:20.126229+00:00"} +{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:14:20.529991+00:00"} +{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:14:20.935654+00:00"} +{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:14:21.341176+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:14:21.341799+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:14:22.231687+00:00"} +{"event": "connect", "actor": "TimmyEval", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect \u001b[0m If you n...", "timestamp": "2026-03-28T19:17:31.862892+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mGate\u001b[0m A deliberate threshold into Timmy's world. The air is still here, as if entry itself matters. \u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m", "timestamp": "2026-03-28T19:17:32.269727+00:00"} +{"event": "command", "actor": "timmy", "command": "enter", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:17:32.675348+00:00"} +{"event": "command", "actor": "timmy", "command": "workshop", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:17:33.078265+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:17:33.484067+00:00"} +{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:17:33.889665+00:00"} +{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:17:34.295231+00:00"} +{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:17:34.700773+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:17:34.701330+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:17:35.654223+00:00"} +{"event": "connect", "actor": "TimmyEval", "output": "\u001b[1m\u001b[34m==============================================================\u001b[0m Welcome to \u001b[1m\u001b[32mtimmy_world\u001b[0m, version 6.0.0! If you have an existing account, connect to it by typing: \u001b[1m\u001b[37mconnect \u001b[0m If you n...", "timestamp": "2026-03-28T19:31:52.782015+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:31:53.394240+00:00"} +{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:31:53.796861+00:00"} +{"event": "command", "actor": "timmy", "command": "gate", "output": "\u001b[1m\u001b[36mGate\u001b[0m A deliberate threshold into Timmy's world. The air is still here, as if entry itself matters. \u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m", "timestamp": "2026-03-28T19:31:54.202470+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mGate\u001b[0m A deliberate threshold into Timmy's world. The air is still here, as if entry itself matters. \u001b[1m\u001b[37mExits:\u001b[0m enter\u001b[0m", "timestamp": "2026-03-28T19:31:54.605358+00:00"} +{"event": "command", "actor": "timmy", "command": "enter", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:31:55.007358+00:00"} +{"event": "command", "actor": "timmy", "command": "workshop", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:31:55.409107+00:00"} +{"event": "command", "actor": "timmy", "command": "look", "output": "\u001b[1m\u001b[36mWorkshop\u001b[0m Benches, tools, half-built mechanisms, and active prototypes fill the room. This is where ideas become artifacts. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Workbench\u001b[0m", "timestamp": "2026-03-28T19:31:55.814849+00:00"} +{"event": "command", "actor": "timmy", "command": "courtyard", "output": "\u001b[1m\u001b[36mCourtyard\u001b[0m The central open court of Timmy's place. Paths lead outward to work, memory, prayer, and watchfulness. \u001b[1m\u001b[37mExits:\u001b[0m gate, workshop, archive, and chapel \u001b[1m\u001b[37mYou see:\u001b[0m a Map Table\u001b[0m", "timestamp": "2026-03-28T19:31:56.220756+00:00"} +{"event": "command", "actor": "timmy", "command": "chapel", "output": "\u001b[1m\u001b[36mChapel\u001b[0m A quiet room set apart for prayer, conscience, grief, and right alignment. The tone here is gentle and unhurried. \u001b[1m\u001b[37mExits:\u001b[0m courtyard \u001b[1m\u001b[37mYou see:\u001b[0m a Book of the Soul and a Prayer Wall\u001b[0m", "timestamp": "2026-03-28T19:31:56.626349+00:00"} +{"event": "command", "actor": "timmy", "command": "look Book of the Soul", "output": "\u001b[1m\u001b[36mBook of the Soul\u001b[0m A doctrinal anchor. It is not decorative; it is a reference point.\u001b[0m", "timestamp": "2026-03-28T19:31:57.029105+00:00"} +{"event": "disconnect", "actor": "timmy", "timestamp": "2026-03-28T19:31:57.029536+00:00"}