Git-native, event-sourced task management.
cargo install spool-cli # CLI
cargo install spool-ui # TUI (optional)Tasks are stored as append-only event logs in .spool/events/. Every change is tracked, branches work naturally, and merge conflicts resolve automatically.
Run spool-ui for a terminal interface. Press ? for all shortcuts.
Navigation
j/kor↑/↓— Move up/downg/G— Jump to first/last[/]orOption+←/→— Switch viewTab— Toggle detail panelEnter— Show detail / select
Tasks
n— New taskc— Complete taskr— Reopen taskv— Cycle status (Open/Complete/All)o— Cycle sort order/— Search
Streams
s— Streams viewn— New stream (in streams view)d— Delete stream (in streams view)
General
h— History view?— Show shortcutsq— QuitEsc— Back / quit
cd your-project
spool initCreates .spool/ with events/ and archive/ directories. Commit this to git.
spool add "Implement authentication"
spool add "Fix login bug" -p p0 -t bug -d "Users getting logged out unexpectedly"
spool add "Backend API" -p p1 -t feature -a @alice --stream <stream-id>Options: -p priority (p0-p3), -t tag (repeatable), -d description, -a assignee, --stream stream ID.
spool list # Open tasks (default)
spool list -s all # All tasks
spool list -s complete # Completed only
spool list -a @alice # By assignee
spool list -p p0 -t bug # By priority and tag
spool list --stream <id> # By stream
spool list --no-stream # Tasks without a stream
spool list -f json # JSON output
spool list -f ids # IDs only (for scripting)spool show <task-id>
spool show <task-id> --events # Include event historyspool update <id> -t "New title"
spool update <id> -d "New description"
spool update <id> -p p1
spool update <id> --stream <stream-id>spool assign <id> @alice # Assign to user
spool claim <id> # Assign to yourself
spool free <id> # Unassignspool complete <id> # Default resolution: done
spool complete <id> -r wontfix # Other: duplicate, obsolete
spool reopen <id> # Reopen completed taskStreams group tasks into collections (features, sprints, areas).
spool stream add "api" -d "Backend API work"
spool stream list
spool stream show <id>
spool stream show --name "api"
spool stream update <id> -n "new-name" -d "New description"
spool stream delete <id> # Must have no tasksspool rebuild # Regenerate caches from events
spool archive --days 30 # Archive old completed tasks
spool archive --dry-run # Preview what would be archived
spool validate # Check event file integrity
spool validate --strict # Fail on warnings tooInstead of mutable records, every change is an immutable event:
{"v":1,"op":"create","id":"k8b2x-a1c3","ts":"2026-01-13T12:00:00Z","by":"@alice","branch":"main","d":{"title":"Fix bug"}}
{"v":1,"op":"assign","id":"k8b2x-a1c3","ts":"2026-01-13T12:01:00Z","by":"@bob","branch":"main","d":{"to":"@bob"}}
{"v":1,"op":"complete","id":"k8b2x-a1c3","ts":"2026-01-13T14:00:00Z","by":"@bob","branch":"main","d":{"resolution":"done"}}Events are stored in daily JSONL files: .spool/events/2026-01-13.jsonl
State is materialized by replaying events. Caches (.index.json, .state.json) are gitignored and rebuilt on demand with spool rebuild.
.spool/
├── events/ # Daily event logs (committed)
│ └── 2026-01-13.jsonl
├── archive/ # Monthly archives (committed)
│ └── 2026-01.jsonl
├── .index.json # Cache (gitignored)
├── .state.json # Cache (gitignored)
└── .gitignore
| Operation | Description |
|---|---|
create |
Create task with title, description, priority, assignee, tags |
update |
Update task fields |
assign |
Change assignee (null to unassign) |
complete |
Mark complete with resolution |
reopen |
Reopen completed task |
comment |
Add comment |
link / unlink |
Manage relationships (blocks, blocked_by, parent) |
set_stream |
Set or remove task's stream |
create_stream |
Create stream |
update_stream |
Update stream metadata |
delete_stream |
Delete stream |
archive |
Archive completed task |
Format: {timestamp}-{random} where timestamp is Unix ms in base36 and random is 4 alphanumeric chars.
Example: k8b2x-a1c3
git add src/feature.rs .spool/events/
git commit -m "Implement feature and update task"#!/bin/sh
# .git/hooks/post-merge
spool rebuildEvent files are append-only. On conflict, keep both sets of events:
# Keep all events from both versions, then:
spool validate
spool rebuildspool validate --strict || exit 1# Get task IDs
TASKS=$(spool list -f ids)
# Process JSON
spool list -f json | jq '.[] | select(.priority == "p0")'
# Batch operations
for id in $(spool list -f ids -t bug); do
spool update $id -p p1
doneMIT