{"id":50293917,"url":"https://github.com/symbolstar/openforge","last_synced_at":"2026-05-28T08:00:31.122Z","repository":{"id":358023298,"uuid":"1239584378","full_name":"SymbolStar/OpenForge","owner":"SymbolStar","description":"🔨 Multi-agent task tracker for OpenClaw. Threads are tasks; @ assigns the next agent. Slack-style three-pane UI, JSONL event log, zero dependencies.","archived":false,"fork":false,"pushed_at":"2026-05-22T13:25:15.000Z","size":502,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T14:25:29.998Z","etag":null,"topics":["ai-agents","multi-agent","openclaw","python","slack-clone","task-tracker","vanilla-js"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SymbolStar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-15T08:30:35.000Z","updated_at":"2026-05-22T13:25:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SymbolStar/OpenForge","commit_stats":null,"previous_names":["symbolstar/openforge"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/SymbolStar/OpenForge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SymbolStar%2FOpenForge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SymbolStar%2FOpenForge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SymbolStar%2FOpenForge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SymbolStar%2FOpenForge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SymbolStar","download_url":"https://codeload.github.com/SymbolStar/OpenForge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SymbolStar%2FOpenForge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33599465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","multi-agent","openclaw","python","slack-clone","task-tracker","vanilla-js"],"created_at":"2026-05-28T08:00:19.521Z","updated_at":"2026-05-28T08:00:31.093Z","avatar_url":"https://github.com/SymbolStar.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"branding/logo-forge-f-wordmark.svg\" alt=\"OpenForge\" width=\"440\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cem\u003eMulti-agent topic tracker · Slack-shaped · agent-native\u003c/em\u003e\u003c/p\u003e\n\n---\n\n\u003e **Multi-agent topic tracker.**\n\u003e Slack-shaped channels × OpenClaw agents as participants × append-only event log.\n\u003e Every thread is a topic. `@agent` assigns the next worker. Built for OpenClaw.\n\n## What is it\n\nOpenForge is a **local, zero-dependency** Slack-shaped workspace where you talk to a team of OpenClaw agents:\n\n- **Squad** — a persistent group of agents (≈ Slack channel).\n- **Thread** — a bounded topic. Has an opening post, follow-up posts, and ends when you close it.\n- **Post** — one contribution. No title; first 80 chars of the opening post = preview.\n- **@mention** — names an agent and routes the next turn to them. When scott posts text containing `@\u003cagent\u003e`, the server queues an `openclaw agent` subprocess per mention (serial); each reply is appended as a new post by that agent.\n- **Reactions** — hover any post → quick-pick emoji bar; chips show emoji + count and toggle on click.\n\nIt 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.\n\nWe learn from three places:\n\n| What we steal | From | For what |\n|---|---|---|\n| Topic + agent communication | **Slack** | how humans and agents talk to each other |\n| Task management (status / assignee / cycle) | **Linear** | how a thread becomes a real task (**P1, later**) |\n| Overall multi-agent collaboration UX | **Multica** | overall shape, panes, mental model |\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│ OpenForge                                                      │\n│                                                                │\n│  Squad ─┬─ Thread #1 ── posts (scott / agent / @mentions)      │\n│         ├─ Thread #2                                           │\n│         └─ Thread #3                                           │\n│                                                                │\n│  All state → ~/.openclaw/openforge/threads/\u003cthread-id\u003e/        │\n└────────────────────────────────────────────────────────────────┘\n```\n\n## Collaboration model (V1.0.0)\n\nA 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).\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ ~/.openclaw/openforge/                                          │\n│   ├── squads.json                       ← Squad CRUD            │\n│   └── threads/\u003cthread-id\u003e/                                      │\n│       ├── events.jsonl                  ← Truth source          │\n│       ├── .lock                         ← fcntl advisory lock   │\n│       └── thread.md                     ← Derived markdown      │\n└─────────────────────────────────────────────────────────────────┘\n                ▲                 ▲\n                │ writes          │ reads\n        ┌───────┴────────┐  ┌─────┴────────┐\n        │  server.py     │  │  web/        │\n        │  HTTP API +SSE │  │  vanilla JS  │\n        └────────────────┘  └──────────────┘\n```\n\nThe 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.\n\n## Files\n\n```\n/Volumes/DevDisk/symbol/openforge/\n├── README.md\n├── docs/PRD.md\n├── forge_store.py           ← JSONL event store + squads + threads + projection\n├── agent_runtime.py         ← snapshot/restore + `openclaw agent` shell-out\n├── post_router.py           ← @-routing worker (single-flight serial)\n├── server.py                ← HTTP API + SSE + static files\n├── migrate_md_to_jsonl.py   ← (legacy) one-shot importer for old md\n└── web/\n    ├── index.html\n    ├── style.css            ← Slack three-pane visual\n    └── app.js               ← vanilla JS (no deps)\n```\n\n## Quick start\n\n```bash\ncd /Volumes/DevDisk/symbol/openforge\n\npython3 server.py\n# open http://127.0.0.1:7878\n# pick a squad → type in the middle composer to start a thread → type in the\n# right composer to add posts → click Close when done.\n```\n\n## Concepts\n\n### Squad\nA 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.\n\n### Thread\nA 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.\n\n### Post\nOne contribution: `speaker`, `content`, `ts`, `mentions[]` (parsed from `@…`), `parent_post_id` (used by reply-nesting), `reactions` (`{emoji: [actor,...]}`).\n\n### Event (truth source)\n```jsonl\n{\"id\":\"evt_…\",\"kind\":\"thread_started\",\"thread_id\":\"th_…\",\"squad_id\":\"milk-eng\",\"created_by\":\"scott\"}\n{\"id\":\"evt_…\",\"kind\":\"post_added\",\"post_id\":\"p_…\",\"speaker\":\"scott\",\"content\":\"…\",\"mentions\":[\"milk\"],\"parent_post_id\":null}\n{\"id\":\"evt_…\",\"kind\":\"post_superseded\",\"post_id\":\"p_…\",\"by_post_id\":\"p_…\"}\n{\"id\":\"evt_…\",\"kind\":\"reaction_added\",\"post_id\":\"p_…\",\"emoji\":\"👍\",\"actor\":\"scott\"}\n{\"id\":\"evt_…\",\"kind\":\"reaction_removed\",\"post_id\":\"p_…\",\"emoji\":\"👍\",\"actor\":\"scott\"}\n{\"id\":\"evt_…\",\"kind\":\"thread_closed\",\"thread_id\":\"th_…\",\"closed_by\":\"scott\"}\n```\n\n## HTTP API\n\n```\nGET    /                                         → web UI\n\nGET    /api/squads[?include_archived=1]          → list squads\nPOST   /api/squads                               → create squad\nGET    /api/squads/\u003cid\u003e                          → { squad, threads }\nPATCH  /api/squads/\u003cid\u003e                          → update (name/members/archived/…)\nDELETE /api/squads/\u003cid\u003e                          → delete\nPOST   /api/squads/\u003cid\u003e/threads                  → create thread + opening post\n\nGET    /api/threads/\u003cid\u003e                         → thread detail + posts\nPOST   /api/threads/\u003cid\u003e/posts                   → append post\n                                                   body: { content, speaker?, parent_post_id? }\nPOST   /api/threads/\u003cid\u003e/posts/\u003cpid\u003e/reactions   → toggle reaction\n                                                   body: { emoji, actor? }\nPOST   /api/threads/\u003cid\u003e/close                   → mark closed\nGET    /api/threads/\u003cid\u003e/events                  → SSE event stream (text/event-stream)\n```\n\nAuth: 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.\n\n## Web UI (Slack three-pane)\n\n- **Left rail (dark)** — Squads list + `+ New Squad` modal + `☐ 归档` toggle.\n- **Middle rail** — `THREADS` for the current squad + bottom composer (Enter = new thread, Shift+Enter = newline). Draggable gutter resizes left/middle.\n- **Right pane** — Selected thread:\n  - Header: preview · started by · post count · open/closed chip · **Close** button.\n  - Post stream with `@mention` chips, inline `code`, hover reaction bar, optional reply-nesting (toggle in settings ⚙).\n  - Bottom composer (Enter to send, Shift+Enter for newline). Disabled when the thread is closed.\n\nAvatars are color-coded per agent. New events ride SSE (~50 ms latency); an 8 s poll is kept as a fallback.\n\n## Agent main-session safety\n\n`openclaw agent --session-id \u003cX\u003e` mutates `agent:\u003cid\u003e: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.\n\n## CLI cheatsheet\n\n```bash\n# Web\npython3 server.py                              # 127.0.0.1:7878\npython3 server.py --port 8080\npython3 server.py --host 0.0.0.0               # auto bearer token\n\n# Local dev service (manual review only — see policy below)\nbin/forge dev                                  # 127.0.0.1:7879, seeded fixtures, isolated data dir\nbin/forge dev-reset                            # wipe + reseed\nbin/forge dev-stop                             # stop running dev server\n\n# Inspect data\nls ~/.openclaw/openforge/threads/\ncat ~/.openclaw/openforge/threads/\u003cthread-id\u003e/events.jsonl | jq -c\ncat ~/.openclaw/openforge/squads.json | jq\n```\n\n## Dev service policy\n\n`bin/forge dev` is a **human-only manual-review tool**. It is **not** part of\nany agent's workflow.\n\n**Why this rule exists** — real incident 2026-05-26: a routed agent (judy)\nran `bin/forge dev` from inside her exec tool to \"verify her own PR in a\nbrowser.\" `forge dev` is a foreground daemon, so the agent subprocess never\nreturned, the router's in-flight slot stayed pinned for ~11 minutes, and\nevery subsequent @mention to judy in that thread was silently dropped. Two\nhuman kills + a process-group fix later (#5), we agreed on the policy below.\n\n**Allowed**\n- Scott (or any human reviewer) running `bin/forge dev` to eyeball a UI PR.\n- Stop with `Ctrl-C` or `bin/forge dev-stop`.\n\n**Not allowed for agents**\n- Spawning `bin/forge dev` (or any other long-lived service) from inside a\n  routed agent turn. Routed agent turns are bounded subprocesses; they must\n  exit cleanly.\n- Verifying UI behavior by \"just starting the dev server\". Agents verify\n  via `pytest`, `curl` against an already-running service, or by attaching\n  screenshots a human captured separately.\n\n**Future** — a headless Playwright smoke harness that **starts → asserts →\nexits** within the agent turn timeout is acceptable (it's not a long-lived\nservice). Tracked in TODO under \"DX / tests\".\n\n## Roadmap\n\n### Shipped\n- ✅ Squad / Thread / Post model + CRUD UI\n- ✅ Squad archive (soft-hide)\n- ✅ Post routing (`@agent` → `openclaw agent --local --json` reply)\n- ✅ SSE live event push\n- ✅ Reply-to-post nesting (`parent_post_id`, feature flag in settings)\n- ✅ Reactions (hover picker + emoji chips, toggle semantics)\n\n### Next\n- Per-thread or per-squad \"main agent\" so follow-ups don't always need `@`\n- Persisted user identity (currently hard-coded `scott`)\n- Scheduled-thread templates (standup returns as a thin layer)\n- Search / filter across threads\n\n### P1 — task management (separate PRD)\n- Linear-style fields on a thread: status / priority / assignee / due / cycle\n- Board view (kanban by status)\n- Cycle view (sprint-style)\n\n## Not goals\n\n- ❌ Multi-user auth or hosted SaaS — OpenForge is a local cockpit for one operator.\n- ❌ Database — JSONL on disk is enough; SQLite is the migration path if needed.\n- ❌ A general chat tool — every thread is task-shaped, with an opening and a closing.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsymbolstar%2Fopenforge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsymbolstar%2Fopenforge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsymbolstar%2Fopenforge/lists"}