๐ค Multica Deep Dive โ How to Build a Managed-Agents Platform ๐
A complete, actionable build guide derived from a deep read of multica-ai/multica (~22k stars, ~42 MB, dual-language Go + TypeScript monorepo). If you read only one section before coding, read ยง3 The Core Idea and ยง5 The Agent Backend Interface. Everything else hangs off those two ideas. ๐ง What Multica Is โ and What It Is Not โก The 30-Second Mental Model ๐ก The Core Idea โ Don't Build the Agent Loop, Wrap It 4.1 ๐ Process / Service Topology 4.2 ๐ Repo Layout (top-level) 4.3 โ๏ธ Tech Stack (the load-bearing pieces) 5.1 ๐ The Interface 5.2 ๐ญ The Factory 5.3 ๐ The Canonical Implementation Pattern (Claude Code) 5.4 ๐ Per-Backend Quirks Worth Knowing 5.5 ๐ Why This Design Wins 6.1 ๐ Lifecycle (Daemon.Run) 6.2 ๐ The Poll Loop 6.3 โ๏ธ Per-Task Pipeline (handleTask โ runTask) 6.4 ๐ Auto-Detection of Installed CLIs 6.5 ๐ Stable Daemon ID 6.6 ๐ค Profiles 7.1 ๐ Per-Task Workdir 7.2 ๐งฉ The "Meta-Skill" โ Native Config File per Provider 7.3 ๐ Skill Files in Native Skill Directories 8.1 ๐ Reproducible Installs via Lockfile 8.2 โ๏ธ The Prompt vs Skill Split 8.3 ๐๏ธ Per-Agent Customization 9.1 ๐ Mid-Flight Session Pinning 9.2 โถ๏ธ Resume on Next Claim 9.3 ๐ Resume Fallback 9.4 ๐๏ธ GC 10.1 ๐ญ Polymorphic Actors 10.2 ๐ Multi-Tenancy 10.3 ๐พ Persistence Layer 10.4 ๐ Layering: Handler โ Service โ Repo 10.5 ๐ก In-Process Event Bus 10.6 ๐ Two WebSocket Subsystems 10.7 ๐ Single-Node vs Multi-Node Realtime 10.8 ๐ Strict UUID Parsing (a real bug in disguise) โฐ Autopilots โ Scheduled and Triggered Automation 12.1 ๐ฆ The Three-Package Split 12.2 ๐ Server State vs Client State 12.3 ๐งฉ Internal Packages Pattern 12.4 ๐ pnpm Catalog 12.5 ๐ซ The No-Duplication Rule 13.1 ๐ GoReleaser for the CLI 13.2 ๐ณ Docker for the Server 13.3 ๐ง The Makefile (the workflow tour) 13.4 โ CI 13.5 ๐ Self-Host Gating ๐ Engineering Practices Worth Stealing ๐ฑ Phase 1 โ Skeleton (1 day) ๐ Phase 2 โ Issues CRUD (2 days) ๐ Phase 3 โ User-Facing WebSocket (1 day) ๐ Phase 4 โ The Agent Backend Interface (1 day) ๐ Phase 5 โ Local Daemon Skeleton (2 days) โ Phase 6 โ Task Lifecycle End-to-End (3 days) ๐ง Phase 7 โ Skills + Per-Provider Config Injection (1 day) โก Phase 8 โ Daemon Wakeup over WS (ยฝ day) โถ๏ธ Phase 9 โ Resumable Sessions (1 day) โ Phase 10 โ Add a Second + Third Backend (1 day) โฐ Phase 11 โ Autopilots (1 day) ๐ฆ Phase 12 โ Packaging + Self-Host (1 day) โ ๏ธ Common Pitfalls and Hard-Won Guardrails ๐ Files to read first (in order) โ๏ธ Default config values ๐ The unified message taxonomy (don't deviate) ๐ The unified result statuses ๐ฃ๏ธ The agent's CLI vocabulary (what the meta-skill teaches) ๐ญ The polymorphic-actor pattern ๐ซ Hard rules (non-negotiable) Tagline. "The open-source managed agents platform. Turn coding agents into real teammates โ assign tasks, track progress, compound skills." Positioning. A Linear-shaped project-management surface (issues, projects, comments, inbox, real-time updates) where AI coding agents are first-class citizens alongside humans: An agent has a profile, shows up on the board, can be @-mentioned. You assign an issue to an agent the same way you assign to a colleague. A local daemon on the user's laptop picks up the work, runs the chosen agent CLI (Claude Code, Codex, Cursor, Gemini, Copilot, OpenCode, โฆ), streams progress, and reports back. Skills (markdown bundles) are injected into every task so capabilities compound. Autopilots are cron/webhook-triggered automations that fire agent runs without human assignment. It IS: A control plane / orchestration layer A managed-teammate UI (Linear-clone with agents) A daemon that runs agent CLIs and streams events A skills + autopilots system It IS NOT: An agent loop (no LLM calls, no tool-use parser, no RAG) A library โ it's a deployable platform Tied to one model provider โ supports 11 different agent CLIs The closest cousin in spirit is Linear ร LangGraph โ but the LangGraph part is delegated to whichever third-party agent CLI is installed on the user's machine. This decision is the most important one in the entire codebase. Internalize it before going further. โโโโโโโโโโโโโโโโโโโโ โ Browser / Desk โ โ (Next.js / EL) โ โโโโโโโโโโฌโโโโโโโโโโ โ HTTPS + WS โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ โ Server (Go: Chi + WS) โ โ source of truth โ Postgres + (opt) Redis โ โโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโ โ WS push โ HTTPS poll โ wakeup โ (every 3s) โโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโ โ Daemon on user's laptop โ โ runs the agents โ (same Go binary, cobra) โ โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ exec.Command โโโโโโโโโโโโฌโโโโโโโโผโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโ โผ โผ โผ โผ โผ claude codex cursor gemini opencode ... Three runtime artifacts, all from the same monorepo: Artifact Built from Runs where Server binary server/cmd/server Your infra (Docker / VPS / k8s) multica CLI + daemon server/cmd/multica User's laptop (Homebrew / install.sh) Web app apps/web (Next.js) + apps/desktop (Electron) Browser / Mac / Win / Linux The single decision that lets a small team ship this much surface area: Stop trying to be an agent runtime. Be the control plane that dispatches to existing agent CLIs. Concretely: Define one Go interface โ Backend โ with a streaming Execute method. Write one implementation per CLI (claude, codex, cursor, gemini, โฆ). Each implementation is just an exec.Command plus a streaming-stdout parser. Translate every CLI's idiosyncratic JSON dialect into your own unified message taxonomy (text / thinking / tool-use / tool-result / status / log / error). Everything above this layer (assignment, scheduling, comments, autopilots, skills, UI) treats agents uniformly. If you only adopt one architectural idea from Multica, this is it. It's what makes the project tractable, vendor-neutral, and trivially extensible (one new file = one new agent). The README explicitly cites the inspiration: "It mirrors the happy-cli AgentBackend pattern, translated to idiomatic Go." [Frontend] โ [Go API + WS] โ [Postgres + pgvector] โ โ Redis streams (optional, for multi-node fanout) โ โ Daemon WS + HTTP poll โ [Local Daemon] โ spawns โ [agent CLIs] apps/ web/ Next.js 16 App Router desktop/ Electron (electron-vite) docs/ Mintlify/MDX docs packages/ core/ Headless logic โ zustand stores, react-query, api client (zero react-dom) ui/ Atomic primitives (shadcn / Base UI; zero business logic) views/ Business components/pages (zero next/* or react-router) server/ cmd/server/ HTTP API entry cmd/multica/ CLI + daemon (cobra) entry cmd/migrate/ Migration runner internal/ handler/ HTTP handlers (Chi) service/ Business logic daemon/ Local daemon daemonws/ Daemon-side WS hub realtime/ User-facing WS hub + Redis stream relay cli/ CLI helpers auth/ JWT + Google OAuth middleware/ Auth, CSP, request log events/ In-process event bus pkg/ agent/ *** The Backend interface + 11 implementations *** db/queries/ sqlc input db/generated/ sqlc output migrations/ 156 SQL files (Postgres) sqlc.yaml e2e/ Playwright (against full docker-compose) .github/workflows/ ci.yml, desktop-smoke.yml, release.yml .goreleaser.yml Makefile docker-compose.{,selfhost.,selfhost.build.}yml Server (Go 1.26) github.com/go-chi/chi/v5 โ router + middleware chain jackc/pgx/v5 + pgxpool โ Postgres sqlc โ typed SQL โ Go (input: pkg/db/queries/, output: pkg/db/generated/) gorilla/websocket โ both user-facing and daemon-facing WS redis/go-redis/v9 โ optional fanout golang-jwt/jwt/v5 โ auth spf13/cobra โ CLI for multica binary robfig/cron/v3 โ autopilot scheduler resend-go โ email aws-sdk-go-v2/s3 + CloudFront signed URLs prometheus/client_golang โ metrics stdlib log/slog + lmittmann/tint (pretty in dev) Frontend (TS / React 19) React 19, TS 5.9, Vite, Tailwind v4 Zustand 5 for client state, TanStack Query 5 for server state โ strict split TanStack Table 8 Vitest 4 + Testing Library, Playwright for e2e Turborepo for orchestration, pnpm catalog for unified version pinning Infra PostgreSQL 17 + pgvector Redis 7 (optional) GoReleaser for CLI binaries (mac/linux/win ร amd64/arm64) Homebrew tap (multica-ai/homebrew-tap) auto-published on tag Docker images on GHCR for self-host Everything below is in server/pkg/agent/. Read agent.go first when reproducing this project. package agent type Backend interface { Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error) } type ExecOptions struct { Cwd string Model string SystemPrompt string MaxTurns int Timeout time.Duration SemanticInactivityTimeout time.Duration // kill if no semantic event in N ResumeSessionID string // resume previous agent session CustomArgs []string // appended after our flags McpConfig json.RawMessage // written to temp file, --mcp-config } type Session struct { Messages --version; CheckMinVersion(name, version) is the gate that prevents the daemon from registering a runtime that's too old. stderr_tail.go Bounded 64 KB ring buffer. Critical: without this, native crashes in the underlying CLI bubble up as "exit status 3" with no diagnostic. proc_other.go / proc_windows.go Process group + window-hide cross-platform helpers. Adding an agent = one Go file. That's it. No protocol changes, no DB migrations, no UI changes. No vendor lock. Users keep their own subscriptions / API keys / config for whichever CLI they prefer. No risk of being out of date. The agent CLI gets better โ your platform gets better, for free. Failure surface is bounded. A CLI crash doesn't crash your server. server/internal/daemon/daemon.go (~53 KB). Runs on the user's machine via multica daemon start. Daemon.Run) 1. Bind health port early (default :19514) โ /health endpoint โ fail-fast if another daemon is already running 2. resolveAuth() โ load token from ~/.multica/config.json 3. syncWorkspacesFromAPI โ for each workspace user belongs to: - probe each agent CLI via exec.LookPath - run agent.DetectVersion + CheckMinVersion - POST /api/daemon/register with {name, type, version, status} - cache returned runtimeIDs 4. Start background goroutines: - workspaceSyncLoop (30s) โ re-sync workspace membership - taskWakeupLoop โ open daemon WS, listen for instant wakeups - heartbeatLoop (15s) โ POST /api/daemon/heartbeat response may piggyback: PendingUpdate, PendingModelList, PendingLocalSkills, PendingLocalSkillImport - gcLoop โ clean ~/multica_workspaces/ for done issues - serveHealth โ local /health JSON (uptime, active task count) 5. Enter pollLoop (the heart of the daemon) sem := make(chan struct{}, cfg.MaxConcurrentTasks) // default 20 for { runtimeIDs := d.allRuntimeIDs() for i := 0; i _PATH # override binary path MULTICA__MODEL # override default model So the daemon adapts to whatever's installed without user config โ and users can pin specific binaries when they want. EnsureDaemonID(profile) writes a UUID to ~/.multica/profiles//daemon.id once and reuses it forever. Without this, hostname drift (e.g. .local suffix appearing/disappearing on macOS) would mint duplicate runtime rows on the server. LegacyDaemonIDs(host, profile) is sent at register-time so the server can merge old hostname-derived rows. multica setup self-host --profile staging lets one machine talk to multiple servers. Each profile gets its own ~/.multica/profiles// with config, daemon ID, health port, and workspace root. This is the second-most important design decision after ยง3. Each agent self-bootstraps via its own native config-file convention โ you don't invent a protocol. ~/multica_workspaces/ {workspace_id}/ {task_id_short}/ workdir/ โ cwd of the agent process; git checkout lives here output/ โ collected outputs logs/ โ captured stdout/stderr .gc_meta.json โ {issue_id, workspace_id, completed_at} Isolation is per-task, not per-issue. Reuse on the same agent+issue is opt-in via task.PriorWorkDir. execenv.InjectRuntimeConfig writes a config file at the workdir root that each agent reads natively at startup: Provider Config file written claude CLAUDE.md codex / copilot / opencode / openclaw / hermes / pi / cursor / kimi / kiro AGENTS.md gemini GEMINI.md The content is built by buildMetaSkillContent(provider, ctx) and is essentially a system prompt teaching the agent to act as a Multica teammate: Identity block โ "You are: {agent name} (ID: โฆ)" + agent's persona instructions. CLI catalog โ every multica subcommand the agent may use: Read: issue get, issue list, issue comment list, workspace members Write: issue create, issue update, issue assign, issue label add, issue subscriber add, issue comment add, label create, autopilot create|update|trigger|delete Hard rule: always pass --output json so the agent gets stable IDs. Multi-line content rule: must use --content-stdin with HEREDOCs (because bash doesn't expand \n in double-quoted strings โ observed empirically, hard-coded as a guard). Provider-specific gotchas โ e.g. Codex tends to follow a per-turn reply command literally โ instruct it to use --content-stdin. Workflow section โ branches on task kind: chat, quick-create, autopilot run-only, comment-triggered, default. The agent now has who it is and what tools it has and how to use them, all via the file format it already reads natively. Zero protocol invention. Skills are written into each agent's native skills directory: Provider Skills directory claude .claude/skills/ codex .codex/skills/ cursor .cursor/skills/ openclaw .openclaw/skills/ opencode .config/opencode/skills/ copilot .github/skills/ pi .pi/skills/ hermes (fallback) .agent_context/skills/ Each agent discovers them through its own native mechanism. You write to disk; the agent CLI does the rest. A Skill is just: { name: string, content: string /* markdown */, files: { path: string, content: string }[] } That's it. The platform value comes from management (per-workspace catalog, agent linkage, marketplace install, lockfile), not from format complexity. skills-lock.json at repo root pins each marketplace skill: { "skills": { "frontend-design": { "source": "github.com/anthropics/skills", "ref": "abc123โฆ", "computedHash": "sha256:โฆ" }, ... } } Sources include anthropics/skills, shadcn/ui, vercel-labs/agent-skills. computedHash makes installs verifiable. A subtle but important discipline: the prompt is minimal; skills carry context. BuildPrompt(task) is one short paragraph per task kind. Everything that describes how the platform works lives in the meta-skill (CLAUDE.md / AGENTS.md), which you'd otherwise have to re-emit in every prompt. The agent table stores the dials a user has over an agent's behavior: instructions โ persona / system prompt skills[] โ linked skill IDs (joined to per-workspace skill catalog) custom_env โ k/v injected per task (with a daemon-side blocklist) custom_args โ appended after the daemon's built-in CLI args mcp_config โ raw JSON, written to a temp file and passed --mcp-config model max_concurrent_tasks visibility โ workspace | private LaunchHeader(provider) is shown in the UI so users see the skeleton their custom_args extend. Coding agents have expensive context. Throwing it away on each turn is wasteful. Multica handles this with two pieces of forwarded state: As soon as a backend emits a SessionID, the daemon calls client.PinTaskSession(taskID, sessionID) โ server stores it on the task row. Crash-safe: if the daemon dies mid-task, the resume pointer is already on the server. When the server hands the next task on the same agent+issue, it includes: PriorSessionID โ passed back as ExecOptions.ResumeSessionID (e.g. claude --resume ) PriorWorkDir โ daemon calls execenv.Reuse(...) instead of execenv.Prepare(...) โ same git checkout, same scratchpad If a resume fails before establishing a session (Status==failed && PriorSessionID!="" && SessionID==""), the daemon retries once with ResumeSessionID="" โ fresh start. This rescues the user from a stale session ID without infinite-looping. gcLoop cleans ~/multica_workspaces/: Workdirs whose issue is done|cancelled and older than MULTICA_GC_TTL (default 24h) Orphan dirs (no .gc_meta.json) older than MULTICA_GC_ORPHAN_TTL (default 72h) Server returning 404 on the issue โ immediate clean The single most enabling schema decision: issues.assignee_type CHECK (assignee_type IN ('member', 'agent')) issues.assignee_id UUID comments.author_type CHECK (author_type IN ('member', 'agent')) inbox.recipient_type ... Once you commit to polymorphism on every actor field, agents are free citizens everywhere in the API โ no special endpoints, no parallel UI. Every query filters by workspace_id. Membership table gates access (member row joins user and workspace with a role). The frontend sends X-Workspace-ID on every request to route to the active workspace. Middleware: Auth(queries) โ JWT or PAT DaemonAuth(queries) โ daemon token RequireWorkspaceMemberFromURL(queries, "id") RequireWorkspaceRoleFromURL(queries, "id", "owner", "admin") 156 numbered SQL migration files (server/migrations/001_init.up.sql โฆ) โ immutable history; never edit an applied migration. sqlc turns pkg/db/queries/*.sql into typed Go code in pkg/db/generated/. pgxpool throughout; no ORM. pgvector enabled for embedding-based search (skills, issues). handler (Chi routes) โ HTTP/WS adapters; never touch DB โ service โ business logic; transactions; calls multiple queries โ queries (sqlc) โ typed SQL only Constructor-based DI: taskSvc := service.NewTaskService(queries, pool, hub, bus, daemonWakeup) autoSvc := service.NewAutopilotService(queries, taskSvc, ...) No globals. No init(). events.Bus is a synchronous publisher with topic-based listeners. Order of registration matters and is documented in cmd/server/main.go: // Subscribers MUST register BEFORE notifications, because notifications // depend on the subscriber list being up to date. events.RegisterSubscriberListeners(bus, queries) events.RegisterNotificationListeners(bus, queries, ...) events.RegisterActivityListeners(bus, queries) events.RegisterAutopilotListeners(bus, queries, autoSvc) When a service emits an event, listeners write derived state (inbox items, activity rows) and emit broadcaster events that flow out over WS. Path Audience Auth Purpose /ws Browser / Desktop JWT (PAT or session cookie); origin check against ALLOWED_ORIGINS Stream updates: new issues, comments, presence, task progress /api/daemon/ws Daemon Daemon token Server โ daemon wakeups when a task is queued Without REDIS_URL: in-process Hub โ single API node. With REDIS_URL: realtime.NewShardedStreamRelay uses Redis streams to fan out events across nodes. Sharding key + per-shard consumer groups. The same daemon-wakeup channel routes through daemonws.NewRelayNotifier(hub, sharded) so a runtime connected to API node A can be woken when node B ingests its task. There's a legacy / dual / sharded env switch (REALTIME_RELAY_MODE) for safe rollouts. Key principle: don't make Redis required. Single-node self-host should run with just Postgres. CLAUDE.md documents three named helpers, born from bug #1661 where a generic util.ParseUUID silently returned the zero UUID, causing DELETEs to return 204 while matching zero rows: parseUUIDOrBadRequest(s) // for user input โ returns 400 on invalid parseUUID(s) // for trusted round-trips โ panics โ caught by Recoverer loadIssueForUser(ctx, queries, key) // accepts UUID or "MUL-123" human ID loadAgentForUser(...) The lesson: typed parsers at every trust boundary. Never roll a generic helper that hides errors. server/internal/service/autopilot.go + cron.go. Two modes: create_issue โ scheduler creates a new issue and assigns it to the agent. Normal task flow follows. run_only โ no issue exists; scheduler enqueues a task in agent_task_queue with autopilot context. Daemon picks it up; the meta-skill detects MULTICA_AUTOPILOT_RUN_ID and switches to autopilot workflow (no multica issue get calls). triggers table holds: cron โ robfig/cron expression + timezone webhook โ endpoint hash (data model exists, dispatch not wired yet per CLI_AND_DAEMON.md) api โ manual API trigger (same status) runAutopilotScheduler(ctx, queries, autopilotSvc) ticks; due triggers call autopilotSvc.RunOnce. CLI exposes only cron triggers today: multica autopilot trigger-add \ --cron "0 9 * * 1-5" \ --timezone "America/New_York" This is where the project's discipline really shows. The rules are codified in CLAUDE.md and enforced via package boundaries. packages/core/ headless logic - zustand stores (ALL of them, even view-related) - react-query hooks - api client - StorageAdapter, NavigationAdapter (interfaces) - ZERO react-dom - ZERO localStorage (use StorageAdapter) - ZERO process.env packages/ui/ atomic primitives (shadcn / Base UI variant) - components/ui/button.tsx, card.tsx, ... - ZERO @multica/core imports - ZERO business logic packages/views/ business components/pages - One component per route (IssuesPage, AutopilotsPage, ...) - ZERO next/* imports - ZERO react-router-dom - ZERO direct store imports (read via core hooks) - Routing via NavigationAdapter apps/web/ Next.js wiring apps/desktop/ Electron wiring - Each provides StorageAdapter, NavigationAdapter, CoreProvider - This is the ONLY layer where Next.js / Electron APIs appear TanStack Query for everything API-derived. Always. Zustand for UI-only state (selection, modals, drafts, presence). WebSocket events invalidate Query. They never write directly to stores. All workspace-scoped queries key on wsId, so workspace switching invalidates automatically. Packages export raw .ts / .tsx. Consumer's bundler (Vite / Next) compiles directly. Zero-config HMR, instant go-to-definition, no build step between packages. pnpm-workspace.yaml declares a catalog of pinned versions. Every package imports "react": "catalog:". Bumps happen in one place. "If the same logic exists in both apps, it must be extracted to a shared package." Frequently restated in CLAUDE.md. This is what keeps a web + desktop app from diverging. .goreleaser.yml builds: darwin / linux / windows ร amd64 / arm64 Both legacy-named and versioned tarballs (legacy keeps old multica update working โ backwards compat) Checksums Auto-publishes a Homebrew formula to multica-ai/homebrew-tap on tag User install paths: brew install multica-ai/tap/multica curl https://multica.ai/install.sh | sh iwr https://multica.ai/install.ps1 | iex All scripts support --with-server to bring up the full stack alongside the CLI. Dockerfile (server) + Dockerfile.web (frontend) โ published to GHCR (ghcr.io/multica-ai/multica-backend, multica-web). Three compose files: docker-compose.yml โ dev (only Postgres) docker-compose.selfhost.yml โ production self-host docker-compose.selfhost.build.yml โ override that builds locally Unusually polished at 12.5 KB: make dev # start dev stack make selfhost # production self-host make selfhost-build # build locally instead of pulling make selfhost-stop make check # full CI pipeline locally make sqlc # regenerate typed SQL make migrate-up / migrate-down / migrate-status make migrate-new name=add_foo_table make db-reset # refuses if DATABASE_URL points to remote make worktree-env # generate .env.worktree with unique DB name + ports # โ run multiple git worktrees in parallel against one Postgres .github/workflows/ci.yml โ two jobs: frontend โ pnpm + Node 22 + turbo build typecheck test --filter='!@multica/docs' backend โ Go 1.26 + Postgres 17 + pgvector + Redis 7 services; go build ./..., run migrations, go test ./.... Separate REDIS_TEST_URL=redis://localhost:6379/1 for runtime-local-skill tests. .github/workflows/release.yml โ auto-fires on v* tag: Go tests โ GoReleaser โ GitHub Releases + Homebrew tap. .github/workflows/desktop-smoke.yml โ Electron build/package per platform. ALLOW_SIGNUP=false ALLOWED_EMAIL_DOMAINS=acme.com [email protected],[email protected] Plus MULTICA_DEV_VERIFICATION_CODE for local dev (rejected when APP_ENV=production). A grab bag, ranked by leverage: CLAUDE.md as the engineering bible (21 KB). Every architectural rule is documented with the bug number that motivated it. Hard rules, hard reasons. AGENTS.md is a 2 KB pointer that just tells agents to read CLAUDE.md. Single source of truth, thin pointers everywhere else. Constructor-based DI everywhere. No globals. No init(). Mockability comes for free. Test placement is rule-bound: shared logic tests live in the package they test; framework-specific wiring tests live in the app. Every Go file has a _test.go peer (often the same size or bigger). CI uses real Postgres + Redis services (not testcontainers). Faster, simpler. Bounded stderr ring buffer for every spawned process. Without this, native crashes show only "exit status 3". Polymorphic actor fields from day one (*_type + *_id). Retrofitting is painful. Workspace-scoped query keys. Switching tenant invalidates cache automatically. Zero-config monorepo. Packages export raw TS; consumer bundler compiles. Instant HMR + go-to-definition. Mid-flight pinning. Pin volatile state (session ID) to the server as soon as it's produced โ don't wait for completion. Worktree-friendly Makefile. Generate .env.worktree with unique DB name + ports. Run N branches in parallel against one Postgres. Don't make Redis required. Optional fanout, single-node default. Two-tier model resolution: explicit override > daemon-wide env > CLI default. No mandatory choice. MULTICA_* env vars + agent.CustomEnv merge with a blocklist. Users can set their own env without overriding daemon-set vars. Auto-detect installed CLIs via exec.LookPath. Daemon adapts to whatever's installed; explicit overrides exist when needed. chi.Recoverer so panics from parseUUID (the trusted variant) don't crash the server โ they're logged and 500'd. Listener registration order is documented in code comments, because it's load-bearing. Per-tenant security guard: daemon refuses to spawn if task.WorkspaceID == "". No silent fallback to user-global config across workspaces. Health port bound first. Detects another daemon already running before doing anything else. Stable daemon ID persisted to disk. Hostname drift is a real source of duplicate runtime rows. Backwards-compat legacy-named tarballs so old multica update keeps working forever. Build a minimum-viable Multica clone. Each phase is shippable. Don't skip ahead. Init monorepo: apps/web, packages/core, packages/ui, packages/views, server/. pnpm workspace + Turborepo. Postgres locally; one migration: user, workspace, member. Email + password (or magic-link) auth โ JWT. Health endpoint. Basic Chi router. Structured logging via slog. Done when: make dev brings up Postgres + Go server + Next.js, you can sign up and see your workspace. Migrations: issue, issue_label, comment. Polymorphic assignee_type + assignee_id. sqlc + queries. Handler โ service โ repo for issues + comments. Linear-shaped UI: list, detail, create modal. TanStack Query for everything API-derived. Done when: Humans can create, assign, comment on issues, like a tiny Linear. /ws endpoint with JWT auth + origin check. In-process events.Bus. Listeners that emit broadcaster events on issue/comment changes. Frontend WS client invalidates Query on relevant events. Done when: Two browser tabs see each other's edits in real time. This is the keystone. Get it right. server/pkg/agent/agent.go โ interface, types, factory. claude.go โ first implementation. Streaming stdout parser, bounded stderr tail, per-message-type translation to your taxonomy. version.go, models.go. Unit tests with a fake CLI (a shell script that prints canned NDJSON). Done when: A unit test can run Backend.Execute("hello") against a fake stdout fixture and observe the unified message stream + final result. Cobra CLI: multica daemon start. Health port bind (fail-fast). Stable daemon ID persisted to disk. LoadConfig probes installed CLIs via exec.LookPath. POST /api/daemon/register. Heartbeat loop. Done when: Daemon starts, registers a runtime, server shows it online. DB: agent, agent_task_queue, runtime, task tables. Server endpoints: claim task, start, messages (batch), usage, complete, fail, status. Daemon poll loop with semaphore + round-robin. Per-task workdir: ~/multica_workspaces/{ws}/{task}/workdir/. Inject CLAUDE.md (or AGENTS.md) at workdir root with a minimal meta-skill. Build agentEnv with MULTICA_* vars; merge agent.CustomEnv with blocklist. Run agent โ stream messages โ report. Done when: UI shows live token-by-token output for a real assigned issue. Skill model: { name, content, files[] }. Per-workspace catalog. Write skills into native dirs (.claude/skills/, etc.). Build the meta-skill content: identity + CLI catalog + workflow. Add multica issue CLI subcommands so the agent can call them: get, list, comment add (with --content-stdin), update, assign, label add. Done when: An agent on an assigned issue calls multica issue get and multica issue comment add and the comments appear in the UI authored as the agent. /api/daemon/ws endpoint. daemonws.Hub with task-wakeup channels per runtime. sleepWithContextOrWakeup returns immediately on wakeup. Done when: Latency from "assign" to "agent message arrives" is --output json multica issue list --output json multica issue comment list --output json multica workspace members --output json multica issue create --title ... --content-stdin ... --output json multica issue assign --to --output json multica issue label add --label ... --output json multica issue subscriber add --user ... --output json multica issue comment add --content-stdin <<EOF ... EOF --output json multica label create --name ... --color ... --output json multica autopilot create / update / trigger / delete ... CREATE TABLE issue ( id UUID PRIMARY KEY, workspace_id UUID NOT NULL REFERENCES workspace, title TEXT NOT NULL, content TEXT, status TEXT NOT NULL, assignee_type TEXT CHECK (assignee_type IN ('member', 'agent')), assignee_id UUID, creator_type TEXT CHECK (creator_type IN ('member', 'agent')), creator_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), ... ); Every server query filters by workspace_id. Every TanStack Query key includes wsId. packages/core/ has zero react-dom, zero localStorage, zero process.env. packages/views/ has zero next/*, zero react-router-dom. packages/ui/ has zero @multica/core imports. Listener registration order: subscribers before notifications. Daemon refuses to spawn if task.WorkspaceID == "". Always pass --output json from the agent's CLI calls. Always use --content-stdin with HEREDOCs for multi-line content. WS events invalidate Query; they never write directly to stores. Migrations are append-only. Never edit an applied migration. Multica's superpower isn't novel ML โ it's discipline: One interface for agents (Backend.Execute), eleven implementations. One workdir convention (~/multica_workspaces/{ws}/{task}/), every agent self-bootstraps via its native config-file format. One source of truth (Postgres), one event bus, two WS subsystems with distinct audiences. One engineering bible (CLAUDE.md), every rule annotated with the bug that produced it. If you internalize ยง3 (don't build the loop, wrap it) and ยง5 (the Backend interface), and you keep that discipline as you grow, you can recreate this in ~10โ14 days of focused work for a v1. Now go build. If you found this helpful, let me know by leaving a ๐ or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! ๐
