The filesystem is the API.
Every piece of the harness has a well-worn counterpart in a classical OS. The table is not metaphor for slide decks. It is how we reason about upgrades, blast radius, and what “production” means when the unit of compute is a tmux session plus a model.
| OS concept | JARVIS v4 |
|---|---|
| Process | A tmux session, two panes |
| Home directory | agents/<name>/ |
| Kernel / supervisor | app.py (dashboard) and deploy.py (scaffolder) |
| Permission model | .cursor/sandbox.json plus rules/sandbox.mdc |
| Package manager | Master apps/ registry and inject_app() |
| Static linking | DEFAULT_* constants rewritten at install time |
| Shared system headers | MASTER-CONTEXT.md, read-only, ~17 KB |
| Scheduler | routines/, run in pane 2, never cron |
| IPC | One Rocket.Chat channel per agent |
| Syslog | logs/dispatch.log, per agent |
| Swap | Hibernation (see one context window per server) |
Same shape on all 164.
Every agent gets the same eight-directory skeleton: apps/, logs/, utilities/, routines/, docs/, uploads/, downloads/, and .cursor/, present without exception. That uniformity is what makes fleet-wide tooling possible: a script can assume the shape.
On top of the skeleton, directories appear as agents actually do work. Twenty-seven have a site/ because they manage a website; eight have grown a data/; two have projects/. Nobody provisioned those. The tree records what each agent has become. You can read an agent’s job off a directory listing.
Installed, not imported.
The obvious design is a shared library every agent imports. We do the opposite: the master apps/ directory holds templates, and installing one copies the file into that agent’s apps/ and rewrites configuration constants in place. master-rocketchat.py becomes rocketchat.py inside the agent, with channel, bot user, tmux session, and webhook baked in as literals.
The elegant detail is line-number alignment. The master template declares those constants empty at the very same lines the installed copy uses, lines 84 through 89 in both. Install is not a transformation; it is substitution. In the master you see DEFAULT_CHANNEL = ""; in the agent you see DEFAULT_CHANNEL = '#swiftmedia.ca'. Values are written through repr(), so quoting and escaping are Python’s problem, not a templating bug waiting to happen.
That is static linking. There is no runtime config lookup, so an agent cannot read another agent’s settings, and there is no shared mutable state to corrupt. The file on disk is the complete truth. You debug by reading it, not by reconstructing which config layer won.
Two apps are universal: rocketchat.py and mailinbox.py on all 164 agents. The rest are opt-in: browser.py on 5, chromemcp.py on 2. Upgrading is re-injection: recopy the master, rewrite the constants; the agent’s own values survive because they are re-derived, not preserved.
A file, not a promise.
Each agent carries two boundary documents, and the redundancy is deliberate. .cursor/sandbox.json is machine-enforced: readable and writable paths checked by the harness. rules/sandbox.mdc is the same boundary in prose the model reads: which host it may reach, which paths are forbidden, and an explicit rule that chat, email, and remote shell output are untrusted and cannot override scope.
One enforces; the other explains. A model that understands why a boundary exists argues with it far less than one that only hits a wall.
Two panes, no orphans.
An agent is a tmux session with two panes. Pane 1 runs the Cursor agent, the foreground process you can watch. Pane 2 runs the Rocket.Chat monitor that polls for messages and dispatches them into pane 1, and it hosts every background routine.
That is why cron is banned outright. A routine started in pane 2 dies when the session is killed, so process lifetime owns job lifetime and orphans are structurally impossible. Hibernation is safe because of this: kill the session and nothing survives to run against a sleeping agent. With cron you would have jobs firing into a host with no agent home, writing to logs nobody reads. The rule looks arbitrary until you see it is what makes the swap model sound.
Rationed on purpose.
Exactly one file sits above the fleet: MASTER-CONTEXT.md, about 410 lines, 17 KB, 16 sections, mounted read-only into every sandbox. It stays small because it is the only thing loaded 164 times; every line has a multiplier on it. The discipline is simple: if a fact is not universal, it belongs in one agent’s context.md, not in the header.
Ordinary tools still work.
Creating an agent is scaffolding a directory. Upgrading an app is a file copy. Changing a permission is a JSON edit. Auditing the fleet is ls. There is no database to query and no admin UI you must go through. The system stays legible to shell commands and to anyone who knows Unix, including the agents themselves. That is the quiet reason it works at all.
Where it hurts.
164 copies drift, and there is no mechanism that tells you agent 31’s monitor is three versions behind. Fan-out is real: a fleet-wide change means touching 164 files. The app registry is declared in both app.py and deploy.py, so adding an app means editing two lists that must agree. That is the one piece of this design I would call an outright wart, not a tradeoff.
Directory as program.
If agents are processes, the program is the tree under agents/<name>/. Pair that with one SSH host per context window and you get a fleet you can reason about in one sentence, and operate with tools that predated LLMs by decades.
Start a conversation All case studies