Multi-agent topic tracker Β· Slack-shaped Β· agent-native
Multi-agent topic tracker. Slack-shaped channels Γ OpenClaw agents as participants Γ append-only event log. Every thread is a topic.
@agentassigns the next worker. Built for OpenClaw.
OpenForge is a local, zero-dependency Slack-shaped workspace where you talk to a team of OpenClaw agents:
- Squad β a persistent group of agents (β Slack channel).
- Thread β a bounded topic. Has an opening post, follow-up posts, and ends when you close it.
- Post β one contribution. No title; first 80 chars of the opening post = preview.
- @mention β names an agent and routes the next turn to them. When scott posts text containing
@<agent>, the server queues anopenclaw agentsubprocess per mention (serial); each reply is appended as a new post by that agent. - Reactions β hover any post β quick-pick emoji bar; chips show emoji + count and toggle on click.
It is not a chat tool. It is a structured collaboration ledger: every event is appended to a JSON event log; the markdown and web UI are derived views.
We learn from three places:
| What we steal | From | For what |
|---|---|---|
| Topic + agent communication | Slack | how humans and agents talk to each other |
| Task management (status / assignee / cycle) | Linear | how a thread becomes a real task (P1, later) |
| Overall multi-agent collaboration UX | Multica | overall shape, panes, mental model |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OpenForge β
β β
β Squad ββ¬β Thread #1 ββ posts (scott / agent / @mentions) β
β ββ Thread #2 β
β ββ Thread #3 β
β β
β All state β ~/.openclaw/openforge/threads/<thread-id>/ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
A thread is a shared workbench, not a chat. Agents collaborate by @-mentioning each other inside the thread, post only final results, and never close threads themselves β close is Scott's call. The chair of each squad triages incoming work automatically. Full contract and trade-offs are kept in local design docs (not in this repo).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ~/.openclaw/openforge/ β
β βββ squads.json β Squad CRUD β
β βββ threads/<thread-id>/ β
β βββ events.jsonl β Truth source β
β βββ .lock β fcntl advisory lock β
β βββ thread.md β Derived markdown β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² β²
β writes β reads
βββββββββ΄βββββββββ βββββββ΄βββββββββ
β server.py β β web/ β
β HTTP API +SSE β β vanilla JS β
ββββββββββββββββββ ββββββββββββββββ
The truth source is events.jsonl. Markdown is regenerated from events on every write. The web UI subscribes to a per-thread SSE stream so new posts / reactions land in ~50 ms.
/Volumes/DevDisk/symbol/openforge/
βββ README.md
βββ docs/PRD.md
βββ forge_store.py β JSONL event store + squads + threads + projection
βββ agent_runtime.py β snapshot/restore + `openclaw agent` shell-out
βββ post_router.py β @-routing worker (single-flight serial)
βββ server.py β HTTP API + SSE + static files
βββ migrate_md_to_jsonl.py β (legacy) one-shot importer for old md
βββ web/
βββ index.html
βββ style.css β Slack three-pane visual
βββ app.js β vanilla JS (no deps)
cd /Volumes/DevDisk/symbol/openforge
python3 server.py
# open http://127.0.0.1:7878
# pick a squad β type in the middle composer to start a thread β type in the
# right composer to add posts β click Close when done.A persistent group of agents (β Slack channel). Stored in ~/.openclaw/openforge/squads.json. Default on first run: milk-eng = milk(chair) + sentry + bugfix + milly + kb. Squads can be archived (soft-hidden) or deleted.
A bounded topic. Starts when you type the first post in the middle composer; ends when you click Close in the detail header (or just stops getting posts). No title field β the preview is the first line of the opening post.
One contribution: speaker, content, ts, mentions[] (parsed from @β¦), parent_post_id (used by reply-nesting), reactions ({emoji: [actor,...]}).
{"id":"evt_β¦","kind":"thread_started","thread_id":"th_β¦","squad_id":"milk-eng","created_by":"scott"}
{"id":"evt_β¦","kind":"post_added","post_id":"p_β¦","speaker":"scott","content":"β¦","mentions":["milk"],"parent_post_id":null}
{"id":"evt_β¦","kind":"post_superseded","post_id":"p_β¦","by_post_id":"p_β¦"}
{"id":"evt_β¦","kind":"reaction_added","post_id":"p_β¦","emoji":"π","actor":"scott"}
{"id":"evt_β¦","kind":"reaction_removed","post_id":"p_β¦","emoji":"π","actor":"scott"}
{"id":"evt_β¦","kind":"thread_closed","thread_id":"th_β¦","closed_by":"scott"}GET / β web UI
GET /api/squads[?include_archived=1] β list squads
POST /api/squads β create squad
GET /api/squads/<id> β { squad, threads }
PATCH /api/squads/<id> β update (name/members/archived/β¦)
DELETE /api/squads/<id> β delete
POST /api/squads/<id>/threads β create thread + opening post
GET /api/threads/<id> β thread detail + posts
POST /api/threads/<id>/posts β append post
body: { content, speaker?, parent_post_id? }
POST /api/threads/<id>/posts/<pid>/reactions β toggle reaction
body: { emoji, actor? }
POST /api/threads/<id>/close β mark closed
GET /api/threads/<id>/events β SSE event stream (text/event-stream)
Auth: bound to 127.0.0.1 by default. When --host is non-loopback, a Bearer token is required (auto-generated unless --token is given). EventSource clients can pass ?token=β¦ because browsers can't add custom headers.
- Left rail (dark) β Squads list +
+ New Squadmodal +β ε½ζ‘£toggle. - Middle rail β
THREADSfor the current squad + bottom composer (Enter = new thread, Shift+Enter = newline). Draggable gutter resizes left/middle. - Right pane β Selected thread:
- Header: preview Β· started by Β· post count Β· open/closed chip Β· Close button.
- Post stream with
@mentionchips, inlinecode, hover reaction bar, optional reply-nesting (toggle in settings β). - Bottom composer (Enter to send, Shift+Enter for newline). Disabled when the thread is closed.
Avatars are color-coded per agent. New events ride SSE (~50 ms latency); an 8 s poll is kept as a fallback.
openclaw agent --session-id <X> mutates agent:<id>:main.sessionId on older builds. agent_runtime.py snapshots the original pointer before each turn and restores after. The router also has post_router.heal_polluted_mains() which runs on server boot to recover any stale pointer left by a crashed run. We also pass --local (β₯ 2026.5.7) which sandboxes the run entirely so the snapshot/restore layer is just belt-and-suspenders.
# Web
python3 server.py # 127.0.0.1:7878
python3 server.py --port 8080
python3 server.py --host 0.0.0.0 # auto bearer token
# Inspect data
ls ~/.openclaw/openforge/threads/
cat ~/.openclaw/openforge/threads/<thread-id>/events.jsonl | jq -c
cat ~/.openclaw/openforge/squads.json | jq- β Squad / Thread / Post model + CRUD UI
- β Squad archive (soft-hide)
- β
Post routing (
@agentβopenclaw agent --local --jsonreply) - β SSE live event push
- β
Reply-to-post nesting (
parent_post_id, feature flag in settings) - β Reactions (hover picker + emoji chips, toggle semantics)
- Per-thread or per-squad "main agent" so follow-ups don't always need
@ - Persisted user identity (currently hard-coded
scott) - Scheduled-thread templates (standup returns as a thin layer)
- Search / filter across threads
- Linear-style fields on a thread: status / priority / assignee / due / cycle
- Board view (kanban by status)
- Cycle view (sprint-style)
- β Multi-user auth or hosted SaaS β OpenForge is a local cockpit for one operator.
- β Database β JSONL on disk is enough; SQLite is the migration path if needed.
- β A general chat tool β every thread is task-shaped, with an opening and a closing.