# Chronis
**The agent-native task CLI.** Event-sourced, TOON-optimized, built for AI agents and humans alike.
Inspired by [beads_rust](https://github.com/Dicklesworthstone/beads_rust) — the pioneering local-first issue tracker that proved agents can self-manage tasks via CLI. Chronis takes that idea further: every action is an immutable event, state is derived from projections, and output is optimized for LLM token consumption.
## Why Chronis?
We built hundreds of tasks with beads_rust — 62+ issues across 280+ agent iterations. It works. But we kept hitting walls:
| No event history — "how long was this blocked?" requires grepping git log | **Event sourcing** — every mutation is an immutable event with timestamps |
| Agent output burns tokens — JSON lists, ASCII tables, verbose prose | **TOON output** — `--toon` flag cuts token consumption ~50% on every command |
| No undo — agent closes wrong task, manual re-open is the only fix | **Temporal replay** — reconstruct state at any point from the event stream |
| Cascade operations require scripting — closing an epic means N separate commands | **`--cascade`** — claim or close an epic and all children in one command |
| Completed tasks clutter listings forever | **Archiving** — `cn archive --all-done` hides completed work, `cn unarchive` restores |
| Git sync = `git commit && push`, merge conflicts stall agents | **HTTP sync** with remote Core — no git conflicts, works with dirty worktrees |
| No approval gates or workflow orchestration | **Event-sourced workflows** — claim, complete, approve with first-write-wins semantics |
Credit where it's due: beads_rust by [@Dicklesworthstone](https://github.com/Dicklesworthstone) got the core workflow right — `ready -> claim -> done -> sync`. Chronis preserves that simplicity while adding the observability and durability that production agent systems need.
## Install
```bash
cargo install chronis
```
## Quick Start
```bash
cn init # Create .chronis/ workspace
cn task create "Design auth module" -p p0 # Create a task
cn task create "Write tests" --type=bug # Create a bug
cn list # List all tasks (human-readable)
cn list --toon # List all tasks (agent-optimized)
cn ready --toon # Show unblocked open tasks
cn claim <id> --toon # Claim a task
cn done <id> --toon # Complete a task
cn archive --all-done # Clean up finished work
cn sync # Sync with remote Core
```
## TOON: Agent-Native Output
This is chronis's signature feature. Pass `--toon` to **any command** for [TOON (Token-Oriented Object Notation)](https://github.com/toon-format/toon) output — the same format used by AllSource's MCP server. No braces, no quotes, no wasted tokens.
### The problem
Traditional CLI tools output for humans. When an LLM agent runs `cn list`, it gets:
```
╭──────────┬──────┬──────────────────────┬─────┬─────────────┬─────────┬─────────╮
│ ID │ Type │ Title │ Pri │ Status │ Claimed │ Blocked │
├──────────┼──────┼──────────────────────┼─────┼─────────────┼─────────┼─────────┤
│ t-abc1 │ task │ Design auth module │ p0 │ open │ - │ - │
│ t-abc2 │ bug │ Fix login redirect │ p1 │ in-progress │ agent-1 │ - │
│ t-abc3 │ task │ Write integration... │ p2 │ done │ agent-2 │ - │
╰──────────┴──────┴──────────────────────┴─────┴─────────────┴─────────┴─────────╯
```
That's **~180 tokens** of box-drawing characters, padding, and repeated dashes that carry zero information for the model.
### The solution
```bash
cn list --toon
```
```
[id|type|title|pri|status|claimed|blocked_by|parent|archived]
t-abc3|task|Write integration tests|p2|done|agent-2|||false
```
**~60 tokens.** Same information, ~50% fewer tokens. Every field is positional, pipe-delimited, with a header row. LLMs parse this natively.
### TOON across all commands
| `cn list --toon` | ASCII table | `[header]\nrow\nrow` |
| `cn ready --toon` | ASCII table | `[header]\nrow\nrow` |
| `cn show <id> --toon` | Multi-line prose | `key:value` lines + child/timeline tables |
| `cn claim <id> --toon` | "Claimed task t-abc1 (agent: agent-1)" | `ok:claimed:t-abc1` |
| `cn done <id> --toon` | "Completed task t-abc1" | `ok:done:t-abc1` |
| `cn task create --toon` | "Created task t-abc1: Title" | `created:task:t-abc1:Title` |
| `cn archive --toon` | "Archived task t-abc1" | `ok:archived:t-abc1` |
### Agent workflow (4 commands, ~20 tokens of overhead)
```bash
export CN_AGENT_ID=agent-1
cn ready --toon # → [id|type|title|pri|status|...]\nt-abc1|task|...
cn claim t-abc1 --toon # → ok:claimed:t-abc1
# ... agent does work ...
cn done t-abc1 --toon # → ok:done:t-abc1
cn list --toon # → verify state
```
Compare to JSON-based tools where the same loop burns ~200+ tokens on structural overhead alone.
## Commands
### Task Lifecycle
| `cn init` | | Initialize a `.chronis/` workspace in the current directory |
| `cn task create <title>` | | Create a task (see flags below) |
| `cn list [--status=open]` | `cn ls` | List tasks, optionally filtered by status |
| `cn ready` | `cn r` | Show tasks that are open and unblocked |
| `cn show <id>` | `cn s` | Task details, children, and event timeline |
| `cn claim <id>` | `cn c` | Claim a task (uses `CN_AGENT_ID` env var, defaults to "human") |
| `cn done <id> [--reason=...]` | `cn d` | Mark a task as done |
| `cn approve <id>` | | Approve a task |
| `cn archive <ids...>` | | Archive tasks (hide from default listings) |
| `cn archive --all-done` | | Archive all completed tasks |
| `cn archive --done-before=30` | | Archive tasks done 30+ days ago |
| `cn unarchive <ids...>` | | Restore archived tasks |
All commands accept `--toon` for agent-optimized output.
### Task Creation Flags
```bash
cn task create "Title" \
-p p1 \ # Priority: p0 (critical), p1, p2 (default), p3
--type=epic \ # Type: task (default), epic, bug, feature
--parent=<id> \ # Parent task ID (for hierarchy under epics)
--blocked-by=<id1>,<id2> \ # Tasks that block this one
-d "Description text" # Description
```
### Bulk Actions (Cascade)
Cascade operations apply to a task and all its children — useful for closing out an entire epic:
```bash
cn claim <epic-id> --cascade # Claim epic + all children
cn done <epic-id> --cascade # Mark epic + all children as done
cn done <epic-id> --cascade --reason="Sprint complete"
```
Cascade walks the parent-child tree depth-first, processing children before the parent. Tasks already in the target state are skipped.
### Archiving
Archive tasks to declutter your default listings. Archived tasks are hidden from `cn list` and `cn ready` but preserved in the event stream — nothing is deleted.
```bash
cn archive t-abc1 t-abc2 # Archive specific tasks
cn archive --all-done # Archive all completed tasks
cn archive --done-before 30 # Archive tasks done 30+ days ago
cn unarchive t-abc1 # Restore an archived task
cn list # Excludes archived (default)
cn list --archived # Show only archived tasks
cn list --all # Show everything including archived
```
### Dependencies
```bash
cn dep add <task-id> <blocker-id> # Add a blocker
cn dep remove <task-id> <blocker-id> # Remove a blocker
```
### Sync
Chronis supports two modes of operation, configured in `.chronis/config.toml`:
| **Embedded** (default) | Local event store per machine | Exchanges events with a remote Core over HTTP |
| **Remote** | All commands talk directly to a remote Core | No-op (already shared) |
#### Embedded mode with sync
Each machine has its own embedded Core for zero-latency reads. `cn sync` pushes local events to a remote Core and pulls new remote events — no git involved, works with dirty worktrees.
```bash
cn sync # Pull from remote Core, push local events
```
**How it works:**
1. **Pull** — fetch new events from remote Core (skip events that originated here)
2. **Push** — send local events to remote Core (skip events that came from remote)
3. Deduplication is idempotent — UUID tracking prevents duplicates even with timestamp overlap, plus `source_id` metadata prevents echo loops
**Multi-machine workflow:**
```bash
# Machine A # Machine B
cn task create "Auth" -p p0 cn task create "Docs" -p p2
cn sync cn sync # pulls A's tasks, pushes B's
cn sync # pulls B's task
```
#### Remote mode
Point chronis at a shared Core instance. Writes go to the remote Core, reads use a local in-memory projection cache that's bootstrapped on startup by replaying all remote events. No sync step needed.
```toml
# .chronis/config.toml
mode = "remote"
[sync]
remote_url = "http://core.example.com:3900"
```
#### Configuration
```toml
# .chronis/config.toml
mode = "embedded" # "embedded" or "remote"
instance_id = "a1b2c3d4-..." # Auto-generated, identifies this machine
[sync]
remote_url = "http://core.example.com:3900" # Remote Core for sync target or primary backend
```
### Visualization
```bash
cn tui # Interactive ratatui TUI (j/k nav, Tab view, c/d/a actions)
cn serve [--port=3905] # Embedded web viewer (Axum + HTMX)
cn serve --open # Auto-open browser
```
### Migration from beads_rust
One command migrates your existing beads issues to chronis — preserving IDs, status, priorities, dependencies, and parent-child relationships:
```bash
cn migrate-beads # Import issues from .beads/ directory
cn migrate-beads --beads-dir=/path/to/.beads
```
Both `.beads/` and `.chronis/` can coexist during transition. Nothing is deleted.
## Chronis vs beads_rust
| Storage model | SQLite + JSONL snapshot | Event-sourced (WAL + Parquet) |
| History | Current state only | Full temporal event stream |
| Agent output | JSON / human text | **TOON** (~50% fewer tokens) |
| Cascade operations | Manual scripting | `--cascade` flag |
| Archiving | Not available | `cn archive --all-done` |
| Approval workflows | Not available | `cn approve` with event trail |
| Sync | `git commit && push` (fails with dirty worktree) | HTTP sync via remote Core (works anytime) |
| Undo | Manual re-open | Replay from event stream |
| TUI | Separate binary (beads_viewer) | Built-in (`cn tui`) |
| Web UI | Separate project | Built-in (`cn serve`) |
| Queryable timeline | No | `cn show <id>` with full event history |
| Data durability | SQLite | CRC32 WAL + Snappy Parquet |
Both tools share the same core workflow: `ready -> claim -> done`. If you're coming from beads_rust, the transition is straightforward — and `cn migrate-beads` handles the data.
## Workflow
```
cn ready --> cn claim <id> --> (do work) --> cn done <id> --> cn archive --all-done --> cn sync
```
For agent orchestration, set `CN_AGENT_ID` to identify which agent claims tasks:
```bash
export CN_AGENT_ID=agent-1
cn claim <id> # Records "agent-1" as the claimer
```
## Best Practices
### Git Ignore for `.chronis/`
Sync is now HTTP-based (no git involvement), so the gitignore is simple — ignore everything except config:
```gitignore
# Chronis (all data is local or synced via HTTP — only config needs tracking)
.chronis/wal/
.chronis/storage/
.chronis/sync/
```
| `.chronis/wal/` | WAL segments (binary, CRC32) | **Ignore** | Machine-local binary data |
| `.chronis/storage/` | Parquet files (binary, Snappy) | **Ignore** | Machine-local, rebuilt from WAL |
| `.chronis/sync/` | Sync state (timestamps, dedup) | **Ignore** | Machine-local sync cursor |
| `.chronis/config.toml` | Workspace config | **Track** | Shared settings (mode, remote URL) |
### When to Use TOON
**Use `--toon` when the consumer is an LLM or a script. Don't use it when a human is reading.**
| AI agents consuming output (MCP, orchestration) | Interactive terminal use by humans |
| CI/CD pipelines parsing task state | Debugging with `cn show` |
| Piping to `cut`, `awk`, or scripts | Demos and screenshots |
| Large task lists (50+) where savings compound | Onboarding new team members |
| Repeated polling (`cn ready` in a loop) | TUI (`cn tui`) or web viewer (`cn serve`) |
For agent environments, set the flag once:
```bash
alias cn='cn --toon'
```
See [docs/BEST_PRACTICES.md](docs/BEST_PRACTICES.md) for the full guide.
## Architecture
Chronis wraps [AllSource](https://all-source.xyz)'s embedded library. Every mutation (create, claim, done, approve, dependency change) emits an event into the WAL. A `TaskProjection` folds these events into queryable task state stored in a DashMap (~12us reads).
```
cn CLI (clap)
|
v
TaskRepository trait
|
v
v v
EmbeddedCore HttpCoreClient
| (remote Core API)
+--> WAL (CRC32, fsync) --> Parquet (Snappy)
+--> DashMap (in-memory reads)
+--> TaskProjection (event folding)
```
In **embedded mode**, data lives in `.chronis/` at the project root. In **remote mode**, all data lives on the remote Core instance.
```
.chronis/
wal/ # Write-ahead log segments (embedded mode only)
storage/ # Columnar event storage (embedded mode only)
sync/ # Sync state — timestamps and dedup cursors
config.toml # Workspace config (mode, instance_id, remote_url)
.gitignore # Excludes local data directories
```
## Event Types
All state is derived from these events:
| `task.created` | `cn task create` |
| `task.updated` | (future: `cn task update`) |
| `task.dependency.added` | `cn dep add` or `--blocked-by` flag |
| `task.dependency.removed` | `cn dep remove` |
| `workflow.claimed` | `cn claim` (first-write-wins) |
| `workflow.step.completed` | `cn done` |
| `workflow.approval.granted` | `cn approve` |
| `task.archived` | `cn archive` |
| `task.unarchived` | `cn unarchive` |
## Quality Gates
```bash
cargo fmt --check # Formatting
cargo clippy -- -D warnings # Lints (zero warnings)
cargo test # 35 tests across 3 suites
```
### Test suites
| `task_lifecycle` | 11 | Create, claim, done, approve, dependencies, epics, archiving, timeline |
| `config` | 9 | Config parsing, defaults, TOML roundtrip, mode handling, file loading |
| `sync_state` | 15 | CoreBackend (embedded + remote), projections, entity filtering, dedup (UUID + metadata), sync state roundtrip, workspace init |
## Acknowledgments
Chronis exists because [beads_rust](https://github.com/Dicklesworthstone/beads_rust) proved that agents can self-manage tasks through a simple CLI. The `ready -> claim -> done` workflow that beads_rust pioneered is the foundation chronis builds on. We're grateful to [@Dicklesworthstone](https://github.com/Dicklesworthstone) and the beads community for blazing that trail.