Everything below runs in a scratch directory and touches nothing else. By the end you will have a tracker two people, or two scripts, can work at once without colliding.
1. Make a tracker#
Creating the first issue creates the file.
$ mkdir -p /tmp/demo && cd /tmp/demo
$ vissue create --project parser "Reject a manifest with no header"
parser-k29f TODO [#C] Reject a manifest with no header
file: /tmp/demo/Software/parser/issues.org
That file is ordinary Org:
#+TITLE: parser issues
#+CATEGORY: parser
#+FILETAGS: :issues:parser:
#+DATE: [2026-08-03 Mon]
#+DESCRIPTION: Issue tracking file for parser specs, plans, and implementation tasks.
#+STATUS: Active
#+TODO: TODO STARTED BLOCKED | DONE CANCELLED
* TODO [#C] Reject a manifest with no header
:PROPERTIES:
:ID: parser-k29f
:CREATED: [2026-08-03 Mon]
:END:
Nothing about it is private to this tool. Open it in Emacs and the agenda,
tag search, and id: links all work.
2. Add work that waits on other work#
$ vissue create --project parser --priority A \
--body "Scope: the error message quoted in the release notes." \
"Publish the release notes"
parser-3xq7 TODO [#A] Publish the release notes
$ vissue update parser-3xq7 --block parser-k29f
parser-3xq7: state TODO -> BLOCKED (auto on block), blocked_by += parser-k29f
Adding a blocker moved the issue to BLOCKED on its own, and the transition is in the logbook. An edge that would close a cycle is refused.
3. Ask what is actually workable#
$ vissue ready
parser-k29f TODO [#C] Reject a manifest with no header
The blocked issue is gone from the list, which is the whole point of ready.
It is the open frontier, not a schedule.
4. Work an issue and close it#
$ vissue claim parser-k29f
claimed parser-k29f by you@yourhost (TODO -> STARTED)
$ vissue update parser-k29f --state DONE
parser-k29f: state STARTED -> DONE, claim released (you@yourhost)
[hint] parser-3xq7 (in parser) lists this as a blocker; clear with `vissue update parser-3xq7 --unblock parser-k29f`
Closing a blocker gives up the claim and names every issue still waiting on it. The hint goes to standard error, so a pipeline reading standard output is unaffected.
5. Watch the backlog open up#
$ vissue update parser-3xq7 --unblock parser-k29f
parser-3xq7: state BLOCKED -> TODO (auto on unblock), blocked_by -= parser-k29f
$ vissue ready
parser-3xq7 TODO [#A] Publish the release notes
6. Put a second worker on it#
This is what the graph is for. A claim records who holds an issue, so the second worker is told there is nothing free rather than repeating the first one’s work. The identity is an opaque string: a person, a machine, a script.
$ VISSUE_AGENT=impl-1 vissue claim parser-3xq7
claimed parser-3xq7 by impl-1 (TODO -> STARTED)
$ VISSUE_AGENT=impl-2 vissue ready
# empty: the only open work is claimed
The second worker sees nothing to take. claims is the standing answer to
who holds what:
$ vissue claims
parser-3xq7 STARTED [#A] 0d impl-1 Publish the release notes (parser)
A claim held by another identity is refused unless you pass --force, which
records the takeover in the logbook rather than losing it.
Where next#
How-to for the rest of the verbs, one task at a time.
Emacs to put this tracker in the Org agenda, where its deadlines and tags already work.
Explanation for why the order is a partial order and why nothing infers an edge you did not write.