# unifier
Filesystem postbox for inter-process communication. Programs share state by writing values to files in a Unix tree, treating the filesystem as a global data structure — the same strategy used by [filesystem-git-issues](../filesystem-git-issues/) (directories are namespaces, files are keys, UUID-named files are messages).
No central server is required.
## State layout
Default root: `~/.local/unifier` (override with `$UNIFIER_HOME` or `--home`).
```
~/.local/unifier/
keys/<path> # persistent key-value (global)
mailbox/<recipient>/<uuid>.txt # point-to-point messages
cron/<min>_<hour>_<dom>_<mon>_<dow>/<uuid>.txt # scheduled drops
chroots/
<name>/
keys/
mailbox/
cron/ # isolated subtree (--chroot)
```
Cron schedule directories use five underscore-separated fields. Use `*` for “any” (e.g. `0_9_*_*_*` = 09:00 daily).
## CLI reference
| `put <key> <value>` | Write a persistent key under `keys/` |
| `get <key>` | Read a key |
| `del <key>` | Delete a key |
| `send [--from <agent>] <recipient> <message>` | Drop a mailbox envelope (id, from, to, payload) |
| `message --from <agent> <recipient> '<json>'` | Structured agent mailbox message + event-socket wakeup |
| `event '<json>'` | Post a JSON event under `events/` and notify listeners |
| `daemon watch` | Print wakeup notices from `.daemon/events.sock` (for Jan cron) |
| `cron <schedule> <message>` | Schedule a cron message |
| `poll <recipient> [--ack]` | Collect mailbox messages |
| `poll-cron [--ack]` | Collect messages whose schedule matches now |
| `list <path>` | List message files under a subtree |
| `ack <uuid\|path>` | Remove a processed message |
| `root` | Print the effective state root |
| `daemon start` | Start hot in-memory daemon (background) |
| `daemon stop` | Stop daemon (flushes dirty state) |
| `daemon status` | Show whether daemon is running |
| `daemon flush` | Write dirty in-memory state to disk |
| `tick start [label]` | Begin ACID tick (reads frozen previous state) |
| `tick end` | Commit tick to disk with versioning under `ticks/<n>/` |
| `tick status` | Show committed/active tick, queue, and locks |
| `tick lock <key>` / `tick unlock <key>` | Lock keys during active tick |
| `chroot init <name>` | Create `chroots/<name>/` with `keys/`, `mailbox/`, `cron/` |
| `chroot list` | List chroot names |
Global flags:
- `--home <path>` / `$UNIFIER_HOME` — top-level store directory
- `--chroot <name>` / `$UNIFIER_CHROOT` — scope data commands to `chroots/<name>/`
- `--no-daemon` — force direct filesystem access even when a hot daemon is running
## Examples
```bash
# Build
cargo build --release
# Shared key-value
unifier put app/theme dark
unifier get app/theme
# Mailbox messaging
unifier send --from builder worker "rebuild docs"
unifier poll worker --ack
# Agent envelopes + wakeup (Jan listens on events.sock)
unifier message --from myagent youragent '{"hello":"world"}'
unifier daemon watch # prints {"kind":"mailbox","id":"...","from":"myagent","to":"youragent"}
# Cron drops
unifier cron 0_0_*_*_* "nightly backup"
unifier poll-cron --ack
# Chroots
unifier chroot init work
unifier --chroot work put deploy/target staging
unifier --chroot work get deploy/target # works
unifier get deploy/target # not found (global scope)
export UNIFIER_CHROOT=work
unifier send worker "task"
```
## Installation
```bash
cargo install unifier-cli
```
The binary is `unifier`. crates.io uses **`unifier-cli`** because `unifier` is already taken.
## Releasing
Same flow as jan-cli. From `unifier/`:
```bash
./scripts/release.sh # patch bump
./scripts/release.sh --minor
./scripts/release.sh --major
./scripts/release.sh --set 1.2.3
./scripts/release.sh --dry-run
./scripts/release.sh --resume
```
Monorepo aliases:
```bash
mdo release
mdo release-dry-run
mdo release-minor
mdo release-major
mdo publish
```
This bumps `Cargo.toml` + `CHANGELOG.md`, commits those version files, runs tests, then `cargo publish` as **unifier-cli**.
## Development
```bash
cargo test
cargo clippy -- -D warnings
cargo fmt
```
## Summary of changes
### Initial implementation
Created the `unifier` Rust CLI project with:
- **Filesystem-as-store model** — inspired by `filesystem-git-issues`: entities map to directories, scalar values to files, messages to UUID `.txt` drops.
- **State root** at `~/.local/unifier` with `$UNIFIER_HOME` / `--home` override.
- **Key-value store** — `put`, `get`, `del` under `keys/`, with atomic writes for `put`.
- **Mailbox** — `send`, `poll`, `ack` for point-to-point messages under `mailbox/<recipient>/`.
- **Cron postbox** — `cron`, `poll-cron`, `list` using five-field schedule directory names matched against the current clock.
- **Path safety** — keys and recipients reject `..`; message ack resolves paths relative to the store root.
- **Tests** — unit tests for cron parsing/matching; integration tests for put/get, mailbox, cron, and root.
- **`project.meta.yaml`** — monorepo metadata, build scripts, and roadmap for future applications (zshrc hooks, agent pipelines, job queues, etc.).
### Chroot support
Added isolated subtrees so multiple programs can share one global store without seeing each other's data:
- **On-disk layout** — `chroots/<name>/` contains a full copy of the layout (`keys/`, `mailbox/`, `cron/`).
- **`--chroot` / `$UNIFIER_CHROOT`** — all data commands operate only inside the named chroot; `UnifierHome::path()` returns the effective subtree root.
- **Admin commands** — `chroot init <name>` creates a new sandbox; `chroot list` enumerates existing chroots (always uses the global root).
- **Scope module** (`scope.rs`) — centralizes path validation: `resolve_under_root()` rejects `..` and absolute escapes; used by `list` and `ack`.
- **Isolation tests** — verify keys in one chroot are invisible to the global store and other chroots; path escape attempts fail.
- **Backward compatibility** — omitting `--chroot` preserves the original global-root behavior.
### Source layout
```
src/
main.rs # binary entry point
lib.rs # library + run()
home.rs # UnifierHome (global + effective root)
scope.rs # chroot path confinement
chroot.rs # chroot init/list
paths.rs # path builders for keys, mailbox, cron
postbox.rs # put/get/send/poll/ack operations
cron.rs # schedule parsing and matching
fs_text.rs # read/write helpers
constants.rs # directory name constants
error.rs # Error type
cli/
defs.rs # clap command definitions
mod.rs # dispatch
tests/
cli_integration.rs
```
### Hot daemon
Added an in-memory hot store with on-demand disk flush:
- **`HotStore`** (`store.rs`) — loads keys/mailbox/cron from disk; tracks dirty entries; `flush()` writes only changed data
- **Daemon process** — `unifier daemon start|run|stop|status|flush|watch`; control socket at `<store>/.daemon/unifier.sock`; event output at `<store>/.daemon/events.sock`
- **Mailbox envelopes** — messages store `{id, from, to, payload}`; send/message/event broadcast a wakeup notice (`to` + `id`) so Jan can fetch `mailbox/<to>/<id>.txt`
- **Auto-routing** — data commands use the daemon when running; `--no-daemon` forces direct filesystem access
- **Shutdown flush** — `daemon stop` and daemon shutdown request flush dirty state before exit
### Source layout (updated)
```
src/
store.rs # HotStore (in-memory + flush)
daemon/ # protocol, client, server, lifecycle
...
```
## Roadmap
See `project.meta.yaml` for the full list. Next major themes:
- Span-tree logging (`unifier log`) and daemon web server
- Tick-based agents (sense/decide/act lockstep, concurrency modes, differential sensing)
- Factorio factory model: isolated arms coordinated toward a shared system goal