agentainer 0.1.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +248 -677
  2. package/agentainer +16 -18
  3. package/agentainer.example.yaml +86 -0
  4. package/bin/agentainer.js +9 -8
  5. package/examples/brainstorm.yaml +27 -128
  6. package/examples/bug-hunt.yaml +51 -96
  7. package/examples/code-review.yaml +73 -0
  8. package/examples/debate.yaml +16 -90
  9. package/examples/incident-response.yaml +52 -109
  10. package/examples/localization.yaml +56 -123
  11. package/examples/quickstart.yaml +48 -0
  12. package/examples/research.yaml +25 -0
  13. package/examples/software-company.yaml +71 -128
  14. package/examples/tdd-pingpong.yaml +36 -68
  15. package/examples/writers-room.yaml +49 -111
  16. package/hooks/claude_stop.sh +5 -3
  17. package/hooks/codex_notify.sh +4 -3
  18. package/lib/cli.py +929 -0
  19. package/lib/config.py +247 -305
  20. package/lib/hooks.py +246 -0
  21. package/lib/lock.py +75 -0
  22. package/lib/log.py +64 -0
  23. package/lib/mail.py +634 -0
  24. package/lib/minyaml.py +1 -39
  25. package/lib/reconcile.py +473 -0
  26. package/lib/sessions.py +223 -0
  27. package/lib/supervisor.py +216 -0
  28. package/lib/telegram.py +372 -0
  29. package/lib/tmux.py +355 -0
  30. package/lib/turn.py +159 -0
  31. package/lib/ui.py +1020 -0
  32. package/llms.txt +145 -429
  33. package/package.json +9 -7
  34. package/scripts/check-deps.js +18 -61
  35. package/ui/app.js +869 -0
  36. package/ui/index.html +348 -0
  37. package/agents.example.yaml +0 -257
  38. package/examples/code-review-broadcast.yaml +0 -109
  39. package/examples/existing-repo.yaml +0 -74
  40. package/examples/multi-language-broadcast.yaml +0 -127
  41. package/examples/ping-pong.yaml +0 -89
  42. package/examples/red-team.yaml +0 -117
  43. package/examples/research-swarm.yaml +0 -129
  44. package/lib/swarm.py +0 -2461
package/README.md CHANGED
@@ -1,727 +1,298 @@
1
- <!-- Banner -->
2
- <p align="center">
3
- <img alt="Agentainer — multi-agent orchestrator for AI coding agents" src="https://raw.githubusercontent.com/mehmetcanfarsak/AgentSwarm/main/assets/banner.svg" width="820">
4
- </p>
1
+ # 🐝 Agentainer
5
2
 
6
- <!-- Badges -->
7
- <p align="center">
8
- <img alt="license" src="https://img.shields.io/badge/license-MIT-blue.svg">
9
- <img alt="zero deps" src="https://img.shields.io/badge/dependencies-zero-brightgreen.svg">
10
- <img alt="platform" src="https://img.shields.io/badge/platform-linux%20%2F%20macOS-lightgrey.svg">
11
- <img alt="tests" src="https://img.shields.io/badge/tests-48%20checks%20passing-success.svg">
12
- </p>
3
+ **Zero-dependency multi-agent orchestrator with a file-based mail model.**
13
4
 
14
- <h1 align="center">Agentainer — Multi-Agent Orchestrator for AI Coding Agents</h1>
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+ [![runtime deps: zero](https://img.shields.io/badge/deps-zero-blue.svg)](https://github.com/mehmetcanfarsak/Agentainer#readme)
7
+ [![platform: linux | macOS](https://img.shields.io/badge/platform-linux%20%7C%20macOS-lightgrey.svg)](https://github.com/mehmetcanfarsak/Agentainer#readme)
8
+ [![line coverage: 100%](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/mehmetcanfarsak/Agentainer#readme)
15
9
 
16
- <p align="center">
17
- <strong>A zero-dependency multi-agent orchestrator for AI coding agents.</strong><br>
18
- Run a team of <b>Claude Code</b>, <b>OpenAI Codex</b>, <b>Gemini CLI</b>, and <b>Hermes</b> side by side in <b>tmux</b> — each in its own directory, each messaging the others only where your config allows.
19
- </p>
10
+ > **Formerly AgentSwarm.** The v1 project was renamed and rewritten around a
11
+ > file-based mail model the old name still resolves to this repo.
20
12
 
21
- <p align="center">
22
- <em>Formerly <strong>AgentSwarm</strong>. Installed globally the command is <code>agentainer</code>; from a clone, <code>./agentainer</code> in the repo root is identical.</em>
23
- </p>
13
+ Agentainer launches AI coding agents (Claude Code, Codex, Gemini, Hermes) each in
14
+ its own `tmux` session and working directory, defined by a single
15
+ `agentainer.yaml`. They talk to each other only where the config's `can_talk_to`
16
+ ACL allows — by **reading a file** to receive and **writing a file** to send.
24
17
 
25
- **Agentainer** is a lightweight, stdlib-only orchestrator (Python 3 + bash + tmux, no runtime dependencies) that launches multiple AI coding agents Claude Code, OpenAI Codex, Google's Gemini CLI, and Hermes — in isolated tmux sessions, each with its own working directory, and routes messages between them under a configurable access-control list. Define your whole agent team in a single YAML file; one command brings it online.
18
+ No runtime dependencies. No API keys required to exercise the mechanics. Agents
19
+ only need to read and write natural-language files; the orchestrator owns all the
20
+ hard logic (routing, ACL, message IDs, threading, read-state, queueing, retries,
21
+ availability, the durable log).
26
22
 
27
- <!-- Demo -->
28
23
  <p align="center">
29
- <img alt="Terminal demo: agentainer up starts four agents, status shows them running, send delivers a task, and logs stream the routed messages" src="https://raw.githubusercontent.com/mehmetcanfarsak/AgentSwarm/main/assets/demo.svg" width="760">
30
- <br>
31
- <em>Start a four-agent swarm, check status, hand it a task, and watch the messages route — in four commands.</em>
24
+ <img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/banner.svg" alt="Agentainer banner: zero-dependency multi-agent orchestrator with a file-based mail model" width="720"/>
32
25
  </p>
33
26
 
27
+ > v2 replaces v1's "tagged XML envelope emitted inside prose and scraped out of a
28
+ > TUI pane" — that was unreliable across LLMs. The file-based mail model works on
29
+ > nearly every tool-calling model, including weak ones.
30
+
34
31
  ---
35
32
 
36
- ## <span id="table-of-contents"></span>Table Of Contents
33
+ ## 📑 Contents
37
34
 
38
- - [Why Agentainer](#why-agentainer)
39
- - [How it fits together](#how-it-fits-together)
40
- - [Requirements](#requirements)
41
35
  - [Install](#install)
42
- - [Quickstart](#quickstart)
43
- - [FAQ](#faq)
44
- - [How it works](#how-it-works)
45
- - [Capturing what an agent says](#capturing-what-an-agent-says)
46
- - [Subagents, parallel work, and busy agents](#subagents-parallel-work-and-busy-agents)
47
- - [Configuration reference](#configuration-reference)
48
- - [Examples](#examples)
36
+ - [Quick start (no API keys)](#quick-start-no-api-keys)
37
+ - [The model in one screen](#the-model-in-one-screen)
38
+ - [Architecture](#architecture)
49
39
  - [Commands](#commands)
50
- - [Project layout](#project-layout)
51
- - [Tests](#tests)
52
- - [Troubleshooting](#troubleshooting)
53
- - [A note on flags](#a-note-on-flags)
54
-
55
-
56
- ## <span id="why-agentainer"></span>✨ Why Agentainer
57
-
58
- - **Multi-agent, one command.** Spin up a whole team with `agentainer up` — folders, hooks, tmux sessions, and first prompts, all wired automatically.
59
- - **Zero runtime dependencies.** Just Python 3 + bash + tmux. No pip installs, no bloated toolchain.
60
- - **Real ACLs, not vibes.** Agents only message who your YAML `can_talk_to` list permits. `"*"` for everyone, an explicit list for tight graphs.
61
- - **Survives restarts.** Each turn's conversation id is recorded; `up --resume` reattaches instead of starting cold.
62
- - **Self-healing routing.** Busy agents queue inbound work, stranded messages get swept, and agents are nudged when a reply reaches nobody.
63
- - **Any mix of CLIs.** Claude, Codex, Gemini, Hermes — or your own type — in the same swarm.
64
-
65
- ## <span id="how-it-fits-together"></span>🏗️ How It Fits Together
66
-
67
- *Architecture at a glance: a single `agents.yaml` file maps each AI coding agent to its own tmux session and working directory, while Agentainer routes and logs every message between them under a `can_talk_to` access-control list.*
68
-
69
- <p align="center">
70
- <img alt="Agentainer architecture: one YAML file maps each agent to its own tmux session and working directory; Agentainer routes and logs permission-checked messages between them" src="https://raw.githubusercontent.com/mehmetcanfarsak/AgentSwarm/main/assets/architecture.svg" width="900">
71
- </p>
72
-
73
- One command starts the swarm: it creates a folder per agent, installs a
74
- completion hook inside each folder, opens a tmux session per agent, launches
75
- the CLI, and types each agent's first prompt into it.
76
-
77
- <details>
78
- <summary>Plain-text diagram</summary>
79
-
80
- ```
81
- agents.yaml tmux
82
- ┌────────────┐ ┌──────────────────┐
83
- │ orchestr. │───────▶│ session: orchestr│──┐
84
- │ researcher │ │ session: research│ │ swarm send --to developer "..."
85
- │ developer │ │ session: develop │◀─┘
86
- │ reviewer │ │ session: reviewer│
87
- └────────────┘ └──────────────────┘
88
- │ │
89
- │ workspace/<agent>/ │ hooks capture each finished turn
90
- ▼ ▼
91
- one folder per agent messages routed + logged
92
- ```
93
-
94
- </details>
40
+ - [Key invariants](#key-invariants)
41
+ - [The control-plane UI (P2–P3)](#the-control-plane-ui-p2p3)
42
+ - [Dynamic reconcile (P4)](#dynamic-reconcile-p4)
43
+ - [Project status](#project-status)
44
+ - [FAQ](#faq)
45
+ - [License](#license)
95
46
 
96
47
  ---
97
48
 
98
- ## <span id="requirements"></span>📋 Requirements
99
-
100
- - **`tmux`** (3.0+)
101
- - **`python3`** — PyYAML is used if present, otherwise a bundled parser handles the config
102
- - **`node`** (16+) — only for the global `agentainer` command; not needed if you run `./agentainer` from a clone
103
- - whichever agent CLIs you reference: `claude`, `codex`, `gemini`, `hermes` — install only the one(s) you actually use
104
-
105
- ## <span id="install"></span>🚀 Install
106
-
107
- **Global, via npm:**
49
+ ## 📦 Install
108
50
 
109
51
  ```bash
110
- npm install -g agentainer
111
- agentainer --version # print the installed version (also: agentainer -v)
112
- agentainer doctor # check tmux/python3 are present; report which agent CLIs it found
52
+ git clone <repo> && cd Agentainer
53
+ npm install # runs the postinstall dep check; no build step
54
+ npm link # puts the `agentainer` bin on your PATH
55
+ agentainer --version
113
56
  ```
114
57
 
115
- `agentainer doctor` verifies the required tools (`tmux`, `python3`) and reports
116
- which agent CLIs are available it never fails on a missing *agent* CLI, since
117
- you may only use one of them.
118
-
119
- **Or from a clone (no npm needed):**
58
+ Once linked/installed, the command is just **`agentainer`**. `tmux` is the only
59
+ external runtime. `node` is required for the npm bin wrapper. PyYAML is used *if
60
+ present*; a bundled `minyaml` parser keeps everything working without it.
120
61
 
121
- ```bash
122
- git clone https://github.com/mehmetcanfarsak/AgentSwarm.git && cd AgentSwarm
123
- ./agentainer --help # same commands as the global `agentainer`, straight from the repo
124
- ```
62
+ ## 🚀 Quick start (no API keys)
125
63
 
126
- ## <span id="quickstart"></span>⚡ Quickstart
64
+ The bundled quickstart uses `bash` loop "mock agents" so you can watch the
65
+ mailroom route mail safely:
127
66
 
128
67
  ```bash
129
- cp agents.example.yaml agents.yaml
130
-
131
- agentainer validate # parse the config, print the resolved swarm, launch nothing
132
- agentainer up # create dirs, install hooks, start tmux, send first prompts
133
- agentainer status # who is running
134
- agentainer attach developer
135
- agentainer down # stop everything
68
+ cp examples/quickstart.yaml my-swarm.yaml
69
+ agentainer up -c my-swarm.yaml
70
+ agentainer status -c my-swarm.yaml
71
+ agentainer send -c my-swarm.yaml --to orchestrator "Build a CSV->Parquet CLI."
72
+ agentainer logs -c my-swarm.yaml -f
73
+ agentainer down -c my-swarm.yaml
136
74
  ```
137
75
 
138
- Give the swarm its actual work:
139
-
140
- ```bash
141
- agentainer send --to orchestrator "Build a CLI that converts CSV to Parquet."
142
- ```
143
-
144
- Watch the traffic between agents:
145
-
146
- ```bash
147
- agentainer logs -f # whole swarm, live
148
- agentainer logs reviewer -n 20 # one agent
149
- agentainer inbox developer # messages an agent received
150
- ```
151
-
152
- `agentainer status` gives you the whole swarm at a glance — who is up, who is mid-turn, and how deep each queue is:
76
+ To run real agents, swap each `command` for the actual CLI you installed and drop
77
+ `capture: none` so turns get detected.
153
78
 
154
79
  <p align="center">
155
- <img alt="agentainer status output: a table of agents with their type, state, turn, queue depth, capture mode, and can_talk_to permissions" src="https://raw.githubusercontent.com/mehmetcanfarsak/AgentSwarm/main/assets/screenshot-status.svg" width="820">
80
+ <img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/demo.svg" alt="Terminal cast of the Agentainer quickstart: up, status, send, then logs showing the mailroom routing mail between mock agents" width="700"/>
156
81
  </p>
157
82
 
158
- ---
159
-
160
- ## <span id="faq"></span>❓ FAQ
161
-
162
- **What is Agentainer?**
163
- Agentainer is a zero-dependency multi-agent orchestrator that runs a team of AI coding agents — Claude Code, OpenAI Codex, Google's Gemini CLI, and Hermes — in isolated tmux sessions, each with its own working directory, and routes messages between them under a configurable access-control list. You describe the whole team in one YAML file and start it with a single command.
164
-
165
- **Which AI coding agents does Agentainer support?**
166
- Claude Code (`claude`), OpenAI Codex (`codex`), Google's Gemini CLI (`gemini`), and Hermes (`hermes`) out of the box. You can also define your own agent type via `agent_types:` — any CLI that takes input on stdin/tty works, with `capture: pane` or `capture: none`.
167
-
168
- **Does Agentainer need Docker or any runtime dependencies?**
169
- No. It is stdlib-only: Python 3, bash, and tmux. PyYAML is used if present, but a bundled parser handles the config without it. Node is only needed for the global `npm` launcher — running `./agentainer` from a clone needs no Node at all.
170
-
171
- **How do I run multiple coding agents together?**
172
- Write a YAML config listing your agents (and who may talk to whom), then run `agentainer up`. It creates a folder and a tmux session per agent, installs a turn-completion hook, launches each CLI, and types in the first prompt. `agentainer send --to <agent> "…"` messages any agent; `agentainer validate` previews the resolved swarm without launching anything.
173
-
174
- **Can agents communicate with each other, and how is that controlled?**
175
- Yes. Agents exchange tagged `<swarm-message>` / `<swarm-send>` blocks. Messaging is a whitelist: an agent may only reach the agents in its `can_talk_to` list (`"*"` means everyone else); anything else is refused. Auto-forwarding (`forward_responses_to`) can relay finished turns hands-free, bounded by a hop count to prevent loops.
176
-
177
- **How do I resume a swarm after a crash or reboot?**
178
- Each finished turn records the agent's conversation id in `.swarm/sessions.yaml`. `agentainer up --resume` reattaches every agent to its own conversation instead of starting cold, keeping any queued mail. Claude resumes with `--resume <id>`, Codex with `resume <id>`; Gemini and Hermes have no recoverable session id and always start fresh (with a warning).
179
-
180
- **Is it safe to let agents run unattended?**
181
- Agents run with whatever privileges their CLI is launched with. Flags like `claude --dangerously-skip-permissions`, `codex --yolo`, and `gemini --yolo` let them act without confirmation, which is usually what you want for an unattended swarm — so point `root` at a disposable directory and never run a swarm over a checkout you can't afford to lose.
182
-
183
- **How is Agentainer different from just opening several terminals?**
184
- It automates the brittle parts: detecting when each turn finishes (via a Stop/notify hook for Claude/Codex, or pane polling for Gemini/Hermes), typing multi-line prompts reliably into a live TUI, pre-trusting folders so the "trust this directory?" modal doesn't eat the first prompt, enforcing the comms ACL, queueing messages to busy agents, and resuming conversations after a restart.
83
+ ## 📬 The model in one screen
185
84
 
186
- ---
187
-
188
- ## <span id="how-it-works"></span>🔧 How It Works
189
-
190
- **One folder + one tmux session per agent.** Agent `developer` gets
191
- `workspace/developer/` and a tmux session named `developer` (plus any
192
- `session_prefix`). The agent's CLI is launched inside that folder, so its file
193
- operations are naturally scoped to it.
194
-
195
- ### Working directories
196
-
197
- By default every agent gets a fresh folder under `root`, created for you. You can
198
- override that per agent, or for the whole swarm via `defaults`:
85
+ An agent's entire world is **two verbs** (read a file, write a file) and **four
86
+ folders**:
199
87
 
200
- ```yaml
201
- swarm:
202
- root: ./workspace
203
- create_workdirs: true # auto-create missing folders (default)
204
-
205
- agents:
206
- - name: developer # -> ./workspace/developer (created)
207
-
208
- - name: reviewer
209
- workdir: ~/projects/acme-api # -> an existing checkout
210
- create_workdir: false # ...and fail loudly if it is not there
211
-
212
- - name: scribe
213
- workdir: "{root}/{name}-notes" # {name} {root} {swarm} {type}
214
- ```
88
+ | 📁 Folder | 📝 Meaning |
89
+ | --- | --- |
90
+ | 📥 `inbox/` | The **one** current unread message (orchestrator releases one at a time). |
91
+ | 📤 `outbox/<name>/` | Write a file here to send to `<name>`. Read `<name>/about.md` for a contact card. |
92
+ | 📖 `read/` | Move a handled message here (best-effort read receipt). |
93
+ | 📨 `sent/` | The agent's own record of delivered mail (orchestrator moves it here). |
94
+ | 🚫 `failed/` | Mail bounced by the orchestrator (ACL violation, etc.), with a `system` explanation. |
215
95
 
216
- - **`workdir`** may be absolute, relative to the config file, or use `~`.
217
- - **`create_workdir: false`** turns a missing folder into an error instead of a
218
- new empty directory — the right setting when you are pointing agents at real
219
- repositories, where a typo should not silently create `~/projcets/acme-api`.
220
- - **`defaults.workdir`** applies to every agent that does not override it. With
221
- a `{name}` placeholder it lays out a folder each; without one, every agent
222
- shares a single directory.
223
- - **Sharing is allowed and sometimes the point** (a driver and a navigator in one
224
- checkout), but agents then overwrite each other's files and interleave commits,
225
- so `validate` and `up` warn when it happens. See
226
- [`examples/existing-repo.yaml`](examples/existing-repo.yaml).
227
- - `root` is still used even when every agent lives elsewhere: it holds `.swarm/`
228
- with the logs, inboxes and the `swarm` shim.
229
-
230
- **Prompts are typed in, not piped.** `swarm up` drops each first prompt into the
231
- agent's input box with a tmux bracketed paste, as one block, then presses Enter.
232
- That is why multi-line prompts survive intact instead of being submitted line by
233
- line.
234
-
235
- Getting that to work reliably against a live TUI took more than a sleep:
236
-
237
- - **Claude Code silently discards keystrokes for several seconds partway through
238
- startup.** Measured on v2.1.205: input at t=2s landed, t=6s and t=12s vanished,
239
- t=20s landed. A fixed `boot_delay_ms` is therefore a coin flip. Before typing,
240
- Agentainer types a throwaway token and waits for the input box to echo it back,
241
- then erases it (`ready_probe`). Enter is never sent, so nothing is submitted.
242
- - **Readiness is not monotonic**, so after pasting, Agentainer checks that the
243
- text actually appeared on screen before pressing Enter, and retries if it did
244
- not. If delivery cannot be confirmed it refuses to press Enter, rather than
245
- submitting a half-delivered prompt.
246
- - **Both CLIs open a "do you trust this folder?" modal** on first run in a new
247
- directory, which would eat the first prompt (Enter answers the dialog). Claude
248
- does this even under `--dangerously-skip-permissions`. Agentainer pre-trusts each
249
- agent's workdir: for codex in its generated `config.toml`, for claude by adding
250
- `hasTrustDialogAccepted` for that path in `~/.claude.json`.
251
- - **Both collapse a long paste into a chip** rather than showing the text —
252
- `[Pasted text #1 +36 lines]` for claude, `[Pasted Content 2580 chars]` for codex.
253
- Delivery verification recognises both.
254
-
255
- **Agents talk in tagged messages.** A message arrives inside an envelope, so the
256
- agent always knows where it starts, where it ends, and who sent it:
96
+ When an agent **stops** with unread mail, the orchestrator sweeps its `outbox`
97
+ and pastes a **nudge** ("you have mail read it, then move it to `read/`"),
98
+ re-injecting the protocol every time (including the allowed-recipient list).
257
99
 
258
- ```
259
- <swarm-message from="lead" to="reviewer" id="m-eb4105" reply-to="m-3f9a1c">
260
- Review finding for ./parse: tokenize() mishandles a trailing backslash.
261
-
262
- printf 'a\' | ./parse
263
- </swarm-message>
264
- ```
265
-
266
- To send one, the agent simply **writes a block in its reply**. The capture hook
267
- reads it when the turn ends and delivers it — no shell, no quoting, so multi-line
268
- bodies, code blocks and backslashes survive intact:
269
-
270
- ```
271
- <swarm-send to="reviewer" reply-to="m-3f9a1c">
272
- Please review src/parse.py.
273
-
274
- printf 'a\' | ./parse
275
- </swarm-send>
276
- ```
100
+ ## 🏗️ Architecture
277
101
 
278
- `<swarm-broadcast>` (no `to`) reaches everyone the sender may talk to. The `id`
279
- and `reply-to` attributes let agents thread a conversation instead of guessing
280
- which question an answer belongs to. Set `message_format: plain` to go back to the
281
- old `[swarm] message from <sender>:` header, and `parse_outbound_tags: false` to
282
- stop reading tags out of replies.
102
+ <a href="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/architecture.svg">
103
+ <img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/architecture.svg" alt="Agentainer architecture: agentainer.yaml drives a CLI core that opens one tmux session per agent; agents read and write mail files that the orchestrator routes" width="720"/>
104
+ </a>
283
105
 
284
- Agents can also send from their shell, which is useful mid-task rather than at the
285
- end of a turn (but then they must quote the text themselves):
286
-
287
- ```bash
288
- swarm send --to reviewer "I finished the parser, please review src/parse.py"
289
- swarm broadcast "heads up: I renamed the config module"
290
- ```
291
-
292
- Either way the message is permission-checked against `can_talk_to`, archived under
293
- `.swarm/inbox/<recipient>/`, and written to the event log.
294
-
295
- The raw tmux equivalent also works, and bypasses permissions and logging:
296
-
297
- ```bash
298
- tmux send-keys -t reviewer -l "your message" && tmux send-keys -t reviewer Enter
299
- ```
300
-
301
- **Agents get reminded when their answer goes nowhere.** A model that was asked a
302
- question will often just *write the answer as prose* and end its turn — and that
303
- prose reaches nobody, because only a `<swarm-send>` block is delivered. So when an
304
- agent owes a reply and finishes a turn without sending one, Agentainer messages it:
305
-
306
- ```
307
- Your last turn sent no message to anyone, and lead is waiting on your answer to
308
- message m-c73724.
309
- ...
310
- <swarm-send to="lead" reply-to="m-c73724">
311
- your answer here, over as many lines as you need
312
- </swarm-send>
313
- ```
314
-
315
- If instead the agent *tried* to send but the block was malformed, it gets the
316
- specific diagnosis — unclosed tag, missing `to`, unknown recipient, permission
317
- denied — so it can correct itself rather than lose the message silently.
318
-
319
- It is reminded at most `max_reply_reminders` times (default **1**), then Agentainer
320
- gives up and stops nagging. An agent that auto-forwards via `forward_responses_to`
321
- is never reminded, since its words did reach someone. Turn it off per agent with
322
- `reply_reminder: false`.
323
-
324
- **Conversations survive a restart.** Each time an agent finishes a turn, its
325
- conversation id is written to `<root>/.swarm/sessions.yaml`:
326
-
327
- ```yaml
328
- agents:
329
- lead:
330
- session_id: "0c2e47e2-5110-4e69-ae45-69d8492d2084"
331
- type: "claude"
332
- transcript: "/root/.claude/projects/.../0c2e47e2-....jsonl"
333
- updated_at: "2026-07-09T21:00:41+00:00"
334
- ```
335
-
336
- If the machine dies, `swarm up --resume` reattaches every agent to its own
337
- conversation instead of starting a fresh one — it does not re-send the first
338
- prompt, and it keeps any mail still queued for that agent:
339
-
340
- ```bash
341
- agentainer sessions # what is recorded, and the command that would resume it
342
- agentainer up --resume # reattach; agents without a recorded id start fresh
343
- ```
344
-
345
- Claude is resumed with `--resume <id>`, codex with `resume <id>`. Set
346
- `swarm.resume: true` to make it the default, and `--no-resume` to override.
347
- Gemini and hermes have no recoverable session id (their turns are scraped from
348
- the terminal), so they always start a fresh conversation, with a warning.
349
-
350
- If your command runs the CLI through an alias or wrapper, flags cannot simply be
351
- appended — give the full recipe:
352
-
353
- ```yaml
354
- - name: lead
355
- command: "bash -ic chy3"
356
- resume_command: "bash -ic 'chy3 --resume {session_id}'"
357
- ```
358
-
359
- **Permissions are a whitelist.** An agent may only message the agents in its
360
- `can_talk_to` list. Anything else is refused with an explanatory error that the
361
- agent sees on its own terminal. Use `can_talk_to: "*"` for "everyone else".
362
-
363
- ---
364
-
365
- ## <span id="capturing-what-an-agent-says"></span>🎬 Capturing What An Agent Says
366
-
367
- Agentainer needs to know when an agent finishes a turn — both to log it and to
368
- support auto-forwarding. How it finds out depends on the CLI, and the two
369
- mechanisms are **not** equally good:
370
-
371
- | `capture` | Used by | Mechanism | Reliability |
372
- |---|---|---|---|
373
- | `hook` | `claude`, `codex` | The CLI runs a program when a turn completes | Exact — the model's final message |
374
- | `pane` | `gemini`, `hermes` | Poll the tmux pane, diff it once it stops changing | Heuristic — sees rendered text |
375
- | `none` | any | Nothing is captured | — |
376
-
377
- - **claude** → a `Stop` hook is written into `<agent-dir>/.claude/settings.json`,
378
- with **no `matcher` key** (`Stop` is not a tool event, and supplying one stops the
379
- interactive TUI from ever running the hook). It reads the session transcript.
380
- Claude fires the hook *before* flushing the assistant message to that transcript,
381
- so Agentainer polls it briefly, and only reads text written after the last user
382
- message — otherwise a turn would silently capture nothing, or re-relay the
383
- previous turn's reply.
384
- - **codex** → the agent gets a private `CODEX_HOME` at `<agent-dir>/.codex/`
385
- with a `notify` program wired up (your `~/.codex/auth.json` is symlinked in, so
386
- it stays logged in, and your existing `config.toml` is carried over). The
387
- generated file keeps `notify` above every `[table]` header -- TOML is
388
- order-sensitive, and a `notify` written after one silently becomes
389
- `projects.<dir>.notify`, which codex never calls.
390
- - **gemini / hermes** → no turn-completion hook exists, so a background watcher
391
- samples the pane and emits the new text once it has been quiet for
392
- `pane_idle_ms`. It filters out the terminal's echo of incoming messages, but
393
- it is still terminal scraping: spinners and redraws can leak in. Prefer having
394
- these agents call `swarm send` explicitly.
395
-
396
- Set `capture:` per agent to override the default for its type.
397
-
398
- ### <span id="subagents-parallel-work-and-busy-agents"></span>🧩 Subagents, parallel work, and busy agents
399
-
400
- Coding agents spawn subagents and background tasks. Four things follow from that.
401
-
402
- **A subagent that calls `swarm send` speaks as its parent.** Subagents inherit the
403
- agent's environment, so `SWARM_AGENT` still names the parent and `can_talk_to` is
404
- enforced against the parent. That is almost always what you want: the swarm sees
405
- one `developer`, not five anonymous workers.
406
-
407
- **Parallel subagents cannot garble each other.** A paste and the Enter that submits
408
- it are two separate tmux calls, so two senders racing on one pane used to produce
409
- one Enter submitting two concatenated messages and another submitting nothing.
410
- Everything that types into a pane now takes a per-recipient lock, so concurrent
411
- sends queue up instead of interleaving. Different recipients are still messaged in
412
- parallel.
413
-
414
- **A message that arrives while an agent is busy is queued, not lost.** The CLIs
415
- hold it in their input box and process it when the current tool call finishes —
416
- codex says so out loud (`Messages to be submitted after next tool call`).
417
-
418
- **An agent that ends its turn saying "I'll respond when the subagent finishes"
419
- gets captured twice.** With `capture: hook`, the Stop/notify hook fires when the
420
- *agent's* turn ends. Claude's `Task` subagents run inside the turn, so the hook
421
- waits for them. But work the agent genuinely backgrounds lets the turn end early,
422
- and then:
423
-
424
- - that interim message is captured, and forwarded if `forward_responses_to` is set;
425
- - when the agent is re-invoked and finishes for real, its answer is captured and
426
- forwarded too. The hop counter records the hop at which the agent *received* its
427
- last message, so a second response does not consume an extra hop — the real
428
- answer is never suppressed by the loop guard.
429
-
430
- Subagent chatter never leaks: Claude writes subagent turns into the same transcript
431
- marked `isSidechain: true`, and the hook skips them, so what gets relayed is the
432
- agent's own final message rather than whatever a subagent happened to say last.
433
-
434
- With `capture: pane` (gemini, hermes) this breaks down. A quiet pane is the only
435
- "turn finished" signal there is, so a pause while a subagent works looks exactly
436
- like a completed turn: the interim "I'll respond when it finishes" is captured and
437
- forwarded, then the real answer is captured separately. Raise `pane_idle_ms` above
438
- the longest silence you expect, or set `capture: none` and have the agent call
439
- `swarm send` when it actually has something to say.
440
-
441
- ### Busy agents and backpressure
442
-
443
- If `b` gives `a` a task, `a` is mid-turn. When `c` then tries to task `a` as well,
444
- the message is refused rather than dropped into a working agent's input box:
445
-
446
- ```
447
- $ swarm send --to a "please review my diff"
448
- xx a is busy right now (working for 42s on a task from b). Please try again after
449
- some time, or put your message in the queue and wait for the answer:
450
- swarm send --to a --queue "..." # delivered automatically when a is free
451
- swarm send --to a --wait "..." # block here until a is free
452
- Meanwhile you are free to do other work.
453
- ```
454
-
455
- So `c` chooses: come back later, `--queue` it and carry on with other work, or
456
- `--wait` and block. A queued message is delivered by the next capture hook that
457
- fires for `a`, the moment it goes idle. Agents are told all of this in the
458
- communication block appended to their first prompt.
459
-
460
- ```bash
461
- swarm status # TURN column: idle / busy 42s / untracked, plus QUEUE depth
462
- swarm queue a # what is waiting for a, and who sent it
463
- swarm queue a --clear # drop it all
464
- swarm idle a # force a back to idle, then drain -- if a capture never fired
465
- ```
466
-
467
- **This is safe against parallel senders.** The busy check and the "now busy" write
468
- happen inside the same per-recipient lock as the paste, so two subagents racing to
469
- message an idle agent cannot both pass the check — one delivers, the other is told
470
- it is busy. A flag checked and set separately would let both through.
471
-
472
- Some honest limits:
473
-
474
- - Busy tracking needs a "turn finished" signal, so it only works for agents with
475
- `capture: hook` or `capture: pane`. A `capture: none` agent reports `untracked`
476
- and always accepts mail.
477
- - With `capture: pane`, "idle" means "the pane went quiet", which a thinking agent
478
- can also look like. Backpressure there is a hint, not a guarantee.
479
- - A turn started by a human typing directly into the pane is not tracked.
480
- - If a capture never fires (crashed CLI, misconfigured hook), the agent would look
481
- busy forever. After `busy_timeout_ms` (default 15 minutes) it is treated as idle
482
- again, with a warning. `swarm idle <agent>` clears it immediately. Any mail queued
483
- for such an agent is not lost either: whenever some *other* agent finishes a turn,
484
- Agentainer sweeps the now-idle agent's queue and delivers what was stranded — so
485
- one missed turn-completion cannot wedge a queue permanently.
486
- - A capture only fires if the agent's `type` matches the CLI its `command` actually
487
- runs. If you point a `type: codex` agent at a `claude` command (e.g. through an
488
- alias), it gets codex's `notify` hook, which claude never calls — its turns are
489
- never detected and it looks busy forever. Set `type` to whatever the command runs.
490
- - `--force` and `--ignore-busy` deliver anyway. The agent's CLI will queue the
491
- message and handle it after the current tool call, so nothing is lost — you just
492
- give up the backpressure.
493
-
494
- ### Auto-forwarding
495
-
496
- `forward_responses_to` relays an agent's finished turn to other agents without
497
- it having to ask:
498
-
499
- ```yaml
500
- - name: researcher
501
- can_talk_to: [orchestrator, developer]
502
- forward_responses_to: [orchestrator] # must be a subset of can_talk_to
503
- ```
504
-
505
- Two agents forwarding to each other would ping-pong forever, so every forwarded
506
- message carries a hop count, and forwarding stops at `max_forward_hops`
507
- (default 3). A fresh message from you resets the count. Auto-forwarding is
508
- powerful but chatty — for most swarms it is better to let agents decide when to
509
- speak, and leave `forward_responses_to` unset.
510
-
511
- ---
106
+ <details>
107
+ <summary>Plain-text architecture (for terminals &amp; screen readers)</summary>
512
108
 
513
- ## <span id="configuration-reference"></span>⚙️ Configuration Reference
514
-
515
- Full annotated example: [`agents.example.yaml`](agents.example.yaml).
516
- Machine-readable summary for agents: [`llms.txt`](llms.txt).
517
-
518
- ### `swarm:`
519
-
520
- | Key | Default | Meaning |
521
- |---|---|---|
522
- | `name` | config filename | Label used in prompts and logs |
523
- | `root` | `./workspace` | Where per-agent folders are created |
524
- | `create_workdirs` | `true` | Auto-create missing agent folders |
525
- | `session_prefix` | `""` | Prepended to every tmux session name |
526
- | `send_delay_ms` | `150` | Pause before pasting into a pane |
527
- | `enter_delay_ms` | `250` | Pause between pasting and pressing Enter |
528
- | `max_forward_hops` | `3` | Auto-forward loop guard |
529
- | `ready_timeout_ms` | `60000` | How long to wait for an agent's input box to respond |
530
- | `busy_timeout_ms` | `900000` | After this, a stuck "busy" agent is treated as idle |
531
- | `message_format` | `tagged` | `tagged` XML-ish envelopes, or `plain` text headers |
532
- | `max_reply_reminders` | `1` | How often to remind an agent that its reply reached nobody |
533
- | `resume` | `false` | Make `up` reattach to recorded conversations by default |
534
- | `pane_idle_ms` | `2500` | Quiet time before a `pane` turn counts as done |
535
- | `pane_poll_ms` | `700` | Pane sampling interval |
536
- | `pane_scrollback` | `400` | Lines of scrollback the watcher diffs |
537
- | `tmux_history_limit` | `50000` | Scrollback kept per agent pane so you can scroll up (`0` = tmux default) |
538
- | `tmux_mouse` | `true` | Enable mouse-wheel scrolling in the panes |
539
-
540
- ### `agents:`
541
-
542
- | Key | Default | Meaning |
543
- |---|---|---|
544
- | `name` | *required* | Folder name **and** tmux session name |
545
- | `type` | `claude` | `claude`, `codex`, `gemini`, `hermes`, or one you define |
546
- | `command` | from type | Exact CLI to run, e.g. `claude --dangerously-skip-permissions` |
547
- | `can_talk_to` | `[]` | Whitelist of agents it may message; `"*"` for all others |
548
- | `first_prompt` | `""` | Prompt typed in after the CLI boots |
549
- | `first_prompt_file` | — | Read the prompt from a file instead |
550
- | `append_agents_that_you_can_talk_to_prompt` | `true` | Append the "here's who you can message and how" block |
551
- | `in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt` | `false` | Append "stand by, your task is coming next" |
552
- | `forward_responses_to` | `[]` | Auto-relay finished turns to these agents |
553
- | `capture` | from type | `hook`, `pane`, `none`, or `auto` |
554
- | `boot_delay_ms` | from type | Grace period before probing the input box (not a delivery guarantee) |
555
- | `ready_probe` | `true` | Wait for the input box to echo a token before typing |
556
- | `busy_check` | `true` | Refuse incoming messages while this agent is mid-turn |
557
- | `parse_outbound_tags` | `true` | Route `<swarm-send>` blocks the agent writes in its reply |
558
- | `reply_reminder` | `true` | Remind it when it owes a reply but sent nothing |
559
- | `resume_args` | from type | Appended to `command` to resume, e.g. `--resume {session_id}` |
560
- | `resume_command` | — | Full replacement command when flags can't be appended |
561
- | `workdir` | `<root>/<name>` | Override the agent's directory (`~`, `{name}`, `{root}`, `{swarm}`, `{type}`) |
562
- | `create_workdir` | from `create_workdirs` | Create the folder if missing, else error |
563
- | `env` | `{}` | Extra environment variables for its tmux session |
564
-
565
- ### `agent_types:`
566
-
567
- Override a built-in launch command, or define a new agent type:
568
-
569
- ```yaml
570
- agent_types:
571
- claude:
572
- command: "claude --dangerously-skip-permissions --model opus"
573
- aider: # a type of your own
574
- command: "aider --yes"
575
- capture: pane # only claude/codex support `hook`
576
- boot_delay_ms: 4000
577
109
  ```
578
-
579
- ### `defaults:` and `templates:`
580
-
581
- `defaults:` supplies any agent key for agents that don't set it, including
582
- `workdir` — useful for putting a whole swarm in one repository. `templates:`
583
- overrides the text Agentainer generates — `comms` and `task_notice` (appended to
584
- first prompts), plus `reply_reminder` and `send_failed` (the nudges) —
585
- with `{agent} {swarm} {peers} {prefix} {inbox} {workdir}` available as
586
- placeholders.
587
-
588
- ---
589
-
590
- ## <span id="examples"></span>🧪 Examples
591
-
592
- Ready-to-run swarms in [`examples/`](examples/):
593
-
594
- | File | Shape | Shows off |
595
- |---|---|---|
596
- | [`research-swarm.yaml`](examples/research-swarm.yaml) | Hub and spoke | A lead delegating to a scout, an analyst and a writer; a custom output folder |
597
- | [`software-company.yaml`](examples/software-company.yaml) | Org chart | Six agents across all four CLIs, with a deliberately restricted comms graph |
598
- | [`bug-hunt.yaml`](examples/bug-hunt.yaml) | Pipeline | `forward_responses_to` chaining reproduce → diagnose → fix → verify, hands-free |
599
- | [`existing-repo.yaml`](examples/existing-repo.yaml) | Pairing | Two agents in one **existing** checkout, with `create_workdirs: false` |
600
- | [`red-team.yaml`](examples/red-team.yaml) | Adversarial triad | Attacker vs defender arbitrated by a referee, over one **existing** checkout |
601
- | [`debate.yaml`](examples/debate.yaml) | Debate + judge | Two advocates argue opposite sides; a judge cross-examines and rules |
602
- | [`writers-room.yaml`](examples/writers-room.yaml) | Hub + review cycle | An editor driving a writer, fact-checker and critic to a finished article |
603
- | [`incident-response.yaml`](examples/incident-response.yaml) | Incident hub | A commander coordinating investigator/responder/scribe under time pressure |
604
- | [`tdd-pingpong.yaml`](examples/tdd-pingpong.yaml) | Tight 2-agent loop | Tester writes one failing test, coder makes it pass, in one shared repo |
605
- | [`brainstorm.yaml`](examples/brainstorm.yaml) | Diverge + converge | Three angled idea-generators fanning out, then a synthesiser ranks them |
606
- | [`localization.yaml`](examples/localization.yaml) | Pipeline | Translate → review → back-check, auto-forwarded via `forward_responses_to` |
607
- | [`multi-language-broadcast.yaml`](examples/multi-language-broadcast.yaml) | Broadcast fan-out | One translator broadcasts a source to N language reviewers; a consolidator compiles and broadcasts back |
608
- | [`code-review-broadcast.yaml`](examples/code-review-broadcast.yaml) | Broadcast hub | One PR broadcast to specialist reviewers (correctness/security/perf); each replies to the coordinator who synthesizes |
609
- | [`ping-pong.yaml`](examples/ping-pong.yaml) | Round-robin | Two agents trade a unit of work, each broadcast naming who is up next (distinct from tdd-pingpong's fixed `forward_responses_to`) |
610
-
611
- ```bash
612
- agentainer validate -c examples/research-swarm.yaml # look before you leap
613
- agentainer up -c examples/research-swarm.yaml
614
- agentainer send --to lead "Research the state of WebGPU compute shaders."
110
+ agentainer.yaml ──▶ agentainer up ──▶ one tmux session + workdir per agent
111
+ (agents + (CLI core │
112
+ can_talk_to) + supervisor) ▼
113
+ agents READ inbox/ WRITE outbox/<name>/
114
+ │ │ │
115
+ │ ▼ ▼
116
+ │ orchestrator routes the mail:
117
+ │ ACL check, message ID, read-state,
118
+ │ queue, durable JSONL log
119
+
120
+ liveness supervisor heartbeat
121
+ (reconciles stale-busy / dead / silent-but-alive)
615
122
  ```
123
+ </details>
616
124
 
617
- `existing-repo.yaml` intentionally refuses to start until you point `workdir` at
618
- a repository that exists.
619
-
620
- ## <span id="commands"></span>💻 Commands
621
-
622
- | Command | Purpose |
623
- |---|---|
624
- | `agentainer up` | Start the swarm. `--only a,b`, `--restart`, `--resume`, `--no-prompt`, `--attach` |
625
- | `agentainer down` | Kill sessions and watchers. `--only a,b` |
626
- | `agentainer restart` | `down` then `up` |
627
- | `agentainer status` | Table of agents, sessions, capture mode, permissions |
628
- | `agentainer attach <agent>` | Attach to an agent's tmux session |
629
- | `agentainer send --to <agent> "msg"` | Deliver a message (`--from`, `--file`, `--queue`, `--wait`, `--ignore-busy`, `--force`) |
630
- | `agentainer broadcast "msg"` | Message everyone the sender may talk to |
631
- | `agentainer sessions` | Show each agent's recorded conversation id (`--raw`) |
632
- | `agentainer queue <agent>` | Show what is waiting for a busy agent (`--clear`) |
633
- | `agentainer idle <agent>` | Force an agent back to idle, then drain its queue |
634
- | `agentainer inbox <agent>` | Print archived messages |
635
- | `agentainer logs [agent] [-f]` | Event log: prompts, responses, messages |
636
- | `agentainer validate` | Parse the config. `--show-prompts` renders final prompts |
637
-
638
- `agentainer my-swarm.yaml` is shorthand for `agentainer up -c my-swarm.yaml`.
639
- `-c` and `$SWARM_CONFIG` both select a config; `-c` wins.
640
-
641
- ## <span id="project-layout"></span>📁 Project Layout
125
+ ## 🛠️ Commands
126
+
127
+ | 🛠️ Command | 📝 Purpose |
128
+ | --- | --- |
129
+ | 🔍 `validate` | Resolve and print the config; launch nothing. |
130
+ | 🆙 `up` | Create dirs + mailbox folders, install per-type turn-detection, open one tmux session per agent. |
131
+ | 🔌 `down` | Tear the swarm down. |
132
+ | 🔁 `restart` | Down then up. |
133
+ | 📡 `status` | Show agent/health summary. |
134
+ | 🔗 `attach` | Attach to an agent's tmux session. |
135
+ | ✉️ `send` | Send a `user` message into the swarm (`--to <agent>`). |
136
+ | 🗂️ `sessions` | List recorded tmux sessions (resume info). |
137
+ | `queue` | Show pending mail per agent. |
138
+ | 😴 `idle` | List agents currently idle. |
139
+ | 📨 `inbox` | Show current inbox message per agent. |
140
+ | 📟 `logs` | Tail the durable JSONL event log (`-f` to follow). |
141
+ | 🪝 `hook` | Turn-completion entry point (called by `claude`/`codex` stop hooks). |
142
+ | 👀 `watch` | Live-watch the supervisor. |
143
+ | 💗 `supervise` | Run one (or the loop of) supervisor tick(s). |
144
+ | 👤 `user` | Toggle `user` availability. |
145
+ | 🌐 `serve` | Serve the mail-app control-plane UI (threads, settings/agent editing, availability, direct-to-pane) on `127.0.0.1`. |
146
+ | `add` | Add an agent to the config (YAML) and bring it up immediately. |
147
+ | 🗑️ `remove` | Remove an agent from the config and stop its session. |
148
+ | ✏️ `edit` | Edit an agent's fields in the config (`-s key=value`, repeatable) and reconcile. |
149
+ | 🔧 `reconcile` | Start agents missing from the running set / stop sessions no longer configured. |
150
+
151
+ See `llms.txt` for a machine-readable summary and `ProjectPlan.md` for the full
152
+ design (source of truth).
642
153
 
643
- ```
644
- AgentSwarm/
645
- ├── agentainer # entrypoint
646
- ├── agents.example.yaml # annotated config
647
- ├── llms.txt # reference for agents configuring this tool
648
- ├── hooks/
649
- │ ├── claude_stop.sh # Claude Code Stop hook
650
- │ └── codex_notify.sh # Codex notify program
651
- ├── lib/
652
- │ ├── swarm.py # tmux orchestration, routing, capture
653
- │ ├── config.py # schema, defaults, validation
654
- │ └── minyaml.py # YAML subset parser, used when PyYAML is absent
655
- ├── tests/validate.sh # full suite: mock agents, no model calls
656
- ├── examples/ # research, software company, bug hunt, pairing, red team, debate, writers' room, incident, tdd, brainstorm, l10n, multi-language + code-review (broadcast), ping-pong (round-robin)
657
- └── workspace/ # created by `up`
658
- ├── <agent>/ # one folder per agent
659
- └── .swarm/
660
- ├── state.json # what `up` started
661
- ├── sessions.yaml # each agent's conversation id, for `up --resume`
662
- ├── bin/swarm # the `swarm` command agents call
663
- ├── logs/ # <agent>.jsonl + swarm.jsonl
664
- ├── inbox/<agent>/ # archived messages
665
- └── run/ # watcher pids, hop counters
666
- ```
154
+ <p align="center">
155
+ <img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/screenshot-status.svg" alt="Terminal output of agentainer status and agentainer queue against a running mock swarm" width="700"/>
156
+ </p>
667
157
 
668
- ## <span id="tests"></span>✅ Tests
158
+ ## 🔒 Key invariants
159
+
160
+ - 🪶 **Zero runtime dependencies, forever** — Python 3 + bash + tmux; bundled
161
+ `minyaml` fallback; stdlib-only UI (P2+).
162
+ - 🤝 **`can_talk_to` is cooperative, not an OS boundary** — enforced for
163
+ well-behaved agents, documented honestly (Decision D15).
164
+ - 📛 **`user` / `system` are reserved virtual mailboxes.**
165
+ - 💗 **Liveness supervisor heartbeat retained** — no event-only redesign.
166
+ - 🗄️ **Durable JSONL event log is the source of truth** for history (TUIs keep no
167
+ scrollback).
168
+ - 🧪 **100% line coverage** via mock agents (bash loops, no API keys).
169
+
170
+ ## 🖥️ The control-plane UI (P2–P3)
171
+
172
+ `agentainer serve -c my-swarm.yaml --port 8080 --token <secret>` starts a
173
+ **zero-dependency** web UI (stdlib `http.server` + a single vanilla-JS page, no
174
+ framework, no build step). It binds `127.0.0.1` by default; any non-loopback bind
175
+ requires a token. The UI is a control plane, so keep it on loopback unless a
176
+ token is supplied.
177
+
178
+ It is a **modern, mobile-friendly mail app** for the swarm:
179
+
180
+ - **Agents overview** — a card per agent with its role, type, and live
181
+ running/idle/busy/unread/queue status.
182
+ - **Mail view** — click an agent to open its correspondence. The left rail lists
183
+ its contacts (the `user`, its peers, `system`) instead of mail folders; pick
184
+ one and every message between the two is reconstructed into a threaded
185
+ conversation (rendered as **markdown**), each with its **delivery status**
186
+ (waiting → delivered → read). Reply to an agent **as the `user`** inline.
187
+ - **Activity + topology** — a global event timeline and a who-talks-to-whom graph.
188
+ - **Direct to the session** — watch each agent's **live tmux pane** and type
189
+ straight into it (bypassing the mailroom) when you need to.
190
+ - **Availability toggle** — flip whether the `user` is available to receive mail
191
+ (off by default: mail is *held*, never bounced) right from the top bar; the
192
+ change is persisted to `agentainer.yaml`.
193
+ - **Settings & agents** — edit swarm settings and **add / edit / delete agents**
194
+ from the UI; every change is written back to `agentainer.yaml` (via the same
195
+ stdlib emitter, so it works with **or without** PyYAML).
196
+ - **Telegram bridge** *(optional)* — configure a bot token + chat id in Settings
197
+ to **mirror** the swarm's mail (all agents or a selected set, plus your own
198
+ mail) to a Telegram chat, and **reply from your phone**: a Telegram message
199
+ reply routes back into the swarm as `user` mail. Stdlib `urllib` only — no new
200
+ dependency; the mirror is best-effort so the network can never stall routing.
201
+
202
+ ## 🔄 Dynamic reconcile (P4)
203
+
204
+ No need to tear the whole swarm down to change it:
669
205
 
670
206
  ```bash
671
- tests/validate.sh
207
+ agentainer add dave --type codex --command "codex" --can-talk-to "alice,user" -c my-swarm.yaml
208
+ agentainer edit alice -s can_talk_to="dave,user" -c my-swarm.yaml
209
+ agentainer remove bob -c my-swarm.yaml
210
+ agentainer reconcile -c my-swarm.yaml # start missing agents, stop orphaned sessions
672
211
  ```
673
212
 
674
- 48 checks over the real code paths — tmux, hooks, locks, queues, the tag parser,
675
- sessions and resume driven by mock agents, so it needs no API key and costs
676
- nothing. It covers the awkward cases: the check-and-set race between concurrent
677
- senders, a queued message beating a reply reminder, a subagent's sidechain turn
678
- being skipped, and a transcript read before Claude has flushed it.
679
-
680
- ## <span id="troubleshooting"></span>🛠️ Troubleshooting
681
-
682
- **"could not confirm the text arrived; NOT pressing Enter".** The agent's input
683
- box never echoed the prompt, so Agentainer refused to submit it. Attach to the
684
- session to see what state the CLI is in -- usually a modal (login, trust,
685
- onboarding) is holding focus. Raise `ready_timeout_ms` if the CLI is merely slow.
686
-
687
- **An agent says it "cannot message" another.** That is the permission check
688
- doing its job — add the recipient to the sender's `can_talk_to`.
689
-
690
- **A `pane` agent forwards garbage.** Terminal scraping picked up a redraw. Raise
691
- `pane_idle_ms`, or set `capture: none` and instruct the agent to call
692
- `swarm send` itself.
693
-
694
- **Forwarding stopped with a hop-limit warning.** Two agents were relaying to each
695
- other. Raise `max_forward_hops`, or break the cycle in `forward_responses_to`.
696
-
697
- **Nothing captured from a claude agent.** Check `<agent-dir>/.claude/settings.json`
698
- exists and `.swarm/logs/hooks.log` for errors. If the agent also looks busy forever,
699
- its `type` probably does not match the CLI its `command` runs (see the capture note
700
- under [Busy agents](#subagents-parallel-work-and-busy-agents)).
701
-
702
- **Can't scroll up in an attached session.** Agentainer raises tmux's scrollback to
703
- `tmux_history_limit` (50000 lines) and turns on `tmux_mouse`, so the wheel scrolls
704
- the backlog; press `q` to leave copy mode. If your terminal grabs the wheel itself,
705
- use `Ctrl-b [` then PageUp. Both options are set on the tmux server before sessions
706
- are created, so a server that was already running keeps its old panes' smaller
707
- buffer restart the swarm (or that pane) to pick up the larger one.
708
-
709
- ## <span id="a-note-on-flags"></span>⚠️ A Note On Flags
710
-
711
- `claude --dangerously-skip-permissions`, `codex --yolo` and `gemini --yolo` let
712
- agents act without asking for confirmation. That is usually what you want for an
713
- unattended swarm, and it means several models are running tools unsupervised in
714
- these directories. Point `root` somewhere disposable, and don't run a swarm over
715
- a directory you can't afford to lose.
716
-
717
- ---
718
-
719
- ## 🤝 Contributing
720
-
721
- Pull requests and issues are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for
722
- the zero-dependency principles, a key-free smoke test, and the PR checklist. A
723
- safe first run with no API keys lives in [quickstart.yaml](quickstart.yaml).
213
+ `add`/`remove`/`edit` rewrite `agentainer.yaml` with a stdlib-only YAML emitter
214
+ (works with **or without** PyYAML) and then `reconcile` the change into effect.
215
+
216
+ ## 📊 Project status
217
+
218
+ - **P1 — Mail runtime (CLI-driven)**: ✅ done.
219
+ - **P2 UI observability**: ✅ done.
220
+ - **P3 — terminal snapshot + send-from-UI**: ✅ done.
221
+ - **P4 dynamic reconcile (`add`/`remove`/`edit`/`reconcile`)**: done.
222
+
223
+ 🎉 100% line coverage across all core + UI + reconcile modules, driven entirely by
224
+ mock agents (no API keys).
225
+
226
+ ## FAQ
227
+
228
+ **🤖 What is Agentainer?**
229
+ Agentainer is a zero-dependency orchestrator that runs several AI coding agents
230
+ (Claude Code, Codex, Gemini, Hermes) at once, each in its own `tmux` session and
231
+ working directory. Instead of agents exchanging messages inside their chat prose,
232
+ they communicate through a **file-based mail model**: an agent receives mail by
233
+ reading a file in its `inbox/` and sends mail by writing a file into
234
+ `outbox/<name>/`. The orchestrator owns all the hard logic — routing, access
235
+ control, message IDs, retries, and the durable log — so the agents only deal with
236
+ plain natural-language files.
237
+
238
+ **🧩 Which AI coding agents does Agentainer support?**
239
+ Claude Code (`type: claude`), OpenAI Codex (`type: codex`), Google Gemini CLI
240
+ (`type: gemini`), and Hermes (`type: hermes`). Turn-completion detection is wired
241
+ per `type` (a Stop hook for Claude, a `notify` program for Codex, and pane polling
242
+ for Gemini/Hermes). Any CLI that can read and write files in a tmux pane can be
243
+ slotted in as a new `type`.
244
+
245
+ **🐳 Does Agentainer need Docker or any runtime dependencies?**
246
+ No. The entire runtime is Python 3, `bash`, and `tmux` nothing to `pip install`.
247
+ PyYAML is used *if present*, but a bundled fallback parser (`minyaml`) keeps
248
+ everything working without it, and CI proves that no-PyYAML path. `node` is only
249
+ needed for the npm launcher, never at swarm runtime. Docker is optional and not
250
+ required.
251
+
252
+ **🚀 How do I run multiple coding agents together?**
253
+ Write one `agentainer.yaml` listing each agent, then run `agentainer up -c
254
+ my-swarm.yaml`. That creates the mailbox folders, installs per-type turn detection,
255
+ and opens one tmux session per agent. `agentainer status` shows the swarm;
256
+ `agentainer send --to <agent> "..."` injects a `user` message; `agentainer down`
257
+ tears it all down. The bundled `examples/quickstart.yaml` runs entirely on
258
+ key-free bash-loop mock agents so you can watch the mailroom route mail with no API
259
+ keys.
260
+
261
+ **💬 Can agents communicate with each other, and how is that controlled?**
262
+ Yes by writing into each other's `outbox/<name>/` folders. Communication is
263
+ controlled by the `can_talk_to` access-control list in the config: an agent may
264
+ only deliver to names listed there (`"*"` means everyone). The ACL is enforced by
265
+ the orchestrator at routing time — a disallowed send is bounced back as a `system`
266
+ message and filed in `failed/`. It is cooperative, not an OS sandbox (an agent with
267
+ filesystem access *could* bypass it), so it's documented honestly rather than
268
+ presented as a security boundary.
269
+
270
+ **♻️ How do I resume a swarm after a crash or reboot?**
271
+ `agentainer up --resume -c my-swarm.yaml` reattaches recorded Claude and Codex
272
+ conversations via their native resume commands (`claude --resume <id>`,
273
+ `codex resume <id>`); Gemini and Hermes always start fresh with a warning, since
274
+ they have no resume bridge. Conversation ids are recorded in
275
+ `<root>/.agentainer/sessions.yaml` as each agent finishes its first turn.
276
+
277
+ **🛡️ Is it safe to let agents run unattended?**
278
+ The design assumes unattended operation. A liveness **supervisor heartbeat**
279
+ (reconciling stale-busy, dead, and silent-but-alive agents) is always running; a
280
+ per-agent health probe catches the "alive but silent" case the supervisor can't
281
+ otherwise see; and several safeguards prevent a wedged agent from stalling the
282
+ swarm — including auto-releasing the next message after N presentations and a
283
+ rate cap on runaway auto-exchanges. That said, agents can run
284
+ `--dangerously-skip-permissions`, so the control-plane UI binds `127.0.0.1` by
285
+ default and requires a token for any remote bind.
286
+
287
+ **⚖️ How is Agentainer different from just opening several terminals?**
288
+ With several terminals you manually paste prompts, watch for completion, and relay
289
+ messages by hand — and a model that forgets a step silently stalls. Agentainer
290
+ automates the whole loop: it releases exactly one inbox message at a time, detects
291
+ each turn's completion per `type`, sweeps outgoing mail on stop, enforces the
292
+ `can_talk_to` ACL, retries nudges, keeps a durable JSONL event log as the source of
293
+ truth, and runs a supervisor so one stuck agent can't wedge the rest. The models
294
+ only read and write files; all coordination is deterministic orchestrator code.
724
295
 
725
296
  ## 📜 License
726
297
 
727
- Released under the [MIT License](LICENSE).
298
+ MIT see [LICENSE](LICENSE).