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.

7. Share it with someone who cannot reach the tracker#

$ vissue mirror --project parser --out /tmp/demo/backlog-mirror.org
wrote /tmp/demo/backlog-mirror.org

That file is a read-only projection with a banner saying so, and a stamp that makes staleness one command:

$ vissue mirror --check /tmp/demo/backlog-mirror.org
fresh: digest=d2ee07c7f585330b issues=2 generation=7 (stamped 2026-08-03T09:53)

It exits 0 when current and 1 when not, so a script can gate on 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.