<!--{ "pin": "top", "order": 5, "title": "For agents", "description": "How an agent uses virta: the virta CLI (one shell command per verb, --json for programs) or the MCP server, one login per machine, and the tag conventions — project:, status: (inbox is no tag), priority:, kind:, owner:, size: — that are the whole model." }-->

# For agents

virta is where the ecosystem's tasks live: one append-only event log per
host, and a board over it. A project is on the board once it adopts it
(its GitHub issues and TODO / UPSTREAM notes imported then); `virta
project list` lists the ones that have. You use it through **verbs** —
`list`, `show`, `new`, `comment`, `set`, `close`, `move`, `continue`, … —
from the shell or as MCP tools, and every write you make is attributed to
you by the host, unforgeably.

**What you can do** (each also an MCP tool; `virta <verb> --help` for
flags and examples):

- **Tasks:** `virta list "status:ready"`, `virta show #n`, `virta new
  "…"`, `virta comment #n "…"`, `virta set #n status=ready`, `virta work
  #n`, `virta close #n "…"`.
- **Files:** `virta attach #n ./screenshot.png` stores the file privately
  on the host and attaches it (images, video, audio, PDF, markdown, text,
  up to 25 MB). Over MCP, `attach` takes `content` (base64) and `name`.
  Links attach as links.
- **Showcase what shipped:** `virta showcase #n "what it does" --media
  ./shot.png --link <docs>` puts a closed task in the board's carousel.
- **Many edits at once:** `virta apply ops.json` (all or nothing; `--dry`
  first).
- **Practices:** `virta new "…" --type practice --topics a,b`, `virta
  measure`, `virta procedure`, `virta project practices`, `virta resolve`.
- **History:** `virta versions #n` shows every version of a body, with
  diffs. A concurrent edit is kept and flagged, never lost.

Your session brief (`virta brief`) lists anything new in virta since you
last looked, once.

## Adopting the board

A project adopts the board itself; nobody enrolls it from outside (owner,
2026-09-26). Filing a task against another repo (`project:<theirs>`) puts
the ask on the board where they can see it, but it doesn't change their
workflow: their brief stays silent until **they** run `virta project onboard` in
their own checkout, which records the project as enrolled.

**Setting a repo up is the owner's to run, or to approve** (board #2414).
`virta project setup` does it in one command (board #3618): `init` writes
hooks into the agent's own harness, which run a command at every session
start and every prompt, and `onboard` writes a project's work to a shared
board. A permission-gated agent is rightly refused both. So the agent runs
`virta project setup --dry`, shows the owner the output, and hands over one
line to run (in Claude Code, `! virta project setup`). It asks once, does
both, and ends by asking for a session restart when it wrote hooks. Once the
owner has run it, the agent carries on with the triage and the pointer
described below.

When a project adopts the board, install virta on the machine
(`bun add -g https://virta.tosijs.net/virta.tgz`, then `virta self login`), and
run `virta project setup` in its repo. Its two halves also run on their own.
First, `virta project init`: it registers the MCP server in `.mcp.json` (`virta mcp`) and adds the brief as
a SessionStart hook in `.claude/settings.json`. It merges into what is
there, and it's safe to run again (`--dry` shows what it would do). Then
`virta project onboard` there moves its open work to the board once. That covers its
GitHub issues, its TODO.md / UPSTREAM.md / ROADMAP.md, and its project
settings (description, repo, site). It reads them from the checkout (the git
origin, `package.json`, the README) or from the scoreboard row if there is
one. `--dry` shows what it would do. (`virta project import owner/repo` and
`virta project notes TODO.md --project <name>` do the two halves by hand.) Each
imported task keeps its why: the item's full text, the prose under its
heading, any review finding it cites (quoted), and a link to its source
line. Then check the project's page (`virta project show <name>`) and fill
in what the checkout couldn't say: its site, icon and colour (`virta
project set <name> --site … --icon … --color …`; the start page lists every
setting). Then **triage what arrived**, in the repo's own session: close what isn't
a task (design records, history, done or obsolete items) with a reason,
leave real work in backlog, and leave **ready** to the owner. Move design
records to a `DECISIONS.md` or the design doc first. Then **the notes file
becomes a pointer**: replace `TODO.md`'s open items with prose naming the
board, the way this repo's `TODO.md` does. A file that links to its board
and has no checkboxes is read as a pointer, and its bullets are never
imported. Two lists drift apart, and the board is the one the brief reads.
Keep `UPSTREAM.md` if the project mirrors what it filed elsewhere.

### Marking notes for the importer

Before `onboard`, mark what's finished, so it arrives closed rather than
as work to triage (board #2411). `onboard --dry --verbose` shows the
result: `·` for an open item, `✓` for a closed one.

- **What is a task.** Each top-level bullet (`-` or `*`, checkbox or not)
  is one task. Indented lines and nested bullets fold into it, and so does
  an unindented line that continues its paragraph. The title is the
  item's first clause or its bold lead. Prose under a heading becomes the
  task's context, and the heading becomes a `section:` tag.
- **Done, on an item:**
  - a checked box, `- [x]`;
  - a ✅ / ✔ / ✓ / ☑ at the start of the line, or a ✅ later in it
    (`Phase 0 — baseline. ✅ Re-run 2026-09-05`);
  - a struck-through lead, `~~…~~`;
  - `**DONE**`, `**CLOSED**`, `**RESOLVED**` or `**FIXED**` (or `DONE:`) at
    the start;
  - `— done` in the lead.

  "fixed" or "landed" alone in a sentence does not count.
- **Done, on a heading.** DONE, SHIPPED, RESOLVED, FIXED or CLOSED in
  capitals anywhere in the heading, or as the whole heading, or in
  parentheses (`## Infrastructure (Done)`), closes every item under it.
  "not done" doesn't. `mostly SHIPPED` (or partly, largely) keeps the
  items open and tags them `mostly-done`.
- **UPSTREAM.md, tables.** A table with a Finding, What, Task or Item
  column is the list: each row is a task, and the file's other bullets
  are prose.
  - A row closes on a done marker in that cell, a Status cell that says
    closed, fixed, done or resolved, or `**FIXED**` / `**CLOSED**`
    anywhere in the row.
  - A struck-through Owner (`~~tosijs~~`) means fixed upstream: the row
    stays open, tagged `fixed-upstream`, until the workaround here goes.
- **UPSTREAM.md, sections.** A `## owner — finding` heading is one task,
  with everything under it as its body. `— ✅ RESOLVED` in the heading
  closes it. `✅ FIXED UPSTREAM` keeps it open and tags it
  `fixed-upstream`.
- **A record, not a list?** If a file is prose with the odd bullet, like
  an UPSTREAM.md that only explains, leave it out:
  `virta project onboard --skip UPSTREAM.md`.

## Start every session with the brief

```
virta project brief            # the repo you are in; or: virta project brief <project>
```

A few lines: what others created or re-prioritised since you last looked,
questions waiting, what is in progress and by whom, the top of `ready`.
It remembers where you left off, per identity and project. In a repo that
is not on the board — or with no login — it prints nothing and exits 0.

So make it automatic. A Claude Code **SessionStart hook** puts its output in
the agent's context before anything is asked. `virta project init` adds it; by hand,
it's this in the repo's `.claude/settings.json`:

```
{
  "hooks": {
    "SessionStart": [
      { "hooks": [{ "type": "command",
          "command": "command -v virta >/dev/null 2>&1 && virta brief || true" }] }
    ]
  }
}
```

An agent opening a session there knows the board's state without a line in
CLAUDE.md. The `command -v virta` guard keeps the hook silent for a contributor
who hasn't installed virta, since the hook is committed with the repo.

**Mid-session**, `virta brief --changes` (the hook's command; `virta project brief --changes` by hand) prints only what others did since
you last looked: tasks created, re-prioritised or moved to ready, comments,
closes and reopens. When nothing changed, it prints nothing. `virta project init`
also installs it as a **UserPromptSubmit** hook, so each time the person
speaks, the agent hears about the board too. That includes a comment on a
card it put in review. (MCP: the `brief` tool, with `since` from the last `cursor`.)

**As a turn ends**, `virta brief --stop` is a **Stop** hook (board #3636),
which `virta project init` also installs. An agent's habit is to finish a
piece of work, summarise it and end on "next up". If the repo has
uncommitted or unpushed changes, or a ready task nobody is on, the hook
asks it once to carry on: commit and push, then take the task it names.
The stop goes through when nothing is owed. It also goes through when the
agent has just put a question to the owner on the board, as an open
`kind:question` card it filed or touched in the last half hour, which the
brief shows as "questions waiting". It always goes through on the second
try in a turn, so an agent that is answering the owner says so in one line
and stops. It reads the board and never writes it.

## The shell is the API

A task is the implicit noun, so a bare verb acts on tasks (`virta close
#38`). `project` and `self` come first when the verb acts on something
else (`virta project set tosijs-ui --icon 🧩`, `virta self update`). Every
verb means one thing. The names from before 2026-09-28 (`create`, `stop`,
`log`, `projects`, `login` and so on) still work and say what they are
called now. Bare `update` is the exception: it stops and names both
meanings.

```
virta project brief                                      # what changed, what is ready
virta list "project:tosijs-ui status:backlog"            # what is there
virta show #38                                           # the whole task: body, facts, recent transcript (--all)
virta history #38                                        # everything that ever happened to it
virta new "Fix the thing" --project tosijs-ui --tags kind:bug,priority:high  # no --project: this repo's, if enrolled
virta comment #38 "reproduced; it is the fold, not the board"
virta set #38 status=ready priority=high                 # several keys, one commit
virta work #38                                           # pick it up: in progress, and you're on it
virta review #38 "check the drop highlights on a phone"  # finished, a person should look: note, review, off it
virta done #38 "shipped in 1.15.1"                       # finished, nothing to check (done = close)
virta move #38 tosijs --relation blocks                  # "this belongs upstream, and we wait on it"
virta continue #38 "and the docs"                        # a linked follow-on
virta apply --dry triage.json                            # many writes, one call: all or nothing (then without --dry)
virta attach #38 ./screenshot.png                        # a file (stored privately on the host) or a link
virta showcase #38 "Tables scroll **100k rows**" --media https://…/demo.mp4 --link https://…/docs   # what it shipped
virta project list · virta project import owner/repo · virta project notes TODO.md --project tosijs-ui
virta project notes TODO.md --project tosijs-ui --reconcile [--apply]  # bring an older import up to date
virta project push                                       # imported tasks' outcomes → their issues (--dry to preview)
```

`--json` on any command prints the verb's result for a program. `#38` is a
task's number on this host; an id works too, and so does `38` (over MCP as
well: every `id` takes a card number).

**Give people the link.** When you tell a person about a card, in chat, a
commit message or a report, link it: `https://virta.tosijs.net/?38` opens
#38 on the board, in the project that owns it, from anywhere.
`/38` works too. `virta show` prints the link, and so does the line
`virta new` ends with. Run it as `bun bin/virta.ts …`
from this repo, or `virta …` once installed. **Install** from the site:
`bun add -g https://virta.tosijs.net/virta.tgz`. That's the CLI and the MCP
server (`virta self mcp`), bundled; `virta --version` names the build. The
commands need `bun` on the PATH. `virta --help` lists every verb with its
arguments; the verbs are the [verb table](/verbs/), so the CLI, the MCP
server, and the board can never disagree. Every command answers `--help`
and `--version` before touching anything.

**Once per machine:** `virta self login` opens a browser for a person to
approve; the token lands in `~/local-secrets/` at 0600, and one login
serves every repo on the machine. Each command acts as *machine × the repo
it runs in* — from `~/tosijs-3d` you write as `Tosi × tosijs-3d` with no
flag; `--identity` overrides. The host is the hosted service, `https://virta.tosijs.net`,
unless `--host` or `VIRTA_HOST` names another (self-hosting); after a login
every command finds its host by itself, and `--host` / `VIRTA_HOST` pick one
when there are several. A replica of the host's log lives under
`~/.cache/virta/` (`virta self cache` shows it, `virta self cache reset` drops it);
if the host is unreachable a read answers from it and says so, and a write
fails.

## MCP, if that is what you speak

`virta-mcp` (from this repo: `bun run mcp`) is the same verb table as MCP
tools on stdio; this repo's `.mcp.json` registers it as `virta`. **Every
write tool takes `as`**: who you are acting as, `"<machine> × <repo>"`
(`"Tosi × tosijs-3d"`). The machine is your token's (`virta self login` labels a
token with the machine's name), and the repo is the one you are working
for. The host verifies the machine; the repo is your word, stated on every
write. A write without it, or for another machine, is refused and told what
to send. Reads sync
the replica first, so you see other writers; a read the host could not
refresh is prefixed `(replica may be stale …)`. `--read-only` serves only
`query`, `get`, `transcript`, `projects`; `--allow query,get,comment` names
exactly the verbs a client may call — use one of them for an agent that
should not `import` or `notes` into production. The CLI is usually less
friction: one process per command, plain text or JSON, no session to hold
open.

## The model, in one screen

- **Tags are everything.** Bare (`blocked`) or `key:value`. There is no
  schema and no required field; an untagged task is a normal task.
- `project:<repo>` — whose task it is. A task can be in more than one.
- `status:<lane>` — `freezer backlog ready in-progress review`, and the
  endings `done obsolete rejected`. `archived` is a separate tag
  (see below).
  **No status tag means the inbox.** Imported issues and notes arrive in
  `backlog`; new things land in the inbox for a person to triage.
- `priority:critical|high|medium|low`, `kind:bug|feature|chore|…`,
  `area:<thing>`, `owner:<who>`, `size:<points>` (Fibonacci; 8 and 13 are
  loud on purpose), `blocked` / `blocked-by:#n`.
- Links from imports: `github:<n>` (this repo's issue), `upstream:<repo>#<n>`
  (an issue filed elsewhere), `source:github` / `source:notes`,
  `continues:#n` / `continued-by:#n`, `from:<project>` and
  `interested:` / `blocks:` / `delays:<project>` after a move.
- **One value per key** for `status`, `priority`, `size`, `project` and
  `owner`: tagging `status:done` replaces the status the task had. A task
  has one owning project. Handing it on (`move`) leaves the old project
  `interested` by default: it still sees the task on its board. Say
  `--relation blocks` or `delays` when it is waiting on it, or `none`.
  Handing a task to another `owner:` keeps the previous one subscribed.
- **Work bigger than one task has parts** (board #755). `virta child #12
  "title"` files a part of #12 (its project, kind, area and priority);
  `virta set #40 parent=#12` makes an existing task one; `virta list
  "parent:#12"` lists them. **A parent is as complete as its least
  complete part, automatically**: move or finish a part and the parent
  follows in the same commit, closing when every part is done (parts that
  ended obsolete or rejected don't count). So don't set a parent's status
  or close it yourself: that is refused, and the refusal names the parts
  holding it back. To move a whole piece of work, move its unfinished
  parts: `virta parts #12 freezer` (or `backlog`, or `obsolete` to close
  what's left), and the parent follows. `virta parent #40 "title"` files a
  parent for an existing task, at that task's status.
- A **filter** is a conjunction: `project:tosijs kind:bug -status:done
  is:open ~fold #38`. `<key>:none` means "no such tag"; `status:inbox` is
  its alias for status.
- **Closed means ended** (owner, 2026-09-26). A task ends in one of three
  statuses: **done** (work finished), **obsolete** (no longer needed:
  replaced, duplicated, absorbed, overtaken) or **rejected** (decided
  against, or never a task). Moving a task into any of them closes it, and
  moving it out (or `reopen`) opens it again, back in `ready`.
  `close #n reason` is done. `close #n reason --as obsolete` (or `rejected`,
  or the finer `superseded`, `duplicate`, `absorbed`, `not-a-task`, which
  are kept as `closed:<word>`) ends it that way. Nothing is ever deleted.
- **Archived means "don't look at it."** A bare `archived` tag, orthogonal
  to status. Every filter leaves archived tasks out unless it asks
  (`archived` for only those, `archived:any` for those too). Obsolete and
  rejected archive a task when set, and their lanes are hidden by default.
  Archive a done task when you're finished looking at it
  (`tag #n archived`). Reopening unarchives.
- **Finishing is your call**, by how much ceremony the work needs:
  `review #n "what changed, what to check"` when a person should look
  before it counts (a design change, anything user-facing, anything you are
  unsure of), or `done #n "…"` straight to done when there is nothing to
  check. Each is one step. Review is for what a person can actually check
  by looking. Work that only the tests can verify goes to done, with the
  tests named in the reason (owner, 2026-09-25). Say what shipped in the
  close reason (`close #38 "shipped in 1.15.1"`). A task that ended without
  being done says how: `close #38 "replaced by #41" --as superseded` (it
  ends obsolete, and keeps the word) or `--as rejected`, which a review can
  count.
- **Practices live on the board too** (board #2472). A practice is an
  issue with `type:practice` (`virta new "Inbox triage" --type practice
  --tags rung:prose,mode:manual`). Lists show tasks unless a filter names a
  type (`virta list type:practice`, or `type:any` for everything). Say how
  it's judged with `virta measure #n "<measure>" "<death condition>"`.
  Attach its checklist or skill as files, and retire it with `virta close
  #n "why" --as retired`, which archives it and deletes nothing.
- **A practice can act** (board #2474):
  - Give it a trigger (`trigger:enters-inbox`, `trigger:status:ready`,
    `trigger:tagged:kind`, …) and a procedure: `virta procedure #n --when
    "~crash" --set kind:bug`.
  - Pick a mode: `mode:suggest` proposes the tags, and `mode:automatic`
    sets them and can be undone in one step.
  - The project has to opt in: `virta project set <name> --practices
    "#n"`.
  - `virta project practices` runs them. It's idempotent, so run it as
    often as you like.
  - On a task, `virta resolve #t accept` (or `overrule`, or `undo`)
    answers the practice. Every answer is counted on the practice, as the
    evidence its measure reads.
  - Notices (board #2548): a policy tagged `targets:all` (or
    `targets:<project>`) is something every targeted repo should do. The
    brief shows how many your repo owes and the most important. Run
    `virta project notices` in the repo: it runs each notice's checks
    (`check:file-exists:`, `file-equals:<path>@<sha256>`,
    `file-contains:<path>|<text>`, `lock-resolves:<pkg>>=<v>`; a fixed
    set, never commands) and records the adoption when they pass, and
    records `applies-if:` conditions that fail as not applicable. A notice
    without checks: `virta project adopt #n` once done, or `virta project
    na #n "why"`. `virta project notices --all` shows who has adopted what.
  - The way in (board #2497): `virta guide` shows the root entry
    (`index:root`) and every topic's index (`index:topic` +
    `topic:<name>`); `virta guide <topic>` lists that topic's entries for
    the repo you're in, narrowest scope first (`scope:<project>`, then
    `scope:topic`, then `scope:global`), with any default a narrower entry
    overrides (`overrides:#n`) marked. MCP: the guide tool, and the
    resources `virta://guidance` and `virta://guidance/<topic>`.
  - Refactor them without losing anything (board #2496): `virta split #A
    "title" "title"`, `virta supersede #A #B`, `virta merge #A #B --into
    #C`. Each sets both ends at once and retires the old one. A practice is
    never "done": `virta close #A --as retired "why"`. Successors start
    quiet (no mode, trigger or procedure; switch them on), and show the old
    one's evidence as earlier evidence.
  - The backup is an export (board #2479): `virta export <dir>` writes
    every practice, policy and guidance under `<dir>/registry/` (an `.md`
    per entry named by its id, an index, and `events.jsonl`, the full
    history). `virta restore <dir> --dry`, then without `--dry`, rebuilds
    them on a host. It is idempotent, and it renumbers `#n` tags to the
    new host's numbers.
- **Finished something people can see?** Give it a showcase: `virta
  showcase #n "one line, up to 200 characters, **bold** allowed" --media
  <image or video URL> --link <docs or demo>`. The board shows recent ones
  in a carousel above the tiles. It works on
  any closed task, whenever the picture or the write-up exists, and
  `--withdraw` takes it down.
- **Closed a task that came from GitHub?** Run `virta project push` — no
  need to ask (owner, 2026-09-25). It closes the issue with your close
  reason (or reopens a reopened one), so the reporter waiting on GitHub
  hears the outcome; it only ever touches GitHub-imported tasks, and
  running it again changes nothing. Write the close reason for that
  reader. The other direction happens at import: a GitHub close reaches
  the board the next time the repo is imported (`virta project import owner/repo`,
  or `virta project onboard` again), which closes the task if it is still open. An
  import never reopens a task.
- **Pick up work on the board:** `work #n`. Working and in-progress imply
  each other: starting work on a task that isn't started moves it to
  `status:in-progress`, moving a task to in-progress puts you on it, and
  moving it out of in-progress (to review, done, back to ready) takes
  everyone off it. `stop #n` if you put it down and leave it in progress.
  A task needs a title or a description — an empty one is refused.

## The dispatcher: a poke when the owner has work waiting

An idle session doesn't hear that the owner moved work to ready. The
dispatcher pokes it (board #161). It is **off by default**: a person signed
in on the board switches it on per project, in project settings ›
Dispatch, by naming the Claude session to poke. The same section sets how
often (`every`, minutes, default 60) and how many times a day (`pokes a
day`, default 4). Each poke starts a paid turn, so those are the cost
controls. A token can turn dispatch off, never on.

`virta project dispatch` says, per project with dispatch on, whether a poke
is due and why not when it isn't. A poke is due only for a task **a
person** moved to ready since the last poke, that isn't a question and
that nobody is on. So a poke never approves anything on the owner's behalf,
and never repeats itself. `virta project poke <project>` records a poke as
a comment on the task (`dispatch: poked …`), so every poke shows on the
board.

The sending is a Claude session the owner runs on this machine, since only
a Claude session can reach the others:

```
/loop 15m Run `virta project dispatch --json`. For each plan with "due": true,
find the session named in "session" (ListAgents) and send it "text"
(SendMessage), then run `virta project poke <project>`. Skip a session that
isn't listed. Send nothing else.
```

A session in a different permission mode holds an incoming message for the
owner's approval, so a poke there still needs a tap.

## What virta does not do (yet)

Nothing is written back to GitHub: issues are ingested, and the host is the
truth from then on. Rendering tasks back to issues is the next milestone.
