14c2e4e831 MUL-6243 feat(issue-status): board fetches by category; archive retires without migration (#7084)
* feat(issue-status): board fetches by category; archive retires without migration (MUL-6243)

Product decisions from the issue thread:

1. Board fetches by CATEGORY, not per status. PAGINATED_STATUSES becomes
   PAGINATED_CATEGORIES and the cache buckets by category, so the board's
   fan-out stays fixed at 7 parallel requests however many custom statuses a
   workspace defines — instead of one more request per status added. Adds
   status_category / status_categories filters to the issue list endpoints,
   backed by the existing issue_effective_status() SQL function.

3. Archiving retires a status from FUTURE use only. It no longer refuses while
   issues still carry it: those issues keep their status and keep resolving to
   their category's behavior (Effective ignores archived_at), while Resolve
   still rejects the status so nothing new can be assigned to it. The exclusive
   catalog lock stays — it is what makes "no NEW assignment after archive"
   exact rather than approximate.

Also lands the frontend catalog foundation: packages/core/issue-statuses with
useIssueStatuses(wsId) (React Query, keyed by wsId per the state rules) and a
pure buildIssueStatusCatalog for non-React callers, plus IssueStatusCategory as
a distinct type from the status key and status_category on issue payloads.

NOT included, and deliberately: widening IssueStatus to string. That change
forces every Record<IssueStatus, …> presentation lookup in packages/views to
resolve a key to its category first, and it is the first task of the follow-up
PR. Everything here is the groundwork that makes it mechanical.

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): restore the status index, make category authoritative, keep archived resolvable

Review round 1 on #7084 (MUL-6243):

1. Category filtering wrapped issue.status in issue_effective_status(), which
   makes the (workspace_id, status) index unusable — and the board switched to
   it unconditionally, so every workspace paid for it immediately. Categories
   are now expanded to concrete status keys (issuestatus.ExpandCategories) and
   filtered with an indexed status = ANY(...). Measured on a 100k-row table
   with the production index: seq scan cost 27013 -> index-only scan cost 6.07.
   A workspace with no custom statuses expands each category to exactly its own
   key, reproducing the pre-feature query. Archived keys are included, since
   issues left on them still belong in their category's column.

2. status_category was only filled for built-ins, so a custom-status issue
   arriving over the websocket was silently dropped by addIssueToBuckets and a
   cross-category update was skipped by the cache coordinator, drifting
   off-window totals permanently. List, grouped, get, create, update and batch
   responses now carry an authoritative category via a per-request Resolver
   (archived included), and the client invalidates instead of no-oping when a
   category is unresolvable.

3. The catalog was fetched without archived entries, so after archiving a
   status the issues still on it lost their name and colour and had their
   category guessed as todo — contradicting the decision that archiving leaves
   existing issues untouched. Resolution now includes archived entries;
   activeStatuses / inCategory expose only assignable ones to pickers.

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): category on every payload exit, one resolver per request, no same-category duplication

Review round 2 on #7084 (MUL-6243):

1. Category fill was incomplete and N+1. The create WEBSOCKET payload was
   unfilled — only the creator's HTTP response was, which is too late for the
   other tabs the event exists to serve. GetIssue, the open_only list, search,
   the duplicate-conflict response, the table rows, the GitHub PR update and
   the onboarding shim were also unfilled. Grouped and batch called the
   single-response helper per row, so each custom-status row hit the catalog
   separately.

   Every exit now goes through newStatusCategoryFiller, a request-scoped
   closure over ONE Resolver: built-in statuses still cost no query, and a page
   of custom-status rows costs one catalog read rather than one per row.

2. A status KEY change within one category duplicated the card and inflated the
   total. patchIssueInBuckets compared patch.status (a key) against loc.status
   (a bucket CATEGORY), so in_review -> a custom human_review took the
   cross-bucket path, then deleted from and re-inserted into the same bucket
   from a pre-delete snapshot. It now resolves the destination category first
   and compares category to category; same-category changes replace in place
   and leave the total alone.

   The destination category is resolved from the patch alone — the merged issue
   inherits the previous status_category, which would have kept a genuinely
   moved card in its old column. patchNeedsInvalidation lets the loaded branch
   of the cache coordinator invalidate instead of silently no-oping on a status
   it cannot resolve.

Also drops the unrelated share_link.sql.go churn from an sqlc version
difference, per the review nit.

The four cache cases were verified to fail against the previous logic (card
duplicated, total +1) before the fix was restored.

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): normalize status_category on every cache write; one filler per create

Review round 3 on #7084 (MUL-6243):

Must-fix — a cached entity kept the STALE status_category after a status
change. The server now sends a category on every issue, so a cached entity is
{status: "todo", status_category: "todo"}; an optimistic patch carries only
{status: "done"}, and a bare {...issue, ...patch} moved the card to the done
bucket while the entity still claimed todo. A single update self-heals from the
server response, but the batch API returns only {updated: n} and does not
refetch bucketed lists, so the inconsistency was permanent — and the next
off-window count then decremented the wrong bucket, recreating total drift.

normalizeStatusPatch now rewrites status_category to match status before any
cache write, applied at applyIssueChange's entry (covering the flat, table and
detail spreads), in patchIssueInBuckets, and in the children mutation. An
unresolvable custom key drops the inherited category rather than keeping it;
patchNeedsInvalidation still routes it to a refetch.

Test fixtures now carry status_category for built-ins, matching what the server
actually sends — without that the regression was invisible.

Non-blocking items also addressed:
- CreateIssue builds one filler and shares it between the broadcast payload and
  the HTTP response, so a custom-status create reads the catalog once.
- Background events via IssueToMap now resolve an authoritative category
  (IssueToMapWithCategory) for the task, issue and autopilot publishers. The
  channel router keeps plain IssueToMap and says why: it always creates with the
  built-in "todo", where key IS category.

The "one resolver serves a whole page" assertion was REMOVED rather than kept:
pg_stat counters lag, so a delta around one request passes even with a per-row
filler. Verified that directly before dropping it. The comment now states what
is and is not pinned instead of implying coverage that does not exist.

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): address review nits

- Normalize the single-update children optimistic patch too, matching the batch
  path. It self-healed on onSettled, but it broke the entity/bucket invariant
  for the duration of the request.
- Rename TestListEndpointsCarryStatusCategory: the old name claimed it covered
  "without per row queries", which the test explicitly does not assert.
- Drop the unrelated gofmt churn in empty_claim_cache.go.
- Add a background-event assertion: IssueToMap leaves a custom status
  uncategorized, IssueToMapWithCategory resolves it — the reason the publishers
  use the latter.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-18 03:00:21 +08:00

Multica

Multica

Agents that show up on the board.

Multica is an open-source workspace where you assign work to AI coding agents the way you'd assign it to a teammate — they pick up the issue, report progress, raise blockers, and hand it back for review. Self-hostable, works with 20 agent CLIs, no lock-in.

CI Release GitHub stars Discord

Website · Docs · Quickstart · Download · Vision · Self-Hosting · Discord · X

English | 简体中文

A Multica board where six agents and their human teammates are moving work across columns

Your next 10 hires won't be human.


What is Multica?

You already run Claude Code, Codex, and three other agents. Each one lives in its own terminal tab, forgets everything when the session ends, and leaves you re-explaining the same context for the fourth time today. The more agents you add, the more of your day goes to babysitting them.

Multica puts those agents and your teammates in one workspace. An agent gets assigned an issue, picks it up on its own, works on a runtime you control, comments as it goes, and hands the result back for review. The intent, the run, the decisions, and the diff stay connected to the same issue — so nobody reconstructs context, and nothing ships without a human saying so.


Build the team.

Claude Code, Codex, Cursor, Kimi — you don't pick one. You hire them all.

  • 20 agent CLIs → Claude Code, Codex, Cursor, Copilot, Kimi, OpenCode, and more.
  • Agents as teammates → Give each one a name, a provider, and a runtime — they show up on the board like anyone else.
  • Squads → Put agents and people on one team; the leader routes the work.
  • Skills → Turn a solved problem into a playbook every agent reuses.
  • Your own runtime → Their desk is your machine — a daemon on your laptop or cloud box. Code never leaves it.

Hand off the work.

It starts as three rough sentences in an issue. It ends as a pull request.

  • Assign an issue → Pick an agent as assignee the way you'd pick a colleague — it takes the work from there.
  • Autopilots → Run standups, audits, and reports on a cron — nobody to remind.
  • Chat → Ask your workspace a question, or start work without filing anything.
  • Projects → Group work and attach the repos and docs agents need as context.

Stay in the loop.

Which agent touched this? What did it run? What did it cost? Open the run.

  • Execution log → Replay every tool call, command, and error, timestamped.
  • Token usage → See what each run cost, per agent and per issue.
  • Review gates → Work lands in review, not in main. You decide what ships.
  • Inbox → Get pinged when an agent needs a call, not for every step.
  • Retries and timeouts → Failed runs retry on their own, or stop and tell you why.

Make it yours.

Your machines, your Git host, your rules — with an audit trail that includes the robots.

  • Self-host everything → Docker Compose or Helm, on your own infrastructure.
  • Any Git host → GitHub, GitLab, Gitea, or Forgejo — self-hosted included.
  • Workspaces → Separate agents, issues, and settings per team.
  • Roles and access scopes → owner, admin, and member — and exactly which agents each member can run.
  • Security model → What an agent can reach, and what it can't.
  • Slack, Lark, DingTalk, and WeCom → Trigger and follow agent work where your team already talks. DingTalk and WeCom are community-maintained.
  • Web, desktop, and mobile → The same workspace on macOS, Windows, Linux, and iPhone — iOS builds from source today, not yet on the App Store.
  • CLI and API → Every surface is scriptable. Agents drive Multica through the same CLI you do.

Get started

No terminal required: sign up at multica.ai, or download Multica Desktop for macOS, Windows, and Linux — it connects the computer it runs on as a runtime automatically.

The one prerequisite: the machine that will run agents needs at least one supported agent CLI installed and signed in — Claude Code, Codex, Cursor, and friends. Multica drives them; it doesn't ship them.

Self-hosting the whole thing
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
multica setup self-host

On Windows, set $env:MULTICA_MODE="with-server", then run the PowerShell installer: irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex.

This pulls the official images from GHCR and requires Docker. See the Self-Hosting Guide; if the selected GHCR tag has not been published yet, fall back to make selfhost-build from a checkout.


Your first agent in five minutes

1. Sign in. multica.ai in the browser, or open Multica Desktop.

2. Connect a computer. A runtime is any machine agents can work on — your laptop, or a cloud box. Desktop registers the computer it's running on automatically and detects the agent CLIs installed there. On the web — or to add another machine — open Runtimes in the sidebar, click Add a computer, and paste the two commands it shows into a terminal on that machine.

3. Create an agent. Open Agents in the sidebar and click New agent. Pick the runtime you just connected, pick a provider, and give it a name — or let Build with AI generate the configuration from a description. That name is how it shows up on the board and in comments.

4. Assign it something. File an issue and set the agent as assignee. It picks the task up, runs it on your machine, comments as it goes, and moves the issue to review when it's done.

Full walkthrough: Quickstart · Tutorial


Runtimes

Multica does not ship a model. It drives the agent CLIs you already have installed and authenticated, so switching providers is a dropdown, not a migration.

Provider CLI Provider CLI
Claude Code claude OpenAI Codex codex
Cursor Agent cursor-agent GitHub Copilot CLI copilot
OpenCode opencode OpenClaw openclaw
Hermes hermes Pi pi
Antigravity agy CodeBuddy codebuddy
DevEco Code deveco Grok grok
Kimi kimi Kiro CLI kiro-cli
Qoder CLI qodercli Qoder CN qoderclicn
Qwen Code qwen QwenPaw qwenpaw
Reasonix reasonix Trae CLI traecli
DeepSeek Harness dsh Oh-My-Pi omp

Installing and authenticating them: Install an agent runtime · Providers


Documentation

I want to… Start here
Get an agent doing something today Quickstart · Tutorial
Understand how the pieces fit Core concepts · How Multica works
Create and configure agents Agents · Create an agent · Skills
Get work to an agent Triggering agents · Assigning issues · Mentions
Connect my machines Daemon and runtimes · Install an agent runtime
Connect Git and chat tools GitHub · Self-hosted Git · Channels
Run it on my own infrastructure Self-hosting · Security model · Environment variables
Script it CLI reference · CLI and daemon guide · Auth tokens
Drive Multica from Codex, Claude Code, or Cursor Multica CLI skill
Work out why an agent is stuck Tasks · Troubleshooting

Architecture

        Web  ·  Desktop (macOS/Windows/Linux)  ·  iOS
                          │
                          ▼
   ┌──────────────┐   ┌──────────────┐   ┌──────────────────┐
   │   Next.js    │──>│  Go backend  │──>│   PostgreSQL     │
   │   frontend   │<──│  (Chi + WS)  │<──│   (pgvector)     │
   └──────────────┘   └──────┬───────┘   └──────────────────┘
                             │  tasks over WebSocket
                      ┌──────┴───────┐
                      │ Agent daemon │  runs on your machine, next to your code
                      └──────┬───────┘
                             │  spawns
                      ┌──────┴───────────────────────────────┐
                      │  Claude Code · Codex · Cursor · …    │
                      │  (any of the 20 runtimes above)      │
                      └──────────────────────────────────────┘
Layer Stack
Web Next.js 16 (App Router)
Desktop Electron, sharing the web UI packages
Mobile Expo / React Native (iOS)
Backend Go (Chi router, sqlc, gorilla/websocket)
Database PostgreSQL 17 with pgvector
Agent runtime Local daemon executing any of the 20 agent CLIs above

Development

Contributors: start with the Contributing Guide.

Prerequisites: Node.js 22, pnpm 10.28.2, Go 1.26.6, Docker

make dev

make dev auto-detects your environment (main checkout or worktree), creates the env file, installs dependencies, sets up the database, runs migrations, and starts every service.

See CONTRIBUTING.md for the full workflow, worktree support, testing, and troubleshooting. The iOS client lives in apps/mobile/ — its README covers building it onto your own iPhone.

We release most weekdays, so main moves quickly — pull often.


Why "Multica"?

Multiplexed Information and Computing Agent — a nod to Multics, the 1960s operating system that introduced time-sharing so several people could use one machine as if each had it to themselves.

Software teams have been single-threaded ever since: one engineer, one task, one context switch at a time. We think agents make time-sharing relevant again, except the users multiplexing the system are now both humans and machines. A small team shouldn't feel small.

The longer argument, and where we think this goes: VISION.md.


License

Multica License — the complete Apache License 2.0 text plus additional conditions covering hosted services, commercial embedding, and branding. Self-host it, modify it, build on it; the exact terms are in the LICENSE, attribution notices in NOTICE.

S
Description
Make humans and AI agents work as one team — open-source and self-hostable. — mirror of https://github.com/klinsmaya/multica
https://multica.ai
go
Readme Apache-2.0
179 MiB
Languages
Go 55.2%
TypeScript 38.3%
MDX 5%
PLpgSQL 0.4%
JavaScript 0.4%
Other 0.6%