* docs(changelog): add v0.3.35 entry for the 2026-07-02 release (MUL-3984)
Summarize the changes on main since v0.3.34: the Issue-surface refactor and its
per-list cache/count accuracy, the chat live-timeline remount fix, four new
features (Show sub-issues toggle, add-label picker in the manual create dialog,
bulk 'Add to agent' on the skill detail page, and S3 path-style addressing
for self-host), plus this release's bug fixes.
Bullets stay in product language across en / zh / ja / ko. Only the Issue
concept is preserved untranslated in the Chinese entry; agent/Squad/sub-issue
follow the translated glossary.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.3.35 bullets to one clause each (MUL-3984)
Per review, cut each bullet to a single short sentence and shorten the release
title. Same coverage across en / zh / ja / ko; content unchanged.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Wire structured feedback kind through the frontend/core feedback path so desktop route-renderer errors submit as bug feedback, and bring the feedback client onto parseWithFallback. MUL-3768
The Slack `/issue` slash command used to directly create a raw issue: the
typed line became the title verbatim and a `todo` issue was assigned to the
agent to work on immediately. That files a rough, unstructured issue and starts
the agent on it before it is well-formed.
Switch the slash command to the quick-create pipeline instead
(TaskService.EnqueueQuickCreateTask, the same path as the web "quick create"
modal): the invoker's natural-language description is handed to the
installation's agent as a prompt, and the agent authors a well-formed issue
(proper title + structured description) in the background, attributed to the
bound member. Because creation is now asynchronous, the ephemeral reply is an
acknowledgement ("On it…") rather than a created-confirmation with a number;
the agent's completion surfaces to the invoker as a Multica inbox notification
through the shared quick-create completion path.
Installation routing and identity/membership checks are unchanged, so the same
workspace boundary and account-binding rules apply. Scope is the slash command
only — the message-based `@bot /issue` still runs through the shared
cross-platform engine (which also serves Lark) and keeps its direct-create
behavior.
- slash_command.go: swap IssueService.Create for EnqueueQuickCreateTask via a
narrow quickCreateEnqueuer interface; prompt is the full text (no title/body
split); drop the now-unused splitIssueText / issueCreatedText / GetWorkspace.
- router.go: wire h.TaskService instead of h.IssueService.
- tests: cover enqueue + ack, multiline prompt pass-through, empty prompt,
unbound, non-member, inactive, team mismatch, and enqueue-failure.
- docs (4 locales): describe the quick-create behavior.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
A Slack user who linked their Multica identity to one bot was re-prompted
to link again when messaging a second bot (a different Slack app) in the
SAME Slack workspace, because bindings are keyed per-installation
(installation_id, channel_user_id).
On an unbound inbound, the identity resolver now looks up an existing
binding for the same (Multica workspace, Slack team, Slack user) via the
new FindReusableChannelUserBinding query and, if that user is still a
workspace member, materializes a binding for the new installation instead
of returning ErrSenderUnbound — so the second bot resolves the user
silently. Reuse is fenced to one Multica workspace AND one Slack team, so
it never crosses either boundary; legacy installs with no recorded team
never reuse.
Adds resolver unit tests for every decision path and updates the Slack
integration docs (en/zh/ja/ko).
MUL-3911
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): native /issue slash command over Socket Mode (MUL-3908)
A message beginning with `/issue` is intercepted by the Slack client as a slash command and never delivered to the app, so the message-prefix /issue never worked on Slack (no event, no 👀, no issue).
Register /issue as a real slash command in the app manifest and handle EventTypeSlashCommand over the existing per-installation Socket Mode connection. It is a one-shot issue creation (no chat session / agent run) that reuses the shared IssueService and the same installation-routing + identity/membership checks as the message path, replying privately via the command's response_url (ephemeral) since a slash command has no message to react to.
Docs: register the command in the manifest and describe the slash-command behavior across all four locales.
Co-authored-by: multica-agent <github@multica.ai>
* docs(slack): add commands scope to /issue manifest; fix chat-run wording (MUL-3908)
Review follow-up: the manifest examples registered features.slash_commands but omitted the commands bot scope, so updating + reinstalling could still fail to grant the /issue command. Add - commands to oauth_config.scopes.bot in all four locales and document it in the permissions table.
Also correct the misleading "no agent run" wording in the slash-command header and router comment: a todo issue assigned to the agent still triggers it via maybeEnqueueOnAssign (issue-assignment), like the message /issue — the slash command only skips the chat session / chat run.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Follow-up to #4724, which added the Trae CLI (traecli) ACP backend but
left the surrounding docs behind.
- install-agent-runtime: add a Trae CLI section (install, ACP transport,
enterprise login, inline runtime brief, MULTICA_TRAECLI_MODEL)
- providers: fix the MCP paragraph — Trae also receives ACP mcpServers
- daemon-runtimes: add Qoder + Trae CLI to the built-in detection list
- README: add Trae CLI to the architecture diagram and runtime row
- bump stale English tool counts (12/13 -> 14) across cross-references;
the '12' lists were already missing Qoder before this change
Scope: English docs only. The ja/zh localizations are separately behind
(they predate Qoder too) and need their own translation-sync pass.
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Adds the official ByteDance TRAE CLI (the `traecli` binary documented at
https://docs.trae.cn/cli — the product paired with the Trae IDE, not the
open-source bytedance/trae-agent) as a built-in agent backend. traecli is
ACP-native, so it is driven over the standard ACP JSON-RPC transport via
`traecli acp serve --yolo`, reusing the shared hermesClient exactly like the
Kiro and Qoder backends.
Validated end-to-end against the real traecli v0.120.42 with a logged-in
account: initialize advertises loadSession:true + mcpCapabilities{http,sse};
session/new returns result.sessionId + models.availableModels (18 models
discovered); session/prompt streams session/update notifications with
sessionUpdate=agent_message_chunk (hermesClient already normalizes this Zed-ACP
wire shape); a real board task ran 14 tool calls and completed in ~47s.
Implementation:
- server/pkg/agent/traecli.go: ACP backend; session/load resume
(loadSession:true), session/set_model, MCP via ACP mcpServers, --yolo
bypass-permissions for headless runs, blocked-arg filtering (acp, serve,
--yolo, --print, --output-format, --permission-mode)
- agent.go: New() + launch header "traecli acp serve"
- models.go: discoverTraecliModels via the shared discoverACPModels
- daemon/config.go: auto-detect the `traecli` binary
(MULTICA_TRAECLI_PATH / MULTICA_TRAECLI_MODEL)
- daemon.go: inline the runtime brief (traecli reads .trae/rules/, not
AGENTS.md) and surface the runtime as "Trae" (providerDisplayName)
- execenv: AGENTS.md + .traecli/skills wiring; ~/.traecli/skills local root
- packages/core mcp-support: traecli consumes mcp_config
- frontend: official Trae provider logo
- docs: providers.mdx matrix + section, CLI_AND_DAEMON.md, README
Tests: fake-ACP unit tests matching the real wire format (streaming,
blocked-arg filtering, session/set_model failure, session/load resume) plus a
gated real-binary smoke test (TestTraecliRealACPSmoke) that skips when traecli
is absent or not logged in. Built-in provider only (mirrors qoder): not in
SupportedTypes / RUNTIME_PROFILE_PROTOCOL_FAMILIES, so no migration is needed.
Resolves#4376.
* docs(changelog): add v0.3.33 entry for the 2026-06-30 release (MUL-3889)
Adds the v0.3.33 release entry to the /changelog page in all four
landing locales (en, zh-Hans, ja, ko), covering the 16 user-visible
changes that landed on main since v0.3.32 (2026-06-29 release).
The entry groups changes into Features / Improvements / Bug Fixes,
using product-language phrasing per the team convention (no
"modified function X" style notes). The Chinese version follows the
team's localization convention: `agent` → `智能体`, `Squad` → `小队`,
while `Issue` stays as-is as the canonical product term.
Release highlights:
- feat(autopilot): View/Write permission layer + Manage Access (MUL-3807)
- feat(slack): unified chat history backfill (MUL-3871) and typing
reaction on inbound messages (MUL-3874)
- feat(skills): import skills from a .skill/.zip archive (MUL-3865)
- feat(cli)!: drop short UUID prefix resolution for `multica issue`
(MUL-3838)
- feat(views): Agents page mobile friendly (MUL-3873)
- improvement: rewrite of the comment routing cascade
(MUL-3794 + MUL-3879 follow-up)
- improvement: docs swap removed Gemini for CodeBuddy (MUL-3861) and
remove 117 dead _one i18n keys (MUL-3877)
- improvement: self-host preflight allows newer Docker Compose
- fix(daemon): reconcile in-flight task and workspace state on WS
reconnect (community contribution, closes#4665)
- fix(agent): recover Antigravity reply from transcript when stdout
is empty (MUL-3726)
- fix(server): skip CLIENT SETNAME for managed Redis compatibility
(MUL-3848, community contribution)
- fix(views): count tasks, not agents, in activity hover header
(MUL-3872)
Verified via the existing `apps/web typecheck`, vitest landing
suite (changelog-page-client.test.ts among them), and eslint on the
i18n directory; all green.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.3.33 entry wording (MUL-3889)
Per feedback that the first draft read too verbose for the public
changelog, trim every bullet of the v0.3.33 entry to one short
sentence and drop the supporting clauses that were rehashing
implementation detail (contributor handles, issue numbers, "30s
ticker" specifics, byline of what the rewrite incidentally fixed).
The net effect is a tighter list that matches the cadence of the
v0.3.32 / v0.3.31 entries already on the page.
Applied identically across en.ts / zh.ts / ja.ts / ko.ts.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Slack is not officially launched yet, so the 'already created your app
with an older manifest — add reactions:write and reinstall' guidance is
unnecessary; nobody is running a pre-launch manifest in production. Remove
the warning callout from all four locales (en/zh/ja/ko).
The reactions:write scope in the manifest and the scope table stay, since
the typing indicator still depends on it.
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): add typing reaction on inbound message (MUL-3874)
Mirror the Feishu typing indicator on Slack: react with 👀 on the user's
message when it is ingested, then remove the reaction when the agent's run
finishes (EventChatDone) or fails (EventTaskFailed).
- New slack.TypingIndicatorManager: Add on ingest, Clear on terminal run
events; state keyed by chat_session_id, bot token re-resolved from the DB on
clear (never held in memory), all failures logged and swallowed (best-effort).
- Wire via the channel-agnostic engine.TypingNotifier seam (slackTypingNotifier
in the ResolverSet) — the Router already calls OnIngested off the ACK path.
- Clear subscribes to the event bus directly so a failed run also drops the
reaction (the outbound replier only handles EventChatDone).
- Skip messages older than 2m so Socket Mode reconnect replays don't restamp.
Requires the installed Slack app to hold the reactions:write scope.
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): clear typing reaction when no task runs; document reactions:write (MUL-3874)
Addresses review feedback on the typing-indicator PR.
1. Stuck reaction on offline/archived agent. The debounced flush
(flushChatRun) enqueues no task when the agent has no runtime or is
archived (or on any enqueue/reload error), so no task lifecycle event is
ever published and the bus-driven clear never fires — leaving the 👀 (and
Feishu's Typing) reaction stuck on the user's message. Fix at the shared
engine seam: add TypingNotifier.OnSettled(ctx, sessionID), which the Router
calls from the flush on every no-task exit (before any offline/archived
notice). Both the Slack and Feishu notifiers route it to manager.Clear, so
the latent Feishu case is fixed too. Adds engine coverage (offline/archived
clear, success does not) and a Slack OnSettled test.
2. Missing reactions:write scope in docs. reactions.add/remove silently fail
without the scope, but the BYO app manifest/docs never listed it. Add
reactions:write to the manifest + scope table and a reinstall note across
all four locales (en/zh/ja/ko).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Gemini CLI runtime was removed in MUL-3617 (#4503), but the canonical
"12 built-in providers" list still advertised [Gemini](/providers#gemini)
(a now-dangling anchor) and omitted CodeBuddy, which the daemon actually
auto-detects (config.go probes `codebuddy`). Swap Gemini -> CodeBuddy
across all 16 occurrences (index / how-multica-works / cloud-quickstart /
daemon-runtimes x en/zh/ja/ko); the count stays at twelve.
MUL-3861
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): add v0.3.32 entry for the 2026-06-29 release (MUL-3840)
Lands the daily release notes for v0.3.32 in all four landing locales (en / zh-Hans / ko / ja). Groups today's PRs by Feature / Improvement / Bug Fix with product-oriented wording, keeping technical commit jargon out of the user-facing changelog.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): drop two fixes from v0.3.32 per release-confirmation feedback
Removes the 'deleted agents hidden from usage leaderboard' (MUL-3771, #4637) and 'Antigravity daemon-mode guidance' fix lines from all four locales, leaving five customer-facing fixes for the 2026-06-29 release. The Issue body on MUL-3840 is kept in sync separately.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): drop reverted self-host onboarding beacon from v0.3.32
The anonymous self-host onboarding source beacon (MUL-3708, #4691) was reverted in #4712 because of issues with the collection path. Remove the corresponding feature bullet from all four locales so the v0.3.32 changelog only advertises what actually ships.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): hold Slack BYO app feature back from v0.3.32 user changelog
Per release confirmation, the Slack bring-your-own-app feature is shipping behind a disabled frontend entry for v0.3.32 — code lands, but it is not publicly available yet. Drop the Slack feature bullet from all four locales and rewrite the entry title around what is actually exposed to users (Remove parent Issue + daemon reconnect + attachment preview improvements).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Match the code fix (#4703): the "link your account" link is built from the web
app URL (MULTICA_APP_URL ?? FRONTEND_ORIGIN), which a normal deployment already
sets — not MULTICA_PUBLIC_URL (the backend/API URL). Updates en + zh + ja + ko.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(slack): declutter the Slack connect UI
Trim the Slack bring-your-own-app UI to match the leaner Lark card and
stop burying the setup behind prose nobody reads:
- Drop the "Required bot scopes: …" block from the connect dialog.
- Shorten the Slack integration card description to mirror the Lark
card; the token/admin details stay in the setup docs.
- Remove the dialog intro paragraph and the per-field token hints;
replace the small "Read the setup guide" link with a larger,
more prominent step-by-step guide link.
Removes the now-unused i18n keys (byo_dialog_intro, byo_bot_token_hint,
byo_app_token_hint, byo_scopes_hint) across en/zh-Hans/ja/ko.
* docs(slack): drop the users:read warning callout
The bot manifest already lists users:read as a required scope (with the
bots.info rationale in the scopes table), so the standalone warning
callout was redundant. Removed across en/zh/ja/ko.
* docs: channel integrations overview + Slack bot page + Slack app setup guide (MUL-3666)
- channels.mdx: channel-engine overview with an architecture diagram (Mermaid),
the inbound pipeline, the session/context model, and the authorization gates
(account binding + workspace membership) — all shared by Lark and Slack.
- slack-bot-integration.mdx: the Slack channel page (mirrors lark-bot-integration)
— BYO connect flow, usage (@ in channel / DM / /issue), one-bot-per-agent,
permissions, and self-host (MULTICA_SLACK_SECRET_KEY).
- create-slack-app.mdx: standalone step-by-step — create a Slack app from a
copy-paste manifest, install it, and grab the bot + app-level tokens.
- meta.json: list the three pages under Integrations.
English (canonical) only this pass; zh/ja/ko localization to follow.
Co-authored-by: multica-agent <github@multica.ai>
* docs(slack): inline full manifest + step-by-step setup into the Slack page (MUL-3666)
The Slack page only linked out for setup, which read as too thin. Fold the
complete, code-verified app manifest and the full walkthrough (create from
manifest → install + bot token → app-level token with connections:write →
connect in Multica) directly into slack-bot-integration.mdx, plus a table
explaining what each scope/event is for.
Remove the now-redundant standalone create-slack-app.mdx (its content lives on
the Slack page) and update meta.json + the channels.mdx links accordingly, so
there's one comprehensive Slack page and no duplicated manifest to drift.
Co-authored-by: multica-agent <github@multica.ai>
* docs(i18n): translate channel + Slack pages to zh/ja/ko and add to nav (MUL-3666)
Adds Simplified Chinese, Japanese, and Korean versions of channels.mdx and
slack-bot-integration.mdx, and lists both pages under Integrations in
meta.{zh,ja,ko}.json. The copy-paste manifest YAML and dotenv blocks are kept
byte-identical to the English source across all languages; in-page anchors in
the channel page point at the slug of each translated heading.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): single app-level Socket Mode connection routed by team_id (MUL-3666)
Reshape the Slack adapter from the stage-3 per-installation Socket Mode model
into the multi-tenant B2 connection model: ONE deployment-level Socket Mode
connection (app-level xapp- token, env MULTICA_SLACK_APP_TOKEN) receives the
Events API stream for every installed workspace and routes each inbound event
to its channel_installation by team_id — the existing
GetChannelInstallationByAppID routing, unchanged.
- AppConnector: the single shared connection (slack/app_connector.go). No leader
election — per the design "one (or a few)" connections are fine: each replica
opens one, Slack delivers each event to one of them, and the existing
(installation, message_id) two-phase dedup guarantees exactly-once processing.
Resolves the per-team bot user id (via the same app_id query) to detect/strip
@-mentions, since one connection serves many workspaces.
- Inbound translation (Events API -> channel.InboundMessage) extracted to
slack/inbound.go as free functions parameterized by the per-team bot identity.
- channel.go trimmed to the outbound Send-only sender; per-installation config
(config.go) no longer carries an app-level token — installs hold only the
per-workspace bot token (xoxb-) for outbound, since xapp- can't be OAuth'd.
- engine.Supervisor now skips channel types with no registered Factory, so Slack
installs (driven by the app-level connector, not per-installation channels) no
longer churn the lease/Build loop.
- Wiring: router.go builds the connector when MULTICA_SLACK_APP_TOKEN is set;
main.go runs it alongside the Supervisor. Feishu untouched; channel_* schema
unchanged.
Verified: go build ./..., go vet ./..., gofmt, and
go test ./internal/integrations/... all pass.
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): OAuth self-serve install backend (MUL-3666)
Add the in-product OAuth install flow that creates Slack installations, the
keystone the B2 connector consumes.
- slack.InstallService: Begin (build authorize URL, seal workspace/agent/
initiator into the OAuth state), Complete (verify state, exchange code via
oauth.v2.access, upsert channel_type='slack' install with the bot token
encrypted at rest, auto-bind the installer's Slack id so their first message
is not dropped), plus List/Get/Revoke. State is stateless: sealed with the
deployment secretbox + an embedded expiry, no session store.
- HTTP handlers (handler/slack.go): member-visible list, admin-only begin +
revoke, and the public OAuth callback (recovers context from the sealed state,
redirects the browser back to Settings → Integrations with a result flag).
- Routes + wiring: workspace-scoped list/begin/revoke mirror the Lark
admin/member split; the callback is a public route like GitHub's. Built from
MULTICA_SLACK_CLIENT_ID/SECRET (+ redirect derived from MULTICA_PUBLIC_URL,
override MULTICA_SLACK_REDIRECT_URL; scopes via MULTICA_SLACK_SCOPES).
- Realtime: slack_installation:created / :revoked events.
Verified: go build ./..., go vet, gofmt, and go test ./internal/integrations/slack/...
all pass (new install_test.go covers state sign/verify/expiry/tamper, authorize
URL, code exchange + encrypted upsert + installer bind, and oauth error paths).
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): in-product OAuth install UI for web + desktop (MUL-3666)
Add the "Connect Slack" self-serve install UI mirroring the Feishu/Lark
integration, completing the in-product install half of B2. Slack's OAuth flow
is a redirect (not a device-code QR poll), so the UI is simpler than Lark's.
- core: SlackInstallation / List / Begin types; api.listSlackInstallations /
beginSlackInstall / deleteSlackInstallation; slackKeys + slackInstallationsOptions
query; realtime invalidation on slack_installation:* events.
- views: slack-tab.tsx (SlackTab settings panel + per-agent SlackAgentBindButton
+ connected badge + disconnect confirm). Connect calls beginSlackInstall and
hands the authorize URL to openExternal (system browser on desktop, new tab on
web); Slack bounces to the backend callback which lands the install, and the
realtime event refreshes the list. Wired into the Settings → Integrations tab
and the agent-detail Integrations tab alongside Lark.
- i18n: en + zh-Hans settings.slack.* strings.
Verified: pnpm typecheck (full monorepo, 6/6) and pnpm lint (@multica/core,
@multica/views — 0 errors) pass.
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): outbound Replier + user-binding redeem flow (MUL-3666)
Fill the stage-3 Replier=nil tail so non-installer Slack users can onboard and
get status feedback — completing B2 end to end.
- slack.OutboundReplier (engine.OutboundReplier): on NeedsBinding it mints a
single-use binding token and DMs/replies a "link your account" prompt with the
redeem URL (wrapped as <url|label> so formatMrkdwn doesn't mangle the
base64url token); on AgentOffline/AgentArchived it posts a status notice; on an
/issue-created Ingest it confirms the new issue. Plain chat stays silent (the
agent's own reply lands via EventChatDone). Reuses the bot-token Send path and
reads the installation row from ResolvedInstallation.Platform — no new transport.
- slack.BindingTokenService: Mint + transactional RedeemAndBind over the generic
channel_binding_token / channel_user_binding queries (channel_type='slack'),
mirroring lark.BindingTokenService. 15-min TTL, SHA256-hashed tokens, the
three typed failure modes (invalid/expired, already-assigned, not-member).
- HTTP: POST /api/slack/binding/redeem (public, session-authed) maps the failures
to 410/409/403. NewSlackResolverSet now takes the replier (nil disables it).
- Frontend: /slack/bind redeem page (packages/views/slack + apps/web route) +
api.redeemSlackBindingToken + en/zh slack_bind copy.
Verified: go build ./..., go vet, gofmt, go test ./internal/integrations/...
(new replier_test.go covers all outcome branches + the prompt URL), plus full
pnpm typecheck (6/6) and pnpm lint (0 errors).
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): address review must-fixes — connector leak, team-keyed install, /issue copy (MUL-3666)
Three fixes from Niko's review:
1. AppConnector.connectOnce leaked the Socket Mode goroutine/connection on a
handler error: it ran sm.RunContext on the long-lived ctx and returned the
error without cancelling it, so a transient DB/router error left the old
connection alive (consuming events into an unread channel) while Run opened a
second one. Each connection now runs under its own cancellable context and a
deferred cancel + join tears it down on every exit path before reconnect.
2. Slack re-install collided with the (channel_type, app_id) unique index:
connecting the same Slack team to a different agent failed because the upsert
conflict key was (workspace_id, agent_id, channel_type). Add a team-keyed
UpsertChannelInstallationByAppID (ON CONFLICT on the (channel_type, app_id)
index, updating agent_id) and use it for the Slack OAuth install, so
re-connecting a workspace moves the bot to the chosen agent instead of
erroring. Feishu's per-agent upsert is unchanged.
3. /issue clarified: it is not a registered Slack slash command (no `commands`
scope), so Slack never routes one to us. Issue creation runs through the
message path — `@bot /issue <title>` in a channel or `/issue <title>` in a
DM — which the engine parser handles. Documented in the connector and the
user-facing copy (en + zh).
Verified: go build ./..., go vet, gofmt, go test ./internal/integrations/...,
make sqlc, plus pnpm typecheck (6/6) and pnpm lint (0 errors).
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): make OAuth install transactional — agent-move binding consistency + cross-workspace guard (MUL-3666)
Address Elon's review: the team-keyed upsert kept the same installation row and
only flipped agent_id, but engine session reuse matches purely on
(installation_id, channel_chat_id) and each chat_session is permanently tied to
the agent it was created under — so after moving a Slack team from Agent A to
Agent B, existing DMs/threads kept routing to Agent A; only brand-new
channels/threads reached B. Cross-workspace re-install was worse: the SQL also
moved workspace_id while the application-layer user/chat-session bindings stayed
behind, inheriting the previous workspace's relations.
InstallService.Complete now runs one transaction (lookup → upsert → retire →
installer-bind), all application-layer per the no-FK rule:
- Look up the existing installation by team_id (config->>'app_id').
- Reject a silent cross-workspace ownership change (ErrTeamOwnedByAnotherWorkspace
→ callback redirects with slack_error=team_in_other_workspace). The owning
workspace must disconnect first.
- On an agent change within the same workspace, retire the installation's
chat-session bindings (new DeleteChannelChatSessionBindingsByInstallation) so
the next message creates a fresh session under the new agent. The chat_session
rows are preserved for history; user bindings stay valid (same users/workspace).
- Installer auto-bind moves into the tx; an already-bound-elsewhere id is a
benign skip, a real DB error aborts the whole install.
InstallService now takes a TxStarter; the queries seam gains WithTx (dbInstallQueries
adapter) so Complete stays unit-testable with a fake tx.
Verified: make sqlc, go build ./..., go vet, gofmt, go test ./internal/integrations/...
(new tests: agent-move retire, same-agent no-retire, cross-workspace reject,
fresh-install no-retire).
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): atomic cross-workspace install guard + green up frontend CI (MUL-3666)
Two things: address Elon's review and fix the failing frontend CI job.
Review (atomic cross-workspace guard): the previous guard was a SELECT before
the upsert, which loses the concurrent-OAuth race — two workspaces can both read
no rows, one inserts, the other's ON CONFLICT update then silently re-points the
team. Move the guard into the upsert itself: ON CONFLICT ... DO UPDATE ... WHERE
channel_installation.workspace_id = EXCLUDED.workspace_id, and map the empty
RETURNING (pgx.ErrNoRows) to ErrTeamOwnedByAnotherWorkspace. The pre-SELECT now
only feeds the agent-change cleanup. Also corrected the error copy: a team stays
bound to its first Multica workspace (revoke is soft, keeping the row + unique
index), so migration is an operator action, not "disconnect first".
CI (frontend vitest, @multica/views#test):
- The agent IntegrationsTab now renders the real SlackAgentBindButton, whose
connected badge calls useQueryClient — absent from integrations-tab.test.tsx's
react-query mock. Hoisted the owner/admin gate above the per-platform sections
(one role notice instead of one per platform), made the agents members_note
generic (en/zh/ja/ko), and updated the test (mock @multica/core/slack, stub
SlackAgentBindButton, assert both platforms).
- Added slack-tab.test.tsx covering the real SlackAgentBindButton / SlackTab.
- locale parity: added the slack (settings) + slack_bind (common) blocks to ja
and ko so every EN key has a translated counterpart.
Verified: make sqlc, go build ./..., go vet, gofmt, go test ./internal/integrations/...;
pnpm --filter @multica/views test (1478 pass), pnpm typecheck (6/6), pnpm lint (0 errors).
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): surface agent-page Slack entry points when Lark is off (MUL-3666)
The agent-detail Integrations tab and the inspector's Integrations section
only considered Lark, so a Slack-only deployment (Lark disabled) showed neither
the Integrations tab nor a Connect-Slack button — the per-agent entry points
were unreachable.
- agent-overview-pane: gate the Integrations tab on Lark OR Slack configured
(new slackInstallationsOptions query), not Lark alone.
- agent-detail-inspector: render SlackAgentBindButton alongside LarkAgentBindButton
in the Integrations section.
- regression test: the Integrations tab appears when only Slack is configured.
Verified: pnpm typecheck (6/6), pnpm --filter @multica/views test (1478+ pass),
pnpm lint (0 errors).
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): BYO-app install backend — paste xoxb+xapp, per-app install keyed by real app id (MUL-3666)
Adds the bring-your-own-app install path so multiple agents can each have
their own bot identity in the SAME Slack workspace (hosted B2 caps at one
agent/workspace). User pastes their app's bot token (xoxb-) + app-level
token (xapp-); we validate the bot token via auth.test, parse the real
Slack app id from the xapp- token, encrypt both tokens, and persist a
per-app installation keyed by that app id (real 'A…' ids never collide
with hosted 'T…' team ids in the existing unique index — no schema change).
- config.go: add app_token_encrypted (BYO discriminator + per-app socket token)
- install.go: extract shared persistInstall (atomic cross-ws guard + agent-move retire)
- byo_install.go: RegisterBYO + auth.test + app-id parse
- handler + route: POST /api/workspaces/{id}/slack/install/byo (admin-only)
- tests: keying, encryption, invalid tokens, auth.test failure, cross-ws, agent move
Follow-ups (separate commits): per-app Socket Mode connector that consumes
the stored app token; in-product BYO install dialog (video + paste form).
Co-authored-by: multica-agent <github@multica.ai>
* refactor(slack): drop OAuth, unify on BYO per-installation model (MUL-3666)
Per product decision, Slack drops the hosted-app OAuth path entirely and
unifies on bring-your-own-app (BYO): every installation carries its OWN
app-level token and gets its OWN Socket Mode connection, so multiple agents
can each have a distinct bot identity in one Slack workspace.
- Remove OAuth install (Begin/Complete/code-exchange/sealed state/OAuthConfig/
default scopes), the OAuth callback + begin handlers + routes, and the
MULTICA_SLACK_CLIENT_ID/SECRET/REDIRECT/APP_TOKEN env wiring.
- Replace the single deployment-level AppConnector with a per-installation
slackChannel (authenticated with its own xapp- token) registered as a channel
Factory, so the engine Supervisor drives one Socket Mode connection per
installation (exactly like Feishu). inbound/outbound/resolvers reused as-is.
- Route inbound by the event's api_app_id (== the installation's real app id),
not team_id.
- InstallService slims to at-rest encryption + the shared persistInstall +
list/get/revoke; install is the BYO paste path only (byo_install.go).
- Tests: drop the OAuth tests; slack + handler + engine all green.
Follow-up (frontend): replace the OAuth "Connect Slack" button with the BYO
paste dialog (the begin endpoint it calls is now gone).
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): verify BYO bot + app tokens are from the same app, and the app token is live (MUL-3666)
Niko review: RegisterBYO only parsed the app id from the xapp string and
auth.test'd the bot token, so pasting app A's bot token with app B's app
token would 'connect' but be broken (inbound on B's socket, outbound with
A's identity). Now: resolve the bot's owning app id via bots.info (on the
bot_id from auth.test) and require it to equal the xapp's app id; and live-
validate the app token via apps.connections.open. Reject (no persist) on
mismatch or a dead app token.
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): in-product BYO install dialog (paste bot + app tokens) (MUL-3666)
The OAuth begin endpoint was removed server-side, so the "Connect Slack"
button now opens a dialog where the admin pastes the bot token (xoxb-) and
app-level token (xapp-) of the Slack app they created, and submits to the
BYO install endpoint. Includes an optional setup-video link (URL constant,
left empty until the walkthrough is recorded).
- core: drop beginSlackInstall / BeginSlackInstallResponse; add
registerSlackBYO + RegisterSlackBYORequest.
- views: SlackAgentBindButton opens the BYO dialog; refreshed comments and
install_supported docs (now means "configured", no OAuth).
- i18n: new slack.byo_* keys + refreshed page_description in en/zh-Hans/ja/ko.
- tests: dialog submit path; views vitest (1479), typecheck, lint, locale
parity all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): Elon review — team_id routing guard, per-agent reconnect, users:read hint (MUL-3666)
1. Inbound routing keys on api_app_id (the APP, not the Slack workspace), so
additionally require the event's team_id to match the installation's stored
team. A distributed BYO app installed into another Slack workspace emits the
same app id and would otherwise mis-route to this Multica installation.
Extracted installationServesTeam() + unit test.
2. BYO install is now agent-keyed (UpsertChannelInstallation, conflict on
workspace_id+agent_id+channel_type): one bot per agent. Disconnect →
reconnect a NEW app for the SAME agent now UPDATES that agent's row in place
instead of violating the (workspace, agent, channel) unique. A unique
violation on the (channel_type, app_id) routing index → ErrTeamOwnedByAnother-
Workspace (the app is already connected to another agent/workspace). No
chat-session retire is needed: a row's agent_id never changes.
3. UX: bots.info (the same-app check) needs the users:read scope — the connect
dialog now lists the required bot scopes including it, and the error text
says so.
Backend build/vet/gofmt/test + views vitest + typecheck + locale parity green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): publish slack_installation:created on BYO connect; refresh stale comments (MUL-3666)
Niko final review: RegisterSlackBYO wrote the response but never published
EventSlackInstallationCreated, so only the installer's own tab refreshed —
other open clients (Settings, Agent Integrations, other tabs) did not see the
new bot in realtime, inconsistent with the revoke event and Lark. Now publishes
it on success via a small publishSlackInstallationCreated helper, with a unit
test (Bus.Publish is synchronous).
Also refreshed comments that still described the removed hosted-OAuth /
single deployment-level AppConnector model (handler SlackInstall field,
channel.go / inbound.go / outbound.go / byo_install.go). PR title updated
separately to the BYO per-installation Socket Mode model.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): add v0.3.31 entry for the 2026-06-26 release (MUL-3748)
Covers the cross-workspace inbox unread dot in the switcher (MUL-3695), the
Composio Go SDK foundation (MVP), per-worktree desktop dev isolation
(MUL-3724), and the new reusable VideoEmbed with the zh docs intro video.
Bug fixes include the editor Tab list-indent / focus-keeping behavior
(MUL-3697), squad leader briefing now keyed by task flag (MUL-3730) plus
the inherited @mention reply skip (MUL-3744), code-block selection
stability during background re-renders (MUL-3621), local handoff-note
version gate for direct agent assigns, comment-edit save loading state
(MUL-3709), search API response parsing, and an actionable error when
self-host hosts are missing Docker Compose v2.
Localized into en / zh-Hans / ko / ja with product-language wording per the
`Issue`-only exception (`agent` -> "agent" / 智能体, `Squad` -> "squad" / 小队).
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.3.31 entries per review (MUL-3748)
Per Bohan's review on MUL-3748: the v0.3.31 copy was too wordy. Shorten
every bullet to a single user-facing sentence and drop the internal
details (worktree mechanics, signed webhooks, hljs spans, account-level
summary call, etc.). en / zh / ko / ja all updated; product-language
wording and the Issue / 智能体 / 小队 rule are preserved.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): isolate pnpm dev:desktop per worktree (MUL-3724)
Two worktrees could not run pnpm dev:desktop at once: both grabbed the
renderer port 5173 and the single-instance lock keyed by the app name
"Multica Canary". The env hooks to override each already existed
(DESKTOP_RENDERER_PORT in electron.vite.config.ts, DESKTOP_APP_SUFFIX in
src/main/index.ts) but nothing derived per-worktree values.
A new dev launcher (scripts/dev.mjs) derives both from the worktree path
for linked worktrees only — reusing the same cksum%1000 offset as
scripts/init-worktree-env.sh, so renderer port is 5173+offset and the app
becomes "Multica Canary <folder>" with its own userData/lock. The primary
checkout is untouched; explicit env vars still win. Backend targeting is
unchanged (apps/desktop/.env*). Also: brand-dev-electron honors the suffix,
turbo globalEnv passes it through, and CONTRIBUTING documents the flow.
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): make worktree dev port/suffix collision-safe (MUL-3724)
Addresses code review on #4598:
- Renderer port base 5173 → 5174 so a worktree whose offset is 0 (e.g.
cksum("/tmp/multica-3494") % 1000 === 0) no longer collides with the
primary checkout's default 5173.
- DESKTOP_APP_SUFFIX is now "<folder>-<offset>" instead of just the folder
name, so worktrees that share a basename at different paths (or names that
slug to the same fallback) get distinct single-instance locks. Without it
the second Electron was still blocked by the shared lock.
- Tests: offset-0 port guard, and same-basename-different-path disambiguation.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(docs): add reusable VideoEmbed and embed intro video on zh homepage
Add a provider-agnostic, click-to-load <VideoEmbed> component (Bilibili now,
YouTube reserved) and embed the Chinese intro video (BV1cv7Y6gEg7) at the top
of the Chinese docs homepage. The facade renders on first paint; the
third-party player iframe only mounts on user click, so first paint pays
nothing for an external player or its trackers. Registered in both docs MDX
component maps so it is reusable on any docs page.
Scope is zh docs only — no usecase, no other locales, no analytics, no video
hosting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs(zh): drop duration from intro video title
Use the duration-free title "Multica 中文介绍视频" for the homepage VideoEmbed
instead of a minute-count phrasing. Copy accuracy only; no component, layout,
or provider changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs: document MULTICA_<PROVIDER>_ARGS default agent argument env vars
The backend default-args env-var layer (MULTICA_CLAUDE_ARGS / MULTICA_CODEX_ARGS /
MULTICA_CODEBUDDY_ARGS) shipped in #1807 but was never added to the docs site
environment-variables page. Document the variable, its precedence relative to
per-agent custom_args, POSIX shell-word parsing, and the shared blocked-flags
filter. Closes the docs follow-up requested in #1467.
* docs: refine MULTICA_<PROVIDER>_ARGS wording and sync zh/ja/ko translations
Reword the English section so daemon-wide default args read as a default
baseline rather than a hard ceiling (per-agent custom_args are appended
afterward and can override), and drop the uncertain --max-budget-usd example.
Sync the new env var row and section into the zh/ja/ko docs pages.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs: add 2026-06-24 changelog entry
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): refine 2026-06-24 entry wording and terms
- Surface the flagship features in the title (Feishu collaboration channel
upgrade + feature rollout) instead of leading with an improvement and a
vague "runtime rollout" phrase
- Fix glossary term: Autopilot -> 自动化 (was 自动任务) in zh
- Make Feishu naming consistent within the entry (was mixing 飞书/Lark)
- Reconcile cross-language mismatch (Gemini CLI removal + Qoder/CodeBuddy/
Antigravity guidance now stated the same in all locales)
- Replace internal/jargon phrasing with product language across en/zh/ja/ko
MUL-3640
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
* docs: tidy agent runtime provider pages, add per-runtime FAQ (MUL-3617)
- Remove the Gemini CLI provider from install-agent-runtime and providers
across all four languages (Google folded the standalone CLI into
Antigravity). Update tool counts 12 -> 11 and the dependent
session-resumption, MCP, and skill-path sections.
- Add the Hermes profile custom_args workaround as a per-runtime FAQ note
under providers#hermes (supersedes #4497, which placed it in agents-create).
- Fix stale Japanese install copy that claimed only Claude Code reads
mcp_config and linked to a non-existent anchor.
Co-authored-by: multica-agent <github@multica.ai>
* docs: add Qoder and CodeBuddy runtimes to provider pages (MUL-3617)
Document the two newly added runtimes on install-agent-runtime and
providers across all four languages:
- Qoder (Alibaba): ACP-over-stdio CLI `qodercli`, shares the transport
with Hermes/Kimi/Kiro; session/resume, ACP mcpServers, dynamic model
discovery, native skills at .qoder/skills/.
- CodeBuddy (Tencent): Claude Code-compatible CLI `codebuddy`, driven via
stream-json; --resume, --mcp-config, dynamic models, .claude/skills/.
Update tool counts 11 -> 13 and the MCP section (now ten of thirteen
consume mcp_config; the other three still ignore it).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs: add 2026-06-23 changelog entry
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): sharpen 0.3.28 title and handoff wording
- Headline the two flagship features in the title: staged Sub-Issues and Qoder runtime support
- Rewrite the vague agent-handoff line to spell out the pre-trigger confirmation (whether/which agent will start, apply without running) and the handoff note as next-run context
- Apply across en/ja/ko/zh
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
* feat(landing): show live GitHub star count on the header GitHub button
Add a small client hook (useGithubStars) that fetches stargazers_count
from the GitHub API and a formatStarCount helper that renders it in
GitHub's compact repo-header style (e.g. "37.6k"). The landing header's
GitHub button now appends a star badge (faint divider + filled star +
count) on both the desktop and mobile menu entries.
Fetched client-side on purpose: LandingHeader is shared across every
marketing page, so one client fetch covers them all without threading a
server value through each render site, and each visitor calls the API
from their own IP, sidestepping the shared-outbound-IP rate limit the
server-side github-release fetcher works around with a PAT. The result
is memoized at module scope (plus in-flight dedupe); a failed fetch
caches null and the button degrades to the plain "GitHub" label.
* fix(landing): drop the star glyph from the GitHub star badge
In the GitHub button context the number already reads as the star count,
so the icon is redundant. Keep the divider + count only.
* feat(issues): stage sub-issues so the parent wakes per stage, not per child
Sub-issues under a parent can be grouped into ordered stages (issue.stage).
The child-done -> parent notification + assignee wake now fire only when a
stage barrier closes: every sub-issue in the lowest unfinished stage has
reached a terminal status (done/cancelled). An unstaged sibling set is one
implicit stage, so the parent is woken once when the last sub-issue finishes
instead of on every child — the default fix for the fire-on-every-child
cascade reported in discussion #4320 / MUL-3508.
Stage advancement stays agent-driven: the server only detects the closed
barrier and wakes the parent assignee, who decides whether to promote the
next stage.
- DB: nullable issue.stage (CHECK >= 1) + sqlc regen
- API: stage on issue create/update/response and batch update
- CLI: `issue create`/`issue update` --stage; new `issue children` command
that lists sub-issues grouped by stage (table + json)
- stageBarrierClosed / stageProgressSummary in issue_child_done.go, with the
wake comment now stage-aware, plus unit tests
- skill docs (multica-working-on-issues SKILL.md + source map)
Web UI (create-form stage picker, sidebar edit, group-by-stage display) is a
follow-up; the API already returns stage for it to consume.
MUL-3508
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): address review on stage barrier (cancel, batch, unstaged)
Resolves the three blockers from the PR review:
1. Cancel can close a stage. The child-done barrier now fires on any
non-terminal -> terminal transition (done OR cancelled), not just done.
isTerminalChildStatus already treats cancelled as terminal (a cancelled
sibling never finishes, so it must not hold a stage open), so a cancelled
last-open child now closes its stage and wakes the parent. Keying on the
transition also makes a later cancelled -> done edit a no-op, avoiding a
lagging duplicate wake.
2. Batch update of stage no longer no-ops. `hasMutation` now includes
"stage", so `{"updates":{"stage":N}}` persists instead of returning
{"updated": 0}.
3. Unstaged children no longer participate in the staged frontier. In a
staged sibling set, NULL-stage children neither hold a stage open nor fire
on their own completion, and the wake comment no longer renders "Stage 0".
This matches migration 123 ("NULL does not participate in staged
grouping") and the CLI's separate unstaged group, removing the footgun
where an unstaged backlog child silently blocked Stage 1.
Tests: cancellation closes a stage (staged + unstaged), unstaged ignored in a
staged set, stage summary skips unstaged, and a stage-only batch update
persists.
MUL-3508
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): stage UI — create picker, sidebar edit, group sub-issues by stage
Frontend for the sub-issue stage feature (web + desktop, shared via packages):
- core: `stage` on the Issue type + create/update request types; zod
IssueSchema parses it (defaults to null for older backends) with schema
tests for the numeric and omitted cases.
- StagePicker component (mirrors the other property pickers): "No stage" +
Stage 1..N, offering one beyond the current/sibling max.
- Create-issue modal: a Stage pill, shown only when a parent is selected,
threaded into the create payload.
- Issue detail sidebar: an editable Stage row + "add property" entry, gated to
sub-issues (issues with a parent).
- Sub-issue list grouped by stage with per-stage headers (flat when unstaged).
- i18n: stage keys across en / zh-Hans / ja / ko (parity test passes).
Verified: full typecheck (6/6), core (591) + views (1433) vitest suites,
lint clean (no new findings). Backend/CLI shipped earlier in this PR.
MUL-3508
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): add stage to Issue fixtures merged from main
The merge brought in new Issue fixtures that predate the required `stage`
field: core issues/batch.test.ts, views batch-action-toolbar.test.tsx, and
the mobile EMPTY_ISSUE_FALLBACK sentinel. Add `stage: null` so they satisfy
the Issue type (mobile reuses core's IssueSchema for parsing, so only the
sentinel needs it).
MUL-3508
Co-authored-by: multica-agent <github@multica.ai>
* fix(web): feed StagePicker the sibling max stage so higher stages stay selectable
The StagePicker accepts maxStage to extend its option list beyond the
floored Stage 1-3, but neither call site passed it, so a parent with an
existing Stage 4/5 child could not pick that stage when creating a new
sub-issue or editing one in the sidebar.
- Compute the sibling max stage at both call sites: the create modal now
loads the parent's children (childIssuesOptions) and the detail sidebar
reuses the already-loaded parentChildIssues.
- Extract maxSiblingStage + stageOptions as pure helpers on stage-picker
and unit-test them (the regression: a Stage 5 sibling keeps Stage 5
selectable and offers Stage 6).
MUL-3508
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Add a Discord invite (https://discord.gg/W8gYBn226t) in three places:
- Website footer: social icon + link in the Resources group (en/zh/ja/ko)
- In-app help menu: Discord item in the help launcher (all 4 locales)
- GitHub repo README: badge + link (README.md and README.zh-CN.md)
MUL-3492
Co-authored-by: multica-agent <github@multica.ai>
* fix(auth): autofocus OTP input on verification step
The email-verification step renders the OTP input without focus, so
users must click the field before typing the code. This is friction on
every login, especially when switching accounts.
Add `autoFocus` to the InputOTP so the cursor lands in the field as
soon as the step mounts. Mirrors the existing email-step input and the
mobile OTP component, both of which already autofocus.
* test(web): polyfill document.elementFromPoint for input-otp in jsdom
Autofocusing the OTP input makes input-otp run its focus-time DOM
measurement, which calls document.elementFromPoint. jsdom doesn't
implement it, so the web login test threw an unhandled error.
packages/views/test/setup.ts already stubs this for the same reason;
mirror the stub in the web test setup (which already stubs
ResizeObserver for input-otp).
* test(auth): assert OTP input autofocuses on verification step
Guards the autofocus behavior: the test fails if the autoFocus prop is
removed from the verification-step InputOTP. Lives in packages/views
since it covers shared component behavior, mocking @multica/core.
* feat(web): add opt-in react-grab dev element inspector
Loads the react-grab overlay (hold ⌘C / Ctrl+C + click to copy an
element's source path + component stack) only when REACT_GRAB is set in
a local, gitignored apps/web/.env.local. Both the NODE_ENV and REACT_GRAB
guards are evaluated server-side in the root layout, so the <Script> tag
is omitted from the HTML for anyone who hasn't opted in — no effect on
other developers or production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(desktop): add opt-in react-grab dev element inspector
Mirrors the web wiring for the Electron renderer: injects the react-grab
overlay (hold ⌘C / Ctrl+C + click to copy an element's source path +
component stack) only when VITE_REACT_GRAB is set in a local, gitignored
apps/desktop/.env.development.local. Guarded by import.meta.env.DEV so the
branch is tree-shaken out of production builds; never activates for other
developers. No CSP/sandbox blocks the unpkg script (webSecurity is off).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): unify react-grab opt-in var to VITE_REACT_GRAB
Use the same env var name as the desktop renderer so one variable name
controls both apps. The desktop renderer is bundled by Vite, which only
exposes VITE_-prefixed vars to client code, so the shared name must carry
the VITE_ prefix; web reads it server-side where the name is unconstrained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(daemon): discover local skills from ~/.agents/skills (MUL-3333)
Upgrade local skill discovery and import from a single provider root to an
ordered multi-root scan: the runtime's own skill directory (e.g.
~/.claude/skills) first, then the cross-tool universal root ~/.agents/skills.
- Rename localSkillRootForProvider -> localSkillRootsForProvider, returning
ordered roots [provider, universal] with a kind classifier.
- listRuntimeLocalSkills iterates the roots, gives each root its OWN visited
set (so a cross-root symlink alias is not collapsed), dedupes strictly by
Key with the provider root winning, and sorts once after the merge.
- loadRuntimeLocalSkillBundle walks the same priority order and only falls
through to the next root on os.IsNotExist; any other stat error is returned
so import never silently resolves a different same-key skill.
- Add a Root ("provider" | "universal") field to the local skill summary
(daemon + handler structs and the TS RuntimeLocalSkillSummary type) so the
UI can label a skill's origin without a future schema break.
Backward compatible: every skill visible today keeps its Key, SourcePath and
FileCount; the universal root only surfaces additional, non-conflicting skills.
Out of scope (follow-up issues): execution-time injection of ~/.agents/skills
into runtimes (e.g. Codex seedUserCodexSkills) and workspace-relative
.agents/skills discovery.
Tests cover universal-root discovery + import, provider-wins conflict
priority, both-roots merge, missing/both-missing roots, nested layouts,
IsNotExist fallback, the no-fallthrough-on-read-error guarantee, and the
per-root visited cross-root symlink alias. Docs updated in en/zh/ja/ko.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): fall through to next root when a same-key dir has no SKILL.md
loadRuntimeLocalSkillBundle previously only fell through to the next root on
os.IsNotExist for the skill DIRECTORY. A provider-root directory that shares a
skill's key but contains no SKILL.md (so listRuntimeLocalSkills descends past
it and surfaces the universal-root skill instead) made load stop on the
invalid provider dir and error — list and load disagreed, and the import the
user picked from the list could not be fetched.
Make the validity predicate match list: a root "has" the skill at a key only
when it is a directory containing a SKILL.md. A missing entry, a non-directory,
or a directory without a SKILL.md all mean "this root doesn't have it" and we
continue to the next root. Only a genuine non-IsNotExist stat error or an
unreadable existing SKILL.md (permission/IO) is returned, so we still never
silently substitute a different-content same-key skill from a lower-priority
root (Eve review #1, preserved by the existing read-error guard test).
Adds regression tests for the provider-dir-without-SKILL.md and provider-non-dir
fall-through cases.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): coerce commit-hash versions to valid semver
normalizeGitVersion checked only the first character (/^\d/) to tell a real
version from a bare commit hash. A hash beginning with a digit (e.g.
'2f24057b') passed that check and was stamped as the app version, but bare
'2f24057b' is not valid semver, so electron-updater threw on launch.
Require a full major.minor.patch prefix; anything else (including a
digit-leading hash) falls back to 0.0.0-<hash> as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(desktop): prefix bare-hash fallback with `g` for valid semver
normalizeGitVersion coerced an untagged build's bare commit hash into
`0.0.0-<hash>`, but that is not guaranteed valid semver: an all-digit
short hash with a leading zero (e.g. `0123456`) produced `0.0.0-0123456`,
and a numeric semver pre-release identifier must not have a leading zero.
electron-updater then threw on launch for exactly the untagged builds
this fallback exists to protect.
Prefix the hash with `g` (mirroring `git describe`'s own `g<hash>`
shorthand) so the pre-release is always a single alphanumeric identifier.
Add a regression test for the all-digit leading-zero case.
Addresses PR #4183 review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(autopilot): default subscriber template (MUL-2533) — server
Add per-autopilot member subscriber template that fans out to every
issue the autopilot spawns. New autopilot_subscriber table; extend
issue_subscriber.reason with 'autopilot' so the dispatch-time fanout
is distinguishable from manual subscriptions.
API: POST/PATCH /api/autopilots accept a `subscribers` array (member
user_type only for the first version); PATCH semantics are full-replace.
GET returns subscribers on the detail endpoint; the list endpoint omits
them to avoid an N+1.
Dispatch: dispatchCreateIssue lists the template inside the same tx as
the issue insert and writes the rows with reason='autopilot' before
EventIssueCreated fires, so notification listeners see the full
subscriber set on the first event.
Co-authored-by: multica-agent <github@multica.ai>
* feat(autopilot): default subscriber template (MUL-2533) — frontend
New SubscriberMultiSelect picker (members-only search + chips) wired
into the create / edit AutopilotDialog. The detail page renders the
saved template as read-only chips; edits flow through the dialog.
TS types expose the new `subscribers` field on Autopilot, plus an
AutopilotSubscriberInput shape for the create/update wire payloads.
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilot): notify template subscribers on issue creation (MUL-2533)
The autopilot create-issue path fans out template subscribers into
issue_subscriber inside the same tx as the issue insert, but the
issue:created notification listener only matches handler.IssueResponse
payloads and only direct-notifies the assignee + @mentions. The autopilot
publishes a map[string]any payload, so the listener falls through and the
template subscribers never receive an inbox item for the creation event —
breaking OQ3 ("reason='autopilot' subscribers receive all subscription
events, consistent with reason='manual'").
Fix it where the divergence lives: in dispatchCreateIssue, right after
EventIssueCreated fires, write an inbox_item (type='issue_subscribed',
severity='info') for each member subscriber and publish EventInboxNew so
the recipient's inbox WS feed updates in real time. The write is after
the tx commit so an inbox hiccup can't roll back the issue; failures are
logged, not propagated. The manual path is unchanged — manual subscribers
don't exist at creation time, so there is nothing to notify there.
Adds a new InboxItemType 'issue_subscribed' (en/zh labels) and two
covering tests in autopilot_subscriber_test.go: one asserts the inbox
row lands for a template subscriber on dispatch, the other asserts the
no-subscriber autopilot stays silent.
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilot): align subscriber PR with current main
Co-authored-by: multica-agent <github@multica.ai>
* fix autopilot subscriber template transaction
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>