Work a cairn backlog from the terminal.
This development line, 0.2.0-alpha.1, reads Cairn formats 1–5.
Format 5 numbers items again, in each type's own rendering (BUG-12 beside
0013), and tags each with a hidden uid; a tag or its first eight hex digits
still finds the item. Format 4 used immutable UUIDs: screens show unambiguous
short references, while writes, undo, clipboard copies, and --plain retain
the full identity. Upgrade Harrow before running cairn migrate on a live
backlog. Cairn's latest release, 0.3.0, does not understand format 4 or 5;
Cairn 1.0.0-alpha.1 does.
cairn keeps a project's roadmap and issues as Markdown files in the
repository, under a schema the project defines. Reading them one at a time is
fine; deciding what forty of them are worth is not. That is what this is for.
harrow harrow 7 items · 7 ready updated just now
──────────────────────────────────────────────────────────────────────────────────────────
╭ Backlog · by milestone ──────────────────────────╮╭ 0018 feature ──────────────────────╮
│ ▾ v0.2 Triage that scales pa…▱▱▱▱▱▱▱▱ 0% 5 ││ ○ Multi-select, for triage that is │
│ ○ 0018 + Multi-select, for triage that … 0/3 p1 ││ actually bulk │
│ ○ 0021 ~ Package it: crates.io and a tap 0/3 p1 ││ backlog · v0.2 │
│ ○ 0017 + Watch the directory instead of… 0/3 p2 ││ ACCEPTANCE │
│ ○ 0019 + Show what changed, from the re… 0/2 p2 ││ 0 of 3 ticked │
│ ○ 0020 + Answer proposals without leavi… 0/2 p2 ││ │
│ ▾ later Someday ▱▱▱▱▱▱▱▱ 0% 2 ││ FIELDS │
│ ○ 0022 ? How much Markdown is worth renderi… p2 ││ priority p1 │
│ ○ 0015 + Every cairn project on this ma… 0/1 p3 ││ area chrome │
╰──────────────────────────────────────────────────╯╰────────────────────────────────────╯
↑↓ move ↵ read c claim s status x close / filter tab board ? help
Run it anywhere inside a repository that has a cairn.toml, the way you run
git.
Watching, and triage.
It is built for a pane beside the work. Something else — an editor, an agent, another person — changes the backlog in the other one, and a status strip under the header says what is happening before you read a single row: how much is moving, how much is stuck, how much is done. An item that moved since the last reading carries a mark for forty-five seconds, so a glance back catches what happened while you were looking elsewhere. Below ninety-six columns the detail pane gets out of the way rather than halving the list.
Then triage. One keystroke per decision — c claims, s picks a status, h
and l walk an item through the columns — and space marks items so the same
decision applies to all of them at once, in one write. cairn next, cairn claim and cairn close are three short commands
and do not need a screen. What commands serve badly is moving through a backlog
— reading forty items, deciding what matters, changing a status and seeing the
columns rearrange — because every one of those decisions costs you an id typed
out again.
So: one keystroke per decision, the cursor stays where it was, and the whole
thing is read-mostly. Writing prose still belongs in $EDITOR, which e opens.
tab moves between them, shift-tab goes back, and 1 to 5 go straight
to one, in the order the tabs are in.
What happened is the other question you arrive with: who changed what,
when, most recent first, read out of the repository's own history. One row
per item changed rather than per commit, because the question is about
items. ↵ opens that item's full history.
Needs you is everything addressed to a person and nothing else: a proposal waiting on a decision, a claim nobody has honoured, work whose acceptance criteria are all ticked and which is still open, something a program filed that nobody owns. Each row is a question and the key that answers it is on the row. When there is nothing, it says so — which is the screen you want most and the one no tracker ever shows you.
The list groups by whatever the project has — milestone by default, and v
cycles the axis. The board deals the same items into the columns the project
declared. The stats is the backlog from a distance: how much is closed, what
is ready, what is blocked, what is in the way of the most other things, how the
milestones stand, and where the work is by type and priority. The log is
what changed, most recent first, out of the Git that is already underneath it.
Five lenses, five questions, and each one has a door from the command line:
| answers | ||
|---|---|---|
--lens needs |
what needs me | proposals, cold claims, work finished and still open |
--lens list |
which items | the backlog, grouped and filtered |
--lens board |
where the work stands | the project's own columns |
--lens stats |
how it stands overall | closed, ready, blocked, by type and priority |
--lens log |
what changed | commits that touched the backlog |
--plain answers any of them without drawing anything, so needs and log
are available to a script or an agent as well as to a person:
harrow --plain --lens needs # 0006 proposal an agent priority=p0
harrow --plain --lens log # 2026-09-23 Oddur 0106 fix(filter): …Nothing needing attention prints nothing and exits 0 — an empty queue is an answer, not a failure.
harrow has no opinion about how your backlog is organised. The statuses are the
ones in your cairn.toml, in the order you declared them. So are the types,
their icons, the fields you can group by, the values a picker offers, and the
columns on the board. Even the colours: if the project says a bug is red, a bug
is red, and the theme fills in the rest.
brew install oddurs/tap/harrow
brew install oddurs/cairn/cairn-next # Cairn 1.0.0-alpha.1, which writes format 5harrow reads a backlog with no cairn installed, and changes one through the
cairn on PATH. oddurs/cairn/cairn-next is the Cairn this harrow is paired
with. Plain oddurs/cairn/cairn is Cairn 0.3.0, the stable release, which
cannot open a format 4 or 5 backlog. The two install the same cairn, so keep
one.
Or download a binary from the latest
release — macOS and Linux,
arm64 and x86_64, statically linked on Linux so one file works on any distro.
Every release carries SHA256SUMS and a build attestation saying which workflow
and which commit produced it:
gh attestation verify harrow-*.tar.gz --repo oddurs/harrowThe tap installs both. Otherwise the binary prints them:
harrow completions fish > ~/.config/fish/completions/harrow.fish
harrow completions zsh > "${fpath[1]}/_harrow"
harrow completions bash > /usr/local/etc/bash_completion.d/harrow
harrow man > /usr/local/share/man/man1/harrow.1Both are generated from the same table as --help, so a flag that exists is a
flag all three know about.
From a checkout:
cargo install --path .Not on crates.io: the name belongs to somebody else, and a qualified package
name is a permanent cost for a cargo install path that reaches people who
already have a toolchain. 0021 has the reasoning.
cairn itself is optional for reading — a backlog is a directory of Markdown —
and required for changing anything. harrow says which of the two it is.
harrow # the interface
harrow -C ../other-project # somewhere else
harrow --view now # open in one of the project's saved views
harrow -f 'priority=p0' # open filtered
harrow --group-by area # grouped by something other than milestone
harrow --lens needs # open on what needs you; also list, board, stats, log
harrow -b # shorthand for --lens board
harrow --stats # shorthand for --lens stats
harrow --plain # one line per item, for scripts
harrow --plain --lens needs # one line per question waiting on somebody
harrow --doctor # check everything harrow depends on
harrow --screenshot 120x40 # render one frame as text, no terminal needed
harrow --fix-terminal # undo a terminal left in mouse-reporting modeHARROW_LOG=/tmp/harrow.log appends diagnostics to a file.
| key | |
|---|---|
↑ ↓ / j k |
move between items |
g / G |
first / last |
pgup / pgdn |
a screenful up or down |
K / J |
scroll the detail pane |
← → / h l |
previous or next group — a column, on the board |
tab / shift-tab |
the next lens, or the one before it |
1…5 |
go straight to a lens |
v / ctrl-v |
arrange it by something else / step to the next arrangement |
S / ctrl-s |
order it by something else / type an order |
V |
look at it the way the project does |
a |
show everything — finished, dropped, and milestones |
/ |
filter, in cairn's own grammar |
f |
open the filter panel |
: |
every command, by name |
enter / o |
read the item in full |
[ / ] |
pick a link in the detail pane; enter follows it |
H |
how this item got the way it is |
P |
the item as an agent's prompt; y copies it |
y / Y |
copy the item's reference / this view as a command line |
space |
mark it for the next change — on a heading, fold |
c / C |
claim / hand back, with a reason if you have one |
s p M |
set the status, the priority, the milestone |
< / > |
move it back or forward through the statuses |
t |
tick an acceptance criterion that has come true |
A |
accept the change somebody proposed |
ctrl-p |
in a picker: ask for the change rather than make it |
N |
add a line to the item's body — why, what you tried |
e |
open it in your editor |
x |
close, with what it concluded |
u |
reopen |
n |
new item |
esc |
back out — one press leaves whatever is open |
r / ctrl-r |
re-read the backlog / reload the config and theme |
? |
help |
q |
quit |
split, check, diagnostics, go-to-the-work and toggle-mouse have no
key: : finds them by name. The help overlay and harrow man are generated from your bindings, not from
that table.
It is meant to be usable without learning any of the above.
| click a tab | switch between the list, the board and the stats |
| click a status | filter to it; click it again to clear |
| click a row | select it; a group heading folds |
| ctrl-click | mark it; shift-click marks the range |
| double-click | read the item |
| drag a card | to another column, which sets its status |
| click a hint | run it — the footer is a row of buttons |
| scroll | move the pane under the pointer, which takes the cursor along only when it must |
m turns mouse capture off when you want your terminal's own text selection
back.
/ takes cairn's grammar, so anything that works after --filter works here:
priority=p0|p1,category!=done clauses are AND, alternatives are OR
milestone= items with no milestone
blocked=true what is waiting on something
body~oauth full text
A clause with no operator is a plain search, so / is useful before any of
that has been learned. The list narrows as you type, and a field the project
does not have is named and refused — on the command line with a non-zero
status, in the interface instead of the list, and never as an empty result
that reads like a true empty set.
The grammar is cairn's, because a saved view is a string the project wrote
for cairn and harrow reads it verbatim. harrow --doctor parses every saved
view in cairn.toml and names any it cannot evaluate.
The Cairn compatibility contract records tested versions,
the pinned corpus, and intentional query differences. Completion-date queries
use closed_at, not updated: editing a finished item does not finish it again.
An item that is finished can say what it concluded: a ## Result section,
which cairn close <ID> --result "…" writes. It is the part of an item worth
handing on, and cairn hands it on: cairn prompt quotes it to whatever depends
on the item.
harrow shows it where it matters. A finished item's Result comes first in the
detail pane and the reader, above everything said about it since, and Body
does not repeat it. An item's Builds on lists each finished dependency
with its Result — or its last dated note, where it has none — in the order
cairn prompt reads them, so the pane and the prompt say the same thing.
| what it does | needs | |
|---|---|---|
x |
asks what the item concluded, and closes it with that as its Result; an empty answer closes it without one | cairn close --result |
P |
shows the item as the prompt cairn compiles for an agent; y copies it whole |
cairn prompt |
:split |
turns the item's numbered steps into items of their own, after showing each one | cairn split |
C |
runs the project's check, and shows which open items an agent will misread under Prompts | cairn check --prompts |
Reading a Result needs no cairn at all: it is read from the file. Each command
is asked of the installed cairn once, at startup, and one it lacks is not
offered — no key, and nothing in the help or the palette. Without
close --result, x asks only for a yes. COMPATIBILITY.md
lists the first cairn that has each.
Yours lives in ~/.config/harrow/config.toml, and every key is optional. The
project's schema stays in the repository's cairn.toml, which harrow never
writes to.
harrow config --write # a commented file with every default
harrow config # what is actually in effect, and where it came fromtheme = "auto"
glyphs = "auto" # or nerd, unicode
group_by = "milestone" # or status, type, area, assignee, none
sort = "" # cairn's spelling: "priority,-updated"
view = "" # open in a saved view from cairn.toml
show_all = false # finished, dropped and milestones too
pane = "list" # or board, stats
watch = true # notice changes at once, not on the next poll
refresh_secs = 3
cairn = "cairn" # a path, if it is not on PATH
editor = "" # falls back to $VISUAL, $EDITOR, vi
[keys]
"x" = "close"ctrl-r re-reads both files and repaints, so choosing colours does not mean
restarting. A file that will not parse is reported and the running config is
kept.
harrow uses your terminal's colours. There is nothing to configure — if your terminal is set to Gotham, harrow is Gotham, and it follows when you change it.
harrow --theme gotham # for one run
harrow themes # everything harrow can find
harrow themes gruv # filtered — Ghostty ships hundredsBuilt in: auto (the default), mono (no colour at all), gotham, night,
paper. Your own files go in ~/.config/harrow/themes/. NO_COLOR,
--no-color and TERM=dumb all select mono, where the glyphs carry what the
colours would have. See THEMES.md.
With a Nerd Font, harrow draws with icons: GitHub's issue states for open, closed, not planned and blocked, a pie that fills while work is under way, a joined progress bar, and icons on the lenses, the toolbar, the section headings and the questions waiting for you. Without one, it draws the Unicode every font has, and the screen is the same as it always was.
harrow --glyphs nerd # for one run
harrow --doctor # shows a sample; boxes mean the font has not got themglyphs = "auto", the default, turns icons on only where it can tell the
terminal has them: Ghostty, WezTerm and kitty ship the symbols, and are
recognised through ssh and inside tmux. A locale that is not UTF-8 keeps
Unicode. A font you installed yourself cannot be seen from here, so if yours is
a Nerd Font, set glyphs = "nerd". harrow config says which set was chosen
and why.
harrow reads the terminal's palette rather than naming ANSI slots and hoping. One batch of OSC queries at startup asks for the background, the foreground and all sixteen entries, and the shades in between — pane surfaces, borders, the selected row — are computed from them. So the selection is a lift of your own page colour rather than reverse video, and a pane sits above the page instead of being the same colour as it.
Measuring also catches what convention gets wrong. Gotham fills its bright slots
with background shades: slot 8 is #10151b against a #0a0f14 page, which is
where convention says to draw borders and dim text, and is invisible there.
Every hue is checked against your background before it is used and derived from
the foreground when it fails. A terminal that does not answer gets the old
behaviour, which still works.
- Reading is direct.
cairn.tomlis parsed with the TOML crate and the item frontmatter with a small parser for the subset cairn writes — not by shelling out tocairn exportand parsing the output back. It is the same information, it costs a millisecond instead of a process, and it keeps working when cairn is not installed.harrow --doctorasks cairn for a second opinion on the count, so a disagreement is visible rather than believed. - Writing is not. Every change is a
cairninvocation:cairn claim 12,cairn set 12 status=doing. cairn owns the write lock, the id allocation, the hooks that keepROADMAP.mdcurrent, and the rules about what a valid item is. A second writer would own none of them. - Everything derived — what is blocked, what is ready, how much of a milestone is done — is computed over the whole set when it is read, because none of it is a property of a single file.
- The core performs no side effects.
Appreturns actions for the shell to carry out, including thecairninvocations, which is why every write can be asserted in a test with no repository underneath it. - A change shows up at once. harrow watches the item directory, so an edit
made in another window — by you, by
cairn, by an agent — arrives without waiting for anything. The three-second poll stays underneath it: it is what notices a change on a network mount or anywhere else the operating system declines to tell anybody. Either way the reading is only sent on when something actually changed, so a screen you leave open does not flicker.harrow --doctorsays which of the two a project is running on. - Work in another worktree shows up too. An agent on its own branch
claims an item in its own copy, and nothing reaches this checkout until the
branch merges. So harrow asks git which items each other worktree has
changed since it diverged from this one, and watches those worktrees like
its own. That work turns as work under way here does, the detail pane names
the branch and what it did, and the claim is announced when it is made.
The record is never overwritten. Statuses, filters, the board's columns and
every write stay this checkout's, and agree with
cairn.
- Reading a 45-item project: 2ms. A frame costs the same whether the backlog has 20 items or 2000; only the rows on screen are built.
- Idle, it redraws once a second, not ten times.
- Not a second source of truth. It shows the files. Close it and nothing is lost, because nothing was ever anywhere else.
- Not an editor. Prose goes in
$EDITOR. This changes fields. - Not machine-wide. It opens the project you are standing in, the way
gitdoes. Every cairn project on the machine in one list is a different tool, and is held rather than planned.
- Every read of cairn or git has a deadline and is killed if it overruns. A write is not: cairn ends its own waits for its lock, and a cairn killed with the lock in hand would stop every writer after it. A write runs in the background, one at a time, with its output in files and in a process group of its own, so the screen stays live and quitting waits for it; it is stopped only past cairn's own age for an abandoned lock.
- A failed read keeps the last good backlog on screen, marked stale, and backs off rather than hammering a directory that is not there.
- One unreadable item file is reported in the diagnostics overlay; the rest still load.
- The terminal is restored on quit, on panic, on
SIGTERM/SIGHUP/SIGQUIT, and through an exit hook that covers every other way the process can leave. Only clicks and scrolling are requested — never motion tracking, which floods a terminal with a report per mouse movement. - If some other program leaves your shell in mouse-reporting mode,
harrow --fix-terminalclears it.
git clone https://github.com/oddurs/harrow && cd harrow
scripts/setup # once: wires the git hooks
scripts/task check # fmt, lint, test, build — exactly what CI runsOne unit of work is one branch, in one worktree, with one pull request:
scripts/agent doctor # is this checkout ready?
scripts/agent start fix/the-thing # branch + worktree, prints the path
cd ../.worktrees/harrow/fix/the-thing
scripts/agent commit "fix(ui): the thing"
scripts/agent pr # checks, pushes, opens the PRmain only ever advances through a merged pull request — the pre-push hook
refuses to push to it, and branch protection refuses again if you get past the
hook. See CONTRIBUTING.md for the whole loop, and
scripts/task for the one seam every piece of automation talks
through: CI, the hooks and the agent script all run the same line, so they
cannot drift.
cargo test # everything
HARROW_UPDATE_SNAPSHOTS=1 cargo test # accept a deliberate screen change
HARROW_FUZZ_SEEDS=100000 cargo test --release --test invariantsThe interface is the product, so it is held to recorded screens rather than to descriptions of them — including a map of the foreground colours, since colour is data too. Randomised key sequences are held to the state invariants, because enumerating the ways modal layers, two views and a live filter interact is hopeless by hand.
Its own backlog is in cairn/items — which is also the fixture
it was developed against. ROADMAP.md is generated from it; do not edit it by
hand.