# 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)
