agentainer 0.1.6 → 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/llms.txt CHANGED
@@ -1,431 +1,147 @@
1
- # Agentainer
2
-
3
- > Agentainer runs several coding-agent CLIs (Claude Code, Codex, Gemini, Hermes) at once.
4
- > Each agent gets its own directory and its own tmux session. A single YAML file defines
5
- > the agents, the command that launches each one, its first prompt, and — as a strict
6
- > whitelist which other agents it is allowed to message.
7
- >
8
- > This file is the reference for LLMs asked to write or repair an Agentainer config.
9
- > If you are configuring a swarm, everything you need is below. Verify your work with
10
- > `agentainer validate --show-prompts`, which parses the config and launches nothing.
11
-
12
- ## Mental model
13
-
14
- - One YAML file describes the whole swarm. `agentainer up` reads it and, for each agent:
15
- creates `<root>/<name>/`, installs a turn-completion hook in that folder, opens a tmux
16
- session named `<session_prefix><name>`, runs the agent's `command` inside the folder,
17
- waits for its input box to become responsive, then types the assembled first prompt in.
18
- - Agents message each other with `swarm send --to <agent> "text"`. The `swarm` command is
19
- on their `PATH` and `SWARM_AGENT` identifies them, so no `--from` is needed. Delivery is
20
- refused unless the recipient appears in the sender's `can_talk_to`.
21
- - A delivered message is pasted into the recipient's pane as
22
- `[swarm] message from <sender>:\n<text>`, archived under `.swarm/inbox/<recipient>/`,
23
- and written to `.swarm/logs/`.
24
-
25
- ## Config file
26
-
27
- Top level keys: `swarm`, `defaults`, `agent_types`, `agents`, `templates`.
28
- Only `agents` is required. Relative paths resolve against the config file's directory.
29
-
30
- ```yaml
31
- swarm:
32
- name: dev-swarm # label used in prompts/logs; default = config filename
33
- root: ./workspace # parent of the per-agent folders; default ./workspace
34
- create_workdirs: true # auto-create missing agent folders; default true
35
- session_prefix: "" # prepended to tmux session names; default ""
36
- send_delay_ms: 150 # pause before pasting into a pane
37
- enter_delay_ms: 250 # pause between pasting and pressing Enter
38
- max_forward_hops: 3 # auto-forward loop guard
39
- ready_timeout_ms: 60000 # how long to wait for an agent's input box to respond
40
- busy_timeout_ms: 900000 # after this, an agent stuck "busy" is treated as idle
41
- message_format: tagged # tagged | plain; tagged is the default
42
- max_reply_reminders: 1 # how often to nudge an agent whose reply reached nobody
43
- resume: false # make `up` reattach to recorded conversations by default
44
- pane_idle_ms: 2500 # quiet time before a `pane` capture counts as a finished turn
45
- pane_poll_ms: 700 # pane sampling interval
46
- pane_scrollback: 400 # lines of scrollback the pane watcher diffs
47
- tmux_history_limit: 50000 # scrollback per agent pane so you can scroll up when attached; 0 = tmux default
48
- tmux_mouse: true # enable mouse-wheel scrolling in the panes
49
-
50
- defaults: # any agent key; applied to agents that omit it
51
- type: claude
52
- append_agents_that_you_can_talk_to_prompt: true
53
- workdir: "{root}/{name}" # optional; see "Working directories" below
54
-
55
- agent_types: # override built-ins, or define new types
56
- claude:
57
- command: "claude --dangerously-skip-permissions"
58
- capture: hook
59
- boot_delay_ms: 3000
60
-
61
- agents:
62
- - name: developer # REQUIRED. Used as the folder name AND the tmux session name.
63
- # Must match ^[A-Za-z0-9_][A-Za-z0-9_-]*$
64
- type: codex # claude | codex | gemini | hermes | any key in agent_types
65
- command: "codex --yolo" # exact CLI invocation; defaults to the type's command
66
- can_talk_to: [reviewer] # whitelist. "*" means every other agent. Default: []
67
- first_prompt: | # typed in after the CLI boots
68
- You are the DEVELOPER...
69
- first_prompt_file: ./p.md # alternative to first_prompt (mutually exclusive)
70
- forward_responses_to: [reviewer] # auto-relay finished turns; must ⊆ can_talk_to
71
- capture: hook # hook | pane | none | auto (default: from type)
72
- boot_delay_ms: 3000 # grace period before probing the input box
73
- ready_probe: true # wait for the input box to echo a token first
74
- busy_check: true # refuse incoming mail while this agent is mid-turn
75
- parse_outbound_tags: true # route <swarm-send> blocks written in its replies
76
- reply_reminder: true # nudge it when it owes a reply but sent nothing
77
- resume_args: "--resume {session_id}" # appended to command by `up --resume`
78
- resume_command: "bash -ic 'chy3 --resume {session_id}'" # or replace it entirely
79
- workdir: ./somewhere # default <root>/<name>; see below
80
- create_workdir: true # false => a missing workdir is an error
81
- env: {KEY: value} # extra env vars for its tmux session
82
- append_agents_that_you_can_talk_to_prompt: true
83
- in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt: false
84
-
85
- templates: # override the text Agentainer generates
86
- comms: | # placeholders: {agent} {swarm} {peers} {prefix} {inbox} {workdir}
87
- You are {agent}. You may message: {peers}.
88
- task_notice: |
89
- Stand by. Your real task arrives in the next message.
90
- reply_reminder: | # placeholders: {agent} {sender} {id} {peers} {problems}
91
- You owe {sender} an answer to {id}. Send it inside <swarm-send to="{sender}">.
92
- send_failed: | # placeholders: {agent} {peers} {problems}
93
- Your message could not be delivered. {problems}
94
- ```
95
-
96
- ## Working directories
97
-
98
- - Default: each agent gets `<root>/<name>`, created on `up`.
99
- - `workdir` overrides it. Absolute, relative to the config file, or `~`-prefixed.
100
- Placeholders: `{name}` `{root}` `{swarm}` `{type}`.
101
- - `defaults.workdir` applies to every agent that omits it. Include `{name}` to give
102
- each agent its own folder; omit it to put the whole swarm in one directory.
103
- - `create_workdirs` (swarm-level) and `create_workdir` (per-agent) control creation.
104
- Set them to `false` when pointing at real repositories, so a mistyped path is an
105
- error rather than a new empty folder.
106
- - Several agents may share one workdir. That is legitimate (pair programming in one
107
- checkout) but they will overwrite each other's files and interleave git commits,
108
- so `validate` and `up` print a warning.
109
- - `root` always holds `.swarm/` (logs, inboxes, the `swarm` shim), even when every
110
- agent works elsewhere.
111
-
112
- ## Tagged messages (the default wire format)
113
-
114
- Inbound, an agent sees an envelope it can parse unambiguously:
115
-
116
- <swarm-message from="lead" to="reviewer" id="m-eb4105" reply-to="m-3f9a1c">
117
- any number of lines, code blocks, backslashes -- all verbatim
118
- </swarm-message>
119
-
120
- Outbound, the agent writes a block **in its reply**; the capture hook routes it when
121
- the turn ends. There is no shell involved, hence no quoting or escaping to get wrong:
122
-
123
- <swarm-send to="reviewer" reply-to="m-eb4105">
124
- multi-line body
125
- </swarm-send>
126
-
127
- <swarm-broadcast>
128
- goes to everyone in the sender's can_talk_to
129
- </swarm-broadcast>
130
-
131
- - `id` is generated per message; `reply-to` is optional and used for threading.
132
- - Tagged sends are permission-checked exactly like `swarm send`, and are queued
133
- automatically if the recipient is busy.
134
- - A `<swarm-send to="...">` naming something that is not an agent is discarded with a
135
- warning (this catches an agent echoing the `AGENT_NAME` placeholder from its prompt).
136
- - Blocks are removed from the text before `forward_responses_to` runs, so a message
137
- addressed to one peer is not also auto-forwarded to everybody.
138
- - `message_format: plain` restores the `[swarm] message from <sender>:` header.
139
- `parse_outbound_tags: false` stops reading tags out of replies. Both are forced off
140
- when `capture: none`, since there is nothing to read.
141
- - With `capture: pane` the tags are recovered from scraped terminal text, so they are
142
- only as reliable as the scrape. Prefer hook-captured agents for tag routing.
143
-
144
- ## Resuming after a restart
145
-
146
- - Every captured turn records the agent's conversation id in `<root>/.swarm/sessions.yaml`
147
- (claude hands it to the Stop hook; codex's is read from the newest rollout file under
148
- that agent's private `CODEX_HOME/sessions`).
149
- - `swarm up --resume` rebuilds each agent's command from that id, skips re-sending the
150
- first prompt, and preserves the agent's queued mail and any reply it still owes.
151
- - Resume recipes: claude `--resume {session_id}`, codex `resume {session_id}`. Neither
152
- gemini nor hermes exposes a session id, so they always start fresh (with a warning).
153
- - `resume_args` is appended to `command`. If the command invokes the CLI through an
154
- alias or wrapper (`bash -ic chy3`), appending is wrong -- set `resume_command` to the
155
- full replacement, using `{session_id}` (and optionally `{command}`).
156
- - Starting an agent fresh removes its stale entry, so a later `--resume` cannot reattach
157
- to a conversation that agent never ran.
158
- - `swarm sessions` prints what is recorded and the exact command that would resume it.
159
-
160
- ## Reply reminders
161
-
162
- A model asked a question will often write the answer as prose and end its turn. That
163
- prose reaches nobody -- only a `<swarm-send>` block is delivered. So:
164
-
165
- - When agent B receives a message from agent A (and B may message A back), B is
166
- recorded as owing a reply.
167
- - If B's turn ends having sent nothing, it is messaged with the `reply_reminder`
168
- template: who is waiting, the message id to quote as `reply-to`, and the exact block
169
- to write.
170
- - If B *tried* to send but the block was malformed -- unclosed, missing `to`, unknown
171
- recipient, permission denied -- it gets the `send_failed` template naming each fault.
172
- - At most `max_reply_reminders` nudges (default 1), then Agentainer gives up silently.
173
- - Anything B delivered counts, including an auto-forward via `forward_responses_to`.
174
- Reminders are sent from `swarm`, so they never create a reply obligation themselves.
175
- - `reply_reminder: false` disables it per agent; it is forced off when the agent's
176
- tags are not parsed (`capture: none` or `message_format: plain`).
177
-
178
- ## The two prompt switches
179
-
180
- Both are per-agent booleans, both default as shown, and both append to `first_prompt`:
181
-
182
- - `append_agents_that_you_can_talk_to_prompt` (default `true`) — appends a block naming
183
- the agents this one may message and showing the `swarm send` / `swarm broadcast` / raw
184
- `tmux send-keys` syntax. Set it to `false` for an agent with no peers, or when you want
185
- to write the communication instructions yourself.
186
- - `in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt` (default `false`) —
187
- appends a "do not start yet, your task arrives in the next prompt" block. Use it for
188
- the agent you will drive manually with `swarm send`, so it acknowledges and waits
189
- instead of inventing work.
190
-
191
- Final prompt order: `first_prompt`, then the comms block, then the task notice.
192
-
193
- ## capture: how a finished turn is detected
194
-
195
- | value | works with | how |
196
- |---|---|---|
197
- | `hook` | `claude`, `codex` **only** | the CLI runs a program on turn completion; exact |
198
- | `pane` | anything | poll the tmux pane and diff it after `pane_idle_ms`; heuristic |
199
- | `none` | anything | no capture |
200
- | `auto` | anything | use the agent type's default (this is the default) |
201
-
202
- - `claude` → a `Stop` hook in `<agent-dir>/.claude/settings.json`; reads the transcript.
203
- - `codex` → a private `CODEX_HOME` at `<agent-dir>/.codex/` with a `notify` program.
204
- - `gemini`, `hermes` → no such facility, so they fall back to `pane`. Setting
205
- `capture: hook` on them (or on a custom type) prints a warning and uses `pane`.
206
- - `pane` capture is terminal scraping. It filters the echo of incoming messages, but
207
- spinners and redraws can leak through. When output quality matters, prefer
208
- `capture: none` and tell the agent in its prompt to call `swarm send` itself.
209
-
210
- `forward_responses_to` requires `capture` to be `hook` or `pane`; combining it with
211
- `capture: none` is a config error.
212
-
213
- ## Rules the validator enforces
214
-
215
- - agent names are unique and match `^[A-Za-z0-9_][A-Za-z0-9_-]*$`
216
- - every name in `can_talk_to` and `forward_responses_to` refers to a real agent
217
- - an agent may not list itself in `can_talk_to`
218
- - `forward_responses_to` ⊆ `can_talk_to`
219
- - `forward_responses_to` is empty unless capture is enabled
220
- - `type` is a built-in or defined under `agent_types`
221
- - `first_prompt` and `first_prompt_file` are not both set
222
-
223
- ## Commands
224
-
225
- ```
226
- agentainer up [--only a,b] [--restart] [--no-prompt] [--attach]
227
- agentainer down [--only a,b]
228
- agentainer restart
229
- agentainer status
230
- agentainer attach <agent>
231
- agentainer send --to <agent> [--from <name>] [--file F] [--queue] [--wait]
232
- [--wait-timeout S] [--ignore-busy] [--force] "message"
233
- agentainer broadcast "message"
234
- agentainer queue <agent> [--clear]
235
- agentainer idle <agent> [--no-drain]
236
- agentainer inbox <agent> [-n N]
237
- agentainer logs [agent] [-n N] [-f]
238
- agentainer validate [--show-prompts]
239
- ```
240
-
241
- Config resolution: `-c PATH`, else `$SWARM_CONFIG`, else `./agents.yaml`, else the
242
- `agents.yaml` beside `agentainer`. `agentainer x.yaml` is shorthand for `up -c x.yaml`.
243
-
244
- ## What an agent sees at runtime
245
-
246
- Environment inside every agent's tmux session:
247
-
248
- ```
249
- SWARM_AGENT this agent's name SWARM_CONFIG absolute path to the YAML
250
- SWARM_PEERS comma-separated can_talk_to SWARM_ROOT the swarm root directory
251
- SWARM_SESSION its tmux session name SWARM_NAME the swarm's name
252
- PATH has <root>/.swarm/bin first, providing `swarm`
1
+ # 🐝 Agentainer
2
+
3
+ > Zero-dependency multi-agent orchestrator with a file-based mail model. Launch
4
+ > Claude Code, Codex, Gemini, or Hermes each in its own tmux session; let them
5
+ > message each other only where `can_talk_to` allows. No runtime dependencies,
6
+ > no API keys required to exercise the mechanics.
7
+
8
+ ## 🧭 What it is
9
+ Agentainer v2 replaces v1's fragile "tagged XML envelope inside prose" messaging
10
+ with a **file-based mail model**: agents *read a file* to receive and *write a
11
+ file* to send. The orchestrator owns all hard logic — routing, ACL, message IDs,
12
+ threading, read-state, queueing, retries, availability, the durable log. The
13
+ model only reads and writes natural-language files.
14
+
15
+ ## 📬 Core model (two verbs, four folders)
16
+ - Agents communicate by reading/writing files under their mail dir.
17
+ - `inbox/` the **one** current unread message (orchestrator releases one at a time).
18
+ - `outbox/<name>/` write here to send to `<name>`; read `<name>/about.md` for a contact card.
19
+ - `read/` move a handled message here (best-effort read receipt).
20
+ - `sent/` the agent's own record of delivered mail (orchestrator moves it here).
21
+ - `failed/` mail bounced by the orchestrator (ACL violation, etc.), with a `system` explanation.
22
+
23
+ ## 🔁 Why a rewrite
24
+ v1 scraped messages out of a fullscreen-TUI pane. That was unreliable across LLMs.
25
+ The v2 mail model works on nearly every tool-calling model, including weak ones.
26
+
27
+ ## 🚀 Quick start
253
28
  ```
254
-
255
- Sending, from inside an agent:
256
-
257
- ```bash
258
- swarm send --to reviewer "src/parse.py is ready for review"
259
- swarm broadcast "I renamed the config module"
260
- tmux send-keys -t reviewer -l "raw fallback, skips permissions and logging" \
261
- && tmux send-keys -t reviewer Enter
262
- ```
263
-
264
- Loop guard: a forwarded message carries a hop count that increments on each relay and
265
- stops at `max_forward_hops`. A message you send with `swarm send` resets it to zero.
266
-
267
- ## Worked examples in this repo
268
-
269
- - `examples/research-swarm.yaml` — hub and spoke: a lead delegates to a scout
270
- (gemini), an analyst (codex) and a writer; the writer uses a custom `workdir`.
271
- - `examples/software-company.yaml` — six agents across all four CLIs with a
272
- restricted comms graph (developers talk to the architect and QA, never to
273
- each other).
274
- - `examples/bug-hunt.yaml` — a hands-free pipeline built on `forward_responses_to`:
275
- reproduce → diagnose → fix → verify.
276
- - `examples/existing-repo.yaml` — two agents pairing inside one existing checkout,
277
- using `create_workdirs: false` so a wrong path fails instead of being created.
278
- - `examples/red-team.yaml` — adversarial triad: an attacker and defender argue
279
- through a referee over one existing checkout.
280
- - `examples/debate.yaml` — two advocates argue opposite sides; a judge
281
- cross-examines and rules.
282
- - `examples/writers-room.yaml` — an editor drives a writer, fact-checker and
283
- critic to a finished article.
284
- - `examples/incident-response.yaml` — a commander coordinates an investigator,
285
- responder and scribe under time pressure (hub and spoke).
286
- - `examples/tdd-pingpong.yaml` — a tester and coder grow a feature test-first,
287
- one test at a time, via `forward_responses_to`.
288
- - `examples/brainstorm.yaml` — three idea-generators fan out from a facilitator,
289
- then a synthesiser ranks them.
290
- - `examples/localization.yaml` — translate → review → back-check, auto-forwarded
291
- via `forward_responses_to`; a `NEEDS ANOTHER PASS` verdict loops back to
292
- the translator with a `<swarm-send>` block.
293
- - `examples/multi-language-broadcast.yaml` — one translator broadcasts a source to
294
- N language reviewers; a consolidator compiles and broadcasts back. Built on
295
- `broadcast`.
296
- - `examples/code-review-broadcast.yaml` — a coordinator broadcasts a PR to
297
- specialist reviewers (correctness/security/perf); each replies to the
298
- coordinator, who synthesizes. Built on `broadcast`.
299
- - `examples/ping-pong.yaml` — two agents trade a unit of work, each broadcast
300
- naming who is up next (round-robin via `broadcast`, distinct from
301
- tdd-pingpong's fixed `forward_responses_to`).
302
- - `agents.example.yaml` — the fully annotated reference config; every key is
303
- documented inline.
304
-
305
- ## Recipes
306
-
307
- **Hub and spoke** — one orchestrator drives specialists, which reply only to it:
308
-
309
- ```yaml
310
- agents:
311
- - {name: orchestrator, type: claude, can_talk_to: "*",
312
- in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt: true}
313
- - {name: researcher, type: gemini, can_talk_to: [orchestrator]}
314
- - {name: developer, type: codex, can_talk_to: [orchestrator]}
29
+ cp examples/quickstart.yaml my-swarm.yaml
30
+ agentainer up -c my-swarm.yaml
31
+ agentainer status -c my-swarm.yaml
32
+ agentainer send -c my-swarm.yaml --to orchestrator "Build a CSV->Parquet CLI."
33
+ agentainer logs -c my-swarm.yaml -f
34
+ agentainer down -c my-swarm.yaml
315
35
  ```
316
-
317
- **Pipeline** each stage hands off to the next, automatically:
318
-
319
- ```yaml
320
- agents:
321
- - {name: spec, type: claude, can_talk_to: [build], forward_responses_to: [build]}
322
- - {name: build, type: codex, can_talk_to: [review], forward_responses_to: [review]}
323
- - {name: review, type: claude, can_talk_to: []}
324
- ```
325
-
326
- **Isolated worker** — runs alone, talks to nobody:
327
-
328
- ```yaml
329
- agents:
330
- - {name: scribe, type: hermes, can_talk_to: [], capture: none,
331
- append_agents_that_you_can_talk_to_prompt: false}
332
- ```
333
-
334
- ## Busy agents (backpressure)
335
-
336
- - An agent is "busy" from the moment a message is submitted to it until its next
337
- captured turn end. Sending to a busy agent is refused with a message telling the
338
- sender to retry later, `--queue`, or `--wait`.
339
- - `--queue` appends to `<root>/.swarm/run/<agent>.queue.jsonl`; the capture hook that
340
- ends the agent's turn drains one message from it. `--wait` blocks until deliverable.
341
- `--queue --wait` queues, then blocks until the agent picks the message up.
342
- - The busy check and the "now busy" write happen inside the recipient's pane lock, so
343
- two parallel senders can never both see an idle agent and both deliver.
344
- - Turn state is two counters (`delivered`, `completed`), not a boolean: a hook that
345
- ends turn N may land after a message delivered during turn N. `completed` is clamped
346
- to `delivered` on each turn end, because codex folds a queued message into the turn
347
- already running, and an incrementing counter would drift and wedge the agent "busy".
348
- - Only works when the agent is captured. `capture: none` => `busy_check` is forced off
349
- and the agent always accepts mail.
350
- - `busy_timeout_ms` fails open if a capture never fires; `swarm idle <agent>` clears it.
351
- - Auto-forwarded responses to a busy peer are queued rather than dropped.
352
- - Queues self-heal: every turn end sweeps other agents and drains any that have gone
353
- idle (or gone stale-busy past `busy_timeout_ms`), so a single missed turn-completion
354
- cannot strand queued mail forever. A capture only fires when `type` matches the CLI
355
- the `command` actually launches a `type: codex` agent running `claude` never reports
356
- turn ends and looks busy forever.
357
-
358
- ## Subagents and concurrency
359
-
360
- - A subagent inherits `SWARM_AGENT`, so `swarm send` from inside one is attributed to
361
- the parent agent and checked against the parent's `can_talk_to`.
362
- - Every write into a pane takes a per-recipient lock (`<root>/.swarm/run/<session>.lock`).
363
- Without it, parallel senders interleave: a paste and its Enter are two tmux calls, so
364
- one Enter can submit two concatenated messages and another submit nothing.
365
- - A message delivered to a busy agent is queued by the CLI and processed after the
366
- current tool call. It is not lost.
367
- - `capture: hook` fires when the *agent's* turn ends. Claude `Task` subagents run inside
368
- the turn, so the hook waits for them. Backgrounded work ends the turn early: the
369
- interim "I will respond when the subagent finishes" is captured (and forwarded), then
370
- the real answer is captured when the agent finishes for real. The hop counter stores
371
- the hop at which the agent last *received* a message, so a second response reuses the
372
- same hop and the loop guard never suppresses the real answer.
373
- - Claude records subagent turns in the same transcript with `isSidechain: true`. The
374
- Stop hook skips them, so subagent chatter is never relayed as the agent's answer.
375
- - `capture: pane` cannot tell "waiting on a subagent" from "finished": a quiet pane is
376
- its only signal, so it forwards the interim message and then the real one. Raise
377
- `pane_idle_ms`, or use `capture: none`.
378
-
379
- ## Verified CLI behaviours (do not "simplify" these away)
380
-
381
- - The claude `Stop` hook entry must have **no `matcher` key**. With one, the interactive
382
- TUI never runs the hook (headless `-p` still does, which makes this easy to miss).
383
- - Claude fires `Stop` *before* flushing the assistant message to the transcript. Read it
384
- with a short poll, and only consider text after the last user record -- otherwise the
385
- hook captures nothing, or re-relays the previous turn's reply.
386
- - Claude asks "do you trust this folder?" in any new directory, even under
387
- `--dangerously-skip-permissions`. Pre-trust it by setting `hasTrustDialogAccepted`
388
- for that path in `~/.claude.json` before launching.
389
- - Long pastes are collapsed into a chip: `[Pasted text #1 +N lines]` (claude),
390
- `[Pasted Content N chars]` (codex). Delivery checks must recognise both.
391
-
392
- - Claude Code discards keystrokes for several seconds partway through startup
393
- (measured on v2.1.205: t=2s landed, t=6s and t=12s were dropped, t=20s landed).
394
- A fixed `boot_delay_ms` cannot fix this, which is why `ready_probe` types a
395
- throwaway token until the input box echoes it, and why a paste is verified on
396
- screen before Enter is pressed.
397
- - Codex shows a "do you trust this directory?" modal on first run in a new folder;
398
- Enter answers the modal instead of submitting the prompt. The generated
399
- `<agent>/.codex/config.toml` pre-trusts the agent's workdir to avoid it.
400
- - TOML is order-sensitive. In that generated config, `notify` must appear above
401
- every `[table]` header, or it becomes `projects.<dir>.notify` and codex never
402
- calls it.
403
- - Claude collapses a long paste into a `[Pasted text #1 +N lines]` chip, so the
404
- pasted text never appears verbatim on screen. Delivery checks look for the chip
405
- as well as for the tail of the pasted text (the head scrolls out of view).
406
-
407
- ## Gotchas
408
-
409
- - A folder and a tmux session are named after each agent; renaming an agent orphans both.
410
- - Pointing several agents at one `workdir` is allowed and warned about; they share a
411
- filesystem and a git index, so they can clobber each other.
412
- - Two agents with `forward_responses_to` pointing at each other will relay until the hop
413
- limit stops them. Prefer one-directional forwarding, or none.
414
- - `--dangerously-skip-permissions` / `--yolo` let agents run tools unsupervised. Point
415
- `root` at a disposable directory.
416
- - The first prompt is typed into a live TUI. If Agentainer reports "could not confirm
417
- the text arrived", the CLI is usually stuck behind a modal (login, trust, onboarding);
418
- attach to the session and look. Raise `ready_timeout_ms` if it is merely slow.
419
- - PyYAML is used when installed; otherwise a bundled subset parser handles the config.
420
- It supports block maps/sequences, `|` and `>` scalars, flow `[a, b]` / `{a: 1}`,
421
- comments and quoting but not anchors, aliases, tags or multi-document files.
422
-
423
- ## Files
424
-
425
- - [tests/validate.sh](tests/validate.sh): the full suite. Run it after any change; it
426
- uses mock agents, so it needs no API key. 48 checks, all must pass.
427
- - [agents.example.yaml](agents.example.yaml): every option, documented inline.
428
- - [README.md](README.md): human-facing guide, architecture and troubleshooting.
429
- - [lib/config.py](lib/config.py): schema, defaults and validation — the source of truth.
430
- - [lib/swarm.py](lib/swarm.py): tmux orchestration, message routing, capture.
431
- - [hooks/](hooks/): the Claude `Stop` hook and the Codex `notify` program.
36
+ (Install via `npm install` + `npm link` so the `agentainer` bin is on PATH.)
37
+ The quickstart uses `bash` loop "mock agents" (no API keys) so you can watch the
38
+ mailroom route mail safely. Swap each `command` for a real agent CLI to run for real.
39
+
40
+ ## 🔒 Key invariants
41
+ - Zero runtime dependencies (Python 3 + bash + tmux; PyYAML optional, bundled `minyaml` fallback).
42
+ - `can_talk_to` ACL is cooperative, documented honestly, not an OS boundary.
43
+ - `user`/`system` are reserved virtual mailboxes.
44
+ - Liveness supervisor heartbeat retained (no event-only redesign).
45
+ - Durable JSONL event log is the source of truth for history.
46
+
47
+ ## 🛠️ Commands
48
+ - `validate` — resolve and print the config; launch nothing.
49
+ - `up` — create dirs + mailbox folders, install per-type turn-detection, open one tmux session per agent, send the role.
50
+ - `down` / `restart` tear down / down+up.
51
+ - `status` / `attach` / `logs` — health, attach, durable JSONL log (`-f` follows).
52
+ - `send` — send a `user` message into the swarm (`--to <agent>`).
53
+ - `user` — toggle `user` availability (`available`/`away`/`inbox`/`send`).
54
+ - `queue` / `inbox` / `idle` — inspect pending mail, current inbox, idle agents.
55
+ - `hook` / `watch` / `supervise` — turn-completion hook, live pane watch, liveness supervisor.
56
+ - `serve` — mail-app control-plane UI on `127.0.0.1` (per-agent threaded mail rendered as markdown with delivery status, reply as `user`, activity timeline + topology graph, live tmux pane + direct type-in, availability toggle, swarm-settings & agent add/edit/delete written back to YAML, and an optional stdlib Telegram bridge that mirrors mail out and routes phone replies back as `user` mail); token required for non-loopback.
57
+ - `add <name> --type T --command C --can-talk-to X` add an agent to the config and bring it up.
58
+ - `edit <name> -s key=value` edit an agent's fields and reconcile.
59
+ - `remove <name>` remove an agent and stop its session.
60
+ - `reconcile` start agents missing from the running set / stop orphaned sessions.
61
+
62
+ ## 🖥️ Control-plane UI (P2–P3)
63
+ `agentainer serve -c swarm.yaml --port 8080 --token <secret>` serves a zero-dependency
64
+ UI (stdlib `http.server` + one vanilla-JS page; no framework, no build). It shows
65
+ swarm/agent status, logs, inbox, queue, a live terminal snapshot of each agent's tmux
66
+ pane, and lets you inject mail as the `user` mailbox. Binds `127.0.0.1` by default;
67
+ a token is required for any non-loopback bind (the UI is a control plane).
68
+
69
+ ## 🔄 Dynamic reconcile (P4)
70
+ Change the swarm without a full teardown: `add`/`remove`/`edit` rewrite `agentainer.yaml`
71
+ with a stdlib-only YAML emitter (works with or without PyYAML) and then `reconcile` the
72
+ change into effect.
73
+
74
+ ## Verified CLI behaviours (do not "simplify" these away)
75
+ These are observed, load-bearing behaviours of the shipping CLI. An LLM patching or
76
+ "tidying" a config/command around them will produce a subtly broken swarm — keep them.
77
+
78
+ - **`validate` launches nothing.** It resolves and prints the config only. Safe to run
79
+ in any environment.
80
+ - **`up` installs per-`type` turn detection.** `claude` Stop hook; `codex` `notify`
81
+ program; `gemini`/`hermes` pane polling. This wiring is what makes completion
82
+ detectable; do not assume a bare tmux session is enough.
83
+ - **One message in `inbox/` at a time.** The orchestrator releases the next message
84
+ only after the current one is handled (moved to `read/`, or auto-archived after N
85
+ presentations). An agent never sees a queue of inbox files.
86
+ - **Outgoing mail is swept on stop, not in real time.** Writes to `outbox/<name>/` are
87
+ routed when the agent's turn finishes (Stop hook / `notify` / supervisor tick), not
88
+ the instant the file appears. A long-running turn does not deliver mid-turn.
89
+ - **Every delivered message is stamped** with `From:` / `To:` / `Id:` / `Time:` /
90
+ `Re:` headers by the orchestrator; the sender only wrote natural-language body text.
91
+ - **ACL is enforced at routing.** A send to a name not in `can_talk_to` is bounced: a
92
+ `system` explanation is dropped in the sender's `inbox/` and the file moved to
93
+ `failed/`. `"*"` means everyone-else.
94
+ - **`user` send is held, not bounced, when unavailable.** If `user` is in `can_talk_to`
95
+ but `user` availability is `away`, the mail is queued and a `system` ack is returned;
96
+ it is released when `user` becomes `available`.
97
+ - **`read/` is a best-effort receipt.** Moving mail there marks the sender's `sent/`
98
+ copy read; if the model forgets, the orchestrator auto-archives after N presentations
99
+ so a forgetful agent can't wedge or loop the system.
100
+ - **ACL membership = presence of `outbox/<name>/` + `about.md`.** The orchestrator
101
+ creates these when `can_talk_to` includes the name; the model reads `about.md` as a
102
+ contact card. Don't hand-create these folders expecting routing to follow.
103
+ - **`type` `command` mismatch is a hard error at `up`.** If `command` launches a
104
+ different CLI than `type` implies, `up` fails with `ConfigError` instead of silently
105
+ pinning the agent `busy` forever.
106
+ - **`capture: none` auto-upgrades to `hook` on hook-backed types** (`claude`/`codex`)
107
+ at config load, with a warning — a `none` capture on those types would blind turn
108
+ detection.
109
+ - **`add`/`remove`/`edit` rewrite the YAML with the stdlib emitter** and then
110
+ `reconcile`; they work with *or without* PyYAML. Don't assume PyYAML is present.
111
+ - **The liveness supervisor is always on at `up`.** It reconciles stale-busy, dead, and
112
+ silent-but-alive agents every tick; `status` reports `supervisor: alive|down`.
113
+
114
+ ## ⚠️ Gotchas (the quirks that look like bugs but aren't)
115
+ - **Mail sits in `outbox/<name>/` until the turn ends.** If you `send` as an agent and
116
+ immediately check the recipient's `inbox/`, it may not be there yet — wait for the
117
+ sender's turn to finish (or the next supervisor tick).
118
+ - **`inbox/` holds exactly one file.** Don't script around "the newest of many inbox
119
+ files"; there is never more than one.
120
+ - **A `system` mail in `inbox/` is not spam** it's the orchestrator talking back
121
+ (bounce explanation, nudge ack, ping). Treat it as first-class input.
122
+ - **`failed/` means "the orchestrator refused this", not "a crash".** Almost always an
123
+ ACL violation. Read the `system` note inside the file.
124
+ - **`user` is a virtual mailbox, not an agent.** `--to user` routes to the operator;
125
+ `user` availability gates delivery (hold-not-bounce). There is no `user` tmux session.
126
+ - **Reserve the names `user` and `system`.** A real agent may not claim them; the
127
+ validator rejects it.
128
+ - **Mismatched `type`/`command` deadlocks silently** if you bypass the validator (e.g.
129
+ hand-editing the YAML and skipping `validate`). Always `validate` before `up`.
130
+ - **`capture: none` is only valid for pane-polling types** (`gemini`/`hermes`). On
131
+ `claude`/`codex` it is auto-corrected to `hook`; if you really need no detection,
132
+ expect that agent to never report completion.
133
+ - **The no-PyYAML path is supported but stricter.** The bundled `minyaml` parser does
134
+ not support multi-document YAML, anchors, or tags. Keep real configs single-document
135
+ and you'll never notice.
136
+
137
+ ## 📊 Project status
138
+ - **P1 Mail runtime (CLI-driven)**: done.
139
+ - **P2 UI observability**: done.
140
+ - **P3 terminal snapshot + send-from-UI**: done.
141
+ - **P4dynamic reconcile (`add`/`remove`/`edit`/`reconcile`)**: done.
142
+ - 100% line coverage across all core + UI + reconcile modules, via mock agents (no API keys).
143
+
144
+ ## 📚 Docs
145
+ - `ProjectPlan.md` the design (source of truth).
146
+ - `README.md` user-facing overview, install, commands, FAQ.
147
+ - v1 reference (read-only): `/root/AgentSwarm/docs/PROJECT-DOCUMENTATION.md`.