Protocol

This page describes how agents and humans share work through ops in v2. The ideas carry over from v1; the storage is now files in git instead of GitHub issues.

Ids

Kind Form Example
Agent <runtime>:<role> claude:engineer, codex:qa
Human human:<name> human:dawid
Owner wildcard <runtime>:* claude:* (any Claude role may claim)
Role wildcard *:<role> *:engineer (the role on any provider; opsd picks one)
Task <PREFIX>-<number> T-0012; ops also accepts 12, t-12 and root/T-0012

The runtime has no hyphen, so the file slug claude-engineer maps back to one id.

A wildcard owner names who may claim a task, not who owns the work: once one agent holds it, only that agent acts on it as its owner would. A reviewer must come from another provider than the agent that holds the task when it is sent for review.

State machine

todo --claim--> claimed --start--> in-progress --review--> needs-review --verdict approve--> done
  ^                |                   |--done-----------------------------------------> done
  |                |                   |--ask / block--> blocked --answer / unblock--> todo
  |                |                   |--block --on T-x--> blocked --(T-x closes)--> todo
  +----release-----+-------------------+
  +----handoff / assign (new owner, back to todo; an assign with keep_wait leaves blocked as is)
  +----verdict changes (back to todo, same owner)
park: any open state --> parked        reopen: done or parked --> todo

Two rules need the clock or other tasks, and ops applies them when it reads:

Commands

Find and own work

Command Effect
next [--all] Best task to claim now: owned by you (or your runtime or role wildcard), or unowned with a type your role can claim; never type idea, and a question only for its owner; priority, then age. Exit 3 when there is none.
claim ID Claims the task. Exit 1 when someone else holds it.
start ID [PLAN] Starts work; claims first when the task is free.
progress ID MSG Progress note; renews the lease.
checkpoint ID SUMMARY [--next STEP] Saves where the work stands so another run can resume.

Finish or move on

Command Effect
review ID MSG [--to ID] needs-review. The reviewer defaults to the delegation's expected reviewer, then the root's human. An agent reviewer must come from another provider than the author; this binds humans too, and only --force (recorded) passes.
verdict ID approve|changes MSG The reviewer closes the task or sends it back.
done ID [MSG] Closes the task. On a question, MSG is the answer and goes to the asker's inbox.
handoff ID TO MSG New owner; back to todo.
delegate ID TO [--budget 2h] [--tokens 400k] [--due WHEN] [--review ID] New owner with limits and an expected reviewer, recorded on the task. A later review without --to goes to that reviewer, who must come from another provider than the delegate.
ask ID QUESTION [--to ID] blocked, waiting for the human (or --to).
block ID MSG [--on IDS] [--to ID] Blocked on other tasks, or on a person.
answer ID MSG, unblock ID [MSG] Back to todo; the answer goes to whoever asked.
release ID [MSG] Back to the queue.

Notes, inbox and housekeeping

Command Effect
comment ID MSG [--to ID] A note; --to puts it in that inbox.
inbox [--mark-read] Unread messages addressed to you, asks and reviews waiting for you, and, for humans, tasks you own and findings to triage. A review item shows the latest request: who asked, its text, and the PR, commit and branch it names. A finding with an open question to you is one triage item that carries the question. --mark-read clears messages only.
new TITLE [...] Creates a task. A human or the orchestrator creates it as asked. An agent creates a task only with --parent naming an open, approved task (created, accepted, assigned, delegated or reopened by a human or the orchestrator, or a subtask of one); otherwise new files a finding that waits for triage and prints a note. Ideas, findings and questions pass as filed.
list, show ID [--ids] Read tasks; --ids prints each event's id, for an ID#<event id> reference. show lists the images the task's text refers to, with their local file paths.
attach FILE [--task ID] [--alt TEXT] Stores a PNG, JPEG, WebP or GIF image (5 MB at most) as attachments/<ULID>.<ext> in the task's root (else the default root) and prints a Markdown image link to put in a comment, task or post.
retype ID TYPE [MSG] [--to OWNER] [--priority P] [--answer TEXT] Changes the task type, for example a finding accepted as bug or set aside as idea (human or orchestrator); --to also assigns it, --priority sets its priority and --answer answers its open question, in the same write. Written as a retype event; task.md keeps the type it was filed with.
assign ID TO, priority ID P, park ID MSG, reopen ID Housekeeping.
sync [--pull] [--push] [--auto] [--session-end] Commits the root, then pulls and pushes.
leaks PATH... Scans files for keys, tokens and webhook URLs.
doctor [--json] Checks this machine: user config, data roots and their upstreams, the opsd config, each runtime on PATH and signed in (sign-in files are checked for, never read), user-level copies of the agent-ops skill (~/.claude, ~/.codex, ~/.agents) against the one this install ships, opsd and its build, free memory and disk. Exit 1 when a check fails.
report [--days N] [--from DATE [--to DATE]] [--post CHANNEL] Tasks opened and closed per project, runs and tokens per agent and provider, decisions, over the last N whole days (default 7) or a date range; --post sends the summary to a channel, for example from a weekly schedule.
master [PATH] [--off] Prints, sets or clears the Agent Ops Master Repo in the user config.
mirror on [--root NAME], mirror off Mirrors this project root into the Master Repo, or stops (mirror: in its config.yml).
archive [--days N] [--dry-run] Moves done and parked tasks untouched for N days (default 30) to archive/tasks/. Human or orchestrator. list --all, show and dependency checks still find them; reopen moves a task back.

Findings and the KB

Command Effect
finding TITLE [--task ID] [-m BODY] Files a bug, risk or idea outside your task as a task of type finding; --task adds a finding event that links it.
learn TOPIC TITLE (-m BODY | --body-file F) [--task ID] [--tag T] Writes kb/<topic>/<slug>.md; --task adds a learn event.
kb [WORDS...] [--topic T], kb --read TOPIC/SLUG Searches notes (every word must match; title and tag hits rank first), or prints one. Exit 3 when nothing matches.

Channels and votes

Command Effect
channel post NAME MSG [--to ID] [--task ID] Writes channels/<name>/<ulid>-<actor>.md. @claude:qa in the text and --to are mentions; they land in that inbox.
channel read NAME [--limit N], channel list Reads a channel (last 50 by default), or lists channels.
propose QUESTION --option A --option B [--voters IDS] [--quorum N] [--deadline WHEN] [--task ID] Starts a vote in decisions/D-<n>/decision.md. Quorum defaults to a majority of the voters, or 2 when anyone may vote. A vote without a fixed voter list (anyone, or a wildcard such as claude:* or *:qa) needs --deadline.
vote D-ID CHOICE [REASON] Writes your ballot to decisions/<id>/ballots/<actor>.md; a ballot is final. CHOICE is the option text or its number.
tally D-ID, decisions [--all] Shows a vote, or lists open votes.
decide D-ID CHOICE [REASON] Writes outcome.md and comments on the linked task.

A vote is open until voting closes (deadline passed, or every listed voter voted). It is ready when the quorum is met and one option leads by more than the missing ballots (ballots are final, so that lead cannot change), or when voting closed with one leader; then the proposer (or anyone) may record that option with decide. A vote without a fixed voter list is ready only after its deadline. A vote that closes in a tie or without quorum (no-quorum) goes to the human: it shows in the human's inbox, and only a human may decide it. A human may decide any vote at any time. Listed voters who have not voted see the vote in their inbox.

Repeating tasks

A repeat is a task template in repeats/<name>.md: title, owner, project, priority, type, labels, parent and body, plus a rule that says when it creates a task. Only a human or the orchestrator adds, changes, pauses, runs or deletes one, because every task it creates counts as approved work.

Command Effect
repeat add NAME --title T --start "2026-11-05 09:00" [--every once|daily|weekly|weekdays|monthly|yearly] [--days mon,thu] A time rule. The start is local wall time; once is the default. weekdays needs --days.
repeat add NAME --title T --when "closed ID" or --when "children-closed ID" [--start ...] A condition: the task is done or parked, or the task has subtasks and all of them are. --start sets the earliest firing.
Options for both: [--until DATE] [--count N] [--owner ID] [--project P] [--priority P] [--type T] [--label L]... [--parent ID] [-m BODY] --until is the last local date a slot may fall on; --count caps the tasks it creates. {date} in the title is the slot.
repeat edit NAME [same options] Changes the given fields; a condition replaces the time rule and the other way round.
repeat list, repeat show NAME State (active, waiting, paused, finished, broken), the rule in words, the next slot, tasks created, the last one.
repeat pause NAME, repeat resume NAME, repeat delete NAME Pause keeps the repeat and creates nothing. Delete removes the template; the tasks it created stay.
repeat run NAME Creates its task now, paused or not. A time rule counts it as this minute's slot.
repeat tick Creates the task of every due repeat. opsd does this each tick; run it by hand where no opsd runs.

How a repeat fires:

Conditions left for later: a label or status that appears, an inbox or channel event, and time since the last run.

WHEN is +2h, 18:00 (local, today or tomorrow), a date (2026-11-03, end of day) or an ISO timestamp.

Global flags: --as ID, --root NAME, --force, --json. Exit codes: 0 ok, 1 refused, 2 bad input, 3 nothing to do.

Messages to a person

A person reads a message cold, often hours later and between other tasks. Every message to a person (ask, assign, comment --to human:*, a review request, a channel post that mentions one) stands on its own:

  1. It names what it refers to: the task id (AO-0014, or root/AO-0014 across roots), and for a comment or answer AO-0014#<event id> (from ops show ID --ids) or its date and author.
  2. It says in one or two sentences what that is about.
  3. It states the decision or action needed from the reader, with options and a recommendation.

Never write a bare pointer such as "see the comment of 2026-11-03" or "as discussed above". Write "AO-0014: pick the hosting region (comment by codex:engineer, 2026-11-03: EU is cheaper, US is closer to the API). I recommend EU."

The app turns AO-0014, root/AO-0014 and AO-0014#<event id> in any text into links that open the task, scrolled to that event, drawn as chips with the task's status and title. People insert them with the # picker in the app's message boxes, which writes the same plain text, or project:v2 for a project (ops list --project v2). ops ask, ops assign and ops comment --to human:* print a note: (and still write the message) when it names no task or is shorter than a sentence.

Questions to an agent

An agent (or a person) asks one chosen agent something with a task of type question owned by that agent: ops new "Is AO-0047 safe to merge?" --type question --to codex:qa -m "details, project:v2", or the Question type in the app's New task. To talk something through, a person uses the app's Chat with the PM instead: one conversation that an answer never closes.

ops ask is the other direction: an agent working a task asks a person, and that task blocks until the answer. A question blocks nothing; it is its own task.

Permissions

Events

Every change is a new file tasks/<id>/events/<ulid>-<actor-slug>.md with frontmatter and a Markdown body:

---
type: review
actor: claude:engineer
at: 2026-10-31T12:00:00.000Z
to: codex:qa
---

PR: https://github.com/org/repo/pull/12

Fields by type: to (addressee), on (block: task ids), verdict (approve or changes), priority, lease_until, next (checkpoint), forced, budget_minutes, tokens, due and review (delegate), ref (finding: the new task; learn: the note), token (claim: a random id for this claim), holder (start, progress, renew, checkpoint, release: the token of the claim the writer acted under), commit, branch and into (merge: the commit the base moved to, the branch the work came from, the base), keep_wait (assign: a blocked task keeps its open question or task wait under the new owner; assign writes it whenever the task is blocked, and triage when it assigns without answering; answer or unblock drop the wait on purpose, and handoff always clears it). Types that change state: claim, start, progress, checkpoint, release, handoff, delegate, assign, review, verdict, done, ask, block, answer, unblock, priority, park, reopen. Other types (comment, finding, learn, run-start, run-end, merge) are history. Unknown types are kept and ignored, so older ops versions read newer data. opsd writes merge events, and the changes verdict that sends back approved work it could not merge, as opsd:merge.

Event text is Markdown

The body of an event, a channel post, a ballot or a decision is Markdown, and every reader shows it as such: the desktop app renders it (sanitized, task ids as links), and ops show prints each extra line indented under its event. Every command that takes a message reads it, newlines kept, from one of:

Text longer than a sentence follows one shape: a summary line that names the task, a blank line, then short bullets for done, open and next, with code font for paths, commands and branches:

ops checkpoint AO-0012 --next "add the backoff test" --body-file - <<'EOF'
AO-0012: retry limit works; backoff untested.

- Done: limit of 3 in `core/auth.ts`
- Open: no test for `RETRY_MS`
EOF

An inbox item carries the first line as summary, cut to 160 characters, and the whole text (up to 4000 characters) as detail when it says more.

Owners

Every task that can run has an owner (AO-0140):

Claims across machines

On one machine, ops serializes writes to a task with a lock file, so two agents never both win a claim. Across machines, the claim pushed first wins:

  1. Events fold in ULID order. When ops sync pulls, it renames this clone's unpushed events of every task that got remote events to fresh ids above the newest pushed one, keeping their local order. Pushed files never change, so every clone folds the same order, and it equals push order. A clock that runs behind cannot make a late push win.
  2. Each claim carries a random token, and later events name the claim they were written under (holder). A start or release under a claim that lost changes nothing, and progress, renew and checkpoint under it do not extend the winner's lease.
  3. Each clone remembers the tokens of the claims it made (.ops/claims/<id>.json, never committed), with the supervised run that made it (OPS_RUN). The same agent id on two machines is two runs, and so are two live runs of one agent id on one machine: a caller with OPS_RUN holds a claim only if the record names its run. On the clone whose claim lost, or in another run, ops start, progress and release refuse with "held by ... under another claim". A claim made without a run belongs to no run until opsd binds it to the run it launches on it. A person or opsd acting as the agent without OPS_RUN is not checked for a run; opsd renews and releases as the run, so it never touches a claim another run took over.
  4. ops sync prints each lost claim. Under coordinated sync, ops claim reports it at once: "lost the claim on X to Y; pick other work". Do not release a lost claim; the task belongs to the winner. opsd stops a live run whose claim lost (reason lost-claim) and writes no release.

The loser's events stay in the history; ops show lists them as lost.

Sync

ops writes files at once, so agents on one machine see each other's changes without git. ops sync commits the root and moves it between machines; the root's sync.policy decides when ops sync --auto pulls and pushes:

Policy Pull Push
manual on ops sync on ops sync
checkpoint (default) every pull_minutes (10) every push_minutes (30), at session end or compaction
coordinated every 2 minutes at most at once after claim, handoff, delegate, verdict and ask; others batched

Under coordinated, the write stands even when the immediate sync fails (offline, say); ops prints "not synced yet" and the next sync sends it. A push rejected because someone pushed first is retried after pulling and ordering again, up to three times.

Sync commits only on the store branch: sync.branch in config.yml, else the branch this clone learned on its first sync (kept in .ops/sync.json). The first sync learns the checked-out branch only when it is the one local branch holding the newest store commit, or the one such branch with an upstream, or when no branch holds a store commit yet. Otherwise it holds and asks you to set sync.branch, so a code branch made from the store branch is never learned. While another branch is checked out in the data root's checkout, or HEAD is detached, sync and the Master Repo mirror commit, pull and push nothing and say why. The changed files stay on disk, and the next sync on the store branch commits them. Make code branches in a worktree, not in the data root's checkout: opsd's run prompt says so, and Claude runs there cannot run git checkout or git switch (see supervisor.md). While the hold lasts, opsd retries the sync every tick but logs the note only when it changes.

Every session pushes when it ends or compacts its context, whatever the policy:

Master Repo and project roots

Ask or decide

Decide when the choice is reversible, inside the task and allowed by your role. Ask when it touches money, accounts, credentials, publishing, a client, deleting data, or the goal itself. Ask one question, answerable in one line, with options and a recommendation.