This commit was merged in pull request #359.
This commit is contained in:
166
docs/adr/023-workshop-presence-schema.md
Normal file
166
docs/adr/023-workshop-presence-schema.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# 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).
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user