# ADR-023: Workshop Presence Schema **Status:** Accepted **Date:** 2026-03-18 **Issue:** #265 **Epic:** #222 (The Workshop) ## Context The Workshop renders Timmy as a living presence in a 3D world. It needs to know what Timmy is doing *right now* — his working memory, not his full identity or history. This schema defines the contract between Timmy (writer) and the Workshop (reader). ### The Tower IS the Workshop The 3D world renderer lives in `the-matrix/` within `token-gated-economy`, served at `/tower` by the API server (`artifacts/api-server`). This is the canonical Workshop scene — not a generic Matrix visualization. All Workshop phase issues (#361, #362, #363) target that codebase. No separate `alexanderwhitestone.com` scaffold is needed until production deploy. The `workshop-state` spec (#360) is consumed by the API server via a file-watch mechanism, bridging Timmy's presence into the 3D scene. Design principles: - **Working memory, not long-term memory.** Present tense only. - **Written as side effect of work.** Not a separate obligation. - **Liveness is mandatory.** Stale = "not home," shown honestly. - **Schema is the contract.** Keep it minimal and stable. ## Decision ### File Location `~/.timmy/presence.json` JSON chosen over YAML for predictable parsing by both Python and JavaScript (the Workshop frontend). The Workshop reads this file via the WebSocket bridge (#243) or polls it directly during development. ### Schema (v1) ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Timmy Presence State", "description": "Working memory surface for the Workshop renderer", "type": "object", "required": ["version", "liveness", "current_focus"], "properties": { "version": { "type": "integer", "const": 1, "description": "Schema version for forward compatibility" }, "liveness": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of last update. If stale (>5min), Timmy is not home." }, "current_focus": { "type": "string", "description": "One sentence: what Timmy is doing right now. Empty string = idle." }, "active_threads": { "type": "array", "maxItems": 10, "description": "Current work items Timmy is tracking", "items": { "type": "object", "required": ["type", "ref", "status"], "properties": { "type": { "type": "string", "enum": ["pr_review", "issue", "conversation", "research", "thinking"] }, "ref": { "type": "string", "description": "Reference identifier (issue #, PR #, topic name)" }, "status": { "type": "string", "enum": ["active", "idle", "blocked", "completed"] } } } }, "recent_events": { "type": "array", "maxItems": 20, "description": "Recent events, newest first. Capped at 20.", "items": { "type": "object", "required": ["timestamp", "event"], "properties": { "timestamp": { "type": "string", "format": "date-time" }, "event": { "type": "string", "description": "Brief description of what happened" } } } }, "concerns": { "type": "array", "maxItems": 5, "description": "Things Timmy is uncertain or worried about. Flat list, no severity.", "items": { "type": "string" } }, "mood": { "type": "string", "enum": ["focused", "exploring", "uncertain", "excited", "tired", "idle"], "description": "Emotional texture for the Workshop to render. Optional." } } } ``` ### Example ```json { "version": 1, "liveness": "2026-03-18T21:47:12Z", "current_focus": "Reviewing PR #267 — stream adapter for Gitea webhooks", "active_threads": [ {"type": "pr_review", "ref": "#267", "status": "active"}, {"type": "issue", "ref": "#239", "status": "idle"}, {"type": "conversation", "ref": "hermes-consultation", "status": "idle"} ], "recent_events": [ {"timestamp": "2026-03-18T21:45:00Z", "event": "Completed PR review for #265"}, {"timestamp": "2026-03-18T21:30:00Z", "event": "Filed issue #268 — flaky test in sensory loop"} ], "concerns": [ "WebSocket reconnection logic feels brittle", "Not sure the barks system handles uncertainty well yet" ], "mood": "focused" } ``` ### Design Answers | Question | Answer | |---|---| | File format | JSON (predictable for JS + Python, no YAML parser needed in browser) | | recent_events cap | 20 entries max, oldest dropped | | concerns severity | Flat list, no priority. Keep it simple. | | File location | `~/.timmy/presence.json` — accessible to Workshop via bridge | | Staleness threshold | 5 minutes without liveness update = "not home" | | mood field | Optional. Workshop can render visual cues (color, animation) | ## Consequences - **Timmy's agent loop** must write `~/.timmy/presence.json` as a side effect of work. This is a hook at the end of each cycle, not a daemon. - **The Workshop frontend** reads this file and renders accordingly. Stale liveness → dim the wizard, show "away" state. - **The WebSocket bridge** (#243) watches this file and pushes changes to connected Workshop clients. - **Schema is versioned.** Breaking changes increment the version field. Workshop must handle unknown versions gracefully (show raw data or "unknown state"). ## Related - #222 — Workshop epic - #243 — WebSocket bridge (transports this state) - #239 — Sensory loop (feeds into state) - #242 — 3D world (consumes this state for rendering) - #246 — Confidence as visible trait (mood field serves this) - #360 — Workshop-state spec (consumed by API via file-watch) - #361, #362, #363 — Workshop phase issues (target `the-matrix/`) - #372 — The Tower IS the Workshop (canonical connection)