Ambient system companions over one privacy-preserving signal daemon (aggregate-only, no keystroke content): a git-driven terminal garden and IOKit hardware collectors. ambient daemon macos privacy terminal

Commit 109aad5d6b

109aad5d6bc44031975f68a798e116214994e9d4

parent: b60200227d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-04 17:27 UTC

Document the menu-bar face

README: three faces built rather than two, a phase-plan entry, the workspace
layout, how to launch the app and add it to Login Items, and the pet's own
state path with why it is not beside the socket.

The design document is corrected to match what was built: state is a
line-based pet.state rather than pet.json, and the Swift side carries two tests
rather than none. A spec that disagrees with the code is worse than no spec.

Closes #19
CHANGELOG.org +14
@@ -11,6 +11,20 @@ renumbered or removed.
1111
1212* Unreleased
1313
14** Added
15- =menubar-pet=, the third face: a menu-bar pet with one life. It ages, neglect
16 kills it over a week through visible stages, and death is permanent for that
17 pet. A new one arrives when you come back, and the dead are kept in a
18 cemetery that is never pruned.
19- =pet-life=, the Rust crate behind it. Everything derives from timestamps
20 rather than accumulating, so death happens at =last_activity + 7 days= and
21 not when a program noticed — sleep, reboots, daemon restarts and the app not
22 running change nothing. Birth requires activity, so a fortnight away leaves
23 one grave rather than a chain of pets born, never fed and dead.
24- =History::open_read_only= and =History::last_activity_ms=. Read-only because
25 the ordinary open prunes, and a face polling every five seconds must not
26 delete the daemon's history as a side effect of reading it.
27
1428** Fixed
1529- The privacy ship-gate's forbidden-symbol scan missed =${BUFFER}=. The braced
1630 form is ordinary zsh and does not contain the substring =$BUFFER=, so the
README.md +38 −6
@@ -20,10 +20,10 @@ the others.
2020 ▼ ▼ ▼ ▼ ▼
2121 terminal-pet garden menubar-pet sonifier wallpaperd
2222 (TUI) (TUI) (SwiftUI) (AVAudio) (image/window)
23 ✅ built ✅ built planned planned planned
23 ✅ built ✅ built ✅ built planned planned
2424```
2525
26**Two faces are built.** The other three are the roadmap, not the product —
26**Three faces are built.** The other two are the roadmap, not the product —
2727see "Phase plan". Every face draws from the **same signal bus**: renderers
2828never poll hardware, never read the shell, they subscribe over a local Unix
2929socket (`$XDG_RUNTIME_DIR/signald.sock`). That buys one privacy boundary to
@@ -134,12 +134,19 @@ ambient-companions/
134134│ ├── terminal-garden/ # first renderer: git aggregates as plots
135135│ │ ├── src/lib.rs # signals → plots → render (unit-tested)
136136│ │ └── src/main.rs # live subscribe + redraw loop
137│ └── terminal-pet/ # second renderer: shell + machine + health
138│ ├── src/lib.rs # signals → PetState → render (unit-tested)
139│ └── src/main.rs # live subscribe + redraw loop
137│ ├── terminal-pet/ # second renderer: shell + machine + health
138│ │ ├── src/lib.rs # signals → PetState → render (unit-tested)
139│ │ └── src/main.rs # live subscribe + redraw loop
140│ └── pet-life/ # the one-life mechanic: ageing, death, cemetery
141│ ├── src/lib.rs # pure over timestamps (unit-tested)
142│ └── src/main.rs # one-shot: prints state as JSON
140143├── shell-hooks/ # zsh hooks: aggregate-only terminal collector
141144│ ├── signald-hooks.zsh
142145│ └── README.md
146├── menubar-pet/ # SwiftPM sibling: the third face
147│ ├── Sources/PetKit/ # the pet-life JSON contract (tested)
148│ ├── Sources/menubar-pet/ # status item + timer + menu
149│ └── scripts/bundle.sh # assembles the .app (LSUIElement)
143150└── macos-collector/ # SwiftPM sibling (NOT in the cargo workspace):
144151 ├── Package.swift # macOS IOKit hardware collector (Phase 3)
145152 ├── README.md # the Swift↔Rust wire contract (byte layout)
@@ -238,7 +245,15 @@ and — for the differential test — `zpty` modules ship with zsh).
238245 `SignalName::CollectorUp`, one per `Source` — the schema's `v5` addition —
239246 so a dead collector reaches a face rather than a log file. `terminal-pet`
240247 renders it: a pet that cannot feel its own hardware looks sick.
241- **Phase 3 renderer — menu-bar permadeath pet.** *Post-1.0.*
248- **Phase 3 renderer — menu-bar permadeath pet. ✅ Done.**
249 `menubar-pet/` is a sibling SwiftPM package: a status item, a timer and a
250 menu. One life — it ages, neglect kills it over a week through visible
251 stages, and death is permanent for that pet. A new one arrives when you come
252 back, and the old one is remembered in a cemetery that is never pruned.
253 The mechanic lives in the `pet-life` Rust crate, which the app runs on a
254 timer; the app decodes no wire and opens no socket, so there is still one
255 decoder and one privacy boundary. Everything derives from timestamps, so
256 sleep, reboots and the app not running change nothing.
242257- **Phase 4** — sonification (SSH-utility first, then continuous). *Post-1.0.*
243258- **Phase 5** — live wallpaper (homelab, Path A) + e-ink/poster reuse.
244259 *Post-1.0.*
@@ -275,6 +290,17 @@ terminal-garden # your repos, as plants that grow and wilt
275290terminal-pet # the shell and the machine, as a mood
276291```
277292
293The third face lives in the menu bar. Launch `menubar-pet.app` from the
294Homebrew prefix, and add it under System Settings → General → Login Items to
295have it there every day:
296
297```sh
298open "$(brew --prefix)/opt/ambient-companions/menubar-pet.app"
299```
300
301It has one life. Neglect it for a week — no typing, no commits — and it dies
302for good, and the next one starts when you come back.
303
278304`brew services` logs to `$(brew --prefix)/var/log/`. To run the agent by hand
279305instead, `packaging/net.krz.signald.plist` is a launchd template; its header
280306comment carries the `sed` line that fills in the paths and the `launchctl load`
@@ -292,6 +318,12 @@ and every renderer, and its test pins the values below;
292318| history db | `$XDG_RUNTIME_DIR/signald.sqlite` | `~/.local/state/signald/signald.sqlite` |
293319| terminal spool | `$XDG_RUNTIME_DIR/terminal.spool` | `~/.local/state/signald/terminal.spool` |
294320
321The menu-bar pet keeps its own state at
322`$XDG_DATA_HOME/ambient-companions/pet.state`, else
323`~/.local/share/ambient-companions/pet.state`. Deliberately not beside the
324socket: those paths follow `$XDG_RUNTIME_DIR` where it is set, which is a tmpfs
325wiped every reboot, and a graveyard a reboot can erase is not a graveyard.
326
295327The db and the spool are derived from the socket's directory, so `--socket`
296328moves all three together. Override individually with `--db` and `--spool`, and
297329the spool from the shell side with `$SIGNALD_SPOOL` — it must match whatever
docs/superpowers/specs/2026-09-04-menubar-pet-design.md +19 −7
@@ -125,8 +125,14 @@ not mutate the daemon's store.
125125
126126### Persistence
127127
128`$XDG_DATA_HOME/ambient-companions/pet.json`, else
129`~/.local/share/ambient-companions/pet.json`.
128`$XDG_DATA_HOME/ambient-companions/pet.state`, else
129`~/.local/share/ambient-companions/pet.state`.
130
131Implemented as a line-based format rather than the JSON this design first
132called for. Only `pet-life` reads it, so a hand-rolled JSON *parser* would have
133been all risk and no benefit; the terminal spool already stores plain
134whitespace-separated records for the same reason. JSON remains the contract
135handed to the app, and that direction only needs emitting.
130136
131137Deliberately **not** under the daemon's state directory. That path derives from
132138the socket, which follows `$XDG_RUNTIME_DIR` when set — a tmpfs on systems that
@@ -187,15 +193,21 @@ are deterministic and instant — no sleeping, no clock mocking.
187193- `last_activity_ms` against an in-memory history fixture, including the case
188194 where retention has pruned everything.
189195
190The Swift side stays untested: it is a status item, a timer and a menu, and the
191logic lives in Rust. What is checked is that the bundle assembles — the `.app`
192exists and its `Info.plist` carries `LSUIElement`. Claiming UI coverage that
193does not exist would be worse than saying this plainly.
196The Swift side is almost untested: it is a status item, a timer and a menu, and
197the logic lives in Rust. Two things are checked. The bundle assembles — the
198`.app` exists and its `Info.plist` carries `LSUIElement`. And `PetKit` decodes
199the exact bytes `pet-life` pins, including the dead-pet case where `name` is
200null, which a plain `String` would reject. That second one departs from this
201design deliberately: the Rust-to-Swift contract has drifted twice in this
202project and a pinned fixture caught it both times.
203
204The menu bar's actual appearance is not verified by anything, and cannot be
205without a human looking at it.
194206
195207## Documentation
196208
197209- README: the faces diagram gains a third built face; a phase-plan entry;
198 workspace layout; launch instructions; the Paths table gains `pet.json` with
210 workspace layout; launch instructions; the Paths table gains `pet.state` with
199211 a note on why it is not under the runtime directory.
200212- CHANGELOG: an Unreleased entry.
201213