Headway board CLI
headway is a CLI over a running notedeck's embedded relay. It keeps its own
nostrdb cache, reconciles with the relay each run (NIP-77 negentropy, falling
back to NIP-01, or fully offline against the cache), folds the board locally, and
forwards edits back so the running app sees them. Source: crates/headway_cli.
Running it
Prefer a built binary; fall back to cargo:
# build once, then call the binary directly (fast, no rebuild per command)
cargo build -p headway_cli # produces target/debug/headway
target/debug/headway <command>
# or, one-off:
cargo run -q -p headway_cli -- <command>
In examples below, headway means whichever form you're using.
headway --help lists the commands grouped by what they act on, plus the flags
every run takes, one line each. Each command then has its own page — headway <command> --help (or headway help <command>) — with just that command's flags
and examples; headway help options has the every-run flags in full (env
fallbacks, defaults) and how <card>/<col> arguments are read.
Logging in
Everything operates on your own board once you're logged in — show to read,
the rest to edit. Relay defaults to ws://127.0.0.1:6677 (notedeck's embedded
relay); override with --relay <url> or HEADWAY_RELAY. If no relay answers,
the CLI works offline against its cache and edits reach the app on the next
connected run.
If a command fails because you're not logged in, ask the user to run
headway login. Don't handle the key yourself.
Multiple boards
A board is identified by a slug scoped to your key, so one identity can hold
several boards (e.g. a personal headway board and a work board).
Always target a non-default board with the per-run --board <id> flag — never
the stateful headway board <id> switch. Pass --board <id> on every
command in a sequence, so each call is self-contained:
headway --board work show
headway --board work add "Fix the relay reconnect" --col todo -l bug
headway --board work move 1a2b3c4d… --col done
Why avoid the stateful switch: headway board <id> persists the selection to a
file (<data-dir>/headway-cli/board) that is shared mutable state. The
running notedeck app — or any other headway process — can flip it between your
commands, so a show that targeted work can be followed by an edit that
silently lands on headway (you'll see a confusing "no card matching" when the
card "vanishes"). The --board flag is scoped to one run and can't be changed
underneath you, so a multi-step edit always hits the board you meant.
Board selection precedence, highest first: the --board <id> flag (one run
only) → the board named by a headway:<board>/<word-id> card ref (see below) →
$HEADWAY_BOARD → the board stored by headway board <id> → the default
headway. If you're acting on many boards in one session, prefer --board; only
fall back to $HEADWAY_BOARD (an env var, also stable for the session) when you
truly want every command to default to the same non-default board.
Full card refs self-route. A selector like headway:commerce/purse-metal-toilet
already names its board, so headway show headway:commerce/purse-metal-toilet (and
move, comment, …) targets commerce automatically — no --board needed. The
headway:<board>/<word-id> that show prints is a working address wherever you
paste it, which gives you the same self-contained, un-raceable targeting that
--board does — prefer either over relying on the stateful switch. The
scheme-less shorthand commerce/purse-metal-toilet self-routes too, and hex
prefixes resolve against the current board (so they still need --board to reach
another one). A bare word-id (purse-metal-toilet, no board segment) does
not self-route — it can't pick a board on its own — but it does resolve
against the board you've already selected, so headway --board commerce show purse-metal-toilet works without repeating the slug in the id. Like a hex prefix,
a bare word-id also matches by unique prefix — headway --board commerce show purse-metal (or even purse) resolves as long as it's unambiguous. Anything that
isn't purely hex digits is treated as a word-id, so only an all-0-9a-f selector
is a hex prefix. When a guess misses, the error suggests the real cards that share
its leading word instead of a bare "no card matching". Two refs naming different boards
in one command — or a ref that disagrees with an explicit --board — are an
error, not a silent resolution on the wrong board.
headway board with no argument is a harmless read — it lists the boards in
the cache and marks the current one with *; use it to discover slugs. Just
don't rely on its persisted * selection for edits. To create a board that
doesn't exist yet, headway --board work seed, or headway seed -t "Tune Assistant" to derive the slug (tune-assistant) from the title. seed never
falls back to the persisted current board or $HEADWAY_BOARD: with neither
--board nor --title it refuses, like next and migrate.
The golden rule: show before you edit
Cards are addressed by their event id, and columns by id or
case-insensitive name. When scripting the CLI, pass a hex id from
show --json. Any unique prefix resolves, so the full 64-char id is overkill —
a 16-char (8-byte) prefix is plenty for a board with a handful of cards, and
even an 8-char prefix is usually unambiguous. Use a short prefix for automated
edits; just lengthen it (or fall back to the full id) if you ever hit an
"ambiguous card prefix" error. The human-readable
show instead displays a muted reference like headway:headway/maple-river-canyon
(a friendly rendering of that same event id, for quoting in commits/chat); it also
resolves as a <card> argument, but prefer the hex id for automated edits.
Always run show first to read the current ids and column names, then act on what
you actually see — never assume an id or that a card is where you expect.
headway show # human-readable: columns, titles, labels, word-ids
headway show --archived # also list archived cards in full (default: count only)
headway show --all # every board in the cache, each printed in full and led
# by its slug (with --json, a JSON array of boards)
headway show --json # machine-readable, for parsing (always includes archived)
headway show <card>... # print the given cards (word-id or hex) in full
# `git show`-style detail, not the whole board; with
# --json each card gains a `column` field for the column
# it sits in
By default show collapses archived cards to a one-line count to keep the board
readable; pass --archived to list them (e.g. to find an id for restore).
show prints each card as <title> [labels] headway:<board>/<word-id>, with
the reference muted at the end of the line.
Which form to use — MANDATORY when talking to a human (chat, a commit
message, a PR, a board comment): refer to every card by its full scheme
reference, headway:<board>/<word-id> — the exact string show prints, scheme
and board included (headway:dave/maple-river-canyon; a notebook node is
notebook:mango-sibling-false). This is the only form the user's client
parses into a live link/chip. Any other form renders as dead text, so it is
never acceptable in human-facing prose.
Do not use, ever, when addressing a human:
headway#maple-river-canyon or dave#maple-river-canyon — the #/hash form
does not linkify. This is the most common mistake; there is no # in a
Headway reference.
- a bare
maple-river-canyon (no scheme, no board) — doesn't resolve at all.
- the scheme-less
dave/maple-river-canyon — fine as a CLI argument, but it does
not linkify in prose; always add the headway:/notebook: scheme when
writing for a human.
The full headway:<board>/<word-id> is also self-routing and unambiguous no
matter which board is current when it's read. Apply this to every ref in a
message, not just the first — a comment that names five cards writes all five as
full scheme references. When you edit the board (move, label, archive, …), pass
the canonical hex id from show --json instead, so an automated edit can
never hit the wrong card.
All of these resolve as a <card> argument, to the same card every time:
- a hex event id, full or any unique prefix (a 16-char prefix is plenty) — preferred for editing
headway:dave/maple-river-canyon — the full reference; it names its board, so
it self-routes there without --board (see Multiple boards)
dave/maple-river-canyon — the scheme-less shorthand; self-routes too. A bare
maple-river-canyon with no board segment is not a card ref and won't resolve.
Default board columns: Backlog, Todo, In Progress (in-progress),
In Review (in-review), Done (done). A column argument matches an id
or a name case-insensitively, so --col "in progress", --col in-progress, and
--col "In Progress" are equivalent.
Commands
| Command | What it does |
|---|
show [cards...] [--archived] [--all] [--json] | Print the board, or only the given cards (--archived lists archived cards; --all prints every board) |
seed [--board <slug>] [-t|--title <t>] | Create a board (born sealed): the --board slug, else one derived from --title; refuses with neither |
add <title...> [--col <c>] [-l <labels>] [--parent <card>] [--desc <text>|--desc-file <path>] | Add a card (defaults to the first column; -l/--label tags it; --parent creates it as a subissue; --desc/--desc-file sets its description at creation) |
move <card> --col <c> [--row <n>] | Move a card to a column (optional position) |
title <card> <title...> | Edit a card's title |
desc <card> <text...> | Edit a card's description |
label <card> [labels...] | Set labels — positional, comma-separated, or -l (no labels clears them) |
priority <card> <level> | Set priority: none/low/medium/high/urgent (none clears it) |
parent <card> [parent] | Make a card a subissue of [parent]; omit the parent to detach |
block <card> --on <other> | Mark <card> as blocked by <other> (see Dependencies) |
unblock <card> --on <other> | Remove the <card>-blocked-by-<other> edge |
relate <card> --to <other> | Relate two cards (an undirected "see also"; see Dependencies) |
unrelate <card> --to <other> | Remove the relation (from either endpoint) |
due <card> <date> | Set a due date (YYYY-MM-DD, or none to clear) |
estimate <card> <n> | Set an estimate — a number (or none to clear) |
seq <card> [--in <c>] [--json] | Print where a card sits in a container's work-order: its place, neighbours, and how many ready cards are ahead (see Work order) |
seq <card> <pos> [--in <c>] | Position a card in a container's work-order (see Work order) |
next [--in <c>] [--ready] [-n <k>] | Print the ready frontier — what to work on next (see Work order) |
grep <pattern> [--board <b>] [--in <c>] [-i|-s] [--color <when>] [--[no-]pager] [--json] | Search card text (title, description, comments, review comments) and print each matching line under its card's ref (see Searching cards) |
comment <card> <text...> [--reply-to <c>] | Comment on a card (NIP-22); --reply-to threads under another comment or a review comment |
comment <card> --path <f> --line <a[-b]> [--old] [--record <sha>] <text...> | Inline review comment on a review record's commit (newest record by default); --record alone comments on the whole commit |
review <card> [--explainer <url>] [--explainer-source <f>] [--code-source <f>] [--deploy <url>] [--commit <rev>] | Record a commit for review on the card: sha, title, branch, host, repo path and agentium: ref, read from git in --repo-dir (default cwd) and $AGENTIUM_SESSION; --deploy is the URL where this commit's build is live; the source flags store the explainer's Markdown and its code walk on the record (repeatable, text, 32 KiB a file) |
diff <card> [--record <sha>] [--note explain|code] | Print the commit the card's newest review record names (git show-style), fetching it from the recording host if this one lacks it; falls back to the card's Headway: trailer. --note prints the commit's explain git note instead |
delete <card> | Remove a card (reversible tombstone) |
archive <card> | Archive a card off the board |
restore <card> | Restore an archived card |
link <card> --to <board> | Also place the card on another board (it stays on this one) |
move-board <card> --to <board> | Move the card off this board onto another |
board [id] | Switch the current board to id, or (no arg) list boards and mark the current one |
rename <title...> | Rename the current board's display title (slug unchanged) |
share <npub|hex> --board <id> | Make someone a member of a sealed board you own (see Sharing a board) |
terminal <col> [on|off] | Mark a column as a "done" column, or clear it with off (see Terminal columns) |
login <nsec> | Store a signing key so later runs just work |
logout | Forget the stored signing key |
add accepts -l/--label to tag the new card in one step. The flag is
repeatable and each value may be comma-separated, so -l a,b --label c and
-l a -l b -l c are equivalent:
headway add "Fix the relay reconnect" --col todo -l bug,p1
label <card> takes the same spellings — separate positionals, one
comma-separated positional, or the -l/--label flag — so these all set the
same two labels. Note it replaces the card's set rather than adding to it, and
label <card> with no labels at all is what clears:
headway label 1a2b3c4d… bug p1
headway label 1a2b3c4d… bug,p1
headway label 1a2b3c4d… -l bug,p1
headway label 1a2b3c4d… # clears
add can also set the new card's description at creation, saving a follow-up
headway desc <card> … (and the need to learn the new card's id first).
--desc <text> takes the description inline; --desc-file <path> reads it from
a file, or from stdin when <path> is -, so a long multi-line markdown
description can heredoc in with no shell-escaping. The two are mutually exclusive
and trailing whitespace is trimmed (so a heredoc's closing newline doesn't ride
along). Both compose with --col/-l/--parent:
headway add "Rework the sync engine" --col todo --desc "Backfill stalls past the maxSyncEvents cap."
headway add "Migration plan" --col todo --desc-file - <<'EOF'
## Migration plan
1. Fork the session loop
2. Retire the per-app poller
EOF
Every new card says where it came from. When a Dave agentic session creates
a card, the description starts with a backlink to that session, so whoever
reads the card later can open the transcript that produced it instead of hunting
for it:
headway add "Cap the media cache" --col todo --desc-file - <<EOF
Created from $AGENTIUM_SESSION while profiling image loads.
Eviction never runs past 2 GB …
EOF
$AGENTIUM_SESSION is your own agentium:<word-id> (see the agentium skill
for the fallback when it's unset). This applies to every add — follow-ups,
subissues from an epic breakdown, cards filed in passing — and only at
creation; later edits to the description keep the line rather than re-adding it.
Outside a session, name the card or conversation it came from instead (e.g.
Split out of headway:dave/maple-river-canyon).
Other flags: --board <id> (target another board for one run; see Multiple
boards), --db <path> (cache dir),
--author <pk> (read someone else's board), -h/--help; headway help options has them in full. --on <card> names
the blocker for block/unblock; --to is the target board for
link/move-board and the partner card for relate/unrelate; --in <c> is
the container for seq/next. headway <command> --help lists a command's own
flags without the rest.
When a card's work is committed, record it with headway review <card> from the
checkout (plus --explainer <url> when there is one, and --deploy <url> when
the commit's build was deployed somewhere it can be opened and tried) before the
done comment. The record is the machine-readable twin of that comment: headway
reads it to show the commit's diff, and the app's review pane links the
explainer and the deployed build beside it. When the explainer was written as
explain Markdown, pass its source too:
--explainer-source ~/explainers/<word-id>/rec/<word-id>.md (the explain inline output) and, for a change with a code walk,
--code-source ~/explainers/<word-id>/<word-id>.code.md. Give the paths in
full: they are read from the current directory, the checkout, and one that
won't read refuses the whole record. Each is stored on the record under its file
name, sealed with the board like any card content, and show --json returns it
in the review's sources, which is where explain book builds the card's
chapter from. The visual-explainer skill says how to write both. show <card> lists a card's records under review, and
headway diff <card> prints the commit itself: it looks in the recorded
checkout, then any checkout of the same repo (matched by root commit), then a
bare cache under the headway-cli data dir, and fetches over ssh from the
recording host when none has it — never prompting, never creating refs in your
checkout. explain also writes the commit's git notes (refs/notes/explain for
the page, refs/notes/explain-code for the walk); headway diff <card> --note explain|code prints one, read from the same place as the commit. Colleagues
without headway read them with git log --notes=explain --notes=explain-code
after fetching refs/notes/explain*.
Inline review comments left on a commit (in the app's review pane, or by
comment --path) print under their record in show <card>, threaded, each with
its word-id and path:line (path:old a-b for deleted lines). Answer one with
headway comment <card> --reply-to <word-id> <text>: the reply goes on the same
record under the same lines, not into the card's comment thread. It is signed
by the comment key like any comment (see below).
When the app sends review comments or an X send-back into your session, the
message ends with this same hint. Answer each comment where it was left, so the
reviewer sees the answer beside it, rather than only in your transcript:
headway show headway:headway/some-card # each review comment prints with its word-id
headway comment headway:headway/some-card --reply-to ball-double-claw "done in abc1234"
headway comment headway:headway/some-card --path src/lib.rs --line 40-44 "kept the clone: X needs it"
When commenting a finished card's commit hash, a Dave agentic session should also
quote its own agentium: session ref (from $AGENTIUM_SESSION) beside the hash —
see AGENTS.md "When done with the work" and the agentium skill.
Subissues
A card can be a subissue of one parent card (GitHub sub-issue semantics:
one parent per child, any number of children per parent). Use this instead of
the old hand-maintained epic pattern — an epic label plus a word-id checklist
in the description — whenever work breaks down into trackable pieces: the
rollup is derived from the board, so it can never go stale.
headway add "wire up the parser" --col todo --parent <epic> # create as a subissue
headway parent <card> <epic> # make an existing card a subissue (or re-parent)
headway parent <card> # omit the parent to detach
Progress is derived, not stored: a child counts as done when it sits in a
terminal column of its board (see "Terminal columns" below — In Review and
Done on the default board), or is archived. There is no checkbox to tick —
moving the child card is the progress update.
How it renders:
-
The board listing (show) marks parent cards with a dim n/m rollup.
-
Card detail (show <epic>) gains a subissue of line on children and a
derived checklist on parents:
subissues (2/4 done)
[x] route media loads through imgproxy headway:headway/mushroom-include-wolf
[ ] cap media cache size with eviction headway:headway/extend-decrease-visit
-
show --json gains parent (hex), parent_ref (a full
headway:<board>/<word-id>), and subissues per card.
Notes: re-parenting that would create a cycle is refused; children may live on
a different board than the parent; nesting works (a child can itself be a
parent) but each rollup counts direct children only.
Work order: what to do next (next and seq)
Beyond columns and subissues a board carries a work-order — a deliberate
sequence over cards that answers "what should I pick up next?". Two commands read
and write it:
headway next prints the ready frontier: the cards workable right now,
in work-order. next alone prints just the first (the single best next thing);
next --ready prints the whole frontier; next -n <k> caps it at k. More
than one card can be ready at once — that's the parallel-dispatch signal
(independent cards you could hand to different workers at the same time).
headway seq <card>, with no position flag, prints where the card sits in
that order and writes nothing: its place in the whole walk (31 of 40), the
cards just before and after it, whether it has a rank of its own or is placed
by creation time, how many ready cards next would hand out ahead of it,
and why it isn't ready itself if it isn't (done, In Progress, blocked by a
card, or has open subissues). The two counts differ on purpose: the place
counts every card, "ready ahead" only the frontier. A card the container's
walk never reaches is an error. --json gives {ref, container, index, of, sequenced, before, after, ready_ahead, ready, not_ready_why}. Use it rather
than next --ready | grep -n, which drops blocked and In Progress cards.
headway seq <card> <pos> positions a card in a container's work-order, which
is how you curate the order next reads. <pos> is --first, --last,
--after <card>, or --before <card>.
--in <c> names the container whose order you mean: a card ref targets that
card's subissues, a board slug targets the board root (its top-level cards);
omitted, it's the board root. next refuses to guess the board — pass
--board <id> or an --in <headway:board/word-id> ref (never the persisted
current board).
headway --board dave next # the single best next card
headway --board dave next --ready # the whole ready frontier, in order
headway --board dave next -n 3 # the top 3
headway seq <card> # where <card> sits in the board-root order
headway seq <epic-child> --in <epic> # where it sits within its epic
headway seq <card> --first --in dave # make <card> the first board-root task
headway seq <epic-child> --after <sib> --in <epic> # order within an epic
What "ready" means. A card is ready when it is not done (not sitting in a
terminal column — see "Terminal columns" below), not In Progress (someone
already has it), not blocked (no block
edge pointing at an unfinished card — see Dependencies), and not a parent with
unfinished subissues (an epic's real work is its children, which are in the
frontier themselves, so the epic card isn't dispatchable). On the default board
both In Review and Done are terminal, so a card in review reads as done
and next will not resurface it — matching an integration-test-at-the-end flow
where review-stage work no longer blocks its dependents.
Ordering, and the priority caveat. Within a container, members run in
seq-order where a seq has been set, else creation order — the board root
included. The walk is depth-first: each card is followed by its own subissues
before the next card. next does not sort by the priority field —
priority is a human-facing label, not an input to the frontier. So a board where
nobody has run seq has no real work-order: next --ready is just the board in
default order, and you should judge the biggest win yourself rather than trust the
first line. Curate with seq to make next meaningful.
Searching cards (grep)
headway grep <pattern> finds the cards that say something, without a
show --json + show <card> loop: it searches every card's title,
description, comment thread and inline review comments in one run and prints
each matching line under the card's full headway:<board>/<word-id> ref and
title, with the field it came from and how long ago it was written (2h ago).
Cards are listed most recent first, by their newest matching line, across
every board searched. A comment is dated by itself; a title or description by
the edit that last set it. Labels and review records' commit subjects are not
searched. It's a read command — it never signs.
<pattern> is a regex, smart-case by default: an all-lowercase
pattern matches any case, one with an uppercase letter matches exactly. -i
forces case-insensitive, -s case-sensitive. Quote a pattern with spaces —
grep takes exactly one.
- Scope: every board you can read by default, your own and the ones shared
with you — the current board doesn't narrow it.
--board <b> searches just
that board. --in <card> searches only that card and everything under
it (the card itself is included, unlike next --in), on the board its ref
names; --in <board-slug> is the whole current board.
- Archived cards are always searched; their column shows as
archived.
(--all and --archived are still accepted and change nothing.)
- On a terminal the matches are colored and paged, like
git grep and
agentium grep: the pager is $HEADWAY_PAGER, else $PAGER, else
less -R. Piped, output is plain and unpaged. --no-pager / --pager force
paging off or on; --color auto|always|never overrides coloring
(--color always keeps the highlight when piping into your own less -R).
Agents reading the output should pass --no-pager or --json, though a pipe
already turns both off.
--json is an array with one object per matching card — ref, id,
board, title, column (archived for an archived card) and matches
([{field, text, created_at}], field one of title/desc/comment/review,
created_at unix seconds), in the same most-recent-first order.
"No matches" is [] (plain output says no matches); either way it exits 0.
headway grep 'relay reconnect' --board notedeck
headway grep -i nostrdb # every board you can read
headway grep sealed --in headway:notedeck/saddle-because-liquid # one epic
headway --json grep 'Changelog' | jq -r '.[].ref'
Terminal columns: what counts as "done"
Doneness — a card clearing its dependents, dropping out of the next frontier,
and rendering as done (strikethrough, n/m rollups) — is decided by which
terminal columns a board marks. A card in any terminal column is done.
- On the default board, both In Review and Done are terminal. So a
card in review already counts as done: it stops blocking its dependents and
next won't resurface it. This suits an integration-test-at-the-end flow.
- A board that marks no terminal column falls back to treating its last
column as terminal — the original behaviour, so older boards are unchanged.
- Mark or clear the flag with
headway terminal <col> [on|off], which
republishes the board definition. Boards seeded before this feature carry no
marks (last-column fallback) until you opt a column in — e.g. headway --board work terminal in-review on.
show --json reports each column's terminal flag. Terminal is about doneness
only; it doesn't merge or reorder columns — In Review and Done stay distinct
lanes you move cards through.
Dependencies: block / unblock (and relate)
A card can be blocked by any number of other cards — a directed dependency
edge saying "don't start this until that is finished". It is a separate axis
from parent/subissue: a card can be both a subissue and blocked, and an edge may
point at a card on another board.
headway block <card> --on <blocker> # <card> is blocked by <blocker>
headway block <card> <blocker> # same — the blocker may be a 2nd positional
headway unblock <card> --on <blocker> # drop the edge
Unlike parent (one parent, re-parenting replaces it), blockers accumulate:
each block adds to the card's set, and unblock removes one edge.
An edge edit that changes nothing says so, and never as a resolution failure —
so don't retry with a longer id. Re-adding an edge that's already there (block,
relate) or dropping one that isn't (unblock, unrelate) is idempotent
success: exit 0 with ok (0 events) — <card> is already blocked on <blocker>
(--json adds a "noop" field). A block or parent that would close a cycle
is a real error, and names what it refused:
error: refused: blocking <card> on <blocker> would create a dependency cycle (…)
error: action produced no events (unknown card or column?) remains only for
edits the reducer genuinely couldn't resolve.
A blocker is cleared the same way a subissue is done — when it sits in a
terminal column of its board (In Review or Done on the default board; see
"Terminal columns" below), or is archived. There's nothing to tick: moving the
blocker is the unblock.
How it renders:
-
The board listing (show) prefixes a blocked card with a dim ⊘ glyph. A card
whose blockers are all cleared loses the glyph, so ⊘ always means "held back
right now".
-
Card detail (show <card>) gains blocked by / blocks sections — the
forward edges and the reverse ones (cards this one is holding up) — with x
marking a cleared blocker so open ones stand out:
blocked by (2)
[x] land the negentropy reconcile headway:headway/mushroom-include-wolf
[ ] cap media cache size headway:headway/extend-decrease-visit
-
show --json gains blocked (bool — any unfinished blocker), plus
blocked_by and blocks arrays of {id, ref, title, done}.
-
next and next --ready skip a card with an unfinished blocker — this is
the main reason to record dependencies, since it keeps the frontier to work
that's actually startable (see Work order).
relate <card> --to <other> is the third edge kind: an undirected "see also"
between two cards. It's symmetric (both ends list each other, and unrelate
works from either), may cross boards, and is purely informational — it carries no
ordering and never affects is_blocked, the ready frontier, or any rollup. Card
detail shows it as a related section and show --json as a related array.
Cross-board cards: link and move-board
Board membership is placement-driven: the same card — one issue with all
its overlays (title, description, labels, comments, parent/subissues) — can sit
on several boards at once. Two commands manage this:
headway --board work link 1a2b3c4d… --to personal # now on both boards
headway --board work move-board 1a2b3c4d… --to personal # re-homed: off work, on personal
link adds a placement on the target board and keeps every placement the
card already has. Edits made anywhere show everywhere — it's the same card,
not a copy. Re-linking an already-linked card is harmless (it just re-ranks).
move-board is link + remove from the source board: the card keeps its id,
word-id, and all overlays, and now lives only on the target (plus any other
boards it was already linked to).
- On the target, the card lands in the column whose id matches its current
column (e.g. a card in
in-review stays in in-review), falling back to
the target's first column when no such column exists there.
- The card is resolved on the source board, so combine
--board <source>
with --to <target>. The target board must already exist — seed it first
with headway --board <target> seed if it doesn't.
Sharing a board
A sealed board can be shared with another nostr identity, who then reads and
edits it from their own CLI or app:
# owner: gift-wrap the board's channel key to the member (kind-1082 in a 1059)
headway --board ios-port share npub1…
# member: read and edit the owner's board — its slug is enough
headway --board ios-port show
headway --board ios-port add "From a member" --col todo
- Owner-only. A member's
share is refused; so is sharing to yourself.
- Sealed boards only. A plaintext board has no channel to hand out —
headway migrate --board <id> it first (never on a real board casually).
--board is required, never the persisted current board: a share can't
be revoked, so the board must be named on purpose.
- Needs a reachable relay. A key-share is sent once and never re-sent by a
later run, so an offline share is refused rather than silently lost.
- Not idempotent on the wire (a fresh throwaway wrap key each run), but a
repeat share is harmless: the member's roster dedups it.
- The member's edits fold on the owner's side attributed to the member's pubkey.
Working on a board someone shared with you
headway board lists it under a "shared with me" heading, after your own
boards, with its owner (short npub) and card count. show --all includes it
too (JSON boards carry an owner hex).
- The slug resolves the owner. With no
--author, a --board <slug> or a
headway:<slug>/<word-id> card ref that isn't one of your boards folds the
one board shared with you under that slug — so a card ref pasted from the
owner works as-is.
- Your own board wins. If you have a board with that slug in your cache,
it is used instead;
--author <owner> reaches the shared one.
- Same slug from two owners is an error that lists both owners in full;
pick one with
--author <npub|hex>. An explicit --author always wins.
seed and migrate never resolve to someone else's board: they only act on
boards you own.
Commenting as an agent (a separate comment key)
An agent that runs headway as its user can sign its comments with its own
key, so they show as the agent's rather than the user's. Everything else (reads,
adds, moves) stays the user's:
HEADWAY_COMMENT_NSEC_FILE=~/.local/share/jex0/nsec \
headway comment headway:headway/some-card "…"
# or inline: HEADWAY_COMMENT_NSEC=<nsec|hex>, or per run: --comment-nsec <nsec|hex>
Precedence is --comment-nsec > $HEADWAY_COMMENT_NSEC >
$HEADWAY_COMMENT_NSEC_FILE. The file holds the key as nsec or hex (a trailing
newline is fine); if it can't be read the command fails rather than quietly
signing as the user. Dave sessions usually have HEADWAY_COMMENT_NSEC_FILE
already set through session_env in dave_settings.json, so a plain
headway comment signs as the agent.
The comment is sealed into the board's channel with the user's access, so the
comment key needs no membership and holds no board key. It only works on
sealed boards: a plaintext board shows just its owner's own events, so the
CLI refuses rather than publish a comment nobody would see.
Typical workflow
Move a card from In Progress to Done:
headway show --json # match the title, grab its hex `id`
headway move 1a2b3c4d… --col done # move by hex id (a column may match by name)
headway show # verify it landed in Done
To address a card by title, read show --json and match the title to its hex
id, then pass that id. Resolution errors are explicit: an ambiguous hex prefix
says "ambiguous card prefix", an unknown reference "no card matching", and a bad
column lists the valid column names — re-read show and retry with a corrected
argument.
Notes
- Edits print
ok (N events); offline edits append — offline, not forwarded to the app, meaning they're cached but haven't reached the running notedeck yet.
seed errors if a board already exists; that's expected — just show instead.
- A board seeded on one machine reaches the account's other machines through
the running notedecks' private relay. Its key-share travels twice, as a
gift-wrap and as a PNS note, because a private relay may serve gift-wraps only
to an authenticated reader. If a board is missing on one machine, run any
headway command on the machine that has it (each cache sends its boards' PNS
key-shares once), give the notedecks a moment to sync, and list again there.
- The cache lives at
<data-dir>/headway-cli unless --db overrides it. The CLI
and the running app converge through the relay, so either side's edits show up
on the other after a reconcile.