Repository navigation
DevSwarm
anti-hall integrates with DevSwarm, a desktop multi-workspace IDE in which each workspace is a git worktree on its own branch with its own AI agent. The app ships a CLI, hivecontrol, that is on every workspace's PATH. anti-hall adds a message mesh, a roster, a background supervisor and an ingest daemon on top of it.
The integration is dormant unless the session runs inside DevSwarm (DEVSWARM_REPO_ID is set) or a DevSwarm companion is installed. Kill switch: DISABLE_ANTIHALL_DEVSWARM=1.
Sources of truth: docs/KB-devswarm-hivecontrol.md, docs/KB-devswarm-app-db.md, the devswarm skill, and the "DevSwarm" section of docs/GUIDE.md.
- Primary: the workspace on the main checkout. DevSwarm creates it on import; it cannot be archived or deleted. It plans, fans work out to children and verifies their results.
- Child: a workspace the Primary spawned, with its own worktree and branch. A child uses subagents for its own parallel work and never spawns workspaces of its own (two tiers only).
- Parentage comes from the workspace's source branch (
DEVSWARM_SOURCE_BRANCH); empty means root/Primary.
The mesh is one message store per project, shared by every worktree of the repository. It is the only channel anti-hall allows between workspaces, and it is all-to-all, not only parent to child.
-
Project key.
repoKeyis the sanitized repository name plus the first six hex characters of a sha256 of the git common dir (companion/lib/devswarm-repokey.js). There is one canonical resolver; no key means the mesh stays dormant. -
Storage.
~/.anti-hall/devswarm/store/<repoKey>/devswarm.db(SQLite throughnode:sqlite, with an append-only NDJSON journal as the fallback backend). Tables:messages(append-only, deduplicated by content hash),registry,cursors,gates. Hooks never open the database; they read a projection,summaries/<repoKey>.json. -
Other state under
~/.anti-hall/devswarm/: workspace descriptors, heartbeats, liveness verdicts, cursors, locks, a delivery write-ahead log (wal/), read receipts and archives. -
Messages have an urgency (
low,normal,high,urgent); broadcasts go to a shared broadcast partition. - Read is split from acknowledge. Reading an inbox is read-only and returns the command that acknowledges it; only that command moves the cursor.
-
Guards keep everyone on the mesh. The app's native message verbs and
SendMessageto a workspace peer are blocked, and only a workspace's main thread may own its mailbox; a subagent that tries is blocked. -
CLI.
plugins/anti-hall/scripts/devswarm.js(a dispatcher; the verbs live inscripts/devswarm-lib/). Every verb prints one JSON line and exits 0 (ok) or 2 (not ok). Main verbs:send,mesh read|history,inbox …,roster,heartbeat,register,gate,done,archive,diagnose,reconcile. -
Invariants.
tests/mesh-invariants.harness.test.jssweeps seeded operations through the real CLI with an isolatedHOME. It checks, among others, that the cached summary equals a fresh recompute, that cursors never move backwards, that delivered plus unread equals total for every reader (no loss, including after an injected crash), and thatdoctor --checkwrites nothing.
devswarm.js roster is a view of the project's registry table: one row per workspace with its mesh address, what it is working on (its latest heartbeat), its unread counts and the highest unread urgency, plus the latest broadcasts. Plain mode shows live workspaces and a +N archived line; --all adds archived rows and --json gives everything. Titles and sidebar order come from the app database, which anti-hall reads read-only (never message bodies or credentials; tests/companion/devswarm-app-db-hygiene.test.js).
The plain roster writes nothing. The registry is written by register, ensure (run on each inbox pull), register-primary, spawn, the self-heal passes and archive (which tombstones a row; it never deletes it).
companion/devswarm-supervisor.js is a one-shot sweep, run on an interval (default 90 s) by a launchd agent on macOS or a systemd user timer (or cron) on Linux. It is opt-in: companion/install-devswarm-supervisor.js installs it, and the update flow re-runs the installer when it finds an active DevSwarm session. A lock stops two sweeps overlapping.
One sweep: liveness, reconcile, deferred work, app-database sync, auto-archive, retention and housekeeping.
Recovery is layered, and the automatic path never kills anything:
- A child reports on itself.
- A stale child (idle transcript and idle git and unread mail past a threshold) gets a nudge.
- After the allowed nudges, the verdict is
escalatedand the Primary is told.
The only kill path is the on-demand companion/devswarm-recover.js CLI.
Auto-archive archives a child only when every condition holds: it is done (all finish gates set), its merge is proven against the remote default branch, its worktree is clean, it has no unread direct mail, it is not the Primary, it has not been focused in the app recently, and it has been idle long enough.
No automated deletion. tests/hygiene/devswarm-lifecycle-no-auto-delete.test.js pins that the prune and delete paths are reachable only from the interactive CLI verb, and that no hook, supervisor, ingest, installer, monitor or statusline file references them. Deleting archived workspaces takes a dry run, then explicit --confirm-ids with a short-lived plan nonce, and it refuses any non-interactive caller.
Wake paths for the Primary. A session cron (devswarm.wakeCron, default 7,37 * * * *) runs an inbox tick, and the devswarm-wake-watch monitor watches for new mail. Each covers a gap in the other: a session cron is gone after a harness restart, and the monitor exits on purpose when no child is live. A Primary with a live child and a missing wake path gets a NO MAILBOX WAKE PATH line, and one capped Stop block when both are missing.
After an app crash DevSwarm can restore workspaces paused. The Node supervisor only records evidence (devswarm.pausedProbeMax probes per sweep); the engine reports such a workspace as paused? with its evidence and never as paused, until a confirmed crash fixture exists (devswarm_rt.paused_signal, default none).
companion/devswarm-ingest.js drains the app's native message queue (hivecontrol workspace monitor) into the mesh store, so a message sent with the app's own tools still reaches the mesh. It does not ingest transcripts.
- One consumer per project, enforced with an exclusive lock.
- Raw output is fsynced to a write-ahead log before parsing, and open batches are replayed on start; if the log cannot be written, it fails closed.
- Writes are deduplicated by content hash, so replays are safe.
- Installed per project by
companion/install-devswarm-ingest.js(launchd with KeepAlive on macOS, systemd with restart or cron on Linux).
ANTIHALL_INGEST_DRY_RUN=1 makes the repair and fold functions in scripts/devswarm-lib/repair.js report without writing; tests and experiments that use an isolated HOME must set it, because a launchd agent and the real store are not isolated by HOME.
| Part | Node (plugins/anti-hall/) |
Engine (engine-proto, ah-engine/src/) |
|---|---|---|
| Hooks | hooks/devswarm-*.js |
scripted checks (engine/logic/) |
| Mesh reads | scripts/devswarm.js |
mesh.rs: read-only, byte-for-byte parity with Node |
| Mesh writes | scripts/devswarm-lib/ |
meshw/: behind mesh.engine_writes (off, shadow, on); defers to Node (exit 75) when it cannot reproduce Node |
| Supervisor | companion/devswarm-supervisor.js |
dssup/: devswarm_sup.mode (witness by default, Node does the work) with a per-duty rollback list; never produces a stale verdict itself, and recover is never scheduled |
| Ingest | companion/devswarm-ingest.js |
dssup/ingest/: devswarm_ingest.mode (witness by default); waits on a live Node lock holder so nothing is drained twice |
| Runtime state |
devswarm_rt/, with Node as a witness |
The engine's DevSwarm defaults are in plugins/anti-hall/engine/defaults.pristine/devswarm_*.toml and mesh*.toml. Recovery and migration stay Node tools so they work with the engine down.
All in ~/.anti-hall/settings.json, changed through /anti-hall:settings. Main groups: devswarm.supervisorMode; devswarm.autoArchive.* (mode, idle minutes, per-sweep cap); devswarm.nudge*, devswarm.idleSec, devswarm.maxRecoveries; devswarm.retention.*; devswarm.wakeCron, devswarm.wakeWatch; and the hook switches devswarm.parentGate, devswarm.childGate, devswarm.childRole, devswarm.commsGuard, devswarm.inboxReadGuard. The devswarm skill has the full list with defaults.
anti-hall by Mohammed Talas (@talas9) · Repository · Docs site · Discussions · Contributor wiki: the repository is the source of truth; fix this wiki when they disagree.
🗺️ How it works
- 🏗️ Architecture
- ⚙️ Engine internals
- 🌐 DevSwarm
- 🤖 Repo automation
🛠️ How we work
📄 Templates
🔗 Elsewhere