# car-registry

File-based agent registry for cross-process discovery, used by menubar
/ tray UIs that want to enumerate locally running agents without
running a coordinating daemon.

## How it works

Each agent owns one JSON file under `~/.car/registry/<name>.json`. On
startup it calls `AgentRegistry::register`, which atomically writes the
entry (temp file + rename). Periodically it calls `heartbeat` to bump
`last_heartbeat_at`. On shutdown — or after a panic, on next launch —
`unregister` removes the file. UIs poll with `list`, optionally
calling `reap_stale(timeout)` first to drop entries whose last
heartbeat is too old.

There is no daemon, no shared lock, no IPC. The directory is the
shared state; atomic writes give consistency.

## API

```rust
use car_registry::{AgentEntry, AgentRegistry, AgentStatus};

let reg = AgentRegistry::new()?;             // ~/.car/registry/
reg.register(AgentEntry {
    name: "musicart".into(),
    display_name: Some("MusicArt".into()),
    dashboard_url: "http://127.0.0.1:8723".into(),
    status: AgentStatus::Running,
    port: Some(8723),
    pid: Some(std::process::id()),
    registered_at: 0,                        // 0 = now
    last_heartbeat_at: 0,
})?;

reg.heartbeat("musicart")?;
let agents = reg.list()?;
reg.reap_stale(std::time::Duration::from_secs(120))?;
reg.unregister("musicart")?;
```

## File layout

```
~/.car/registry/
  musicart.json
  flyx.json
  car-host.json
```

Each file is a `AgentEntry` serialized as JSON. Filenames mirror the
agent name; non-filesystem-safe characters are rejected at register
time.

## Why a separate crate

Agents that consume the registry don't necessarily depend on
`car-engine` (a tray UI does not need the runtime). Splitting the
registry off keeps it a thin dependency.

## Tracking issue

#111 — agent registry + menubar discovery.
