One task per section. For the guided path through the basics, read Getting started first. To wire a coding seat, read Harness.

Send a project to another checkout#

Put a route table in ~/.config/vissue/config.toml so a process whose default root is one tracker can still write another.

[layouts.work]
root = "~/work-notes"
prefix = "Issues"

[routes]
parser = "work"
$ vissue create --project parser "Measure the cold-start path"
$ vissue show parser-ab12
$ vissue --no-route create --project parser "Stays on the default root"

identity appends one route: line per routed name. projects lists those names with the default-layout projects.

Filter and count#

--project (also -p or -P) and --state apply to list, count, ready, graph, roadmap, and export. A project name matches the directory on disk without regard to case.

$ vissue list --project parser --state TODO
$ vissue count --ready

Feed another tool#

export writes one JSON object per line, carrying every property, the logbook, the body, and the file line range.

$ vissue export --project parser | jq -r '.id + " " + .state'

The schema is in the Reference. It is the seam other tools consume, so fields are added rather than repurposed.

Hand an issue to someone as the specification#

show --org writes the heading out whole: the property drawer, the logbook, and the body, exactly as the file holds them.

$ vissue show --org parser-1a2b > ISSUE.org

Use this rather than body-excerpt whenever the text is the thing being worked from. body-excerpt is a preview and stops at 40 lines, so a longer issue loses its tail without saying so. The logbook travels with the export, carrying the notes written when the work was handed over.

An issue whose text looks like credential material is refused rather than exported, the same way a preview of it is.

Plain show prints the metadata and then the body, and show --json carries both along with the logbook, so a caller that already fetched the issue does not need a second read to learn what it asks for.

Reject work by redirecting it#

reject closes an issue as CANCELLED and points at a replacement: an existing id with --to, or a newly created heading with --project and a title. --reason is recorded on the source.

$ vissue reject parser-1a2b --to parser-9k0m --reason "duplicate of the rewrite"
$ vissue reject parser-1a2b --project parser "Rewrite the manifest parser" --reason "superseded"

A write that still believes the heading is open must name what it read:

$ vissue update parser-1a2b --state DONE --if-state STARTED
$ vissue update parser-1a2b --state DONE --if-gen 3167

If a reject landed first, --if-state STARTED refuses and the source stays CANCELLED. Two disagreeing closes do not last-write-wins: the first terminal stays, the second is :SIBLING_TERMINAL:, and resolve picks one.

$ vissue resolve parser-1a2b --state CANCELLED

check warns when a DONE body reads as a reject, when a body [[id:]] is connected by no edge at all, and when a sibling terminal is unresolved.

Both of the first two are narrower than they sound, on purpose. “Reads as a reject” means the shapes a rejection is written in – vissue reject, “superseded by”, “rejected in favour of”, or a heading naming the outcome – and not the word itself: a bug report about validation says “rejected” in every other sentence, and warning about those is how a check gets ignored.

The mention warning is narrower twice over. The edge can be DISCOVERED_FROM, PIVOTED_TO, PARENT or BLOCKED_BY, because a parent naming its child is a stated relation already. And the prose near the link has to claim the relation the properties name – “discovered while”, “filed from”, “pivoted to” and their neighbours – because a body links other issues for every reason there is: an umbrella rolls up what it does not close, a note says see also. A link that claims nothing is a link, not a missing edge.

What merges and what conflicts#

The file is not a CRDT. Fields that are sets can be unioned if two replicas are joined by hand. Fields that are a single value cannot.

Field

On a merge

logbook, append body, tags, BLOCKED_BY

Union. Duplicates drop.

state, claim, PIVOTED_TO, SIBLING_TERMINAL

Conflict. Use reject, resolve, or update --if-state.

Record what was done#

append adds a dated, attributed report to the issue’s body.

$ vissue append parser-1a2b --file SUMMARY.md
$ vissue append parser-1a2b --text "landed; back-pressure still open"

--file - reads stdin. Markdown is safe: a line that would otherwise end the issue is indented on the way out, so a bullet list does not split the heading in two.

This is the counterpart to note. A note is one line in the logbook, which is the audit trail of what happened to the issue; a report is prose about the work, which belongs under the heading where a reader looks for what the issue is about.

Open what the work stands on#

recall is the working set for one issue: the plan above it, what blocks it and what each of those produced, the issue it was bounced from, and what it has produced itself. Run it after claiming and before starting.

$ vissue claim parser-3xq7
claimed parser-3xq7 by impl (BLOCKED -> STARTED)
  `recall parser-3xq7` for 2 declared inputs, a plan above it
$ vissue recall parser-3xq7
$ vissue recall parser-3xq7 --json

It walks declared edges rather than ranking the corpus, so the set is what the plan says the work needs. related is the verb for the other question.

Name what a unit of work produced#

A claim says who is working and a note says what happened. Neither says what the work made, so the next unit had to reread a transcript to find out.

deedar records a product as a deed and freezes it. deed cites that accession on the heading, and the next issue’s recall hands it on:

$ deedar create patch --id deed-patch-backpressure --name "back-pressure" \
    --tree . --diff work.patch --functionary impl --agent impl
$ vissue deed parser-3xq7 --add deed-patch-backpressure
parser-3xq7: deeds += deed-patch-backpressure
$ vissue update parser-3xq7 --state DONE

Whoever picks up the issue this one was blocking opens the product directly:

$ deedar get $(vissue recall parser-9k2m --deeds-only)

The tracker stores the accession and nothing else. deed --remove drops a citation, and deed with neither flag lists them.

Check that the citations still resolve#

The tracker stores accessions and never opens the store, so it cannot tell you whether a cited deed is still there. check says whether a citation is shaped like one; the deed store says whether it is one. Compose them:

$ vissue recall parser-3xq7 --deeds-only | deedar evidence -
deed-patch-backpressure ok
deed-file-manifest FAILED: deed not found: deed-file-manifest
1 of 2 verified

deedar evidence - reads one id per line, checks each, and exits non-zero if any failed, so a job can gate on it. Run it over a whole project by feeding it every citation in the corpus:

$ vissue export | jq -r '.properties.DEEDS // empty' | tr ", " "\n" \
    | grep . | sort -u | deedar evidence -

Find everything standing on a product#

deedar current tells you a cited deed has been superseded. The next question is which work stood on it, and that is backlinks given an accession rather than an id:

$ vissue backlinks deed-patch-backpressure
parser-3xq7            (cites) (parser)
parser-9k2m            (cites) (parser)
api-4tn8               (body mention) (api)

(cites) is a :DEEDS: citation; (body mention) is prose naming the accession and nothing declared. Under routing the scan covers every tracker in reach, since a product has no project of its own and anything can stand on it.

The corpus decides which namespace the argument is in. A known issue id is an issue whatever it looks like, so a project literally named deed keeps working. A token nobody minted reads as an accession, and nothing else does. An accession nobody cited answers empty rather than failing, because the product may be real and simply unused.

Chained, that sweeps a corpus for work standing on a superseded product:

$ vissue export | jq -r '.properties.DEEDS // empty' | tr ", " "\n" \
    | grep . | sort -u | deedar current - \
    | awk '$2 == "SUPERSEDED" { print $1 }' \
    | while read -r deed; do vissue backlinks "$deed"; done

Decide something several agents disagree about#

vote records one ballot per identity and counts them. consensus weighs the same ballots by how much the group listens to each agent.

$ VISSUE_AGENT=reviewer vissue vote parser-3xq7 --for hold
$ vissue vote parser-3xq7
$ vissue consensus parser-3xq7

With no [consensus.trust] configured the weighted answer is the count as a fraction. Configure the rows and the report says where the two differ, which agent carried how much of the result, and whether the group can reach an agreement at all.

Follow the graph#

tree walks children and blockers below an id; graph emits the whole thing as Graphviz DOT; backlinks finds everything pointing at an id, or citing a deed accession; cycles reports a blocker loop; ancestors and impact bound the walk by hop depth.

$ vissue tree parser-3xq7
$ vissue graph --project parser | dot -Tsvg > backlog.svg
$ vissue ancestors parser-3xq7 --depth 3
$ vissue impact parser-k29f --depth 3

Keep the corpus consistent#

check validates every parent and blocker edge, that parent chains terminate, that dates parse, and that ids are unique. It exits non-zero on an error, so CI can gate on it. hygiene adds the claims that are not actually workable.

$ vissue check
$ vissue hygiene --stale-days 3

Tell whether a copy of the backlog is current#

digest hashes the corpus, combined and per project, so a consumer can compare two points in time without reading every issue.

$ vissue digest -P atlas -P beacon
combined=7f91ad67512010d0 issues=109 generation=3167 projects=2
6cdab6af46e1c979      12  atlas
671d99c6181c1494      97  beacon

The per-project lines are the point: a changed combined digest says something moved, and the sub-digests say which project. The hash is xxh3 over the JSONL export, so it tracks content and ignores formatting that changes nothing.

Every mirror carries that digest in its header, which makes freshness one command that exits 0 when current and 1 when not:

$ vissue mirror --check Software/atlas/issues-mirror.org
stale: Software/atlas/issues-mirror.org
  stamped digest=0000000000000000 at=2026-08-03T09:53 issues=12
  current digest=d2ee07c7f585330b issues=12 generation=3167
  moved: atlas 1111111111111111 -> 6cdab6af46e1c979

The check reads the projects from the stamp, so a caller need not repeat them.

See who is holding what#

Claiming stamps an identity and a timestamp onto the issue.

$ vissue whoami
rgoswami@workstation
$ vissue claims --by grind-worker-3 --json | jq -r '.[0].claimed_at'

The identity comes from VISSUE_AGENT, then agent in vissue.toml, then user@host.

Moving to STARTED by any route takes the claim if no one holds it. BLOCKED keeps it, because the holder is still on the issue. Returning to TODO, or closing, gives it up and writes a logbook note naming who held it and since when, so the history outlives the properties.

hygiene reports claims held longer than stale_claim_days (default 7) and STARTED issues nobody has claimed.

Report progress on an issue someone else holds#

note adds a dated entry to the top of an issue’s logbook and touches nothing else.

$ vissue note parser-k29f "grammar table regenerated; fuzz corpus next"
parser-k29f: noted

Fold in work discovered outside the tracker#

Something without write access to the tracker appends plain * TODO <title> headings to an inbox Org file on whatever shared storage it can reach. fold turns each unstamped heading into a tracked issue, then flips the heading to DONE and stamps it with the assigned id in place, so the inbox doubles as its own receipt and folding twice creates nothing.

$ vissue fold inbox.org --project parser
folded 2: parser-x1a2 parser-y3b4

A fold that fails partway still stamps the issues it created, so a rerun does not create them twice.

See what is due#

agenda lists open and blocked issues whose deadline or scheduled date falls inside a horizon, overdue first. A blocked issue still appears, because its date does not stop mattering while it waits.

$ vissue agenda -d 30
2026-07-23  deadline  12d overdue parser-k29f  STARTED  [#A]  Reject a manifest... (parser)

Emacs shows the same dates in its own agenda without this command; see Emacs.

Watch for changes without re-reading everything#

A write advances a generation counter and appends to a log, both beside the project directories. A poller compares the counter, then reads only what is new.

$ vissue gen
3167
$ vissue events --since 3155 -n 5
$ vissue wait --last 3167 --timeout-ms 30000   # exits 2 on timeout
$ vissue wait --id parser-1a2b --until-terminal --timeout-ms 30000
DONE 3182
$ vissue ping --detail "external change"       # wake pollers by hand

wait --until-terminal needs --id. It prints DONE <gen> or CANCELLED <gen> and exits 0, or TIMEOUT <state> <gen> and exits 2.

Set VISSUE_EVENTS=0 to suppress emission when the tracker must stay untouched.

Attach to the optional serve cache#

vissue serve -d owns the control socket and keeps a parsed catalog in memory. Interactive clients attach for issue/ready and push notifications instead of polling wait. The files stay the store: stop serve, delete the socket directory, and every CLI verb still works.

$ vissue serve -d
$ vissue serve status
live: true

initialize must send a non-empty agent. A claim over the socket stamps that agent, not the process VISSUE_AGENT that started serve. The wire is on Control.

Browse the terminal board#

vissue tui paints from the files first, then attaches to serve when the socket is free. --offline never probes a socket and never starts serve. A live serve bound to another root stays on the files: the board does not send mutations to the wrong vault.

$ vissue tui
$ vissue tui --offline

Panes: Ready, List, Claims, Agenda, Search. Detail tabs on the selected row: show, excerpt, tree, related, recall. The last is the working set: what the node stands on and what those inputs produced. Keys (also on ?):

Key

Action

j=/=k, arrows

Move

Tab / 1-5

Pane

Enter

Focus detail

p

Project filter

/

Search

c

Claim

n

Note

d

Cite a deed

s

Cycle TODO / STARTED / BLOCKED

D

DONE (confirm)

X

CANCELLED (confirm)

o

Open (shared selection)

y

Copy id

R

Reload

?

Help

q / Esc

Quit / back

The status line reads serve:live|offline|mismatch gen=N rev=M agent…=. Body edits stay in the file.

Summon the desktop palette#

vissue hud opens on the project list. Enter opens one project’s ready forest; List / Claims / Agenda then stay inside that project. Filters, search, and add sit on one row. / focuses search. Esc returns to the list. Ready and List nest children under PARENT. The selected row keeps the issue visible (properties above a wrapping body), with tree / related / notes / recall on the right. List titles wrap inside the pane. The tree tab expands or collapses the outline. The recall tab is the working set: the plan above the issue, each declared input with what it produced, and the last thing said about it. A 360-pixel overlay has room for three tab labels, so recall sits past the edge of the strip; Enter cycles to it. Escape on the project list unmaps the overlay; vissue hud --toggle shows it again. Closing the mapped window quits. n shows the logbook and writes a note; d opens the recall tab and cites a deed on the selected issue, refusing a value that is not an accession where you can see it. Keys are a catalog; ~/.config/vissue/keys.toml remaps them; reserved Esc=/=Enter=/=Tab=/?=. s cycles TODO/STARTED/BLOCKED, c claims, p project, D=/=X close, o opens, ? help, a adds. Tab or 14 switch panes.

--rofi is the seat dmenu picker. Return opens the heading in $EDITOR. Alt+c claims. Alt+n notes.

$ vissue hud
$ vissue hud --rofi
$ vissue hud --rofi --mode list
$ vissue hud --rofi --mode new

Point at a different layout#

Use --prefix, VISSUE_PREFIX, or a vissue.toml at the root:

prefix = "projects"

[issues]
default_priority = "B"
id_length = 5
stale_claim_days = 3

Each layer overrides key by key, so a file that tunes one knob leaves the rest alone.

Detect the project from the working directory#

With a .project-ctx.toml carrying [project] name = "..." in or above the current directory, --project may be omitted.

Know which tracker you are writing to#

create, update, claim, and refile all write, and claiming counts as a write because it stamps the holder onto the issue. When a wrapper or the environment sets VISSUE_ROOT, a bare vissue writes to that tracker from any directory. Pass --root explicitly for a scratch tracker, and vissue identity reports which binary and which root are in play before you commit to a mutation.

Ask the binary what it accepts#

vissue surface walks the parser and prints every subcommand with its aliases and long flags as JSON. Read it from a wrapper, a completion table of your own, or a check over this command line.

$ vissue surface | jq -r '.[] | select(.hidden == false) | .name'
$ vissue surface | jq -r '.[] | select(.name == "count") | .flags[]'

Parsing --help answers the same question worse. Help text is laid out for a person, so a wrapper reading it breaks the first time a description grows long enough to wrap. It also cannot tell a flag from a word in a sentence about one.

Install shell completions#

Generated from the binary’s own argument definitions, so they cannot drift.

$ vissue completions bash > /usr/share/bash-completion/completions/vissue
$ vissue completions zsh  > ~/.zfunc/_vissue
$ vissue completions fish > ~/.config/fish/completions/vissue.fish
$ vissue man > ~/.local/share/man/man1/vissue.1