unifier-cli 0.2.0

Filesystem postbox for inter-process communication via a Unix tree
Documentation
# 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

| Command | Description |
|---------|-------------|
| `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