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
#+VISSUE: 1
#+CATEGORY: parser
#+FILETAGS: :issues:parser:noexport:
#+TAGS: { bug(b) feature(f) task(t) chore(c) plan(p) }
#+TAGS: docs(d) perf ignore ARCHIVE
#+PRIORITIES: A C C
#+EXCLUDE_TAGS: noexport
#+SELECT_TAGS: export
#+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. An agent must not Write or StrReplace that file: vissue and the MCP server are the writers, and two creates on the same file serialize under a lock. A hand edit does not join that lock.

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. A parent with :ORDERED: t holds later children out of that list until the earlier ones are DONE or CANCELLED.

4. Work an issue, name what it made, and close it#

$ vissue claim parser-k29f
claimed parser-k29f by you@yourhost (TODO -> STARTED)

$ vissue deed parser-k29f --add deed-patch-manifest-header
parser-k29f: deeds += deed-patch-manifest-header
parser-k29f: 1 deed
  deed-patch-manifest-header

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

The accession in the middle is the handoff. deedar mints one for the thing the work produced and keeps the bytes; the tracker keeps the id, so whoever picks up the next issue can open the product instead of asking what happened. Nothing here requires deedar to be installed: the tracker only ever stores the id.

5. Watch the backlog open up, and hand the product on#

$ 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

Unblocking dropped the edge, so recall now has nothing to hand over. Ask it before clearing the blocker instead, which is the ordinary order: the next worker claims, reads what its inputs produced, and only then starts.

$ vissue recall parser-3xq7
parser-3xq7            BLOCKED   Publish the release notes  (parser)

Inputs
  parser-k29f            DONE      Reject a manifest with no header  [blocked-by]
    deed-patch-manifest-header

Produced
  (nothing cited yet)

Body
  (no body)

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. Compare-and-swap, then watch the log#

Two writers do not last-write-wins. Name the generation you read, and a later write refuses if the corpus moved:

$ vissue gen
7
$ vissue update parser-3xq7 --state DONE --if-gen 7

gen, events --since, and wait are the log a second process polls. The tracker is those files and that log. Completing a session elsewhere does not close an issue.

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