mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
309 lines
17 KiB
Markdown
309 lines
17 KiB
Markdown
# SwarmForge — Description, protocol, and migration requirements
|
|
|
|
> Working document. Project source: https://github.com/unclebob/swarm-forge
|
|
> Goal: understand SwarmForge's architecture and evaluate adapting it to **pi** as the agent, on **Linux**, with **DeepSeek / GLM / Qwen** models.
|
|
|
|
---
|
|
|
|
## 1. Project description
|
|
|
|
**SwarmForge** is a **tmux**-based agent orchestration platform that turns a swarm of AI agents into a coordinated software engineering team. It was created by Robert C. Martin and applies his own engineering discipline (TDD, Gherkin/acceptance testing, mutation testing, CRAP/DRY analysis) to the problem of coordinating agents.
|
|
|
|
Core idea: **each agent lives in its own git worktree and its own tmux session**, and agents communicate via a **file-based handoff protocol** delivered by a daemon. There are no direct messages between agents and no direct access to the tmux socket by them.
|
|
|
|
### Branch structure
|
|
|
|
| Branch | Description | Roles |
|
|
|---|---|---|
|
|
| `main` | **Documentary**: shared operational scripts + default constitution articles | — |
|
|
| `two-pack` | Fast backend workflow (TDD + hardening, no Gherkin) | `coder` → `cleaner` → `coder` |
|
|
| `four-pack` | Compact workflow with Gherkin specification | `specifier` → `coder` → `refactorer` → `architect` → `specifier` |
|
|
| `six-pack` | Full workflow with all quality gates separated | `specifier` → `coder` → `cleaner` → `architect` → `hardender` → `QA` → end |
|
|
|
|
Each executable branch contains the project config: `swarmforge.conf` (topology), `roles/<role>.prompt` (per-role prompts) and `constitution.prompt` + articles (shared rules). On startup, the `./swarm` wrapper downloads the shared operational scripts from `main` (first time only) and launches the orchestrator.
|
|
|
|
### How it works at a high level
|
|
|
|
1. **Declarative configuration**: `swarmforge.conf` defines the swarm window by window:
|
|
```
|
|
window <role> <agent> <worktree> [task|batch] [extra-args...]
|
|
```
|
|
2. **Launcher** (`swarmforge.bb`, Babashka): validates the config, initializes the git repo if needed, creates a **worktree per role** under `.worktrees/`, creates a **tmux session per role** on a project-owned socket and launches each agent with its initial prompt.
|
|
3. **Agents**: each runs as an interactive TUI in its tmux pane, inside its worktree, with the handoff scripts on its `PATH`.
|
|
4. **Daemon** (`handoffd.bb`): owner of the tmux socket. Watches agent outboxes, delivers handoffs to recipient inboxes and wakes agents with a message typed into their pane.
|
|
5. **Handoff protocol**: agents create validated drafts, receive them as tasks or batches (`task`/`batch`), and report completion with `done_with_current.sh`.
|
|
6. **Optional viewer**: terminal adapters (`terminal-adapters/*.sh`) open one window per role for real-time observation, with a watchdog that reopens closed windows without losing agent state.
|
|
|
|
### Key features
|
|
|
|
- **Config-driven topology**: swarm shape comes from `swarmforge.conf`, not from code.
|
|
- **Per-project roles**: `swarmforge/roles/<role>.prompt` per branch/backlog.
|
|
- **Layered constitution**: `constitution.prompt` directs agents to read articles under `swarmforge/constitution/articles/` (shared engineering, handoff and workflow rules + local per-branch rules).
|
|
- **Per-role backends**: each role can use a different agent CLI (`claude`, `codex`, `copilot`, `grok`).
|
|
- **Observable**: one terminal window per role, or headless in tmux.
|
|
- **Self-hosted and light**: only needs tmux, git, zsh and Babashka; all state lives in `.swarmforge/` inside the project.
|
|
- **Operational robustness**: host sleep prevention (`caffeinate`/`systemd-inhibit`), task resumption after restart, file-based audit (`new` → `in_process` → `completed`).
|
|
|
|
---
|
|
|
|
## 2. Handoff protocol (summary)
|
|
|
|
The protocol separates **state** (files on the filesystem, durable and auditable) from **control** (tmux, only for notification and liveness).
|
|
|
|
### Messages
|
|
|
|
Only two types, both strictly validated:
|
|
|
|
```
|
|
type: git_handoff type: note
|
|
to: <role>[,<role>...] to: <role>[,<role>...]
|
|
priority: NN (00-99) priority: NN (00-99)
|
|
task: <stable-name> message: <1 line, max 80 chars>
|
|
commit: <10 hex>
|
|
```
|
|
|
|
- `git_handoff`: the sender has committed work; the receiver does `merge_and_process <role> <commit>`.
|
|
- `note`: short message; only when the constitution or role explicitly authorizes it.
|
|
|
|
### Flow
|
|
|
|
1. The agent commits and writes a **draft** with headers only.
|
|
2. `swarm_handoff.sh` is the **validation gate**: rejects reserved fields, unknown roles, invalid priorities, ambiguous commits (canonicalizes the hash with `git rev-parse --disambiguate`) and bodies that are not generated.
|
|
3. The helper generates the payload (`id`, `from`, `role`, `task`, `created_at`, body) and installs it atomically in `outbox/`.
|
|
4. The **daemon** polls (1s), copies the handoff to each recipient's `inbox/new/` (adding `recipient` and `enqueued_at`) and wakes the receiver.
|
|
5. The receiver runs `ready_for_next.sh` → moves to `inbox/in_process/` (adds `dequeued_at`) and prints `TASK:`/`BATCH:` with the payload.
|
|
6. On completion, `done_with_current.sh` moves to `inbox/completed/` (adds `completed_at`) and picks up the next task if one exists.
|
|
7. The daemon moves the sender's original to `sent/` or `failed/`.
|
|
|
|
### Wake-up (control plane)
|
|
|
|
The daemon "wakes" an agent by typing into its tmux pane:
|
|
|
|
```
|
|
tmux send-keys -t <session> -l "You have new handoff mail. If idle, run ready_for_next.sh."
|
|
tmux send-keys -t <session> C-m # Enter
|
|
tmux send-keys -t <session> C-j # robustness LF
|
|
```
|
|
|
|
The agent receives it as a user message. Protocol rules: if it is already working, **ignore the wake-up**; `done_with_current.sh` picks up the next task when finished. In practice, an agent with a message queue (like pi) enqueues the wake-up and delivers it when the turn ends.
|
|
|
|
### Chain rules
|
|
|
|
- Intermediate roles **always forward** a `git_handoff` to the next role in the chain, no matter what (even if the change is non-functional).
|
|
- The final handoff of the chain (broadcast) is **merge-only**: recipients merge and do not forward.
|
|
- Task names (`task:`) are preserved along the chain.
|
|
|
|
---
|
|
|
|
## 3. Migration requirements
|
|
|
|
### 3.1 Agent contract (necessary condition)
|
|
|
|
SwarmForge requires the agent to be a **long-lived interactive process in a tmux pane** that satisfies:
|
|
|
|
1. **Interactive CLI (TUI/REPL)** that keeps running — wake-ups arrive as typed text + Enter; a one-shot CLI cannot receive work.
|
|
2. **Initial prompt via command line** (or injectable via `tmux send-keys` after startup).
|
|
3. **Work in the worktree directory** (`cd <worktree> && <agent> ...`).
|
|
4. **Ability to run commands** (the helpers `swarm_handoff.sh`, `ready_for_next.sh`, `done_with_current.sh` are shell/bb on `PATH` — they are model-agnostic).
|
|
|
|
Everything else in the protocol (handoffs, worktrees, daemon, wake-ups, watchdog) **does not know about the model**: the only integration point is the launch arm in `swarmforge.bb` and the validated backend list in `parse-config`.
|
|
|
|
### 3.2 Validation: pi as agent
|
|
|
|
**Fits out of the box.** Verified in docs and installed binary:
|
|
|
|
- `pi "<prompt>"` starts the TUI, **sends the initial message and stays interactive** (confirmed in `dist/modes/interactive/interactive-mode.js`).
|
|
- **tmux officially supported** (`docs/tmux.md`). Recommendation: tmux ≥ 3.5 with `extended-keys-format csi-u` for modified keys; the basic protocol (Enter) works with any version.
|
|
- **Compatible wake-up**: in pi `Enter` = send, `Ctrl+J` = new line (the daemon's `C-j` is harmless).
|
|
- **Message queue**: a message typed while pi is working is **enqueued and delivered when the turn ends** — ideal for the protocol's wake-up semantics.
|
|
- **Sessions**: `pi -c` (continue), `--session`, `--name "SwarmForge <Role>"` (session name).
|
|
|
|
Operational requirements with pi:
|
|
|
|
| Requirement | Detail |
|
|
|---|---|
|
|
| Trust prompt | pi asks on startup in a new project and **would block the agent**. Use `-a/--approve` in the launch arm or pre-seed `~/.pi/agent/trust.json`. |
|
|
| Fixed model | Use `--model <provider>/<model>` so each role does not start at the login selector. |
|
|
| Runtime | Node.js ≥ 22 (npm install) or standalone script; Linux supported natively. |
|
|
|
|
Proposed launch arm in `swarmforge.bb`:
|
|
|
|
```clojure
|
|
"pi" (str "pi -a --name " (sq (str "SwarmForge " display))
|
|
" --model " (sq model) " "
|
|
(extra-args-prefix row)
|
|
"\"$(cat " (sq (str prompt-file)) ")\"")
|
|
```
|
|
|
|
### 3.3 Validation: opencode as agent
|
|
|
|
**Fits with a mandatory adaptation.** Verified on the real v1.18.15 binary and in source:
|
|
|
|
- `opencode` (no args) starts the persistent **interactive TUI**.
|
|
- ⚠️ The TUI **does not accept an initial message via CLI**: `--prompt` in TUI mode calls a Node `rl.question` (waits for stdin input); only `--mini --prompt` sends it as a message, but with `interactive: false` (runs and exits). `opencode run "<msg>"` is **headless one-shot** — not usable as a swarm agent.
|
|
- **Solution**: launch the TUI (`opencode --auto`) and **inject the prompt with `tmux send-keys`** after startup — the same mechanism the daemon already uses to wake. About ~10 lines in `launch-role!` (launch → sleep → `send-keys -l "$(cat prompt)"` + Enter).
|
|
|
|
```
|
|
opencode --auto -m <provider>/<model> # in the role's tmux session
|
|
# after ~2s:
|
|
tmux send-keys -t <target> -l "<initial prompt>" ; tmux send-keys -t <target> C-m
|
|
```
|
|
|
|
Operational requirements with opencode:
|
|
|
|
| Requirement | Detail |
|
|
|---|---|
|
|
| Permissions | `--auto` (auto-approve; also the hidden aliases `--yolo` / `--dangerously-skip-permissions`) — equivalent to the swarm's autonomous mode. |
|
|
| Sessions | `-c/--continue`, `-s/--session` for the restart flow ("on restart, run ready_for_next.sh"). |
|
|
| Runtime | Static binary (npm `opencode-ai` or GitHub release); Linux supported. |
|
|
| Future | `opencode serve` + `attach`/SDK/ACP would allow a native message queue without keyboard wake-ups (would require changing the architecture, not adapting it). |
|
|
|
|
### 3.4 Models: DeepSeek / GLM / Qwen
|
|
|
|
| Model | pi | opencode |
|
|
|---|---|---|
|
|
| **DeepSeek** | **Native**: `DEEPSEEK_API_KEY`, provider `deepseek`, `--model deepseek/...` | **Native** in catalog (`models.dev`): `deepseek-*` |
|
|
| **Qwen** | **Native**: `QWEN_TOKEN_PLAN_API_KEY`, providers `qwen-token-plan` / `-individual` / `-cn` (China) | **Native**: `qwen3.x-*`, `alibaba-*/qwen*` |
|
|
| **GLM (Zhipu)** | **Not native**: needs a custom provider extension (OpenAI-compatible, `api: "openai-completions"`, `thinkingFormat: "zai"`) or OpenAI-compatible proxy | **Native**: `glm-4.x`/`glm-5.x` (`opencode-go/glm-*`, `alibaba-*/glm-*`) |
|
|
|
|
Note: pi already implements the *thinking* formats of all three families (`thinkingFormat: "deepseek" | "zai" | "qwen"` in `docs/custom-provider.md`), which simplifies GLM integration: you only need to register the endpoint and models with that extension.
|
|
|
|
### 3.5 Linux (runtime)
|
|
|
|
| Requirement | Status | Detail |
|
|
|---|---|---|
|
|
| `zsh` | **Hard requirement** | Scripts use `#!/usr/bin/env zsh`. Arch: `pacman -S zsh`. |
|
|
| `tmux` | Required | Recommended ≥ 3.5 (pi with extended keys). |
|
|
| `git` | Required | Worktrees and commit protocol. |
|
|
| Babashka (`bb`) | Required | Launcher and all helpers are Babashka (cross-platform). |
|
|
| Node.js ≥ 22 | pi only | npm install of pi (or standalone script). |
|
|
| Terminal | **Headless works** | By default on Linux (no `osascript`/`wt.exe`) the launcher falls back to `none`: attaches the current shell to the first role's session and the rest stay detached (`tmux -S <socket> attach -t swarmforge-<role>`). The swarm runs fully without windows. |
|
|
| Automatic windows (optional) | To build | Write a `terminal-adapters/wezterm.sh` (or kitty) for Linux: 5-function contract (~40 lines). WezTerm is the most scriptable (`wezterm cli`); Ghostty on Linux has no remote control. |
|
|
| Shutdown | Plan for | The `close-swarm` script lives on the `main` branch; executable branches do not carry it — copy it into the project or use shutdown by "closing the first window". |
|
|
| Sleep prevention | Works | `systemd-inhibit` on Linux (systemd running). Disable with `SWARMFORGE_PREVENT_SLEEP=0`. |
|
|
|
|
### 3.6 Necessary code changes (minimal)
|
|
|
|
In `swarmforge/scripts/swarmforge.bb` (the working branch, e.g. `four-pack`):
|
|
|
|
1. **`parse-config`**: add the backend to the validated list, e.g. `#{"claude" "codex" "copilot" "grok" "pi"}`.
|
|
2. **`launch-command`**: add the new backend's arm (pi: section 3.2; opencode: section 3.3).
|
|
3. **`check-backend-dependencies!`**: no changes — already checks that the binary exists on `PATH`.
|
|
|
|
In the project config:
|
|
|
|
- `swarmforge.conf`: `window coder pi master` (or `opencode`), with `[task|batch]` and extra args per role.
|
|
|
|
Optional depending on goal:
|
|
|
|
- Shared constitution articles in `swarmforge/constitution/articles/` of the branch (the wrapper only *stages* them in `scripts/shared-articles/`; confirm agents read what the branch needs).
|
|
- Linux terminal adapter (section 3.5).
|
|
- `close-swarm` in the project.
|
|
|
|
---
|
|
|
|
## 4. Architecture diagram
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph Config["Configuration (per project/branch)"]
|
|
CONF["swarmforge.conf<br/>window role agent worktree [task|batch] [args]"]
|
|
ROLES["swarmforge/roles/<role>.prompt"]
|
|
CONST["swarmforge/constitution.prompt<br/>+ constitution/articles/"]
|
|
end
|
|
|
|
subgraph Launcher["Launcher — swarmforge.bb (Babashka)"]
|
|
PARSE["Validate config and prompts"]
|
|
WT["Git worktrees<br/>.worktrees/<role> (branch per role)"]
|
|
TMUX["tmux sessions<br/>swarmforge-<role> · project-owned socket"]
|
|
LAUNCH["send-keys: export SWARMFORGE_ROLE<br/>+ PATH helpers + cd worktree<br/>+ <agent> '$(cat prompt)'"]
|
|
end
|
|
|
|
subgraph Swarm["Swarm (1 agent per role)"]
|
|
A1["Agent TUI<br/>(tmux pane)"]
|
|
A2["Agent TUI<br/>(tmux pane)"]
|
|
A3["Agent TUI<br/>(tmux pane)"]
|
|
end
|
|
|
|
subgraph State["Durable state — filesystem (.swarmforge/handoffs)"]
|
|
OUT["outbox/ · sent/ · failed/"]
|
|
IN["inbox/ new · in_process · completed"]
|
|
end
|
|
|
|
subgraph Control["Control — daemon handoffd.bb"]
|
|
DAEMON["Poll outbox → deliver to inbox<br/>→ wake-up via tmux send-keys"]
|
|
end
|
|
|
|
subgraph Viewer["Viewer (optional)"]
|
|
ADAPT["terminal-adapters/*.sh"]
|
|
WATCH["swarm-window-watchdog"]
|
|
end
|
|
|
|
CONF --> PARSE
|
|
ROLES --> PARSE
|
|
CONST --> PARSE
|
|
PARSE --> WT --> LAUNCH
|
|
PARSE --> TMUX --> LAUNCH
|
|
LAUNCH --> A1 & A2 & A3
|
|
A1 & A2 & A3 -->|"helpers on PATH:<br/>swarm_handoff.sh"| OUT
|
|
OUT --> DAEMON
|
|
DAEMON -->|"deliver .handoff"| IN
|
|
DAEMON -->|"wake-up: text + Enter"| A1 & A2 & A3
|
|
A1 & A2 & A3 -->|"ready_for_next.sh<br/>done_with_current.sh"| IN
|
|
A1 & A2 & A3 -->|"work (git)"| WT
|
|
A1 & A2 & A3 -->|"observe: tmux attach"| TMUX
|
|
TMUX --> ADAPT --> WATCH
|
|
```
|
|
|
|
## 5. Protocol diagram (one handoff cycle)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
autonumber
|
|
participant S as Sender agent (e.g. coder)
|
|
participant V as swarm_handoff.sh (gate)
|
|
participant O as outbox/ (sender)
|
|
participant D as Daemon handoffd.bb
|
|
participant I as inbox/ (receiver)
|
|
participant R as Receiver agent (e.g. cleaner)
|
|
|
|
Note over S: git commit (message with role byline)
|
|
S->>S: Write draft (type/to/priority/task/commit)
|
|
S->>V: swarm_handoff.sh `<draft>`
|
|
V->>V: Validate: known roles, priority 00-99,<br/>canonical commit (10 hex, --disambiguate),<br/>reserved fields, body forbidden
|
|
V->>O: Install generated .handoff (id, from, role,<br/>task, created_at, merge_and_process payload)
|
|
V-->>S: HANDOFF QUEUED
|
|
O->>D: Poll (1 s)
|
|
D->>I: Copy to each recipient inbox/new/<br/>+ recipient, enqueued_at headers
|
|
D->>R: tmux send-keys -l 'You have new handoff mail...'<br/>+ C-m (Enter) + C-j (robustness)
|
|
Note over R: If busy → ignore (queue or next<br/>done_with_current will pick it up)
|
|
R->>I: ready_for_next.sh → move to in_process/<br/>+ dequeued_at header
|
|
I-->>R: TASK: `<path>` / BATCH: `<items>` + PAYLOAD
|
|
R->>R: merge_and_process `<sender>` `<commit>`<br/>+ process the task in its worktree
|
|
R->>I: done_with_current.sh → completed/<br/>+ completed_at header
|
|
I-->>R: Next task or NO_TASK
|
|
D->>O: Move original to sent/ (or failed/)
|
|
```
|
|
|
|
### Inbox task lifecycle
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> new: daemon delivers .handoff
|
|
new --> in_process: ready_for_next.sh (dequeued_at)
|
|
in_process --> completed: done_with_current.sh (completed_at)
|
|
in_process --> in_process: next queued task
|
|
new --> [*]: NO_TASK (empty queue)
|
|
failed --> [*]: delivery error (sender outbox)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Executive summary
|
|
|
|
1. **The architecture is model-agnostic**: the only integration point for a new backend is the launch arm + the validated backend list in `swarmforge.bb`; the handoff protocol, worktrees, daemon and wake-ups do not know about the agent.
|
|
2. **pi fits directly**: interactive initial message via CLI, tmux supported, message queue aligned with wake-up semantics, native DeepSeek/Qwen and GLM with a small extension.
|
|
3. **opencode fits with an adaptation**: the TUI does not accept an initial prompt via CLI → inject via `tmux send-keys` after startup (mechanism already in the system). Native DeepSeek/GLM/Qwen.
|
|
4. **Linux is a first-class citizen by design**: the swarm lives in tmux, not in windows; headless runs fully. Automatic windows are only an optional terminal adapter.
|
|
5. **Minimum requirements**: zsh + tmux (≥3.5 recommended) + git + Babashka + (Node.js for pi) + ~15 lines of changes in `swarmforge.bb` + provider config.
|