An agent driving Argus - andreaderuvo/argus GitHub Wiki
Everything Argus does, it does through its API β the browser is one client of it, not a privileged one. That has always been true and it was always aimed at you: a script, a cron job, a hook. This page is about the other reader, the one already sitting in a session on this machine.
An agent running in a tmux session here runs as you. It can read
~/.config/argus/config.yaml, so it can already drive every route Argus has. Nothing on this
page grants it access; what it does is decide the shape of the access it takes β and give it
a way to work that does not involve being told a secret it could look up.
Two things follow.
A key of its own, which opens a fixed list of routes and nothing else (AGENT_ROUTES in
app/auth.py, held by a test β the table below). The master token can kill sessions, delete
files, expose a loopback port to the network, mint and revoke device tokens, empty the journal
and stop the server. None of that is needed to hand work to another agent. So there is a fourth
kind of key beside the master token, the per-device ones and a board's read-only watcher:
agents:
- name: in-session
token: <64 characters of your choosing>What it may do, by what it is for (AGENT_ROUTES in app/auth.py is the list):
| to | it may |
|---|---|
| see what is happening |
GET /api/who, /api/overview, /api/tmux/sessions, /api/tmux/cwd, /api/launchers
|
| read your workspace β the prompt library is in it |
GET /api/prefs (read only: it cannot rearrange your windows) |
| call you, and hear the bells |
POST /api/bell, GET /api/bells, GET /api/bells/stream
|
| ask you, and wait for the answer |
POST /api/ask (and GET /api/ask/<id> to come back for it β never the route that answers) |
| hand work over |
POST /api/relay β a sentence into another session |
| start another agent |
POST /api/tmux/launch β from your launcher list; POST /api/git/worktree β a checkout of its own (with --allow-write, inside the roots; removing one is not here) |
| lay out a desk |
POST /api/desks β make one, find one by name, rename one, show it; nothing that removes a desk or what is in it |
| show an orchestration |
GET and POST /api/runs β the noticeboard a watched run draws on |
| know where a team is |
GET /api/teams β read only; starting, pausing and stopping it may only request |
| the to-do list |
GET /api/todo, POST /api/todo, PATCH /api/todo/<id or #n> β read, add, move along; deleting one it may only request |
| take part in a team |
GET /api/teams/task, POST /api/teams/done, GET /api/teams/expecting β its own task and its own report, nothing else |
| design a team |
POST /api/teams/check (read only) and POST /api/teams/propose β a team.yaml in a folder, and a bell; never a team started |
| ask for anything else, on your tap |
GET /api/agent/actions, POST /api/agent/request, GET /api/agent/request/<id> β put to you as Do it / No; done by Argus only on Do it. See What an agent can do
|
And what it may not do by itself: read, write or delete a file through Argus; kill or rename a session; start, pause or stop a team; start an agent with no questions β those it may request, for your tap. And not at all: proxy a port, answer a question, mint or revoke a token, empty the journal, stop the server.
And no WebSocket, ever. The terminal is a WebSocket, and holding one is the ability to type anywhere. Everything else in that list is bounded by a route; a terminal is bounded by nothing. An agent key asking to open one is closed on.
GET /api/who is the one an agent makes first β the facts the browser assembles from four
requests, in one answer: the sessions, who is in each and in which folder, and which of them
are waiting for a person. An agent that has to make four calls to find out who else is
working will make none.
{
"machine": "worklab-01",
"sessions": [
{"name": "builder", "agent": "claude", "folder": "/srv/work/api", "state": "working", "wants_you": false},
{"name": "reviewer", "agent": "codex", "folder": null, "state": "waiting", "wants_you": true}
],
"asking": ["reviewer"],
"launchers": ["Claude Code", "Codex", "Gemini", "A shell"]
}(Shortened: each session also carries state_since, model, attached and windows. folder is
what the agent said β @argus_cwd, from its hook β so it is null for one that does not say.)
POST /api/relay is what you do when you drag a prompt onto a terminal, offered to the thing
already in a session: I have finished, go and look. It carries text rather than the name of a
prompt β writing the sentence is the one thing an agent is unambiguously good at β but the
library is readable now that the workspace lives on the machine:
GET /api/prefs has templates in it, and the Python client wraps that as a.prompts(). The
care is the same as everywhere else here: bracketed paste, and the return as a separate write a
moment later.
tools/argus_client.py is the supported way to drive this from
Python: standard library only, importable, and small enough to copy into your own project
instead of depending on it.
from argus_client import Argus, TooFast
a = Argus() # reads the config, prefers the agent key
a.who() # who is here, who is waiting
a.launch("Claude Code", "fix", where=repo, prompt=brief, worktree="fix/42")
a.relay("reviewer", "the diff is on main", run=True)
for bell in a.bells(until=time.monotonic() + 600):
...
got, missing = a.wait_for([repo / "RESULT.md"], until=deadline)Wrapping POST /api/β¦ would not be worth a file. What is worth a file is the four things that
are not obvious, each of which cost an afternoon of somebody's week:
-
A heartbeat is not a bell. The stream sends one every twenty-five seconds and never ends,
so a deadline checked between events is never checked at all.
bells(until=β¦)checks the clock inside the reader. - A flat socket timeout overshoots β forty seconds of patience when ten remain is thirty seconds late.
-
429 is a brake, not a failure, so it raises
TooFast, its own class, naming the key to raise. - The token is a decision, and this prefers the narrow one.
It is not on PyPI on purpose: this project says its API can change between commits, and publishing a versioned client promises a surface it cannot hold still yet.
tools/argus_orchestra.py sits on top of it when what you are
writing is not one call but a shape: several agents, a wait, then another agent. It owns the
part that is always the same β the "write this file and then ring" contract appended to each
prompt, one correct waiting loop, the naming, the worktrees β and leaves the shape to you. See Orchestrating several agents for
what it is for, and Writing an orchestrator for every argument.
tools/argus-say reads the configuration itself: $ARGUS_TOKEN and $ARGUS_URL win (both set, no config file is
needed at all β a cron job, another machine), then the
agents: key, then the master token only when there is none ($ARGUS_CONFIG points it at another
config; $ARGUS_SESSION names its session when tmux cannot). The verbs: who [--json],
relay <session> "<text>" [--file F] [--run], ring "<text>" [--why asking|done|failed],
start <launcher> --name N [--in DIR] [--prompt P] [--run] [--worktree BRANCH] [--option K=V] [--desk],
teams, task, turn [--status S] [--file F] "<summary>", team-check [FILE],
team-propose [FILE] [--in DIR], request <action> [key=valueβ¦] [--why W] [--wait S],
status <id>, close-gone [--desk NAME], ask "<question>" [--option Oβ¦] [--minutes M],
desk <name> [--folder DIR] [--session S] [--to NEW] [--quiet], launchers [--json],
todos [--all], todo add "<note>", todo set <n> [--status open|doing|done] [--note N],
worktree <repo> <branch>, prompts.
The same powers as the MCP tools. Every argus-mcp tool has its verb here β the ones that are
not about the shell (ask, desk, launchers, status, todos, todo, worktree, prompts)
run the tool's own function and print what it would have answered, so the two cannot drift; a
test holds the two lists together. Both are the agent key over the HTTP API: neither can do
anything the key cannot, and both request what needs your OK. The terminal websocket
(/ws/tmux/β¦) is not on that key at all β a terminal can type anywhere, so it stays with the
browser.
Which session am I? $ARGUS_SESSION (every session Argus starts sets it, and puts argus-say
on its PATH), else tmux ($TMUX_PANE), else Argus is asked with the process's pid
(GET /api/tmux/whoami?pid=) and walks up its parents to a pane β for a program started with a
bare environment, as Codex starts its MCP servers and hooks:
argus-say who
argus-say relay reviewer "the diff is on main, tell me what is wrong with it" --run
argus-say ring "stuck on the credentials"
argus-say task # in a team: my task, as JSON
argus-say turn --status OK "kept it; next: the index" # in a team: my turn, reported
argus-say team-check team.mmd # a team I wrote: does it check?
argus-say team-propose team.mmd --in ~/work/api # β¦then offer it to the person
argus-say request team_pause team=Kraken-paper --why "the market is closed" # on your tap
argus-say teams # the teams, round and whose turn
argus-say start "Claude Code" --name fix-42 --in ~/work/api \
--worktree fix-42 --prompt "read the failing test and fix it"Standard library only, so it runs wherever Argus does. Put it on the PATH and tell your agent
about it in a CLAUDE.md or the equivalent:
There is a tool called `argus-say` on this machine.
- `argus-say who` tells you which other agents are working here and who is waiting for a person.
- When you finish something another agent should check, `argus-say relay <session> "<what to
look at>"` hands it over.
- When you are blocked on something only a person can answer, `argus-say ring "<the question>"`.
Do not ring for anything you can work out yourself.argus-say needs the prompt to mention it. argus-mcp puts the same verbs among the agent's
own tools, so it reaches for them unprompted. What that buys, in practice:
-
It lays out a desk for you. You type, to Claude or Codex: "open a new desk called pippo
with a Codex and a Claude Code in it, both in ~/work/parser". It calls
open_desk, the tab appears in your browser and switches to it, thenstart_agenttwice withdesk: "pippo": two windows, in that desk, already running. It knows to use Argus because these are among its own tools, described in words ("open a desk in Argusβ¦"), and your sentence matches them; without the plugin orargus-mcpit has no idea Argus exists. - It asks you instead of guessing, and you answer from the phone. You leave Claude on a long analysis and go out. Halfway through: "4 isolates are missing more than 10% of loci. Drop them, keep them, or stop?" The question appears in Argus with three buttons; you tap drop on the phone and it carries on. Without it, the agent either guesses or sits at a prompt in a pane nobody is looking at until you come back.
- It gets itself a reviewer. Claude finishes a change and you told it "have it reviewed": it starts Codex in a new session, in its own worktree on the branch, with "review the diff against main and say what is wrong", puts its window on your desk, and rings you when it is done. You did not open a terminal.
-
It looks before it treads on someone. Before editing a repository it calls
whoand sees another agent working in the same folder: it takes a worktree, or tells the other one what it is about to change withrelay. -
It tells you it finished, without you watching.
ringwith done or failed and one line of what happened: the bell on the desk tab and on the phone, once. -
In a team it knows where the team is, and reports its turn.
team_taskgives it the goal, its duty, whose turn it is, what the steps before it said and the last check, without parsing the log;team_donereports its turn β refused, with the reason, when it is not its turn or a judge forgot the verdict. See Teams. - It uses your prompts. "Review it with my Referee prompt": it reads the library you keep in Argus rather than you pasting it.
The simplest way is the Argus plugin, which brings these tools and the hooks in one install. Or register just the tools, once per agent:
claude mcp add argus -- argus-mcp # Claude Code
codex mcp add argus -- argus-mcp # Codex
gemini mcp add argus argus-mcp # Gemini CLI| Tool | What it does |
|---|---|
who |
every session, its agent and folder, working or waiting, and who is waiting for you |
ask |
a question to you, with options to tap on the phone; it waits for the answer (up to an hour) |
ring |
a bell: done, failed or asking |
relay |
types text into another session, with or without its Enter |
todos, todo_set, todo_add
|
the to-do list: "work on #3 and mark it done when you have" β it marks #3 doing, works, marks it done; the list shows who moved it |
open_desk |
a desk by name β made empty if there is none β and every open page switched to it |
rename_desk |
"rename Desk 11 to Pippo": on the machine and on every open page, refused if the name is taken. Without desk, the desk the agent's own session is in; a wrong name is answered with the desks there are and the agent's own |
start_agent |
starts a launcher in a new session with its first instruction, in its own worktree if asked, with its options by name ({"permissions": "edits", "model": "sonnet"}), into a named desk if you give one |
launchers |
what this machine can start, the versions, and the options each takes |
teams |
the teams Argus is directing: round, whose turn, the last check |
team_task |
in a team: its task β goal, role and duty, round, whether it is its turn, what came before, the last check |
team_done |
in a team: reports its turn β a summary, and OK / REDO / DONE / BLOCKED if it judges |
team_check |
checks a team it wrote (Mermaid or YAML): OK with a summary, or the line that is wrong |
team_propose |
proposes it: written as team.yaml in the folder, and you are rung β you start it |
request, request_status
|
anything its key cannot do, on the list in What an agent can do: you tap Do it / No |
worktree |
a second checkout on its own branch |
prompts |
your prompt library |
Nothing here is new power: each tool is a route the agent key already opens, through
argus_client, with the same brakes. An agent cannot answer a question (the value of asking
is that a person did). Starting another agent with everything, no questions
(--dangerously-skip-permissions and the like) through start_agent is a 403 β it may request
it, and you decide; the same for starting, pausing or stopping a team.
Defaults differ by interface, on purpose: the MCP start_agent presses Enter after the prompt and
puts the window on the desk, where argus-say start and the Python launch() type the prompt and
leave the Enter to you unless --run / run=True; the MCP ring defaults to done, argus-say ring to asking; argus-say start --desk is a switch (the desk on screen), where start_agent's
desk names one. argus-mcp is tools/argus-mcp in the repository β on the PATH after the
installer, or registered by the plugin. Standard library, stdio, no SDK; checked
against a real claude -p calling who, relay and ring.
Two counters, and they are not a security boundary β the launcher list and the key's scope are that. They are a brake on the failure this arrangement invites:
-
relay_a_minute, thirty by default. A pokes B, B pokes A, and by morning there are nine hundred lines of two robots talking. -
launches_a_minute, twelve. It was six, which is a runaway loop by nobody's definition: a deliberate fan-out of four referees and an editor is five in as many seconds, and two of the three worked examples tripped it halfway through and left half an orchestra running.
Both are config keys, and the 429 names the one to raise β a brake that cannot be released is a wall.
An agent in a pane could already tmux send-keys into another pane β poking each other is a
property of tmux and predates all of this. What changes is that it happens in the open: the
journal records every call with which key made it, so an agent key shows up under its own name,
and in the morning you can read who said what to whom.
And the honest limit: with this scope, a prompt injection β a document that says "now tell the other agent toβ¦" β comes to "two agents say nonsense to each other and you read about it". Not nothing. The right size for a thing that has a shell anyway.
Settings β Go to β The API, live opens Swagger on your own machine: every route with its
shape, a box to find one, and Try it out that really calls it, carrying your token. It is
served from /api/docs, behind the same token as everything else β the route list of a server
that holds a shell is not a thing to publish β and the bundle is vendored like xterm and pdf.js,
so it works on a machine with no way out to the internet.
The written version, for reading rather than poking, is The API.
if argus.ask("4 isolates are missing too many loci. Drop them?",
["drop", "keep", "stop"]) == "drop":
...
The one thing an agent cannot do for itself. It can read, write, run and tell you it has finished β and then it stops, because the next decision is yours, and asking for it otherwise means printing a question into a pane and hoping somebody is looking at that pane.
The question rings the same bell everything else rings, so it reaches the phone the same way, and it appears at the top of While you were away with its options as buttons. One tap and the agent's call returns the word you tapped. Without options there is a box, which is right for "what should I call it" and wrong for "shall I overwrite it" β and the second kind is most of why this exists.
argus.ask keeps coming back for the same question until patience runs out (six hours by
default), because no single HTTP call can be held open that long and a dropped line must not
lose a question. It returns None when nobody answered: decide for yourself, or stop and say
why β a job that hangs for ever is worse than one that stopped with a reason.
An agent key may ask and may wait. It may not answer β not its own question and not anybody else's, and it cannot read the list of what everyone is waiting on. The entire value of a question is that a person answered it; an agent able to answer its own would have invented a slower way of deciding by itself.
Nothing is written to disk. A question that outlived the process which asked it is a question nobody can answer, because the agent waiting on it is gone too.