An agent driving Argus - andreaderuvo/argus GitHub Wiki

An agent driving Argus

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.

The thing to be clear about first

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.

The two calls that make it worth doing

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.

From Python, in one file

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.

Without handling a token at all

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.

As MCP tools the agent sees for itself

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, then start_agent twice with desk: "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 or argus-mcp it 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 who and sees another agent working in the same folder: it takes a worktree, or tells the other one what it is about to change with relay.
  • It tells you it finished, without you watching. ring with 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_task gives it the goal, its duty, whose turn it is, what the steps before it said and the last check, without parsing the log; team_done reports 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.

The brakes, and what they are not

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.

What is still true afterwards

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.

Reading the API

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.

Asking you a question, and waiting for the answer

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.

⚠️ **GitHub.com Fallback** ⚠️