vissue serve owns a per-user Unix socket and answers JSON-RPC 2.0 in issue terms. It is a notify and cache plane: an in-memory catalog of the same Org files the CLI already parses. A crash loses nothing. The files stay the store.

Clients never bind the socket. Only vissue serve does. The listener is Unix-only. CLI, MCP, the terminal board, and the HUD all keep working with serve down; see Fallback.

Socket#

Default path, first match wins:

$VISSUE_CONTROL_SOCKET
$XDG_RUNTIME_DIR/vissue/control.sock
~/.vissue/run/control.sock

-s / --socket on serve, tui, and hud is the same override as the environment variable. Multiple vaults use distinct sockets; one serve process binds one root and one prefix.

Sibling files sit next to the socket:

Path

Role

control.sock

Unix socket, mode 0600

control.sock.lock

Exclusive flock; never unlinked

control.sock.pid

Owner pid, mode 0600

control.log

Detached serve stderr

hud.sock

HUD summon socket

hud.log

HUD stderr

The parent directory is mode 0700. Override the serve log with VISSUE_SERVE_LOG, the HUD log with VISSUE_HUD_LOG, and the summon socket with VISSUE_HUD_SUMMON_SOCKET.

Accept checks the peer uid (Linux SO_PEERCRED) and closes a foreign connection with no JSON-RPC reply. There is no TCP bind and no token in the file store.

Framing#

Two frames on the same socket. The owner accepts either and replies in the frame the client used.

  • One JSON object per line (JSONL).

  • LSP-style headers ending in Content-Length: N plus a blank line, then N bytes of JSON.

The first-line rule decides the frame: read the first line; if it matches ^[A-Za-z][A-Za-z0-9-]*: treat the stream as headers; otherwise treat that line as a JSONL message. Do not require the first byte to be {. A leading blank line or a UTF-8 BOM is JSONL.

Limits: body at most 8 MiB; at most 32 header lines. A notify that cannot be sent in 2 s drops that client rather than stalling the bus.

Handshake#

JSON-RPC 2.0, protocol version 1. The envelope and initialize stay camelCase (protocolVersion). Issue payloads and pagination (since_revision, claimed_by on list rows, holder on claims rows) are snake_case.

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
  "protocolVersion":1,
  "client":"vissue-tui",
  "agent":"alice@host"
}}
{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":1,
  "capabilities":["issue/list","issue/get","issue/ready"],
  "root":"/path/to/tracker",
  "prefix":"Software",
  "generation":3167,
  "revision":1,
  "identity":"alice@host"
}}

protocolVersion other than 1 is -32602 with {"supported": 1}. Missing or empty params.agent is -32602 (agent is required).

revision is serve-local and starts at 1. It increments on every catalog rebuild, including an Emacs edit that never touched the generation file. generation is the on-disk counter (vissue gen). A client that just attached drops since_revision for one full fetch and never feeds a core generation into issue/list.since_revision.

Identity#

initialize.params.agent is required and non-empty. That value sticks on the connection. issue/claim and issue/update may send agent to override; omitting it means use the initialize agent.

Serve never falls back to process VISSUE_AGENT for a control client, including one that auto-started serve and inherited the parent’s environment. After bind, serve ignores VISSUE_AGENT for RPC identity. CLI and MCP still resolve identity from the environment and config as they always have.

Methods#

Capabilities returned by initialize (v1):

Method

Role

issue/list

Filtered rows; ready: true is the frontier

issue/ready

issue/list with ready: true

issue/get

One issue, IssueDetail plus revision

issue/show

Alias of issue/get

issue/excerpt

Secret-screened body range

issue/search

Substring over id, title, properties, tags, body

issue/claims

Live claims; holder field is holder

issue/agenda

Deadlines and scheduled starts

issue/tree

Children and blockers; format nodes, ascii, dot

issue/related

Bounded neighborhood with evidence

issue/children

Direct children

issue/ancestors

Walk up; optional depth

issue/impact

Walk down; optional depth

issue/backlinks

Everything pointing at the id

issue/open

Shared selection; notifies issue/selected

issue/create

Create; fields of the CLI create verb

issue/update

State, priority, block, unblock

issue/claim

Take the issue; force default false

issue/note

Dated logbook entry

issue/refile

Move to another project

project/list

Project names plus revision

events/since

Pull of the JSONL event log

events/gen

{generation, revision}

identity/get

{identity, root, prefix, version}

Not in v1: issue/fold, issue/hygiene, issue/check, issue/export, issue/mirror, issue/graph, issue/roadmap. Those stay CLI and MCP.

Reads#

issue/list takes optional project, state, ready, query, limit, offset, since_revision. query is a case-insensitive substring over id, title, tags, and properties, not the body. Matching the current serve revision returns {"unchanged": true, "revision": N} and no rows. List rows use claimed_by, not holder.

issue/get / issue/show return id, project, title, state, priority, properties, org_tags, tags, blocked_by, parent, claimed_by, claimed_at, file, line_start, line_end, plus revision.

issue/excerpt returns {id, file, line_start, line_end, text, suppressed}. suppressed is true when the screen hides the range; text is then the suppression sentence that names the path.

issue/claims rows match vissue claims --json: id, project, state, priority, holder, claimed_at, age_days, title.

issue/search default limit is 20. issue/agenda default days is 14. issue/related default depth is 2 and limit is 20. issue/tree with format ascii or dot returns {text}.

Mutations#

Every mutation returns {ok, report, issue, revision, generation}. issue is the post-write detail (null on refile of a vanished source). report is the same text the CLI prints. agent on a mutation is optional and overrides the connection agent.

Mutations go through the same library ops and file lock as the CLI. Serve is not an exclusive writer. A vissue claim in another terminal, an MCP tool call, and an Emacs C-c C-x all land in the file; serve notices via the generation counter and issues.org watches, rebuilds 200 ms after the last signal, and pushes vault/changed.

Notifications#

No id on these messages. Broadcast after the mutating RPC’s response, so the caller can apply its own result before the echo.

  • vault/changed after a catalog rebuild. Params: generation, revision, projects (dirty names), ids when known.

  • issue/selected after issue/open. Params: id, project.

  • serve/shutting_down when the owner is exiting. Params: {}.

events/since is a pull of the on-disk log, not a substitute for vault/changed. Attached clients do not call wait.

Errors#

No HTTP status codes. Application failures carry error.data.code.

Situation

error.code

error.data.code

Parse error

-32700

Invalid request

-32600

Method not found

-32601

Invalid params (including missing agent)

-32602

Internal

-32603

Issue not found

-32004

not_found

Claim conflict

-32009

conflict

Closed issue / invalid state

-32010

invalid_state

Blocker cycle

-32022

cycle

Verbs#

$ vissue serve              # foreground; SIGINT/SIGTERM exit
$ vissue serve -d           # detach; return when the socket accepts
$ vissue serve stop
$ vissue serve restart      # stop, then start -d
$ vissue serve status       # exit 0 if live, 1 otherwise
$ vissue serve status --json
$ vissue tui                # board; auto-start serve if the socket is free
$ vissue tui --offline      # never attach, never spawn
$ vissue hud                # iced task board
$ vissue hud --rofi         # seat dmenu picker
$ vissue hud --rofi --mode new

serve -d places the child in its own process group (process_group(0)) and does not start a new session. The child can still receive SIGHUP from the parent terminal. That is the weaker detach; there is no setsid. A second -d probes the socket, prints that it is already running (pid and socket), and exits 0.

serve status prints:

live: true
pid: 12345
socket: /run/user/1000/vissue/control.sock
root: /path/to/tracker
prefix: Software
generation: 3167
revision: 42
clients: 2

vissue tui paints from the files first (revision 0). Unless --offline, it then attaches to a live socket or starts serve -d on the same root, prefix, and socket. A root or prefix mismatch stays on the files and refuses mutations against the other vault. Status names live, offline, or mismatch, then generation, revision, and agent. Keys are on ?. On Windows only tui --offline runs.

vissue hud execs vissue-hud. Home is the project list; opening a project is the task board over those files. Unless --offline it then attaches to serve the same way the TUI does. --rofi is the seat dmenu picker and does not need serve. Return opens the heading. Alt+c claims. Alt+n notes.

How-to covers the board keys and the overlay; this page is the wire.

Fallback#

The file is the database. Serve is optional.

  • CLI verbs talk to the files whether serve is up or down.

  • vissue-mcp is stdio and stays out of the socket. It does not speak this protocol.

  • vissue tui and vissue hud paint from the files first. If the socket is free they start serve -d. If spawn fails, or the live serve is bound to another root, they keep using the files plus the generation counter, including claim and note. --offline skips attach entirely.

  • Emacs edits issues.org directly. Serve notices the write. vissue.el still shells out to the CLI; see Emacs.

Stop serve, delete the runtime socket directory, and every verb still works.