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 86106aa0f9

86106aa0f91b56a923ae11ebdaa3a0173ed272b3

parent: da37447dbf

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-04 15:43 UTC

README: say what 1.0 is and stop promising five faces

The headline claimed five faces and two exist. The diagram now marks which,
and the three unbuilt renderers are post-1.0 rather than "out of scope",
which read as abandoned.

The phase plan gains a v1.0 entry stating the one claim 1.0 makes: the daemon
runs unattended, recovers from its own failures, and reports when it cannot.

Workspace layout gains signal-client, terminal-pet and supervisor.rs, and the
install section names both faces.

Closes #12
README.md +33 −13
@@ -20,12 +20,14 @@ the others.
2020 ▼ ▼ ▼ ▼ ▼
2121 terminal-pet garden menubar-pet sonifier wallpaperd
2222 (TUI) (TUI) (SwiftUI) (AVAudio) (image/window)
23 ✅ built ✅ built planned planned planned
2324```
2425
25All five draw from the **same signal bus**. Renderers never poll hardware,
26never read the shell — they subscribe over a local Unix socket
27(`$XDG_RUNTIME_DIR/signald.sock`). That buys one privacy boundary to defend
28instead of five, cheap renderers, and reuse of the collection layer into
26**Two faces are built.** The other three are the roadmap, not the product —
27see "Phase plan". Every face draws from the **same signal bus**: renderers
28never poll hardware, never read the shell, they subscribe over a local Unix
29socket (`$XDG_RUNTIME_DIR/signald.sock`). That buys one privacy boundary to
30defend instead of five, cheap renderers, and reuse of the collection layer into
2931non-toy outputs (e-ink dashboards, printed posters).
3032
3133## The privacy boundary, enforced by construction
@@ -120,6 +122,7 @@ ambient-companions/
120122│ │ └── tests/privacy_invariant.rs
121123│ ├── signald/ # the daemon (collectors + live fan-out)
122124│ │ ├── src/lib.rs # git + terminal collectors, hardware ingest, publish
125│ │ ├── src/supervisor.rs # collector liveness, restart, CollectorUp
123126│ │ ├── src/history.rs # SQLite (WAL) history store + recent() query
124127│ │ ├── src/hub.rs # last-value cache + live fan-out
125128│ │ ├── src/main.rs # CLI, producer loop, self-attestation
@@ -127,8 +130,12 @@ ambient-companions/
127130│ │ ├── tests/streaming.rs # last-value cache + live update
128131│ │ ├── tests/hardware_ingest.rs # collector frames reach a subscriber
129132│ │ └── tests/differential_secret_typing.rs # the full privacy ship-gate
130│ └── terminal-garden/ # first renderer: a socket subscriber
131│ ├── src/lib.rs # signals → plots → render (unit-tested)
133│ ├── signal-client/ # socket path + frame iteration (shared)
134│ ├── terminal-garden/ # first renderer: git aggregates as plots
135│ │ ├── src/lib.rs # signals → plots → render (unit-tested)
136│ │ └── src/main.rs # live subscribe + redraw loop
137│ └── terminal-pet/ # second renderer: shell + machine + health
138│ ├── src/lib.rs # signals → PetState → render (unit-tested)
132139│ └── src/main.rs # live subscribe + redraw loop
133140├── shell-hooks/ # zsh hooks: aggregate-only terminal collector
134141│ ├── signald-hooks.zsh
@@ -185,8 +192,8 @@ and — for the differential test — `zpty` modules ship with zsh).
185192 the key, and appends `<epoch_ms> <keys> <session_seconds> <session_id>` count
186193 records (numbers only) to a spool; `collectors::terminal` consumes the spool
187194 and derives `keys_per_min` and `session_seconds`. The **differential
188 secret-typing test is active and passing** — the privacy ship-gate. *The
189 Terminal Pet renderer is still todo.*
195 secret-typing test is active and passing** — the privacy ship-gate. The
196 Terminal Pet renderer landed in v1.0.
190197- **Phase 3 — macOS IOKit hardware collector. ✅ Done (v0.3).**
191198 `macos-collector/` (a sibling Swift package, `swift build`) reads aggregate
192199 hardware scalars via **IOKit only — no `powermetrics`, no root**: CPU load
@@ -198,7 +205,7 @@ and — for the differential test — `zpty` modules ship with zsh).
198205 documented and pinned by a shared canonical-frame test on both sides
199206 (`crates/signal-schema/tests/hardware_wire.rs` decodes the exact bytes the
200207 Swift encoder commits to in `macos-collector/Tests/.../WireTests.swift`).
201 *Still todo:* the menu-bar permadeath pet.
208 *The menu-bar permadeath pet is post-1.0.*
202209- **Hardware ingest. ✅ Done (v0.4).**
203210 `signald` spawns `macos-collector` as a child (`--collector <path>`, or found
204211 on `PATH`) and reads its stdout with the same `wire::read_frame` the socket
@@ -220,9 +227,21 @@ and — for the differential test — `zpty` modules ship with zsh).
220227 roots it was told to watch. A reader now **skips** a frame it cannot decode
221228 instead of dying on it (`wire::Frame::Skipped`), so an older renderer keeps
222229 working against a newer daemon. References to an uncommitted spec are gone.
223- **Phase 4** — sonification (SSH-utility first, then continuous). *Out of scope.*
230- **v1.0 — trust it unattended. ✅ Done (v1.0.0).**
231 1.0 is not feature completeness and not an API promise to anyone else. It is
232 one claim: **the daemon runs unattended for weeks, recovers from its own
233 failures, and says so when it cannot.**
234 `signald/src/supervisor.rs` owns collector liveness. A panic in the git or
235 terminal collector costs one tick instead of the producer thread; the
236 hardware child is respawned with backoff; `run_git` kills a `git` that hangs,
237 because a Rust thread cannot be. Health rides the bus as
238 `SignalName::CollectorUp`, one per `Source` — the schema's `v5` addition —
239 so a dead collector reaches a face rather than a log file. `terminal-pet`
240 renders it: a pet that cannot feel its own hardware looks sick.
241- **Phase 3 renderer — menu-bar permadeath pet.** *Post-1.0.*
242- **Phase 4** — sonification (SSH-utility first, then continuous). *Post-1.0.*
224243- **Phase 5** — live wallpaper (homelab, Path A) + e-ink/poster reuse.
225 *Out of scope.*
244 *Post-1.0.*
226245
227246## Install
228247
@@ -249,10 +268,11 @@ Then add one line to `.zshrc` for the terminal collector:
249268source "$(brew --prefix)/share/ambient-companions/signald-hooks.zsh"
250269```
251270
252Open a new shell and watch the garden:
271Open a new shell and watch a face:
253272
254273```sh
255terminal-garden
274terminal-garden # your repos, as plants that grow and wilt
275terminal-pet # the shell and the machine, as a mood
256276```
257277
258278`brew services` logs to `$(brew --prefix)/var/log/`. To run the agent by hand