Skip to content
oddursPublic

About

Work a cairn backlog from the terminal

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

harrow

ci license: MIT

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.

What it is for

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.

Five ways to look at it

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.

Everything is the project's

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.

Install

brew install oddurs/tap/harrow
brew install oddurs/cairn/cairn-next    # Cairn 1.0.0-alpha.1, which writes format 5

harrow 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/harrow

Completions and the man page

The 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.1

Both 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.

Use

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 mode

HARROW_LOG=/tmp/harrow.log appends diagnostics to a file.

Keys

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.

Mouse

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.

Filtering

/ 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.

Results and prompts

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.

Configuration

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 from
theme        = "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.

Theming

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 hundreds

Built 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.

Icons

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 them

glyphs = "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.

Colour

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.

How it works

  • Reading is direct. cairn.toml is parsed with the TOML crate and the item frontmatter with a small parser for the subset cairn writes — not by shelling out to cairn export and 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 --doctor asks cairn for a second opinion on the count, so a disagreement is visible rather than believed.
  • Writing is not. Every change is a cairn invocation: cairn claim 12, cairn set 12 status=doing. cairn owns the write lock, the id allocation, the hooks that keep ROADMAP.md current, 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. App returns actions for the shell to carry out, including the cairn invocations, 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 --doctor says 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.

Cost

  • 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.

What it deliberately is not

  • 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 git does. Every cairn project on the machine in one list is a different tool, and is held rather than planned.

Durability

  • 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-terminal clears it.

Development

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 runs

One 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 PR

main 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 invariants

The 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.

About

Work a cairn backlog from the terminal

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages