Wires the Phase 1 terminal.Manager into the live daemonws transport so a
browser tab on the Multica Desktop app can attach to a PTY running in
the daemon's task workdir. End-to-end frame path:
Desktop (xterm.js) → /ws/issues/{id}/terminal (server proxy, cookie
or first-frame JWT auth, workspace membership
enforced before upgrade)
→ daemonws hub (new SendToRuntime + TerminalRouter routing
terminal.* frames back to the proxy by
request_id, then re-keyed on session_id)
→ daemon (new terminal_bridge.go owns one Manager per WS
connection, drains PtySession.Output() into
terminal.data frames, surfaces ExitC as
terminal.exit; closeAll on WS disconnect)
→ terminal.Manager.OpenWithInfo (new method) — server resolves
task.work_dir/issue_id/prior_session_id from its DB and embeds
them on the protocol; daemon trusts the server payload, no
daemon-local task cache needed.
Auth + ACL:
- Browser proxy enforces workspace membership before the upgrade.
- TerminalOpenPayload carries the resolved task info; cross-workspace
is structurally impossible at the bridge layer because both
OpenParams.WorkspaceID and TaskInfo.WorkspaceID come from the same
server-resolved field.
- terminal_bridge maps terminal.{Manager.OpenWithInfo} errors to the
protocol error codes (workspace_mismatch / task_not_found /
unsupported_os / spawn_failed / internal).
Resume:
- Server passes prior_session_id; daemon injects CLAUDE_SESSION_ID +
MULTICA_{WORKSPACE,ISSUE,TASK,USER}_ID into the PTY env per the RFC.
`claude --resume \$CLAUDE_SESSION_ID` continues the agent's session.
Lifecycle:
- Daemon installs a fresh terminalBridge per daemonws connection and
tears every PtySession down on disconnect; session_ids minted on one
WS cannot be reused on a reconnect because the server-side routing
registration is also gone.
- daemonws client.send buffer raised from 16 to 256 and read limit
from 4KB to 64KB so PTY traffic fits without evicting connections
used for heartbeat / wakeup hints.
Desktop UI:
- packages/views/issues/components/terminal-panel.tsx renders an
xterm.js console with FitAddon, base64 wire encoding, ResizeObserver
→ terminal.resize, reconnect button, and a clear web-only placeholder
when window.desktopAPI is absent.
- TerminalPanelSection wrapper hangs collapsed in the issue-detail
sidebar next to the execution log so bootstrap doesn't run for
every issue view.
Tests:
- terminal/manager_test.go: OpenWithInfo happy path + cross-workspace
reject (16 tests total, all -race clean).
- daemonws/terminal_test.go: TerminalRouter request_id → session_id
re-keying and unknown-session drop.
- daemon/terminal_bridge_test.go: server-supplied workdir round-trips
through OpenWithInfo, missing-workdir surfaces task_not_found and
never spawns, data+exit round trip.
- GOOS=windows go test -c clean for both daemon and daemon/terminal.
Phase 1 / 2 / 3 / 4 mapping unchanged: Phase 3 (CLI) and Phase 4
(execenv GC hook + issue runs entry + audit log) are still untouched.
Co-authored-by: multica-agent <github@multica.ai>
- Manager.Close concurrent re-entry now blocks late callers on closeDone
so every Close() return shares the "manager drained" guarantee.
- Open cleanup path on lost race with Close calls pty.Wait() to reap the
child synchronously (waitLoop never runs there).
- Tests: concurrent Close callers all observe drained state; Open cleanup
invokes pty.Wait at least once.
Co-authored-by: multica-agent <github@multica.ai>
Round 2 review fixes:
1. PtySession finalize sequence is now
ExitC -> close(output) -> onClose/deregister -> close(done)
so external waiters (bridge / GC hook / audit) can `<-Done()` and
immediately query the manager without a race window.
2. Manager.Close now waits for each session's Done() (not just Close())
so by the time it returns the registry is empty and every session
is fully finalized.
Adds TestSession_DoneFiresAfterDeregister (locks the ordering contract)
and TestManager_CloseWaitsForSessionFinalize (fakePTY.Wait delay proves
Manager.Close blocks through finalize).
Co-authored-by: multica-agent <github@multica.ai>
Wires in the four fixes Emacs flagged on the Phase 1 review:
1. Lifecycle: split stop/done with a WaitGroup. readLoop and idleLoop
exit via <-stop; waitLoop is the finalizer that waits on the WG
before closing output/done. Eliminates the "send on closed channel"
race when the output buffer is saturated. Adds a regression test
that fills output, calls Close, and verifies Done converges + ExitC
fires before output closes (the doc contract).
2. Errors: Manager.Open wraps spawner errors with double-%w so
errors.Is matches both ErrSpawnFailed and ErrUnsupportedOS. Adds a
test with a fake spawner that returns ErrUnsupportedOS.
3. Close path on unix: SIGHUP to the process group, 250ms grace,
SIGKILL, then close fd — comment now matches behavior. Skips the
signal+sleep work entirely when the child already exited naturally.
Manager.Close fans out per-session Close in parallel so the grace
period doesn't multiply by session count.
4. IdleTimeout semantics: removes the NewManager default that
silently rewrote 0 to 60min. Zero/negative now disables, per the
doc comment. Added DefaultIdleTimeout for daemon wiring to opt in
explicitly.
Verified: go test, go test -race, GOOS=windows go test -c.
Co-authored-by: multica-agent <github@multica.ai>
Daemon-side foundation for the Issue → Terminal feature. Manager owns
the lifecycle of all live PtySessions; sessions spawn a shell on a real
PTY via creack/pty (unix-only — Windows returns ErrUnsupportedOS until
ConPty support lands).
Open enforces the cross-workspace ACL — a client acting in workspace A
cannot attach to a task that belongs to workspace B. Each session
injects CLAUDE_SESSION_ID + MULTICA_{WORKSPACE,ISSUE,TASK,USER}_ID into
the child env so `claude --resume $CLAUDE_SESSION_ID` continues the
same session the agent run was using.
Adds the terminal.* WebSocket message types to server/pkg/protocol so
Phase 2 (daemonws routing) and Phase 3 (CLI) can land without touching
the manager.
Tests cover open, data round-trip, resize, explicit close, idle timeout
sweep, manager shutdown, cross-workspace rejection, and unknown task.
A fake Spawner backed by channels lets tests exercise lifecycle without
forking a real shell.
Co-authored-by: multica-agent <github@multica.ai>
Before: dispatchCreateIssue copied autopilot.created_by_type/id onto the
new issue's creator_type/creator_id, and the same fields were used as the
ActorType/ActorID of the issue:created event. Result: any issue spawned by
an autopilot was reported as created by the human who first configured
the autopilot, not by the agent that actually owns the work. Downstream
subscriber/activity/notification listeners inherited the same wrong actor.
After: creator and actor are both the autopilot's assignee agent
(creator_type=agent, creator_id=ap.assignee_id). The human owner is still
recoverable via origin_type=autopilot + origin_id.
Audited the other ap.created_by_* usages: analytics attribution
(autopilotActorID, task.go user-id), and the private-agent visibility
gate in shouldSkipDispatch — all correctly read the autopilot's owner,
not the executor, so they stay as-is.
Co-authored-by: multica-agent <github@multica.ai>
Extends the workspace /usage page Daily tokens chart toggle from
Tokens | Cost to Tokens | Cost | Time | Tasks, so users see daily
run-time and task-count trends alongside spend without leaving the page.
- New SQL `ListDashboardRunTimeDaily`: per-date totals from
agent_task_queue (terminal tasks only), scoped to workspace and
optionally project. Same time anchor as ListDashboardAgentRunTime
so day boundaries line up.
- New handler GET /api/dashboard/runtime/daily + TanStack Query option.
- New DailyTimeChart (single-series, smart h/m/s unit) and
DailyTasksChart (completed + failed stacked).
- Empty-state is per-metric so a workspace with tokens but no terminal
runs (or vice-versa) doesn't get a false "no data".
- i18n: en + zh-Hans daily.metric_time / metric_tasks + titles.
Co-authored-by: multica-agent <github@multica.ai>
* feat(views): show Total in daily token/cost chart tooltips (MUL-2282)
Add a Total row at the bottom of the daily-tokens-chart and daily-cost-chart
tooltips so users can see the precise stack sum on hover, in addition to the
per-stack breakdown.
Implemented by extending shared ChartTooltipContent with an optional `footer`
prop (ReactNode | (payload) => ReactNode) that renders below the items with a
top divider; backwards-compatible (no behavior change when footer is omitted).
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): i18n Total label in chart tooltips (MUL-2282)
Lint rule i18next/no-literal-string flagged the hardcoded "Total" string
in daily-cost-chart and daily-tokens-chart tooltips. Move it to
runtimes.charts.tooltip_total and read via useT.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(daemon): force-stop hung agent runs via idle watchdog (MUL-2281)
A backend whose subprocess hangs on a stuck child process (e.g. claude
blocked on `docker ps` against a frozen dockerd) keeps the daemon's run
record at status="running" until the full DefaultAgentTimeout (2 h)
expires, because cmd.Wait() never returns and Session.Result is never
written. MUL-2225 spent 17+ minutes in this state in the wild.
Add a per-task idle watchdog around executeAndDrain:
- Wrap the caller's ctx so a single cancel propagates to the agent
subprocess (via the ctx passed to backend.Execute) AND the drain loop.
- Stamp lastActivityAt every time the drain loop receives a message.
- Tick at window/2; when idle_for >= window AND session.Messages buffer
is empty, set a fired flag and call cancel.
- Tag the resulting Result.Status as "idle_watchdog" so runTask routes
it through a dedicated failure_reason instead of "agent_error".
Default window is 5 min, configurable via MULTICA_AGENT_IDLE_WATCHDOG;
set to 0 to disable. Tests cover the activity-then-silence case, the
zero-message case, the disabled case, and the happy path.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): skip idle watchdog while a tool call is in flight
A legitimate long-running tool call (npm install, docker build, test
suite) can sit silent between tool_use and tool_result for many minutes.
Without this gate, the watchdog would yank the agent mid-build.
Track unmatched tool_use messages in an atomic counter; only let the
watchdog fire when the counter is zero. tool_result clamps non-negative
so a stray result with no matching use can't re-arm the watchdog one
call too early.
Adds two regression tests:
- DoesNotFireDuringInFlightToolCall: tool_use -> silence past
window -> tool_result -> completed (must NOT fire)
- FiresAfterToolResultIfBackendStaysSilent: tool_use -> tool_result
-> silence past window (MUST fire — backend really is stuck)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(daemon): auto-update CLI when idle (MUL-2100)
Add a periodic poller that checks GitHub for a newer multica release
every hour and self-updates when the daemon is idle, reusing the same
brew-or-download upgrade path the Runtimes-page "Update" button already
runs.
- Refactor handleUpdate to call a shared runUpdate(target) helper so
both server-triggered and auto-triggered upgrades go through the same
brew detection + atomic replace + restart.
- New autoUpdateLoop gates each tick on: opt-out flag, Desktop launch
source, dev-build version, an in-flight update, and active tasks. The
idle gate guarantees we never interrupt a running agent — busy ticks
silently retry at the next interval.
- Config: MULTICA_DAEMON_AUTO_UPDATE=false to disable (also via
--no-auto-update), MULTICA_DAEMON_AUTO_UPDATE_INTERVAL to retune the
poll period.
- IsNewerVersion / IsReleaseVersion helpers in the cli package, with
tests covering patch/minor/major bumps, dev-describe strings, and
malformed input.
- Daemon-side tests cover every skip path (updating, active tasks,
fetch failure, no-newer) plus the success path that fires
triggerRestart while keeping the updating flag held to the end.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): close idle race + verify checksum in auto-update (MUL-2100)
Two issues raised in PR #2679 review:
1. The first idle check in tryAutoUpdate only ran before the release-metadata
fetch, so a poller that won the claim race during the fetch could end up
handing handleTask a task that triggerRestart was about to cancel via root-
ctx cancellation. Add a strict claim barrier: runRuntimePoller now
tryEnterClaim()s before ClaimTask, and tryAutoUpdate flips pauseClaims
under claimMu only after observing claimsInFlight + activeTasks == 0.
Pollers that were already mid-claim hold claimsInFlight > 0, so the barrier
refuses to engage and the update defers to the next tick.
2. The direct-download path replaced the running binary with whatever bytes
GitHub returned, without checking checksums.txt. Pull the manifest first,
buffer the archive, and reject on SHA-256 mismatch before extraction. The
GoReleaser config already publishes checksums.txt; we just consume it.
Also tighten parseReleaseVersion so it stops accepting dev-describe shapes
like "v0.1.13-5-gabcdef0" through the patch trim, matching its docstring.
The auto-update loop already guards on IsReleaseVersion, but the lenient
parser was a footgun and the existing test name even said "not newer" while
asserting the opposite.
Tests:
- TestTryAutoUpdate_DefersWhenClaimInFlightAtBarrier (new race coverage)
- TestTryAutoUpdate_HoldsBarrierAcrossRestart / ReleasesBarrierOnUpgradeFailure
- TestTryEnterClaim_RespectsBarrier
- TestFindChecksumManifestAsset / TestParseChecksumManifest / TestVerifyAssetSHA256
- TestIsNewerVersion: dev-describe cases now expect false (matches docstring)
Co-authored-by: multica-agent <github@multica.ai>
* chore(daemon): default auto-update poll interval to 6h (MUL-2100)
1h was overly chatty for a release that lands at most a few times a week.
Operators who want a different cadence can still set
MULTICA_DAEMON_AUTO_UPDATE_INTERVAL or --auto-update-interval.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(views): progressive disclosure for issue sidebar properties (MUL-2275)
Split sidebar Properties into a core group that always renders
(status / priority / assignee / labels) and an optional group
(due_date / project / parent) that only appears when the issue has
the value set or the user explicitly added it via a new
"+ Add property" picker. A field cleared in-session stays visible
to avoid row flicker; navigating to a different issue reseeds
visibility from that issue's set fields. The standalone "Parent
issue" card is folded into Properties as one of those optional
rows. Adds `defaultOpen` to DueDatePicker / ProjectPicker so a
newly-added row drops the user straight into edit state.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(views): swap sidebar optional set to due_date + labels
Per design feedback: status / priority / assignee / project / parent
are all required and should always render in the sidebar; only
due_date and labels are progressive-disclosure optionals. Move project
and parent rows out of the optional block (drop their +Add property
menu entries and the parent special-case in addOptionalProp). Move
labels into the optional block, gated on the issue's actual attached-
label count (queried via issueLabelsOptions), with defaultOpen wired
through LabelPicker so picking "Labels" from +Add property drops the
user straight into the picker. Tests updated for the new split.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(views): restore standalone parent card, move priority to optional
Parent goes back to its own collapsible section, rendered only when the
issue actually has a parent — matching the pre-MUL-2275 behavior. It is
no longer interleaved with Properties rows.
Priority joins the progressive-disclosure set (priority / due_date /
labels). New issues default to priority "none", so the row is hidden
until set or added via "+ Add property", and PriorityPicker gains
defaultOpen so the field drops straight into edit state when chosen
from the add-property menu.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issue-detail): tighten Add-property popover visual rhythm
Picked up a small visual inconsistency while reviewing the PR's UI:
the "Add property" dropdown floated above the inspector at a noticeably
larger type scale than the property rows, and each item was bare text
while the rows it sat above all rendered with an icon + value pair.
Tweaks:
- Items: `text-sm py-1.5` → `text-xs py-1`, matching the inspector
row typography and trimming row-to-row gap from 12px to 8px.
- Each option leads with the icon the resulting picker uses
(`PriorityIcon` bars / `CalendarDays` / `Tag`) so the dropdown reads
as a preview of what will appear in the new PropRow.
- Focus indicator: replace the default thick focus ring with
`focus-visible:bg-accent + outline-none`, matching the hover state
language — keyboard focus and mouse hover now look the same.
- Popover width: `w-48` → `w-44` since the labels are short and the
visual is now denser; still leaves room for translated strings.
* fix(issue-detail): dismiss Add-property popover when an option is picked
Base UI's `Popover` doesn't auto-dismiss when a child is clicked (it's
not a Menu primitive), so picking an option left the "+ Add property"
popover sitting behind the picker that auto-opens for the newly added
row — two popovers visibly stacked.
Make the Popover controlled with a local `addPropPopoverOpen` state and
close it inside `addOptionalProp` right after enqueuing the row's
auto-open. The picker still pops on mount via `defaultOpen={autoOpenProp
=== key}`, so the user flow is unchanged from their perspective:
Click "+ Add property" → menu opens
Click an option → menu closes AND target picker opens
(Was the same flow on paper before; just had the orphan popover behind
the picker.)
---------
Co-authored-by: multica-agent <github@multica.ai>
Both create dialogs were too wide at 5xl (1024px). Align with the
codebase convention for full create dialogs (create-project,
create-issue expanded) which use max-w-4xl (896px). Keeps both
modals consistent.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(views): refine navigation progress bar with brand color and glow (MUL-2269)
The previous 1px bg-primary bar read as near-black on light theme and
snapped on/off in a single frame, which felt abrupt despite being a small
visual element. Switch to a 2px brand-colored sweep with right-edge glow,
slower 1.4s cubic-bezier easing, and a 200ms fade-out so completion
doesn't pop.
- Container: h-px → h-0.5 (2px); always mounted with opacity-driven fade
- Bar: bg-primary → bg-brand + two-layer box-shadow glow via color-mix
- Keyframe: 1.1s ease-in-out → 1.4s cubic-bezier(0.4, 0, 0.2, 1)
Zero new design tokens (reuses existing --brand) and zero tailwind config
changes. Desktop unaffected — same component, same prefetch=no-op path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): unmount nav progress sweep when hidden (MUL-2269)
Hiding the bar with opacity-0 left the inner element's `infinite` keyframe
animation running on every dashboard page, defeating the perceived-perf goal.
Mount the sweep only while navigating, plus the 200ms fade tail (unmount on
opacity transitionend), so nothing animates while hidden.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs(squad): address plan-review feedback for archive + role plan
Resolve the 4 items the reviewer raised on MUL-2265:
1. TS schema: declare `active_issue_count` as optional (`number | null | undefined`)
so list/create/update Squad responses don't lie about their shape; only
`getSquad` parses through SquadSchema.
2. Archive semantics: restrict TransferSquadAssignees to active issues
(status NOT IN done, cancelled) so dialog count and SQL operate on one set
and terminal-state issues keep their historical assignee.
3. Index assumption: corrected — `idx_issue_assignee (assignee_type,
assignee_id)` exists and is sufficient at realistic squad cardinality;
no new index needed.
4. Fixed `*int64` test comparison and added `.loose()` to SquadSchema per
the local schemas.ts convention.
Co-authored-by: multica-agent <github@multica.ai>
* docs(squad): plan v3 — revert to count-all/transfer-all on archive
Reviewer round 2 surfaced two structural problems with plan v2's
active-only carve-out:
1. useActorName resolves squad names via ListSquads, which filters
archived_at IS NULL. A closed issue with an archived-squad assignee
would render as "Unknown Squad".
2. The status-only update path in UpdateIssue skips validateAssigneePair,
so a done/cancelled issue with an archived-squad assignee could be
reopened to in_progress, violating the "no active issue on an archived
squad" invariant enforced elsewhere.
Both problems disappear by reverting to count-all + transfer-all: after
ArchiveSquad runs, no issue points at the archived squad, so neither
case can occur. The product trade-off is that closed historical issues
now show the leader agent instead of the archived squad in their
"Assigned to" badge — consistent with existing agent-level reassignment
behavior elsewhere in the product.
Field rename: active_issue_count -> issue_count.
TransferSquadAssignees SQL is unchanged (already transfers all).
Co-authored-by: multica-agent <github@multica.ai>
* docs(squad): add Task 2b — wrap DeleteSquad transfer + archive in one tx
Reviewer round-3 flagged that the v3 invariant ("after archive no
issue points to the squad") was asserted on the happy path only.
DeleteSquad's current best-effort impl breaks it two ways:
- transfer failure → slog.Warn but archive proceeds (Unknown Squad,
reopen-into-archived-squad bugs reappear)
- archive failure after a committed transfer → 500 with squad still
active but emptied
Task 2b rewrites DeleteSquad to run TransferSquadAssignees +
ArchiveSquad inside one pgx tx, mirroring the project.go:266-314
pattern. Publish moves below Commit. Adds two regression tests that
lock both partial-write failure modes.
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): replace native confirm() with AlertDialog and rewrite role editor as combobox
Backend:
- Add CountIssuesForSquad sqlc query (counts every issue assigned to a squad,
no status filter — matches the existing transfer-all archive semantics).
- Extend SquadResponse with optional `issue_count` (`*int64` + omitempty,
populated only by GetSquad to avoid an N+1 in the list endpoint).
- Wrap DeleteSquad's transfer + archive in a single pgx transaction so the
v3 invariant ("after archive, no issue points to the squad") is durable
rather than best-effort. Promote slog.Warn to slog.Error and check the
parseUUIDOrBadRequest ok flag (silent zero-UUID was a #1661-class latent
bug). Publish only after Commit so realtime never sees rolled-back state.
- Tests cover happy path (count, transfer-all including terminal statuses)
and both rollback directions (transfer fail / archive fail) via a
fault-injecting tx wrapper.
Frontend:
- Extend Squad TS type with `issue_count?: number | null` (optional —
list/create/update legitimately omit it). Add SquadSchema with `.loose()`
and wrap getSquad with parseWithFallback so older servers and count-error
responses degrade to the dialog's "no count" copy variant.
- Replace `window.confirm()` with shadcn `ArchiveSquadConfirmDialog`
(destructive variant, leader name + count + closed-issue caveat in the
copy, Loader2 while pending). i18n keys added under squads.archive_dialog.
- Rewrite RoleEditor as a Popover + Command combobox: Pencil affordance is
always visible, suggestions aggregate other members' roles, commit only
on Enter or selecting a suggestion (blur discards), per-member savingId
drives Loader2 so the spinner only renders on the row being saved.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): discard RoleEditor draft on close and no-op blank Enter
Two reviewer findings on e0d754bf:
1. Closing the Popover (outside click, Esc, trigger re-click) left `query`
in state, so reopening + Enter would commit the stale draft. Clear
`query` on every non-saving close path.
2. With an existing role, opening the editor and pressing Enter on an
empty input committed "" — `commit` only no-op'd when trimmed matched
value. Treat blank Enter as a no-op; clearing a role would need an
explicit clear action that doesn't exist yet.
Add two regression tests:
- close (via outside click) → reopen surfaces a clean input; Enter does
not commit the stale draft
- blank Enter on an existing role does not call onSave
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): add explicit Clear button to RoleEditor
Role is optional, but the previous fix turned blank Enter into a no-op
without exposing any other way to clear an existing role — that broke a
valid terminal state. Keep blank Enter as no-op; add a "Clear role"
button at the bottom of the popover that only renders when value is
non-empty and routes through onSave("").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Internal navigation on web feels laggy because clicking a sidebar link blocks
0.2–0.6s with zero visual feedback — no prefetch, no Suspense fallback in the
dashboard segment, and no React transition to mark the route commit as pending.
This change adds the three pieces App Router needs to make the click→commit
window feel instant, scoped to the (dashboard) segment so auth/landing keep
their existing chrome:
- NavigationAdapter gains an optional prefetch(path). The web adapter wires
it to router.prefetch; desktop leaves it undefined (react-router has no
equivalent and doesn't need one). AppLink prefetches on hover/focus and
preserves caller-supplied onMouseEnter/onFocus/onClick.
- NavigationProvider wraps push/replace in useTransition and exposes the
pending flag via useIsNavigating(). Every useNavigation().push caller —
sidebar AppLink, command palette, post-create modal jumps — picks this up
automatically.
- New apps/web/app/[workspaceSlug]/(dashboard)/loading.tsx renders a minimal
skeleton during cold transitions inside the dashboard segment only.
- DashboardLayout renders a 1px top progress bar driven by useIsNavigating.
packages/views remains free of next/* imports; desktop is unaffected by
construction (no prefetch, transition flips quickly, no loading.tsx).
Co-authored-by: multica-agent <github@multica.ai>
* feat(task): wire claim lease queries into TaskService and sweeper (MUL-2246)
- ClaimTask now uses ClaimAgentTaskWithLease (generates claim_token + lease)
- StartTask accepts optional claim_token for token-verified start
- AgentTaskResponse includes claim_token for daemon to use
- Daemon client sends claim_token in StartTask body
- Sweeper calls RequeueExpiredClaimLeases each tick
- Legacy daemons without claim_token still work (graceful fallback)
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): address PR #2662 review blockers (MUL-2246)
1. ClaimAgentTaskForRuntime: push runtime_id into atomic SQL WHERE clause
so runtime A cannot claim tasks queued for runtime B under the same agent.
2. Legacy StartAgentTask: add claim_token IS NULL guard so leased rows
cannot be started without token verification. Handler rejects malformed
tokens with 400 instead of silently degrading to legacy path.
3. StartAgentTaskWithClaimToken: validate claim_expires_at >= now(),
preserve claim_token until terminal state (only clear claim_expires_at),
use CTE + UNION ALL for idempotent retry when daemon resends after a
lost StartTask response. Return 409 Conflict on token mismatch/expiry.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): StartTask 409 handling, transport retry, claim_token on FailTask (MUL-2246)
- StartTask 409 (claim superseded): release slot, don't call FailTask
- StartTask transport timeout/5xx: retry once with same token, then
check task status before failing
- FailTask now sends claim_token; server-side FailAgentTask SQL adds
AND (claim_token IS NULL OR claim_token = @claim_token) guard so
stale daemons cannot fail tasks that have been re-claimed
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): close FailTask token bypass and RequeueExpiredClaimLeases liveness gap (MUL-2246)
Blocker 1 - FailTask token validation:
- SQL: change (param IS NULL OR claim_token = param) to
(param IS NULL AND claim_token IS NULL) OR claim_token = param
so tokenless requests can only fail legacy (tokenless) rows.
- task.go: malformed claim_token now returns ErrInvalidClaimToken (400)
instead of being silently dropped to NULL.
- Handler: maps ErrInvalidClaimToken→400, ErrClaimTokenInvalid→409.
- Service: when UPDATE returns no rows but task is still active,
return ErrClaimTokenInvalid (token mismatch) instead of silent success.
Blocker 2 - RequeueExpiredClaimLeases runtime liveness:
- SQL: JOIN agent_runtime, only requeue tasks where runtime is 'online'.
Dead/offline runtime tasks stay dispatched for FailTasksForOfflineRuntimes.
- FOR UPDATE → FOR UPDATE OF atq (required with JOIN).
Regression tests:
- task_claim_token_test.go: malformed, tokenless-on-tokened, wrong-token
- requeue_lease_test.go: SQL must JOIN agent_runtime with online filter
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): move expired lease requeue to ClaimTaskForRuntime preflight, add heartbeat freshness backstop (MUL-2246)
- Add RequeueExpiredClaimLeasesForRuntime: per-runtime preflight self-requeue
in ClaimTaskForRuntime. Runtime proves liveness by actively claiming, so no
heartbeat check needed.
- Update global RequeueExpiredClaimLeases to require ar.last_seen_at freshness
(stale_threshold_secs param). Prevents requeuing to a dead runtime in the
90s gap between lease expiry (60s) and offline detection (150s).
- Add regression tests verifying the heartbeat freshness check and that the
preflight query does not join agent_runtime.
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): use LivenessStore for global requeue, move preflight before empty-cache (MUL-2246)
Blocker 1: Global RequeueExpiredClaimLeases now uses LivenessStore.IsAliveBatch
to verify runtimes are truly alive before requeuing expired leases. When
LivenessStore is unavailable (no Redis), global requeue is skipped entirely —
the preflight self-requeue in ClaimTaskForRuntime handles live runtimes. This
closes the 60-150s gap where a dead runtime still appears online in DB.
Blocker 2: Moved RequeueExpiredClaimLeasesForRuntime BEFORE EmptyClaim.IsEmpty
fast-path in ClaimTaskForRuntime. Expired leases are now requeued (which bumps
the empty cache via notifyTaskAvailable) before the empty check can
short-circuit the claim path.
Also adds ListRuntimesWithExpiredClaimLeases SQL query and LivenessChecker
interface on TaskService.
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): wire EmptyClaimCache into backend taskSvc for backstop requeue (MUL-2246)
The backend taskSvc used by the sweeper only had Liveness wired but not
EmptyClaim. When global backstop requeue called notifyTaskAvailable,
s.EmptyClaim.Bump() was a nil no-op — the handler's empty-cache was never
invalidated, so the daemon's next claim hit a stale empty verdict.
Fix: wire the same Redis-backed EmptyClaimCache into the backend taskSvc
in main.go (same Redis keys as router.go:139 handler instance).
Add regression test verifying backstop requeue invalidates the handler's
empty-cache.
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): global backstop must not requeue — alive runtimes use preflight, dead stay dispatched (MUL-2246)
- RequeueExpiredClaimLeases is now a no-op (returns 0 always)
- Alive runtimes self-requeue via ClaimTaskForRuntime preflight
- Dead runtimes stay dispatched for FailTasksForOfflineRuntimes
- Rewriting to queued on dead runtime creates 2h blackhole (offline
sweeper only handles dispatched/running)
- Test actually calls RequeueExpiredClaimLeases and asserts 0 in all cases
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): remove duplicate usage reporting block after merge conflict (MUL-2246)
The merge resolution introduced a second ReportTaskUsage call after the
status check, duplicating the usage-before-early-return block that already
runs right after runner.run. Remove the duplicate and add a regression test
asserting /usage is called exactly once on the normal completion path.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Add claim_token + claim_expires_at columns to agent_task_queue and three
new SQL queries for the claim lease protocol:
- ClaimAgentTaskWithLease: generates a UUID token and sets a lease expiry
when claiming a task, so the daemon must prove it received the response
- StartAgentTaskWithClaimToken: validates the token on StartTask, preventing
stale daemons from starting requeued tasks
- RequeueExpiredClaimLeases: moves dispatched tasks with expired leases back
to queued for re-claim
This closes the reliability gap where a claim response lost in transit
leaves a task stuck in dispatched until the 60s dispatch timeout fires.
Co-authored-by: multica-agent <github@multica.ai>
Each consecutive run of activities renders as a single "N activities"
summary by default. Clicking expands the block in place. Comments are
unaffected; the most recent activity block stays expanded so users see
"what just happened" without a click.
Refs MUL-2188
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
* docs(email): clarify 888888 is opt-in via MULTICA_DEV_VERIFICATION_CODE; document SMTP option in self-host docs
The startup log line, .env.example, and SELF_HOSTING_ADVANCED.md still
implied that the dev master code 888888 is auto-active whenever
APP_ENV != "production". That has not been true since the master code
was gated behind MULTICA_DEV_VERIFICATION_CODE — the fixed code is
disabled by default and must be opted in explicitly.
Also extend the docs site with the SMTP relay backend added in #1877:
auth-setup, environment-variables, and self-host-quickstart now cover
both Resend and SMTP options in EN and ZH.
Co-authored-by: multica-agent <github@multica.ai>
* docs(email): treat SMTP as an email backend in self-host docs and startup warning
Address review feedback on #2666:
- server: startup warning now fires only when both RESEND_API_KEY and SMTP_HOST
are empty, since either one is a valid email backend. Otherwise the log
mis-tells SMTP-only operators that verification codes go to stdout.
- self-host-quickstart (EN/ZH): tell readers to fetch the verification code
from whichever backend they configured (Resend or SMTP); fall back to
stdout only when neither is configured.
- auth-setup (EN/ZH): \"without Resend\" → \"without any email backend
configured\" so the wording stays correct now that SMTP is a first-class
option.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(realtime): include actor_type in WebSocket broadcast messages
The WS broadcast message format was {type, payload, actor_id} but missing
actor_type. This meant the web UI could not distinguish agent from human
operations in real-time events at the top level.
While payload data for comments (author_type) and activities (entry.actor_type)
already included the type, the top-level message did not — causing the web UI
to display agent CLI operations as human operations when relying on the
broadcast actor identity.
Changes:
- server/cmd/server/listeners.go: add actor_type to all broadcast messages
- packages/core/types/events.ts: add actor_type to WSMessage interface
- packages/core/api/ws-client.ts: pass actor_type to event handlers
- packages/core/realtime/hooks.ts: update EventHandler type signature
- packages/core/realtime/provider.tsx: update EventHandler type signature
Fixes MUL-2260
Co-authored-by: multica-agent <github@multica.ai>
* test: add frame-shape unit test asserting actor_type in WS frames
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(deps): refresh pnpm-lock.yaml after #2665 added test deps to core
#2665 (MUL-2256, fix(realtime)) added `@testing-library/react` and
`react-dom` to `packages/core/package.json` devDependencies, plus moved
`react` from dependencies → devDependencies, but didn't commit the
regenerated lockfile. CI runs `pnpm install` with --frozen-lockfile
(implicit in CI envs), which bails immediately:
ERR_PNPM_OUTDATED_LOCKFILE: pnpm-lock.yaml is not up to date with
packages/core/package.json
* 2 dependencies were added: @testing-library/react@catalog:,
react-dom@catalog:
Frontend CI has been red on main since 7c8cf929. Backend is fine
because Go doesn't share the lockfile.
Lockfile delta is small (+9 / -3): the only changes are the three
specifier blocks for the deps already declared in package.json. No
version upgrades, no transitive churn — `pnpm install` produced an
identical resolved tree minus the missing entries.
* fix(core): name the test wrapper component to satisfy react/display-name
Same source of CI red as the lockfile bump in this PR — #2665 also
introduced packages/core/realtime/use-realtime-sync-ws-instance.test.tsx
where `createWrapper` returned an anonymous arrow component. The
`react/display-name` lint rule (enforced as error in core) flagged it,
and once `pnpm install` was unblocked the next CI step fell through to
this lint failure.
Convert the inline arrow into a named `function Wrapper(...)` —
identical render output, satisfies the rule.
Verified: `pnpm --filter @multica/core lint` → 0 errors (was 1).
The 4 tests in this file still pass.
* fix(realtime): invalidate workspace queries on WSClient instance change
When switching workspaces, the old WSClient is torn down and a new one
is created. Events emitted during the transition are lost because
onReconnect only fires for reconnections within the same instance.
Add an effect that tracks the WSClient instance via useRef and, on
detecting a non-initial new instance, invalidates all workspace-scoped
queries (same set as onReconnect). The first assignment is skipped to
avoid redundant refetches on initial mount.
Closes multica-ai/multica#2562
Co-authored-by: multica-agent <github@multica.ai>
* refactor(realtime): extract shared invalidation helper + add ws instance test
- Extract invalidateWorkspaceScopedQueries() to deduplicate the
invalidation key list shared by onReconnect and ws-instance-change effects
- Add hook test covering: first ws skip, null gap no-op, new instance
invalidates exactly once, same instance no re-invalidation
Addresses review nits from PR #2665.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(email): add SMTP relay as alternative to Resend
Self-hosted deployments often run behind a corporate firewall with an
existing SMTP relay (Exchange, Postfix, sendmail) and no access to
external SaaS APIs. Resend requires a public domain, an API key, and
outbound HTTPS to api.resend.com — all unavailable in air-gapped or
private-network setups.
This adds a second email delivery path using Go's stdlib net/smtp,
activated when SMTP_HOST is set. Priority order:
1. SMTP relay (SMTP_HOST set)
2. Resend API (RESEND_API_KEY set)
3. DEV stdout (neither set)
New env vars (all optional, no breaking change):
SMTP_HOST — SMTP server hostname
SMTP_PORT — port, default 25
SMTP_USERNAME — for authenticated SMTP; empty = unauthenticated relay
SMTP_PASSWORD — used only when SMTP_USERNAME is set
SMTP_TLS_INSECURE — set to "true" to skip TLS cert verification
(for private CA / self-signed certs)
The implementation:
- Dials TCP, creates smtp.Client manually (avoids smtp.SendMail which
does not expose TLS config)
- Tries STARTTLS if advertised; uses InsecureSkipVerify only when
SMTP_TLS_INSECURE=true (opt-in, nolint:gosec annotated)
- Applies PlainAuth only when SMTP_USERNAME is non-empty
- Wraps all errors with context for easier debugging
- Reuses existing HTML templates from buildInvitationParams for
invitation emails (no template duplication)
Also updates .env.example and docker-compose.selfhost.yml with the
new variables and inline documentation.
* fix(email): add dial timeout, session deadline, RFC headers for SMTP path
Address review blockers from multica-eve and Bohan-J (PR #1877):
- net.Dial → net.DialTimeout(10s) + conn.SetDeadline(30s) so a blackholed
SMTP relay cannot hang SendVerificationCode (called synchronously from the
auth handler) or leak goroutines in the invitation path.
- Add Date, Message-ID, and proper Content-Transfer-Encoding headers.
Date is required by RFC 5322; many strict relays reject messages without it.
Message-ID aids deliverability and threading.
- MIME-encode Subject via mime.QEncoding so non-ASCII workspace/inviter names
(CJK, emoji) survive without corruption across any RFC 2047-conformant relay.
- Probe 8BITMIME after (possible) STARTTLS: use Content-Transfer-Encoding 8bit
when the relay advertises 8BITMIME, quoted-printable otherwise — safe for
all relay configurations without forcing base64 overhead.
- Update SELF_HOSTING_ADVANCED.md to document Option B (SMTP relay) alongside
the existing Resend section, including all five env vars and a note that
port 465/SMTPS is not yet supported.
* fix(email): correct has8Bit assignment order (bool is first return of Extension)
handleTask had two early-return paths that ran before ReportTaskUsage:
the cancelledByPoll select and the post-run GetTaskStatus check. Both
silently discarded any usage accumulated by the agent — and both
claude.go and codex.go populate Result.Usage even when runCtx is
cancelled mid-run, so cancelled tasks consistently under-reported tokens.
Hoist ReportTaskUsage to run immediately after the runner returns,
before any early-return path. Add a taskRunner interface seam and a
cancelPollInterval field so tests can inject a fake runner and trigger
the poll-cancellation path on a 10ms ticker without spawning real agents.
Two regression tests cover both leak windows:
- TestHandleTask_ReportsUsageBeforeCancel: post-run /status returns
"cancelled"; usage must be reported before the status check.
- TestHandleTask_ReportsUsageWhenCancelledByPoll: poll goroutine fires
first and cancels runCtx; runner returns usage on Done; assert
poll-status precedes usage (proving the cancelledByPoll branch was
the one exercised, not the post-run path).
Sanity-checked: reverting only the ReportTaskUsage hoist fails both
tests with the original "tokens lost" message.
MUL-2258
Co-authored-by: Jiang Bohan <bhjiang@outlook.com>
Co-authored-by: multica-agent <github@multica.ai>
Without the full [@Name](mention://<type>/<UUID>) syntax, the platform
does not trigger the target agent. Add an explicit, strongly-worded
hard rule at the top of the list so the leader model never forgets.
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): accept avatar_url on CreateSquad
Threads avatar_url through the SQL query, sqlc-generated code, and the Go
handler so the create-squad flow can persist an avatar at creation time
instead of forcing a follow-up PATCH.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar_url to CreateSquadRequest
Extends the TS contract for the new backend field so the frontend can pass
an uploaded avatar URL through api.createSquad.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(squads): rework Create Squad modal to match CreateAgentDialog (MUL-2233)
Replaces the cramped small-dialog flow with the same large-dialog shape used
by Create Agent: identity row (AvatarPicker + name + description with char
counter), grouped Leader picker (My Agents first, then Workspace Agents),
and a new multi-select Additional Members picker covering agents and
workspace members. The members trigger collapses to "+N" once more than
three are selected; promoting an agent to leader auto-drops it from the
additional-members list.
After createSquad, additional members are attached via Promise.allSettled
so a single failure surfaces a warning toast without blocking navigation —
the squad still exists and the user can retry from the Members tab.
Adds packages/views/modals/create-squad.test.tsx covering identity binding,
leader-group ordering, leader/member conflict sanitization, the empty- and
partial-failure success paths, and the create-failure recovery path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): valid trigger HTML + drop conflicted leader from members
Two issues from PR #2645 review:
1. AdditionalMembersPicker's PopoverTrigger was a <button> containing
MemberChip's remove <button>, which React/HTML flags as nested
interactive content (hydration + a11y warning). Render the trigger as
a <div role="combobox"> via Base UI's render prop so the chip's
remove button is valid.
2. sanitizedMembers only hid the leader from rendered/submitted output,
so promoting an additional member to leader then switching leader
away resurrected the hidden pick. Drop it from selectedMembers at
the moment of promotion via handleLeaderChange; sanitizedMembers is
no longer needed.
Adds a test that promotes → switches leader and asserts the member is
not resubmitted.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Backend now validates http/https/ssh/git scheme plus scp-like
`git@host:owner/repo.git` shorthand, but three repo URL inputs were
still `type="url"`. The browser's native URL validation rejected scp
shorthand with "Please enter a URL" before the value could reach the
backend.
- Switch the three inputs to `type="text"` so submission isn't blocked
client-side (project resources picker, workspace repositories tab,
create-project repo picker).
- Extend the en/zh placeholders to show a scp shorthand example
alongside the existing https one.
- Add a repositories-tab test that types `git@github.com:...` and
asserts the input is text-type, passes native validity, and reaches
the update mutation.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): accept SSH repo URLs for github_repo resources (#2484)
The project resource validator rejected anything that wasn't http(s), so
workspace repos configured with an SSH remote (ssh:// or the scp-like
`git@host:owner/repo.git` shorthand) could not be attached to a project.
Both forms are valid git remotes and the daemon hands the URL straight to
`git clone`, so the API has no reason to require https specifically.
Relax the validator to accept http/https/ssh/git schemes and the scp-like
shorthand, while still rejecting pasted garbage (no scheme, missing host,
missing path, ftp://, file://, etc.).
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): reject scp-like URLs with '@' after ':' to avoid panic
isValidGitRepoURL indexed '@' and ':' independently, then sliced
s[at+1 : colon]. For inputs without '://' where '@' appears after the
first ':' (e.g. `host:org/repo@branch`), `at+1 > colon` triggered a
slice-bounds panic instead of a 400. Guard the slice and treat such
inputs as malformed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): disable Claude AskUserQuestion in non-interactive mode (MUL-2244)
GitHub #2588: when Claude Code calls its built-in AskUserQuestion tool
inside the daemon's stream-json runtime, the question never reaches the
user — there's no UI to render it — so the SDK returns an empty answer
and the agent silently "infers" and continues. From the issue's
perspective, execution looks stuck while the agent is actually charging
ahead on its own guess.
Two-part fix:
- `buildClaudeArgs` now passes `--disallowedTools AskUserQuestion` so
the tool is not exposed to the model at all.
- The Claude-specific runtime brief tells the agent to use a `blocked`
issue comment for genuine clarification, or to state an explicit
assumption and proceed.
Adds a regression test that pins both: AskUserQuestion is forbidden in
CLAUDE.md and is NOT mentioned in the AGENTS.md emitted for non-Claude
providers (the tool is Claude-specific).
Co-authored-by: multica-agent <github@multica.ai>
* refactor(daemon): drop CLAUDE.md AskUserQuestion guidance, rely on --disallowedTools
The --disallowedTools flag already prevents Claude from invoking
AskUserQuestion, so duplicating the rule in the runtime brief just bloats
the prompt without changing behavior. Removes the section and its
regression test; the argv-level test in pkg/agent already pins the flag.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Adds a regression test for `anthropic/claude-opus-4.7-20251001` that
exercises all three resolvePricing tolerances at once (provider strip,
Claude dot→dash, date trim). Each step was already covered pairwise;
this nails down their composition so a future change to candidate
ordering can't silently drop a step.
Follow-up to #2654 (MUL-2243); raised in second review.
Co-authored-by: multica-agent <github@multica.ai>
Copilot's `meta.agentMeta.model` reports Claude SKUs with dots
(`claude-opus-4.7`, `claude-sonnet-4.6`, ...), and openclaw / opencode
emit the `<provider>/<model>` form (`anthropic/claude-opus-4.7`). The
maintained MODEL_PRICING table only keys on Anthropic's canonical
dashed form (`claude-opus-4-7`), so every Copilot-routed turn was
falling through to the "Custom model pricing" dialog and silently
contributing $0 to cost totals.
Teach `resolvePricing` two new tolerances, in order before date stripping:
1. Strip a leading `<provider>/` segment — that's routing metadata,
not part of the SKU.
2. For `claude-*` IDs only, normalize dots to dashes. Scoped to
Anthropic because for OpenAI the separator is semantic (`gpt-5.4`
is a distinct SKU from a hypothetical `gpt-5-4`).
Custom pricing still wins over nothing, but the maintained catalog
still wins over a stale custom override (existing invariant preserved
by the test suite).
Co-authored-by: multica-agent <github@multica.ai>
The chat header dropdown was capped at max-w-80 while the trigger
could grow unbounded with the current chat title, so the popup
appeared narrower than the trigger and titles inside were truncated
early. Cap the trigger at max-w-96 and let the popup inherit the
trigger width via --anchor-width with the same upper bound, so the
two stay visually consistent and only truncate at extreme lengths.
Co-authored-by: multica-agent <github@multica.ai>
Sidebar "新建 issue" button, command palette "New Issue", and the `c`
shortcut all hard-coded which create modal to open, ignoring the
persisted lastMode in useCreateModeStore. Pressing `c` after switching
from agent → manual reverted to agent on the next open.
Add `openCreateIssueWithPreference(data?)` helper next to the store.
Generic entries call it; entries that pre-seed manual-only fields
(status, project_id, parent_issue_id from board / list / project /
sub-issue actions) keep opening "create-issue" directly because agent
mode does not honour those seeds.
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): silent background auto-download for updates (MUL-2224)
Flip electron-updater to autoDownload=true so new releases are pulled in
the background without user action; the UI now only surfaces a
"ready to install" prompt once the package is fully downloaded.
- updater.ts: autoDownload=true; update-downloaded forwards version +
releaseNotes; single-flight guard around checkForUpdates() so startup,
periodic, and manual triggers don't pile up overlapping downloads.
- preload: update-downloaded payload now carries { version, releaseNotes? }.
- update-notification.tsx: drop available/downloading UI; ready state has
Later / Restart now and renders the version from the download event.
- updates-settings-tab.tsx: settings copy now describes background download
+ restart prompt instead of a download prompt.
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): swallow unhandled downloadPromise rejection in updater (MUL-2224)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(execenv): native OpenClaw skill discovery via per-task config
MUL-2213 stopped lying about native discovery and routed openclaw skills
to .agent_context/skills/ — a path openclaw's scanner never reads.
Multica skills attached to openclaw-backed agents were still invisible to
the runtime; the AGENTS.md fallback was only a documentation patch.
OpenClaw's skill scanner walks <workspaceDir>/skills/ (plus a few other
roots), and workspaceDir is resolved from the openclaw config file —
specifically agents.list[id].workspace → agents.defaults.workspace →
~/.openclaw/workspace. There is no CLI flag or env var override on the
agent runtime; the only knob is the config file.
This change wires a per-task synthesized config:
1. execenv.prepareOpenclawConfig deep-copies the user's existing
openclaw.json (priority: $OPENCLAW_CONFIG_PATH, else
~/.openclaw/openclaw.json), rewrites agents.defaults.workspace AND
every agents.list[].workspace to the task workdir, and writes the
result to {envRoot}/openclaw-config.json. Provider sections,
registered agents, model providers, gateway settings — everything
openclaw needs to actually start — are preserved as-is.
2. resolveSkillsDir for "openclaw" now points at {workDir}/skills/,
which is the first path openclaw scans under workspaceDir. Skills
written here are picked up natively.
3. daemon.go exports OPENCLAW_CONFIG_PATH={env.OpenclawConfigPath} on
the openclaw subprocess and adds OPENCLAW_CONFIG_PATH to the
custom_env blocklist so users cannot accidentally override it.
4. buildMetaSkillContent now lists openclaw alongside the
"discovered automatically" providers; the .agent_context/skills/
fallback line stays for gemini/hermes.
The new regression test TestPrepareOpenclawSkillWriteMatchesScanPath is
the one MUL-2219's DoD calls out: it resolves the workspaceDir the way
openclaw does (reading agents.defaults.workspace out of the synthesized
config) and proves {workspaceDir}/skills/<name>/SKILL.md is what Multica
actually wrote. The pre-MUL-2219 fix asserted "we wrote a file" without
checking the scanner would ever see it — which is how the dead drop into
.openclaw/skills/ landed in #2621's first commit.
Verified locally: minimum-viable synthesized config validates via
`openclaw config validate`, and `OPENCLAW_CONFIG_PATH=<path> openclaw
config get agents.defaults.workspace` returns the task workdir as
expected. MUL-2219
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): delegate openclaw config parsing to CLI and fail closed
Address Elon's must-fix on PR #2628: the previous implementation parsed
~/.openclaw/openclaw.json with encoding/json, which cannot read JSON5
or follow $include — the OpenClaw spec's actual format. When parsing
failed, prepareOpenclawConfig silently emitted a minimal config, which
could boot OpenClaw without the user's registered agents, model
providers, or API keys.
Two changes:
1. Delegate active-config-path resolution and config reading to the
openclaw CLI itself. `openclaw config file` locates the active
config (covering OPENCLAW_CONFIG_PATH / OPENCLAW_STATE_DIR /
OPENCLAW_HOME / default and the legacy chain), and the wrapper we
write uses $include to point at it so OpenClaw's own loader handles
JSON5, $include nesting, env-substitution, and secret refs. We read
only agents.list via `openclaw config get --json` to rewrite each
entry's workspace — secrets, comments, and includes in the user
config are never touched.
2. Remove the silent minimal-config fallback. Any CLI failure,
malformed output, or write error now surfaces as a hard error from
Prepare / Reuse. The only "synthesize minimal" path left is a fresh
install (CLI reports a path but the file doesn't exist), where
there is no user data to lose.
The per-task override still rewrites every agents.list[].workspace,
not just agents.defaults.workspace — this is intentional task
isolation, documented in prepareOpenclawConfig and the PR body. A
host-scope per-agent workspace would otherwise silently route the
scanner back to the user's shared workspace.
Cleanups Elon flagged in the same review:
- daemon.go inline-system-prompt comment no longer claims openclaw
ignores the task workdir; it does load it now, and the inline brief
is a belt-and-suspenders carryover for older releases.
- execenv.go openclaw block no longer references "skill file paths in
the inline brief" — the brief uses "discovered automatically".
Reuse() switches to a ReuseParams struct so the openclaw binary path
threads through alongside CodexVersion without a 6th positional arg.
MUL-2219
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): grant OpenClaw $include cross-dir confinement for per-task wrapper
The per-task wrapper at envRoot/openclaw-config.json $includes the user's
active config (typically ~/.openclaw/openclaw.json), but OpenClaw confines
$include resolution to the wrapper file's directory unless the target's
parent is granted via OPENCLAW_INCLUDE_ROOTS. Without this, OpenClaw refuses
to follow the link at runtime and the wrapper boots with no user-registered
agents.
prepareOpenclawConfig now returns dirname(activePath) as IncludeRoot, and
the daemon prepends it to whatever the user already has in
OPENCLAW_INCLUDE_ROOTS via the new composeOpenclawIncludeRoots helper
(dedupes, drops empty segments, preserves user-configured roots). Fresh
install emits no $include and leaves the env var untouched.
Adds OPENCLAW_INCLUDE_ROOTS to the custom_env blocklist so a per-agent
override cannot strip the granted root.
Regression tests:
- TestPrepareOpenclawConfigWrapperLoadableUnderIncludeConfinement asserts
every $include target's dirname is covered by the IncludeRoot we surface.
- TestPrepareEnvironmentOpenclawWiresIncludeRoot covers the non-fresh-install
Environment wiring.
- TestComposeOpenclawIncludeRoots covers the daemon-side env composition
(preserve, dedupe, drop empties).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The RUNTIME cell rendered base name + (hostname) with both spans using
flex: 0 1 auto, so the longer hostname dominated and squashed the name
to a single letter. Give the base name shrink priority and let the
hostname own the flex slot with basis-0, so hostname truncates first
while the name stays readable.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): wake leader when dual-role agent posts as worker (MUL-2218)
The squad-leader self-trigger guard skipped a comment whenever the
author equalled the squad's leader id, regardless of the role the agent
was acting in. For an agent that holds both leader and worker roles in
the same squad, this meant the leader role never reacted to its own
worker output and the issue stalled.
Tag each enqueued task with is_leader_task and consult the agent's
most recent task on the issue from both self-trigger guards (comment
path + @squad mention path) — skip only when that task was itself a
leader task.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): inherit is_leader_task on retry task clone (MUL-2218)
CreateRetryTask cloned a parent task into a fresh queued attempt but
omitted is_leader_task from the column list, so the child silently fell
back to the column default (false). For a leader task that hit auto-retry
through MaybeRetryFailedTask, the retried task posed as a worker task —
the self-trigger guard then no longer recognised the leader's own
comments, re-opening the very loop MUL-2218 closes.
Inherit p.is_leader_task in the clone and add a query-level test that
covers both leader and worker retries.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2215: fix(daemon): close handleRuntimeGone success/straggler race
handleRuntimeGone coalesced concurrent recoveries with a per-workspace
`reregisterNextAttempt` slot that was deleted immediately on success. A
late-arriving goroutine whose `removeStaleRuntime` was delayed by mutex
contention could reach the coalesce gate after the winner cleared the
slot, observe no slot, re-claim, and double-register — the source of the
intermittent `register endpoint called 2 times under stampede, want 1`
failure on PR #2348.
The slot delete on success is intentional (a genuinely later distinct
deletion in the same workspace must register again, validated by
TestHandleRuntimeGone_DistinctDeletionsWithinCoalesceWindowBothRecover),
so we can't just extend the slot's lifetime.
Add a second per-workspace gate: `reregisterLastCompletedAt`. Every call
captures `entryAt` at the top of handleRuntimeGone; at the coalesce gate
a caller bails if `lastCompletedAt >= entryAt`, i.e. a peer's register
completed AFTER we entered the function. Same-wave stragglers bail
deterministically; distinct later events have `entryAt > lastCompletedAt`
and proceed.
Extracted the gate into `tryClaimRegisterSlot` / `recordRegisterCompletion`
so the race can be exercised deterministically with synthetic timestamps
instead of relying on `-count=N` to win the scheduling lottery.
- TestHandleRuntimeGone_CoalescesConcurrentCallers: -count=500 -race
clean (previously intermittent).
- New unit tests cover the straggler bail, the distinct-later-event
claim, failure backoff suppression, and peer-holds-slot coalescing.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2215: narrow completion stamp to success path
Second review caught that recordRegisterCompletion stamped
lastCompletedAt on both success and failure. A failed register has not
covered any workspace state, so a same-wave straggler whose entryAt
predates the failure must be allowed to retry once the failure backoff
expires — the previous behavior would let the failure-time stamp also
hide that straggler. workspaceSyncLoop only retries when a workspace's
runtimeIDs fully drain, so partial-deletion recovery has to come from
the straggler path.
Failure path now only updates reregisterNextAttempt; success path keeps
its existing stamp + slot clear. Add a regression test covering the
entryAt-before-failed-completion / arrival-past-backoff edge.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): write OpenClaw skills to .openclaw/skills/ for native discovery
The OpenClaw provider was missing a case in resolveSkillsDir, so workspace
skills attached to OpenClaw-backed agents fell through to .agent_context/
skills/ — a path the openclaw CLI never inspects. The result: agents
created against the OpenClaw runtime saw zero of their loaded Skills in
chat or task runs, even though the meta AGENTS.md content advertised
them as auto-discovered.
Mirrors the same per-provider mapping already in place for OpenCode,
Copilot, Pi, Cursor, Kimi, Kiro. Also adds .openclaw to the repocache
git-exclude list so the per-task skills directory does not pollute
checked-out repos. MUL-2213
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): drop .openclaw/skills dead-drop write; flag openclaw as non-auto-discovery
Reviewer (Elon) pointed out that {workDir}/.openclaw/skills/ is not in any
OpenClaw skill discovery path. Confirmed by reading openclaw upstream
(src/agents/skills/refresh.ts, src/agents/agent-scope-config.ts,
src/cli/program/register.agent.ts):
- OpenClaw scans <workspaceDir>/skills, <workspaceDir>/.agents/skills,
~/.openclaw/skills, ~/.agents/skills, bundled, and config
skills.load.extraDirs.
- workspaceDir is resolved from the openclaw config (per-agent
workspace -> agents.defaults.workspace -> ~/.openclaw/workspace).
It is NOT the cwd of the openclaw process.
- There is no --workspace CLI flag on 'openclaw agent', and no
OPENCLAW_WORKSPACE env var consumed at runtime. The only knob is the
config file.
So {workDir}/.openclaw/skills/ written by Multica is never seen by the
openclaw runtime, and the meta AGENTS.md was lying to the agent by
claiming auto-discovery. Reverts:
- resolveSkillsDir: drop the openclaw case; falls back to
.agent_context/skills/ (same path as hermes).
- agentGitExcludePatterns: drop .openclaw; nothing is written there now.
Also updates the openclaw branch in buildMetaSkillContent to point the
agent at .agent_context/skills/ explicitly (alongside gemini/hermes), so
loaded skills are at least referenced by path in the AGENTS.md context.
The openclaw native loader still won't see them as installed skills.
Native auto-discovery for openclaw needs per-task workspace integration
(e.g. synthesized per-task config via OPENCLAW_CONFIG_PATH that overrides
agents.defaults.workspace, or resolving the agent's actual configured
workspace at exec time) — tracked as follow-up. MUL-2213
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2216: feat(agents,squads): persist Mine/All tab selection per workspace
Tab selection on the Agents and Squads list pages was held in
component-local state, so navigating into a detail page and back
remounted the list and reset the tab to the default "Mine". Move
`scope` into Zustand stores backed by `persist` +
`createWorkspaceAwareStorage`, matching the pattern used by the
Issues view store. Selection now survives list → detail → back
navigation and page reloads, scoped per workspace.
Only `scope` is persisted; `search`, `sort`, and other ephemeral
filters intentionally still reset on remount.
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): reset scope to mine when switching to a workspace with no persisted value
zustand persist.rehydrate() is a no-op when storage returns null, so
workspaces with no entry kept the previous workspace's in-memory scope
("all" leaked from one workspace into the next). Provide a custom merge
that resets to the default "mine" when no persisted state is present.
Add coverage for the missing-storage workspace-switch case for both
Agents and Squads.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(settings): view/edit toggle for repositories tab
Saved repos render as static rows (truncated, monospace) with hover/focus-revealed
Edit + Delete affordances. Clicking Edit flips to the existing Input; on
successful Save the row returns to display mode. Save button is gated on a
dirty check (URL arrays in order) so a clean state reads as "All changes
saved". Resolves user feedback that the always-visible input made saved
state ambiguous (MUL-2217).
- Track editingIndices with a Set; new rows auto-enter edit mode; deleting
a row remaps indices so the wrong row never opens.
- Touch devices and focus-within keep the action buttons reachable.
- New i18n keys in en + zh-Hans (saved_hint, empty, edit/delete_aria, url_empty).
Co-authored-by: multica-agent <github@multica.ai>
* fix(settings): add Cancel affordance to exit clean edit mode
Clicking Edit on a clean saved row opened the row in edit mode with
no way back to display mode unless the user changed the URL and saved,
re-introducing the original saved-state ambiguity after an accidental
click. Add a per-row Cancel (X) button visible only in edit mode that:
- reverts the URL to the saved value for existing rows
- removes the row entirely for never-saved (newly added) rows
- exits edit mode without dirtying Save
Action group is always visible (no hover gate) while editing so the
exit is discoverable. Adds en/zh-Hans cancel_aria string and three
regression tests covering clean-cancel, dirty-cancel, and new-row-cancel.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
- Add Squads to Features list (EN/zh) highlighting team-level agent routing
- Add a short Squads callout to the 'What is Multica?' section
- Remove the outdated 'Multica vs Paperclip' section from both READMEs
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): resolve agent CLIs via login shell when daemon PATH misses them
GUI-launched daemons on macOS/Linux do not inherit the user's interactive
shell PATH, so fnm/nvm/volta multishells and the Anthropic native installer
silently disappear during onboarding even though `claude --version` works
in Terminal. Fall back to `$SHELL -ilc` to ask the login shell for the
canonical absolute path, then verify it with exec.LookPath before trusting
it. Symlinks (fnm/nvm prefix dirs) are resolved while the helper shell is
still alive so per-session paths get canonicalised before they vanish.
Refs MUL-2167, multica-ai/multica#2512.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): strip alias shadowing, harden timeout, lazy-resolve via login shell
Three follow-ups from the PR #2620 review (Elon):
1. Alias shadowing — `command -v claude` in zsh/bash returns the alias
definition, not the binary, and the absolute-path filter then rejects it.
The script now `unalias`/`unset -f` the name before lookup so `command -v`
falls through to the real PATH binary. This is the exact case behind
#2512.
2. Hard timeout — `CommandContext` kills only the shell process. Rc files
that background processes inheriting stdout (`direnv hook`, `nvm` shims,
plain `&`) keep the pipe open and `cmd.Output()` would block for as long
as the survivors live. `Cmd.WaitDelay` forcibly closes the pipes once
the cap elapses, so total startup penalty is bounded by
`timeout + waitDelay` regardless of rc-file content.
3. Lazy fallback — the resolver no longer runs on every daemon start.
`getShellResolved` is `sync.Once`-guarded and only fires when a bare
command name actually misses `exec.LookPath`. Users whose PATH already
contains every agent never pay the rc-file load cost.
Tests: - `TestResolveAgentsViaLoginShell_StripsAliasShadowing` — rc declares
`alias fakeclaude=...`, real binary lives on PATH, resolver must
return the binary, not the alias text.
- `TestResolveAgentsViaLoginShell_HardTimeoutOnBackgroundedStdout` —
rc backgrounds a 60s sleeper holding stdout; resolver must return
inside `timeout + waitDelay + slack`, not 60s.
- `TestLoadConfig_SkipsLoginShellWhenLookPathSucceeds` — when
exec.LookPath finds every agent, SHELL (a marker-writing sentinel)
must not be invoked.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): file-card render for self-host with local storage
Fixes#1520. When self-hosting without S3, the upload handler returns
site-relative URLs like /uploads/workspaces/<wsId>/<file>. Four
frontend regexes only matched https?://, so persisted
!file[name](/uploads/...) markdown failed to parse and leaked through
as raw text in the issue view, chat, skill file viewer, and board
card preview.
Narrow allow-list: the relative branch only accepts /uploads/ — not
any /-prefixed href — so protocol-relative //evil.com/x, path-traversal
/../api/x, and other internal /api/... paths are rejected. Without
this, a stored file-card with an attacker-chosen filename and a
//host/x href would turn into a one-click external-site jump via
window.open from inside an issue (per review feedback on #2349).
Single source of truth: packages/ui/markdown/file-cards.ts now exports
isAllowedFileCardHref + FILE_CARD_URL_PATTERN. The four sites use one
of them, so the next regression is cheaper than restoring four parallel
regexes.
- packages/ui/markdown/file-cards.ts: helper + URL pattern.
- packages/views/editor/extensions/file-card.tsx: Tiptap tokenizer
composes from FILE_CARD_URL_PATTERN.
- packages/views/editor/readonly-content.tsx: sanitiser uses helper.
- packages/ui/markdown/Markdown.tsx: sanitiser uses helper.
- packages/views/issues/components/board-card.tsx: strip markdown
tokens from the line-clamped board preview so raw !file[...] no
longer leaks there either.
- packages/ui/markdown/file-cards.test.ts: covers accept (/uploads/ok,
https://cdn/x) and reject (javascript:, data:, //evil.com/x,
/../api/x, /api/x, empty, ftp:, bare 'uploads/x') for both the
helper and the parser composed from the pattern.
javascript:, data:, and other dangerous schemes remain rejected.
* test(markdown): move file-card href allow-list test into @multica/views
Per review feedback on #2349: keep the test where vitest is already
running instead of bootstrapping a new test runner inside @multica/ui.
The test now lives at packages/views/editor/file-card-href.test.ts and
imports isAllowedFileCardHref / FILE_CARD_URL_PATTERN /
preprocessFileCards from the @multica/ui/markdown public surface,
exercising the same 30 cases.
Reverts the @multica/ui package.json test script + vitest devDep + the
local vitest.config.ts that the previous commit added; the package
goes back to typecheck + lint only, matching every other ui-only
package in the monorepo.
---------
Co-authored-by: Lalbadshah <11599756+Lalbadshah@users.noreply.github.com>
Rename the Deployment type dropdown options to Official App and
self-host so reporters pick the right one without guessing.
MUL-2212
Co-authored-by: multica-agent <github@multica.ai>
* refactor(agents): drop template chooser from create-agent dialog
Removes the blank-vs-template chooser, the template picker, and the
template detail step. The "Create agent" entry point now opens directly
on the form. The createAgentFromTemplate API and types remain
untouched — this only removes the UI entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs(squads): fix stale comment about createAgentFromTemplate
Squad-scoped create flow no longer goes through the template path;
the dialog now only calls api.createAgent then api.addSquadMember.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Adds a dedicated bilingual /docs/squads page covering the squad model
(leader + members), assignment, comment trigger rules, archive
semantics, and the squad CLI surface. Wires the new page into
meta.json and meta.zh.json under the Agents section, and adds
short cross-references from agents, assigning-issues,
mentioning-agents, and the CLI reference so users can discover
squads from the pages they're already on.
MUL-2206
Co-authored-by: multica-agent <github@multica.ai>
When the user opens quick-create with a squad selected, the task is
enqueued against the squad's leader agent — but the squad, not the
leader, is the expected owner. The prompt previously instructed the
leader to "default to YOURSELF" using its own agent UUID, hiding new
issues from the squad's delegation flow.
Surface the squad's id + name on the claim response and branch the
default-assignee instruction in buildQuickCreatePrompt: when SquadID is
present, point --assignee-id at the squad UUID and explicitly forbid
self-assignment.
MUL-2203
Co-authored-by: multica-agent <github@multica.ai>
* feat(squads): add agent live peek hover card on member avatars
Squad members tab now opens a live-state peek card on agent avatar
hover/focus — workload, current issue (clickable), and last activity.
Identity (description / runtime / skills / owner) stays on the existing
AgentProfileCard; new AgentLivePeekCard is the second `hoverCardVariant`
on ActorAvatar so the 23+ existing profile-card call sites keep their
behaviour. Reuses the workspace agent-task snapshot already fetched by
the presence dot, so this adds zero new requests per row. Failed
terminal tasks surface as a small ⚠ on the last-activity line without
polluting workload (workload stays current-state only, matching the
deliberate split documented in core/agents/types.ts).
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): only enable hover card for agent avatars
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Exposes the existing /api/tasks/{id}/cancel backend endpoint as a CLI
command. Combined with upstream #2107 (cancel running agent on
server-side task delete), this gives operators a way to interrupt a
runaway agent push-storm without resorting to admin-bypass on the
downstream PR.
Use cases:
- Titan / DevBot iterating beyond its boundary (e.g. push-skip loops)
- Codex turn that locked in tool-call spam
- Manual recovery when a long-running task needs to stop NOW
Symmetric with 'issue rerun': accepts the short ID prefix shown by
'issue runs', supports --issue scoping, and reuses resolveTaskRunID
for ambiguity handling.
Refs: PR#19 octo-server post-mortem (2026-05-13)
Co-authored-by: yujiawei <yujiawei@mininglamp.com>
* feat(squads): add tooltips and agent detail link to squad member row
Replace native title attributes on the make-leader and remove buttons
with proper Tooltip components, and add a new icon button on agent
rows that navigates to the agent detail page. All three tooltips are
localised.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): keyboard focus visibility + AppLink for agent detail
- Add group-focus-within:opacity-100 so Tab to the row's hover-only
action buttons makes the container visible (previously opacity-0
kept buttons focusable but invisible).
- Replace the agent-detail jump button's onClick+push() with AppLink
href, restoring middle/Cmd+Click new-tab behavior. Removes the
now-unused onViewAgent callback chain.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Drop mx-auto + max-w-2xl wrappers around the Members and Instructions
tab content so the right pane fills the available width like the agent
detail page (TabContent uses flex h-full flex-col p-4 md:p-6).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Text/code attachments (markdown, JSON, .ts, .log, …) need an attachment id
to render through `/api/attachments/{id}/content`. The composer pipeline
was dropping that id at the upload-hook boundary, so the Eye preview gate
only fired for media (PDF / video / audio via filename fallback).
- `useFileUpload` now returns the full `Attachment` (with `link` kept as a
`url` alias) so editor providers can resolve content-type and id.
- New-comment and reply composers hold a `pendingAttachments` state and
feed it to `ContentEditor`; the active subset (those still referenced in
the markdown) is sent on submit as before.
- Comment edit modes (CommentRow + CommentCardImpl) merge pending uploads
with `entry.attachments` for the editor and pipe `attachment_ids` into
`onEdit` so newly uploaded files actually bind to the comment.
- Issue description editor pushes pending `attachment_ids` on every
debounced save and invalidates `issueKeys.attachments` so the preview
Eye survives a refresh.
- `UpdateComment` and `UpdateIssue` handlers accept `attachment_ids` and
call the existing `linkAttachmentsByIDs` / `linkAttachmentsByIssueIDs`
helpers; the bind is idempotent so re-sending an existing id is safe.
Closes MUL-2153.
Co-authored-by: multica-agent <github@multica.ai>
* fix: trigger squad leader agent run when squad is @mentioned in comment
Previously, enqueueMentionedAgentTasks only processed m.Type == "agent"
mentions, skipping squad mentions entirely. The shouldEnqueueSquadLeaderOnComment
path only fires when the issue is already assigned to a squad.
This adds handling for m.Type == "squad" in enqueueMentionedAgentTasks:
when a squad is @mentioned, look up the squad's leader agent and enqueue
a task for them (with the same dedup/self-trigger/archived guards as
direct agent mentions).
Co-authored-by: multica-agent <github@multica.ai>
* fix: add canAccessPrivateAgent gate to squad mention branch
Closes the P1 permission vulnerability where a plain workspace member
could trigger a private squad leader by @mentioning the squad, bypassing
the private-agent access check that the direct @agent mention path
enforces.
Adds regression test TestCreateComment_SquadMentionPrivateLeaderBlocksPlainMember.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): rewrite template catalog as 25 lightweight starters
Replaces every Phase-1 template with a curated set built around the
"persona + intake + scaffold + hard negatives" instruction shape. Cross-
platform survey (Cursor / Cline / Roo / Continue / Custom GPTs) showed
the industry baseline for starter agents is "few but sharp" — single
intent, no methodology buy-in, mostly prompt-only. The original catalog
went the opposite direction (avg 2.5 skills, six-skill Full-stack
methodology stack) and felt heavy for first-time use.
Catalog shape:
- 25 templates across 7 categories: Engineering (8), Product (4),
Writing (5), Design (3), Communication (2), Team (1), Productivity (2).
New Product / Design / Communication / Team domains fill gaps the old
Eng-heavy catalog ignored.
- 16 / 25 are prompt-only (no skill fan-out). Avg 0.56 skill per template
vs. 2.5 prior. Heaviest is 2 skills, only for templates whose intent
cannot be expressed in instructions alone (Playwright runner, single-
file HTML bundlers, design + UX-guidelines pair).
- Universal top-frequency intents that the old catalog missed are now
covered: Code Explainer (intent #1 across every platform surveyed),
Translator (中英), Summarizer, Writing Critic, PRD Drafter/Critic,
RCA Writer, ADR Writer, PR Description Writer, Commit Message Writer.
Loader allows 0-skill templates:
- server/internal/agenttmpl/loader.go drops the "must declare at least
one skill" validation; comment explains the picker's "Prompt only"
rendering path.
- loader_test.go: removed the corresponding negative case, added
TestLoadFromFS_PromptOnlyTemplate as a regression guard.
- agent_template.go handler is unchanged — every len(tmpl.Skills) call
site was already 0-safe (empty fan-out short-circuits the fetch phase
and the in-tx loop both skip cleanly).
Frontend:
- template-picker.tsx: 18 new lucide icons (BookOpen, Bug, GitPullRequest,
GitCommit, AlertTriangle, Scale, ClipboardList, Microscope, UserRound,
Target, Highlighter, Languages, AlignLeft, GraduationCap, Lightbulb,
Type, MessageSquare, Briefcase). Card renders a "Prompt only" badge
when skills.length === 0 instead of "0 skills".
- template-detail.tsx: skill list section is hidden entirely for prompt-
only templates — a header reading "Includes 0 skills" above an empty
list was just visual noise. Instructions section below carries the
agent's identity for these.
- locales/en + zh-Hans agents.json: new create_dialog.template_card.
prompt_only key ("Prompt only" / "纯指令").
Verification:
- go test ./internal/agenttmpl/ — 9/9 pass, including
TestLoad_RealTemplates which fails closed if any new JSON is malformed.
- pnpm typecheck — all 6 packages clean.
- pnpm --filter @multica/views test — 482/482 pass.
- pnpm lint — 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): add category filter pills to template picker
25 templates across 7 categories made the picker scroll-heavy on first
open. Add a single-select category filter row above the grid so a PM
can isolate Product templates in one click, an engineer can jump
straight to Engineering, etc.
Visual reuses the IssuesHeader scope-toggle pattern verbatim — Button
variant="outline" + active class swap (bg-accent / text-muted-foreground)
— so the affordance reads the same as the existing filter pills in
issues / squads / runtimes / my-issues. flex-wrap keeps the 8 pills
(All + 7 categories) honest on narrow widths.
Counts are inlined into the label ("Engineering (8)") rather than
shown as a separate badge — single-line-tall pills look right next to
the picker grid, and surfacing the per-category density up front
doubles as a hint at the catalog's "less but sharper" intent.
When a specific category is active, the grid renders flat (no
section headers) — the active pill already names what's on screen,
and a header reading "Engineering" above an only-Engineering grid is
visual duplication. "All" falls back to the prior grouped layout.
State is component-local (no URL sync, no persistence) since the
picker is dialog-internal transient state — closing the dialog
naturally resets the filter, which is the expected behaviour for a
"choose from a catalog" surface.
i18n: new `create_dialog.template_picker.filter_all` key in en + zh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Create Agent button on the Squad detail Members tab, visible
only to workspace owner/admin (matching the AddSquadMember backend
gate). The dialog reuses the existing CreateAgentDialog — both the
manual and template paths now accept an optional squadId; when set,
the dialog runs addSquadMember after createAgent / createAgentFromTemplate
and skips the navigation to the agent detail page so the user lands
back on the Members tab.
Atomicity is best-effort frontend-serial (no new backend transaction):
on partial failure the dialog surfaces a warning toast and the agent
remains addable from the existing Add Member flow.
Co-authored-by: multica-agent <github@multica.ai>
* fix: execution log name rendering and squad assignee support
- Strip mention markdown in trigger_summary ([@Name](mention://...) → @Name)
so execution log rows show clean text instead of raw markdown
- Add squad to ActorFilterValue type so squad assignees are filterable
- Add squad section to assignee filter dropdown in issues-header
- Add i18n keys for squads_group (en/zh-Hans)
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR #2575 review feedback
1. Extract stripMentionMarkdown as reusable helper with proper regex
- Handles escaped brackets in names (e.g. David\[TF\])
- Skips backslash-escaped mentions (\[@...])
- Handles issue mentions (no @ prefix)
- Does not touch regular markdown links
- 10 unit tests added
2. Squad only appears in Assignee filter, not Creator
- Added showSquads prop to ActorSubContent (default true)
- Creator filter passes showSquads={false}
3. Squad included in Agents scope
- issues-page scope filter now includes squad in agents scope
- 2 regression tests added for scope coverage
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The per-turn prompt in buildCommentPrompt() only injected the squad
leader no_action prohibition inside the 'if TriggerAuthorType == agent'
block. When a member (human) posted a comment like 'LGTM', the squad
leader was triggered but the per-turn prompt did NOT include the
prohibition, causing the model to post noise comments like 'LGTM is a
pure acknowledgment — no reply needed. Exiting silently.'
Fix: move the squad leader no_action rule outside the agent-only block
so it fires for ALL trigger types (agent and member).
Fixes: MUL-2168
Co-authored-by: multica-agent <github@multica.ai>
* feat: support pinyin search in @mention suggestions
Add pinyin matching for Chinese names in the mention suggestion popup.
Users can now search by:
- Full pinyin: 'liyunlong' matches '李云龙'
- Initial letters: 'lyl' matches '李云龙'
- Partial/hybrid: 'liyu' or 'liyunl' matches '李云龙'
Implementation:
- New pinyin-match.ts utility using pinyin-pro library
- Integrated into member, agent, and squad filters in mention-suggestion.tsx
- 21 tests passing (9 unit + 12 integration)
Co-authored-by: multica-agent <github@multica.ai>
* fix: normalize ü→v in pinyin matching for names like 吕布
Enable pinyin-pro's v:true option so 吕→lv instead of lü.
Add test case for 吕布/lvbu matching.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
PR #2564 only added IsSquadLeader handling to the assignment-triggered
workflow path and the Output section. When a squad leader is triggered by
a comment (the common case for re-evaluation), the comment-triggered
workflow path had NO squad leader special handling, so the model still
posted comments announcing no_action/silence.
Changes:
- runtime_config.go: Add IsSquadLeader check to comment-triggered step 4
with explicit prohibition against posting no_action announcement comments
- runtime_config.go: Strengthen Output section from 'may exit silently' to
'MUST exit without posting any comment' with explicit DO NOT examples
- runtime_config.go: Strengthen assignment-triggered step 5 similarly
- prompt.go: Add squad leader no_action rule to per-turn comment prompt
when trigger author is an agent and agent instructions contain the
Squad Operating Protocol marker
- Add tests for both the per-turn prompt and CLAUDE.md generation
Fixes MUL-2168
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): skip leader on comment when a member @mentions any agent (MUL-2170)
When a human commenter routes an issue directly at a specific agent via
[@Name](mention://agent/<id>), the squad leader was still being woken up
to evaluate the same comment. The leader's only real options were to
re-delegate to the agent the member already named or to record
no_action — both of which produce queue noise without changing the
outcome.
This skips the leader-enqueue path entirely when:
- the assignee is a squad,
- the comment author is a member, AND
- the comment body contains at least one agent mention.
Agent-authored comments are intentionally exempt: when an agent posts
an update that @mentions another agent, the leader still needs to
coordinate the thread. The existing leader-self-trigger guard is
preserved. Only the current comment's body is inspected — parent
(thread root) mentions are not inherited here.
Tests cover the helper (mentions parsing) plus the integration matrix:
member plain / member @member / member @non-leader-agent /
member @leader / agent @agent / leader-self.
Co-authored-by: multica-agent <github@multica.ai>
* test(squad): exercise full CreateComment path for leader-skip rule (MUL-2170)
Adds an integration test that drives the HTTP-layer CreateComment handler
(not just the helper) to lock the call-site wiring: a member top-level
comment with an @agent skips the squad leader, and a subsequent plain
reply in the same thread DOES wake the leader — the parent's @agent
mention must not be inherited into the leader-skip decision.
Picks up a non-blocking review note on PR #2569.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): skip leader on any explicit member mention, not only @agent (MUL-2170)
Broaden the leader-skip rule for squad-assigned issues: a member comment
that explicitly @mentions anyone — @agent, @member, @squad, or @all —
counts as deliberate routing and the squad leader stays out. Issue
cross-references (mention://issue/...) are not routing and still trigger
the leader as before.
Per Bohan's follow-up on MUL-2170 — @member should suppress the leader
for the same reason @agent does: the human has already pointed at a
specific recipient, so a leader turn would just be observation noise.
Helper renamed commentMentionsAnyAgent → commentMentionsAnyone with
explicit handling of all four routing mention types. Existing call-site
wiring (current-comment-only, agent-author exemption, leader self-trigger
guard) is unchanged.
Tests updated and extended to cover the full routing matrix:
@member / @squad / @all / @issue (cross-ref) plus the @agent variants
already covered.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The Eye button required a fully resolved Attachment record (URL-lookup
via `resolveAttachment(href)`) before showing. Download only required
the URL, falling back to `openExternal(href)` when the lookup missed.
Result: any case where the URL in markdown couldn't be reverse-matched
to the entity's `attachments` prop (cross-comment copy-paste, stale
caches) silently hid the Preview button while Download kept working —
edit and readonly surfaces diverged for the same content.
Widen the Preview gate to mirror Download: show the Eye whenever the
filename indicates a previewable type. Introduce a `PreviewSource`
tagged union — `{ kind: "full", attachment }` for the existing path,
`{ kind: "url", url, filename }` for the fallback. Media kinds
(pdf/video/audio) render directly from the URL; text kinds still
require an attachment id because the /content proxy is ID-keyed, so
`tryOpen` rejects URL+text combinations and PreviewContent has a
defensive fallback for direct mounts.
Side effects:
- `getPreviewKind` gains filename-extension fallbacks for video/audio
(was PDF-only); without these the URL-only path can't infer kind
when content_type is empty.
- AttachmentList in comment-card.tsx unchanged behaviorally — only the
tryOpen call site is updated to the new signature.
Pre-existing architectural issues (AttachmentList readonly-only,
URL-based attachment lookup, per-entity ownership) are intentionally
out of scope.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Template create used to silently default the runtime to "first usable"
and never collected a model — users had no idea where the new agent
would run or which model it would use until they opened the detail
page. Add a Runtime + Model picker pair above the skill list on the
template-detail step so the choice is visible (and overridable) before
the one-click Use action.
- Extract RuntimePicker out of create-agent-dialog so the form and the
template-detail step share one popover; selection seeding moves into
the picker too, since it's the only place that knows the active
filter (mine/all). Parent keeps just the duplicate-mode pre-fill.
- Mirror RuntimePicker's label-row + trigger DOM in ModelDropdown so
the two pickers render at identical heights when sat side-by-side
(fixes a 6-8px misalignment caused by inconsistent label-row sizing).
- Send model in createAgentFromTemplate; server side already accepts
the field (CreateAgentFromTemplateRequest.Model, omitempty), empty
string still falls through to the runtime's default model.
- Drop the runtime_register_first fallback hint that made the Runtime
trigger two-line in the empty state, breaking alignment with Model's
one-line trigger.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime prompt's Output section unconditionally required all tasks to
post a comment via 'multica issue comment add', which conflicted with the
squad leader protocol that says to 'exit silently' on no_action.
Changes:
- Add IsSquadLeader bool to TaskContextForEnv (detected via Squad Operating
Protocol marker in agent instructions)
- Relax the Output section and assignment-triggered workflow step 5 to
allow squad leaders to exit with only a 'multica squad activity' call
when the outcome is no_action
Fixes MUL-2168
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): resolve squad assignees in issue create/update/assign (MUL-2165)
The CLI assignee resolver only searched workspace members and agents, so a
quick-create input like "assign to <SquadName>" silently fell through to
"Unrecognized assignee: <SquadName>" in the issue description — even though
squads are first-class assignees server-side and the prompt's whole point was
to route the work for the user.
Extend resolveAssignee / resolveAssigneeByID to also fetch /api/squads, teach
the actor display lookup to render squad names in table output, update the
quick-create prompt and runtime-config command listing to mention
`multica squad list` alongside members and agents, and lock in the new
behavior with tests.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): gate squad assignee resolution behind an allowed-kinds set (MUL-2165)
The earlier MUL-2165 fix taught resolveAssignee / resolveAssigneeByID to also
return (squad, ...), but those helpers are shared. Project lead and issue
subscriber callers were still using them, and their target schemas reject
squads — project.lead_type has a DB CHECK constraint
(server/migrations/034_projects.up.sql:10) and the subscriber handler's
isWorkspaceEntity switch only knows member/agent
(server/internal/handler/handler.go:414). So
`multica project create --lead "<SquadName>"` and
`multica issue subscriber add --user "<SquadName>"` would resolve to
(squad, ...) and surface as a 500/403 server-side instead of a clean
CLI-side resolution error.
Thread an assigneeKinds set through the resolver and the pickAssigneeFromFlags
helper. Issue create/update/assign/list pass `issueAssigneeKinds` (all three);
project lead and subscriber pass `memberOrAgentKinds`. The squads fetch is
skipped entirely when not allowed, and the not-found / no-match error wording
adapts to the allowed kinds so it never mentions a type the caller cannot use.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(quick-create): searchable actor picker + squad support (MUL-2163)
- Replaces the flat agent dropdown in the "Create with agent" modal with a
searchable PropertyPicker that lists Agents and Squads in separate
sections, so users can filter by name and pick a squad as the creator.
- Persists the selection as (lastActorType, lastActorId), removing the
agent-only lastAgentId field on the quick-create store.
- Adds squad_id to the quick-create API request and stamps it onto the
task's QuickCreateContext. The handler resolves the squad to its leader
agent (re-using validateAssigneePair) and the daemon claim path injects
the squad-leader briefing when the task carries a squad hint, matching
the behavior of issue-bound squad tasks.
Co-authored-by: multica-agent <github@multica.ai>
* fix(create-issue): forward squad picks across manual→agent switch
Manual mode → agent mode previously only carried `agent_id`, so picking
a squad and then flipping to agent silently fell back to the persisted
actor / first visible agent and lost the user's choice. Carry `squad_id`
on the same branch so the agent panel honors the squad pick.
Adds a sibling test alongside the existing project-carry case.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): unify assignee menu with shared AssigneePicker (MUL-2157)
The Assignee submenu inside IssueActionsMenuItems was a parallel
implementation: no search, no squads, no agent permission check, no
archive filter, no frequency sort. The divergence was most visible from
the Inbox (where the issue detail's sidebar starts collapsed, so users
reach for the 3-dot menu).
Replace the submenu with a single menu item that closes the
surrounding dropdown / context menu and hands off to the shared
AssigneePicker popover — same component already used in the issue
detail sidebar, board cards, batch toolbar, and create-issue modal.
The picker is conditionally mounted to avoid every row in list / board
views subscribing to the members / agents / squads / frequency queries
on mount.
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): mock squadListOptions + add Assignee picker handoff test
`AssigneePicker` reads `squadListOptions` and `assigneeFrequencyOptions`
from `@multica/core/workspace/queries`. Tests that render IssueDetail
or IssueActionsDropdown without those mocks throw at the picker's
useQuery call and cascade into unrelated assertion failures — this is
what was leaving the `@multica/views` test job red on the MUL-2157 PR.
Add the missing mocks. Add a regression test that clicks the Assignee
menu item and asserts the shared picker (search input + Members group)
takes over, so a future regression to the parallel-implementation bug
this PR fixes fails loudly instead of silently.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(usage): mirror Tokens metric toggle onto Usage page Daily chart (MUL-2148)
#2537 added the Cost/Tokens metric toggle to the Daily chart inside the
runtime-detail Usage section (packages/views/runtimes/components/
usage-section.tsx). The workspace-level Usage page at /{slug}/usage
imports the same DailyCostChart primitive but renders it from
dashboard-page.tsx without any toggle wrapper, so #2537 only landed on
half of the surface that says "Daily cost".
This PR mirrors the same pattern to dashboard-page.tsx so users see
the toggle wherever a "Daily" chart appears.
Changes
- `packages/views/dashboard/utils.ts`: new `aggregateDailyTokens` helper
that folds DashboardUsageDaily[] into the same DailyTokenData[] shape
the DailyTokensChart consumes (mirrors aggregateByDate's dailyTokens
branch from the runtimes side, adapted to DashboardUsageDaily field
names).
- `packages/views/dashboard/components/dashboard-page.tsx`: rename
`DailyCostBlock` → `DailyTrendBlock`, add a Cost/Tokens Segmented
next to the section title, switch chart and title based on the
active metric, per-metric empty-state (so a workspace with unmapped
pricing but recorded tokens still gets a real Tokens chart while
the Cost view falls through to the empty-state — same convention as
DailyTab in usage-section.tsx).
- usage.json (en + zh-Hans): split `daily.title` into `title_cost` +
`title_tokens`, add `metric_cost` + `metric_tokens` toggle labels.
* feat(usage): default Daily chart to Tokens metric
Most users land on /{slug}/usage to gauge "how much agent work
happened" rather than "how much was spent." Tokens is the more
universally meaningful axis on first read (Cost depends on having
pricing mapped for every model and on whether the workspace has
unmaintained models). Cost stays one click away via the same toggle.
Also reorder the Segmented so Tokens sits first, matching the new
default.
* feat(usage): add timezone picker to usage page (#2533)
Extracts the runtime detail page's timezone dropdown into a shared
TimezoneSelect at packages/views/common/timezone-select.tsx and reuses
it in the usage page header, immediately to the right of the 7d / 30d
/ 90d segmented control. Defaults to the browser-resolved zone with
the same "(browser)" suffix rendering as the runtime page.
The runtime-detail TimezoneEditor still owns the PATCH mutation; only
the dropdown UI moved. UI-only — no API client / handler changes.
Co-authored-by: multica-agent <github@multica.ai>
* fix(usage): make header wrap so timezone picker fits on narrow widths
The h-12 PageHeader is a single non-wrapping flex row. Adding the
timezone picker with a 180px min-width pushed the title + project
filter + range switch + tz select past the viewport on narrow and
medium widths. Drop the picker's hard min-width, let the header grow
vertically (h-auto + min-h-12) and let the right toolbar wrap. Wide
viewports still render the original single row.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
#2505 (Squad MVP) merged with 29 hardcoded English strings in JSX text
nodes — packages/views/squads/components/squads-page.tsx (4) and
squad-detail-page.tsx (25). The package's eslint config enforces
`i18next/no-literal-string` as ERROR for every .tsx file, so
@multica/views#lint has been red on main, which Turbo cascades to
@multica/web#build, @multica/desktop#build, and @multica/views#typecheck
— effectively blocking every open PR's frontend CI (#2538, #2540, etc.).
Rather than disabling the rule for the Squad files (which would just
hide debt in a high-visibility surface), wire up a proper i18n
namespace and replace every flagged literal.
Namespace plumbing
- New `packages/views/locales/en/squads.json` and
`packages/views/locales/zh-Hans/squads.json` covering all 29 flagged
strings, grouped by surface (page / inspector / name_editor /
add_member_dialog / description_dialog / discard_changes_dialog /
members_tab / instructions_tab).
- Registered in `packages/views/locales/index.ts` and
`packages/views/i18n/resources-types.ts` so `t($ => $.squads.*)` is
type-safe.
Component replacements
- `squads-page.tsx`: add `useT("squads")`, replace 4 literals.
- `squad-detail-page.tsx`: add `useT("squads")` to seven inner
components that hold flagged text (`SquadDetailPage` / `InlineEdit
Popover` / `AddMemberDialog` / `RoleEditor` / `SquadDescriptionEditor`
/ `SquadDescriptionEditorBody` / `SquadOverviewPane` / `SquadMembers
Tab` / `SquadInstructionsTab` / `SquadDetailInspector`), replace all
flagged literals.
- Plural members count uses i18next's standard `_one` / `_other`
suffixes via `t(..., { count })` — matches the convention already
used in `runtimes/usage` and `agents`.
Notes
- A few unflagged user-facing strings remain (tab labels in
squadDetailTabs array, ternary alternatives like `"Save"` inside
`{x ? <Loader/> : "Save"}`, the inline `confirm()` archive prompt,
the `toast.success("Leader updated")` message). The eslint rule
uses `mode: "jsx-text-only"` so it only flags string children of
JSX nodes; attribute strings, object-literal values, and ternary
alternatives slip past. Those are real i18n gaps too but expanding
scope here would gold-plate the CI-unblock fix.
Verification
- `pnpm --filter @multica/views lint`: 0 errors (was 29). Remaining 13
warnings are pre-existing in unrelated files and don't fail CI.
- `pnpm typecheck`: 6/6 packages pass — namespace types resolve, all
selector calls infer correctly.
* feat(sidebar): top/bottom scroll fade mask (MUL-2150)
Apply useScrollFade to SidebarContent so the menu list softly fades
into the header / footer when overflowing, matching the existing
pattern used in chat list and onboarding steps.
Co-authored-by: multica-agent <github@multica.ai>
* fix(ui): useScrollFade re-evaluates on content mutations
ResizeObserver only fires on the observed element's own box. When a
flex / auto-height container's children grow asynchronously (sidebar
pinned items loading from TanStack Query, collapsibles expanding),
scrollHeight changes but clientHeight does not — mask stayed 'none'
until the user scrolled. Add a MutationObserver on childList to
recompute fade when content is inserted or removed.
Co-authored-by: multica-agent <github@multica.ai>
* test(paths): include squads in workspace route consistency check
main added the squads parameterless route to paths.workspace() in #2505
but the C4 consistency assertion wasn't updated, turning frontend CI
red on every PR. Add 'squads' to both the parameterless-method set and
the segment-mapping table.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
#2505 (Squad MVP) added paths.workspace(slug).squads() / squadDetail()
to paths.ts but didn't update paths/consistency.test.ts, whose first
test enumerates ALL parameterless workspace route methods and compares
the actual Set to an explicit expected Set. Squads landed on main, the
test started flagging the unexpected extra entry, and the @multica/core
test job has been red since 29082f7c.
Add "squads" to both:
- the expected-routes Set in `exposes the expected parameterless
workspace route methods` (the test that was failing)
- the expected-segments array in `each parameterless route emits
/{slug}/{segment}` (was silently skipping squads, now covered)
Also extend paths.test.ts with `ws.squads()` / `ws.squadDetail("sq_1")`
expectations so the per-route smoke test mirrors the rest of the
parameterless routes.
No source changes — only test files. The squad routes themselves
already exist on main and match the test's expectations.
The runtime Usage page's Daily timeline only showed daily $ cost, which
hides the underlying usage shape: cost varies wildly by model price, so
a quiet day on Opus can outspend a busy day on Haiku. Add a Cost/Tokens
toggle next to the Daily/Hourly/Heatmap tabs that swaps the chart over
to a four-segment stack of raw token counts (input / output / cache
read / cache write).
No backend changes needed — the existing /api/runtimes/{id}/usage
response already carries the per-day per-model token breakdown; this
just wires up DailyTokensChart on top of the dailyTokens aggregate that
aggregateByDate was already producing.
Co-authored-by: multica-agent <github@multica.ai>
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(agents): three-phase agent quick-create plan
Captures the full design for moving agent creation from manual form +
one-by-one skill attachment to a tiered experience:
- Phase 1 (this PR): one-click curated templates, AI-free.
- Phase 2 (next): AI-recommended skills via the existing quick-create
task mechanism — no new server-side LLM dependency.
- Phase 3 (later): AI creates the whole agent end-to-end, composing
Phase 2 with a new `multica agent create` CLI driver.
Documents the architectural decisions that keep all three phases on
existing infrastructure (no SSE, no server-side LLM SDK, no new WS
channels), the two soft blockers Phase 1 unlocks for later phases
(createSkillWithFiles TX composability + skill same-name dedupe), and
the scope decisions we explicitly opted out of (Anthropic plugin
marketplace, ClawHub UI affordances).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skills): harden import against invalid UTF-8 and binary files
PG rejects two byte patterns in a TEXT column. Both crashed real skill
imports we hit while assembling the template catalog:
- Embedded NUL (0x00) -> SQLSTATE 22021. Already stripped by
sanitizeNullBytes, kept as-is.
- Other invalid UTF-8 (e.g. 0x91 — Windows-1252 smart quote in a skill
whose author saved prose from Word). sanitizeNullBytes now also runs
strings.ToValidUTF8 over the content so the second class no longer
takes the whole import down.
For non-text payloads (images, fonts, archives, compiled binaries),
sanitization isn't the right fix — agents never read those as text,
and the bytes can't survive a TEXT column at all. addFile now skips
them by extension before the per-bundle cap counters tick, logging
the skip so an unexpected drop leaves a breadcrumb.
Function name kept for compatibility with the many call sites; both
behaviours are strict supersets of the original.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(skills): split createSkillWithFiles for tx composition + add workspace find-or-create query
Two soft blockers cleared so create-from-template (next commit) can
fold N skill creates and the agent + binding writes into one outer
transaction:
1. createSkillWithFiles used to Begin/Commit its own tx. Caller
composition was impossible — N invocations meant N separate
transactions and no atomicity over the whole materialise step.
Pull the body into createSkillWithFilesInTx(ctx, qtx, input); the
original function becomes a thin wrapper that manages its own tx
for standalone callers. Existing call sites: zero behaviour change.
2. Add GetSkillByWorkspaceAndName sqlc query — workspace skill lookup
by name, anchored to UNIQUE(workspace_id, name) from migration
008. Lets the template materialiser implement find-or-create:
reuse the workspace's existing skill row when a template
references the same name, rather than crashing on the unique
constraint or polluting the workspace with `<name>-2` clones.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): agent template catalog + create-from-template endpoint
Server-side foundation for Phase 1 of the quick-create roadmap (see
docs/agent-quick-create-plan.md). Adds:
- server/internal/agenttmpl/ — embed-loaded catalog of curated agent
templates. Each template ships pre-written instructions plus a list
of skill URLs that get materialised into the workspace at create
time. Validation runs at startup (init() panics on a malformed
template) so a bad JSON ships as a deploy-time defect, not a
runtime 500. Slug must equal the filename basename so the URL
router is mirror-symmetric with the file layout.
- 11 starter templates covering Engineering / Writing / Building /
Testing (code-reviewer, frontend-builder, planner, docs-writer,
one-pager, html-slides, full-stack-engineer, …).
- Three new endpoints, all behind RequireWorkspaceMember:
GET /api/agent-templates — picker list (no instructions)
GET /api/agent-templates/:slug — detail with instructions
POST /api/agents/from-template — materialise + create
Create flow:
1. Auth + runtime authorization happen BEFORE the GitHub fan-out
so a 403 never wastes 20s of upstream fetches.
2. Pre-flight dedupe by cached_name reuses workspace skills
without an HTTP fetch — second create-from-the-same-template
drops from 20s to <100ms.
3. Parallel fetch (30s per-URL timeout) for the remaining skills.
4. Single transaction: every skill insert, the agent insert, and
the agent_skill bindings. On any upstream fetch failure the TX
rolls back and the API returns 422 with `failed_urls` so the
UI can name the bad source(s).
5. extra_skill_ids (user-supplied additions) are verified through
GetSkillInWorkspace per id before attach, so a malicious client
can't graft a skill from another workspace via UUID guessing.
- multica agent create --from-template <slug> CLI flag dispatches to
the new endpoint with a 60s ceiling, matching `multica skill import`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): one-click create-from-template UI
Frontend half of Phase 1. CreateAgentDialog becomes a state machine
spanning four steps:
chooser → Start blank / From template cards
blank-form → existing manual form (post-chooser)
duplicate-form → existing form pre-filled from a duplicated agent
template-picker → grid of templates, click navigates to detail
template-detail → instructions + skill list preview + one-click Use
Picking a template never lands on the form: name auto-deduped against
existingAgentNames, runtime = first usable one, visibility = private.
Refinement happens on the agent detail page if needed. Same rationale
the doc spells out — templates exist precisely to skip configuration.
New components, all collapsible-by-default so quick-create stays fast:
- template-picker.tsx — categorised grid, lucide icons + semantic
accent tokens resolved through static maps so Tailwind's JIT picks
up every variant (dynamic class strings would silently miss).
- template-detail.tsx — instructions preview, skill list with cached
descriptions, Use CTA. Renders the failedURLs banner when a 422
fires — the only step that can trigger that response.
- instructions-editor.tsx — collapsed preview-card / expanded full
ContentEditor.
- skill-multi-select.tsx + skill-picker-list.tsx — shared multi-
select surface, also adopted by the existing skill-add-dialog.
- avatar-picker.tsx — agent avatar upload, mirrors the inspector's
visual language.
Schema-defended client (CLAUDE.md → API Response Compatibility): the
three new endpoints are wired through parseWithFallback with lenient
zod schemas. Desktop builds outlive any given server — a future
field rename / wrapping must not white-screen older installs.
listAgentTemplates accepts both the current bare array and a future
{templates: [...]} envelope. Coverage: 7 new schema-test cases in
schema.test.ts (null body, missing skills/instructions, malformed
create response, envelope migration).
Catalog + detail go through TanStack Query with staleTime: Infinity —
workspace-independent static data, no per-mount refetch.
Other:
- skill-add-dialog becomes a true multi-select (Confirm button +
checkbox list); attached skills are filtered out of the list.
- agents-page hands the freshly-created Agent back to the dialog so a
follow-up setAgentSkills can attach the form-selected skills.
- agent-overview-pane drops the mx-auto/max-w-2xl frame on config-
tab content; the wider dialog visual language reads better with
tabs filling the column.
- Every new UI string lives in both en/agents.json and
zh-Hans/agents.json under create_dialog.* / tab_body.skills.* —
locales/parity.test.ts blocks drift in CI.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): align skill import test + drop next-only lint suppression
- TestFetchFromSkillsSh_ResolvesRootLevelSkillMd now expects assets/logo.png
to be skipped; matches the new addFile binary-extension guard
(6fafd86e). The .png is intentionally dropped so PG TEXT inserts don't
hit SQLSTATE 22021.
- packages/views shares zero next/* deps, so the @next/next/no-img-element
eslint plugin isn't loaded there. The eslint-disable directive
referencing it produced a hard "rule not found" error in CI lint. Raw
<img> is the right primitive in views; remove the disable comment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(agents): wrap CreateAgentDialog tests in workspace/navigation providers
The dialog now calls useNavigation() and useWorkspacePaths(), both of
which throw outside their providers. The existing tests rendered the
dialog bare and tripped both new requirements:
- NavigationProvider — supply a stub adapter so push() works for the
agent-detail redirect.
- WorkspaceSlugProvider — useWorkspacePaths() requires a slug.
The blank-vs-template chooser is now the default first step; the
existing tests target the runtime picker on the manual form, so the
helper auto-clicks "Start blank" when no template is passed
(duplicate-mode tests skip the chooser).
Manual afterEach(cleanup) + document.body wipe. Base UI's Dialog
portal renders into document.body and leaves focus-guard/inert wrapper
divs behind across tests, so the second test in the suite saw two
"All" / "My Runtime" matches and getByText failed. The wipe is local
to this file rather than the shared setup because it isn't a global
issue — only suites that open Base UI dialogs hit it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
The four user-visible strings exposed by packages/ui rendered untranslated
on every page that used them:
- file-upload-button.tsx — "Attach file" aria-label/title
- sidebar.tsx — "Toggle Sidebar" sr-only label/aria-label/title
- pagination.tsx — "Go to previous/next page" aria-labels
- CodeBlock.tsx — "plain text" language fallback + "Copy code" aria-label/tooltip
Root cause: the package had no i18n hookup at all because the package
boundary rule forbids importing @multica/core. Replicating the pattern
five times would have been the same hack five times. Hooking up
react-i18next directly is the structurally clean fix — i18next is a
generic library, not business logic, and the upstream I18nextProvider
already exposes the instance via context.
To let packages/ui typecheck the selector form standalone (i.e. without
the views resource-types augmentation in scope), the augmentation is
split: views declares everything except the `ui` namespace on a new
global `I18nResources` interface, and packages/ui contributes the `ui`
slice via declaration merging in packages/ui/types/i18next.ts. Views'
resources-types side-effect-imports that file so both packages see the
merged shape during downstream typechecks.
Scope intentionally excludes:
- packages/ui/components/common/error-boundary.tsx — keeping its fallback
in English so a render-time crash never depends on i18n being healthy.
- apps/desktop/src/renderer/src/components/update-notification.tsx —
ships with the next desktop release, not via this PR.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(storage): add GetReader to Storage interface
Adds a streaming read method to the Storage abstraction so callers can
pull object bytes without forcing a full in-memory load. S3Storage wraps
GetObject; LocalStorage opens the file with path-traversal and sidecar
guards. Tests cover happy path, traversal rejection, sidecar rejection,
and missing key.
Used in the next commit by the attachment-preview proxy endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(server): add attachment preview proxy endpoint
GET /api/attachments/{id}/content streams the raw bytes of a
text-previewable attachment back to the client. Exists to (a) bypass
CloudFront CORS, which is not configured on the CDN, and (b) bypass
Content-Disposition: attachment which Chromium honors for iframe document
loads. Media types (image/video/audio/pdf) intentionally do NOT go through
this endpoint — clients render them directly from the signed CloudFront
download_url, which is already served with Content-Disposition: inline.
Hard cap: 2 MB. Larger files return 413. Anything outside the text
whitelist returns 415. The whitelist (isTextPreviewable) mirrors the
client-side dispatcher; the cross-reference comment in file.go flags
the manual sync until a JSON SSOT generator lands.
Response always uses Content-Type: text/plain; charset=utf-8 so a
hostile HTML payload can't be re-interpreted as a document. The
original MIME ships via X-Original-Content-Type for client dispatch.
Cache-Control: no-store so revoked attachment access takes effect
immediately on the next request.
Tests cover happy path (md), extension fallback when content_type is
generic, 415 (pdf), 413 (>2MB), foreign workspace (404 isolation), and
the isTextPreviewable table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core/api): add getAttachmentTextContent + preview error types
Adds an ApiClient method that fetches the text body of an attachment via
the new /api/attachments/{id}/content proxy. Two typed errors —
PreviewTooLargeError (413) and PreviewUnsupportedError (415) — let the
preview modal render specific fallbacks instead of a generic failure.
Refactors the private fetch() into a shared fetchRaw() helper so the
new method inherits the standard infra: auth headers, 401 →
handleUnauthorized recovery, X-Request-ID, error logging, and the
ApiError contract. The previous draft bypassed all of these by calling
window.fetch directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(views/editor): add AttachmentPreviewModal + Eye entry points
In-app preview for non-image attachments. An Eye icon now sits next to
the existing Download button on file cards / readonly file cards / the
standalone AttachmentList. Clicking it opens a full-screen modal that
dispatches by content_type:
pdf: <iframe src={download_url}> — Chromium PDFium
video/*: <video controls src={download_url}> — native controls
audio/*: <audio controls src={download_url}> — native controls
md: <ReadonlyContent> — full markdown pipeline
html: <iframe srcdoc sandbox=""> — fully restricted
text: <code class="hljs"> — lowlight highlight
Media types render directly from the signed CloudFront download_url
(server marks them inline-disposition). Text types fetch through the
new /api/attachments/{id}/content proxy via TanStack Query, wrapped
in useAttachmentPreview() so each entry point owns its own modal
state without depending on a global Provider mount.
Modal sizing: max-w-6xl × min(90vh, 100vh - 2rem) — slightly larger
than create-issue's max-w-4xl since PDF / video need room, but capped
to viewport on small screens. Sub-renderers use h-full to follow the
fixed modal height instead of viewport-relative units.
Images are intentionally NOT touched — the existing ImageLightbox
(extensions/image-view.tsx) already handles them correctly. The new
modal would be churn without user-visible benefit.
Adds i18n keys under attachment.* (en + zh-Hans) and registers
Preview/Download/Upload in the conventions glossary so future
translations stay consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(desktop): enable Chromium PDF viewer for attachment preview
Adds webPreferences.plugins: true to the main BrowserWindow so the
bundled Chromium PDFium plugin activates inside iframes — required for
the attachment preview modal's PDF dispatch. Default is false in Electron;
without it <iframe src=*.pdf> renders blank.
Security trade-off, accepted intentionally and documented inline:
1. This window already runs with webSecurity: false + sandbox: false,
so plugins: true does NOT meaningfully widen the renderer's attack
surface beyond what is already accepted.
2. The only PDFs that reach an iframe here are signed CloudFront URLs
we ourselves issued; user-supplied URLs are routed through
setWindowOpenHandler → openExternalSafely and cannot land in this
renderer.
3. Chromium's PDFium plugin is itself sandboxed and only handles
application/pdf — no Flash/Java/other historical plugin surfaces.
If we ever tighten webSecurity / sandbox, the follow-up is to host the
PDF viewer in a dedicated BrowserView with plugins scoped to that view,
keeping the main renderer plugin-free.
Old desktop builds ship without the preview modal, so the Eye button
never appears and PDF preview is gated by the same release — zero
regression risk for users on stale clients.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the regression reported in https://github.com/multica-ai/multica/issues/2515 that
PR #2437 only half-fixed in v0.2.31.
Two gaps remained on Ubuntu/GNOME:
1. The .deb shipped only the source 1024×1024 PNG under
/usr/share/icons/hicolor/, with no usable smaller sizes. GNOME's hicolor
lookup walks 16…512 and falls back to the theme default when none
match, so the launcher had no icon. The auto-generation pass in
electron-builder silently produced only the source size for us. Drop
pre-rendered 16/24/32/48/64/128/256/512 PNGs into build/icons/ and
point `linux.icon` at the directory so packaging stops depending on
the toolchain re-running that generation correctly.
2. WM_CLASS at runtime was `@multica/desktop`, while the .desktop file
declared `StartupWMClass=Multica`. PR #2437 assumed Electron derives
WM_CLASS from electron-builder.yml's `productName`, but Electron
reads `app.getName()`, which reads the *packaged ASAR's* package.json
— productName if present, otherwise name. Our source
apps/desktop/package.json had no top-level productName, so the ASAR
carried only `name: "@multica/desktop"` and Chromium emitted that as
WM_CLASS, breaking the .desktop association and the dock icon.
Fixed in two anchors for belt-and-braces: add
`"productName": "Multica"` to apps/desktop/package.json (so the ASAR
carries it and app.getName() resolves correctly by default), and call
`app.setName("Multica")` in the production branch alongside the
existing dev-only setName so a future regression in package.json or
the build pipeline cannot silently re-break WM_CLASS.
The `StartupWMClass: Multica` declaration in electron-builder.yml stays
pinned and the surrounding comment has been rewritten to record the
correct WM_CLASS derivation.
Verification on a real Ubuntu install:
- `dpkg-deb -c multica-desktop-*-linux-amd64.deb | grep hicolor` lists
≥8 sizes.
- `xprop WM_CLASS` on the running window prints `"multica", "Multica"`.
- Launcher and dock both show the Multica logo with no manual
~/.local/share/icons workaround.
Co-authored-by: multica-agent <github@multica.ai>
Base UI's Menu uses focus-follows-cursor — hovering a sibling row drags
DOM focus to that row, which made the rename input's onBlur=save fire
just from moving the mouse. The result: clicking the pencil and then
nudging the cursor would silently commit a half-typed title.
Replace the blur handler with a document-level pointerdown listener
(capture phase, so it runs before Base UI's outside-click close handler
unmounts the input). The listener only commits when the user actually
clicks somewhere outside the input. Enter still commits, Escape still
cancels, mouse hover is now a no-op.
MUL-2110
Co-authored-by: multica-agent <github@multica.ai>
Gemini CLI's folder-trust feature throws FatalUntrustedWorkspaceError
(exit code 55) when the current workspace isn't in
`~/.gemini/trustedFolders.json` and the process is headless — no
interactive trust prompt is available. The daemon spawns gemini with
`-p` + `--yolo` in a freshly checked-out worktree that the user has
never trusted interactively, so every run with `security.folderTrust`
enabled fails after ~10s with exit status 55 and no useful output.
Default `GEMINI_CLI_TRUST_WORKSPACE=true` on the child env to short-
circuit `checkPathTrust` in gemini-core. This mirrors gemini-cli's
documented `--skip-trust` flag; the env var has been gemini's
documented headless escape hatch for the entire folder-trust feature
lifetime so the fix works on every gemini version that can produce
the crash. Callers that explicitly set the same key in cfg.Env win,
preserving the ability to opt back into the gate.
Co-authored-by: multica-agent <github@multica.ai>
The gemini CLI's Windows shim emits `Active code page: 65001` (from
`chcp`) to stdout before the real version reaches `--version` output.
The daemon stored the raw concatenation as the runtime version, so the
runtime detail page rendered `Active code page: 65001 0.42.0` instead
of `0.42.0`.
Scan `<cli> --version` line by line and return the first line carrying
a semver-shaped token. Full strings like `2.1.5 (Claude Code)` or
`codex-cli 0.118.0` survive unchanged; unparseable output falls back to
the trimmed raw value.
Co-authored-by: multica-agent <github@multica.ai>
Adds a pencil icon next to the trash icon on each session row in the chat
dropdown. Clicking it turns the title into an inline editable input:
Enter / blur saves, Escape cancels.
Server: new PATCH /api/chat/sessions/{id} handler that updates the title
via the existing `UpdateChatSessionTitle` sqlc query, broadcasts a new
`chat:session_updated` WS event so other tabs / devices stay in sync, and
rejects blank titles. Frontend mutation is optimistic with rollback,
matching the existing delete-session pattern.
MUL-2110
Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 16:57:34 +08:00
415 changed files with 32795 additions and 2437 deletions
@@ -32,6 +32,8 @@ Multica turns coding agents into real teammates. Assign issues to an agent like
No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**, **GitHub Copilot CLI**, **OpenClaw**, **OpenCode**, **Hermes**, **Gemini**, **Pi**, **Cursor Agent**, **Kimi**, and **Kiro CLI**.
For larger teams, Squads add a stable routing layer: assign work to a group led by an agent, and the leader delegates to the right member.
@@ -53,6 +55,7 @@ Like Multics before it, the bet is on multiplexing: a small team shouldn't feel
Multica manages the full agent lifecycle: from task assignment to execution monitoring to skill reuse.
- **Agents as Teammates** — assign to an agent like you'd assign to a colleague. They have profiles, show up on the board, post comments, create issues, and report blockers proactively.
- **Squads** — group agents (and humans) under a leader agent and assign work to the *squad*. The leader decides who should pick it up, so routing stays stable as the team grows. `@FrontendTeam` instead of `@alice-or-bob-or-carol`.
- **Autonomous Execution** — set it and forget it. Full task lifecycle management (enqueue, claim, start, complete/fail) with real-time progress streaming via WebSocket.
- **Reusable Skills** — every solution becomes a reusable skill for the whole team. Deployments, migrations, code reviews — skills compound your team's capabilities over time.
- **Unified Runtimes** — one dashboard for all your compute. Local daemons and cloud runtimes, auto-detection of available CLIs, real-time monitoring.
@@ -128,21 +131,6 @@ Create an issue from the board (or via `multica issue create`), then assign it t
---
## Multica vs Paperclip
| | Multica | Paperclip |
|---|---------|-----------|
| **Focus** | Team AI agent collaboration platform | Solo AI agent company simulator |
| **User model** | Multi-user teams with roles & permissions | Single board operator |
@@ -25,14 +25,30 @@ These have sensible defaults and only need to be set when tuning a large or cons
### Email (Required for Authentication)
Multica uses email-based magic link authentication via [Resend](https://resend.com).
Multica supports two emailbackends. `SMTP_HOST` takes priority when set; otherwise `RESEND_API_KEY` is used. With neither configured, verification codes are printed to the server log — copy them from there to log in.
#### Option A: Resend (recommended for cloud deployments)
> **Note:** If Resend is not configured, generated verification codes are printed to backend logs. A fixed local testing code is disabled by default; to opt in on a private test instance, set `APP_ENV=development` and `MULTICA_DEV_VERIFICATION_CODE` to a 6-digit value. It is ignored when `APP_ENV=production`.
Use this option when your deployment cannot reach the public internet or you already have an internal mail relay (e.g. Exchange, Postfix, SendGrid on-prem).
| `SMTP_TLS_INSECURE` | Set `true` to skip TLS certificate verification (self-signed / private CA certs) | `false` |
STARTTLS is used automatically when advertised by the server. Port 465 (SMTPS / implicit TLS) is not currently supported - use ports 25 or 587 with STARTTLS.
> **Note:** If neither Resend nor SMTP is configured, generated verification codes are printed to backend logs — copy them from there to log in. A fixed local testing code (e.g. `888888`) is **opt-in only**: set `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env` and keep `APP_ENV` non-production. The Docker self-host stack pins `APP_ENV=production`, so the shortcut is ignored there. **Never enable a fixed code on a publicly reachable instance.**
@@ -5,7 +5,7 @@ description: Hand an issue to an agent and it takes over as the official assigne
import { Callout } from "fumadocs-ui/components/callout";
Assign an [issue](/issues) to an [agent](/agents) and it works as the **official assignee** until the work is done — it can read the full issue context (description + all [comments](/comments)) and change status, post comments, and edit fields. This is the **most common and heaviest** of Multica's four trigger paths.
Assign an [issue](/issues) to an [agent](/agents) and it works as the **official assignee** until the work is done — it can read the full issue context (description + all [comments](/comments)) and change status, post comments, and edit fields. This is the **most common and heaviest** of Multica's four trigger paths. The same flow also accepts a [squad](/squads) as the assignee — Multica then triggers the squad's **leader agent** instead.
| Path | When to use | Changes the issue | Context | Priority | Auto retry |
|---|---|---|---|---|---|
@@ -18,7 +18,7 @@ Assign an [issue](/issues) to an [agent](/agents) and it works as the **official
## Assign from the UI
On the issue detail page, click the **Assignee** picker. It lists every member in the workspace plus all non-archived agents. Pick an agent and the issue is assigned right away.
On the issue detail page, click the **Assignee** picker. It lists every member in the workspace, all non-archived agents, and every non-archived [squad](/squads). Pick an agent (or squad) and the issue is assigned right away.
A few rules:
@@ -78,5 +78,6 @@ But **different agents can work on the same issue in parallel** — for example,
## Next
- [**@-mention an agent in a comment**](/mentioning-agents) — a lighter trigger that leaves assignee and status untouched
- [**Squads**](/squads) — assign to a group of agents and let the leader decide who picks it up
- [**Chat**](/chat) — one-to-one conversation outside any issue
- [**Autopilots**](/autopilots) — let agents start work automatically on a schedule
@@ -12,9 +12,11 @@ For the list of environment variables referenced below, see [Environment variabl
## How email + verification code sign-in works
The user enters an email on the sign-in page → the server sends a 6-digit code → the user enters it → the server verifies it → a JWT cookie is issued. Standard flow. It requires [Resend](https://resend.com/) as the email provider:
The user enters an email on the sign-in page → the server sends a 6-digit code → the user enters it → the server verifies it → a JWT cookie is issued. Standard flow. Two delivery backends are supported — pick whichever fits your deployment:
1. Create a Resend account and verify your domain
### Option A: Resend (recommended for cloud / public-internet deployments)
1. Create a [Resend](https://resend.com/) account and verify your domain
2. Create an API key
3. Set the environment variables:
@@ -25,7 +27,22 @@ The user enters an email on the sign-in page → the server sends a 6-digit code
4. Restart the server
**What happens if you don't set `RESEND_API_KEY`**: the server doesn't error, but **every email that should have been sent is written to the server's stdout only**. Handy for local development (copy the code from the logs); in production it's a black hole.
Use this when the deployment can't reach `api.resend.com` or you already have an internal mail relay (Exchange, Postfix, on-prem SendGrid, etc.). `SMTP_HOST` takes priority over `RESEND_API_KEY` when both are set.
```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587 # default 25; use 587 for STARTTLS submission
SMTP_USERNAME=multica # leave empty for unauthenticated relay
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
```
STARTTLS is upgraded automatically when the server advertises it. Port 465 (SMTPS / implicit TLS) is **not** currently supported — use port 25 or 587.
**What happens if you set neither**: the server doesn't error, but **every email that should have been sent is written to the server's stdout only**. Handy for local development (copy the code from the logs); in production it's a black hole.
## Fixed local testing codes
@@ -34,7 +51,7 @@ The user enters an email on the sign-in page → the server sends a 6-digit code
The old behavior where non-production instances accepted `888888` by default has been removed. Unless you explicitly configure it, typing `888888` is treated like any other wrong code.
Local development without Resend should use the generated code printed in server logs. If you need deterministic local/private automation, set `MULTICA_DEV_VERIFICATION_CODE` to a 6-digit value such as `888888`, and keep `APP_ENV` non-production:
Local development without any email backend configured (no Resend, no SMTP) should use the generated code printed in server logs. If you need deterministic local/private automation, set `MULTICA_DEV_VERIFICATION_CODE` to a 6-digit value such as `888888`, and keep `APP_ENV` non-production:
@@ -35,14 +35,28 @@ These are the core variables you must think about before deploying — some have
## Email configuration
Multica uses [Resend](https://resend.com/) to send verification codes and invite emails.
Multica supports two delivery backends — [Resend](https://resend.com/) for cloud deployments, or an SMTP relay for internal / on-premise networks. `SMTP_HOST` takes priority over `RESEND_API_KEY` when both are set.
### Resend
| Variable | Default | Description |
|---|---|---|
| `RESEND_API_KEY` | empty | Resend API key |
| `RESEND_FROM_EMAIL` | `noreply@multica.ai` | Sender address (must be a domain verified in your Resend account) |
| `RESEND_FROM_EMAIL` | `noreply@multica.ai` | Sender address (must be a domain verified in your Resend account; also reused as the `From:` header when SMTP is in use) |
**Behavior when `RESEND_API_KEY` is unset**: the server does not error, but every email that should have been sent (verification codes, invite links) **is written to the server's stdout only**. Convenient for local development — copy the code out of the server logs; **in production, forgetting to set this creates a silent black hole**, with users never receiving email and no error surfaced.
### SMTP relay
| Variable | Default | Description |
|---|---|---|
| `SMTP_HOST` | empty | SMTP relay hostname. Setting this activates SMTP mode and overrides Resend |
| `SMTP_PORT` | `25` | SMTP port. Use `587` for STARTTLS submission; **port 465 (SMTPS / implicit TLS) is not supported** |
| `SMTP_TLS_INSECURE` | `false` | Set `true` to skip TLS certificate verification (private CA / self-signed only) |
STARTTLS is upgraded automatically when the server advertises it. The dial timeout is 10s and the whole SMTP session has a 30s deadline, so a black-holed relay can't hang the auth handler.
**Behavior when neither is set**: the server does not error, but every email that should have been sent (verification codes, invite links) **is written to the server's stdout only**. Convenient for local development — copy the code out of the server logs; **in production, forgetting to set this creates a silent black hole**, with users never receiving email and no error surfaced.
@@ -16,6 +16,10 @@ Same as mentioning a member — type `@` to open the picker and select an agent.
The `@mention` Markdown syntax, the picker, and `@all` semantics are covered in [**Comments**](/comments).
<Callout type="info">
**You can also `@`-mention a [squad](/squads) in a comment.** The same picker surfaces squads alongside members and agents; selecting one inserts `[@SquadName](mention://squad/<uuid>)` and triggers the squad's **leader agent** to coordinate a response — assignee and status stay untouched.
</Callout>
## How it differs from assignment
Both put the agent to work, but the mechanics are entirely different:
@@ -53,6 +57,7 @@ This guard **only blocks direct self-references.** Agent A @-mentioning agent B
## Next
- [**Squads**](/squads) — `@`-mention a squad to have the leader route the question to the right member
- [**Chat**](/chat) — one-to-one conversation outside any issue
- [**Autopilots**](/autopilots) — let agents start work automatically on a schedule
- [**Comments**](/comments) — `@mention` syntax, the picker, and `@all` semantics
**Option B — SMTP relay (internal networks / on-premise):**
For more auth configuration (OAuth, signup allowlist), see [Auth setup](/auth-setup).
Use this when the deployment can't reach `api.resend.com`, or you already have an internal mail relay (Exchange, Postfix, on-prem SendGrid, etc.). `SMTP_HOST` takes priority over Resend when both are set.
```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587 # default 25; use 587 for STARTTLS submission
SMTP_USERNAME=multica # leave empty for unauthenticated relay
SMTP_PASSWORD=...
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
```
Then restart: `docker compose -f docker-compose.selfhost.yml restart backend`.
For more auth configuration (OAuth, signup allowlist) and the full SMTP variable reference, see [Auth setup](/auth-setup) and [Environment variables → Email](/environment-variables#email-configuration).
## 4. First login + create a workspace
Open [http://localhost:3000](http://localhost:3000):
- Enter your email
- Grab the verification code from the Resend email (or, if you haven't configured Resend, from the server container stdout — look for the `[DEV] Verification code` line)
- Grab the verification code from your configured email backend (Resend or SMTP relay); if neither is configured, copy it from the server container stdout — look for the `[DEV] Verification code` line
- Do not use `888888` unless you explicitly set `MULTICA_DEV_VERIFICATION_CODE=888888` on a non-production private instance
- Log in and create your first workspace
@@ -108,7 +122,7 @@ Same flow as Cloud — see [Cloud quickstart → Steps 5-6](/cloud-quickstart#5-
## Common issues
- **Backend won't start**: check container logs with `docker compose -f docker-compose.selfhost.yml logs backend`; usually it's a bad `DATABASE_URL` or `JWT_SECRET` in `.env`
- **Verification code not received**: Resend isn't configured → look for `[DEV] Verification code` in `docker compose logs backend`
- **Verification code not received**: no email backend is configured (neither Resend nor SMTP) → look for `[DEV] Verification code` in `docker compose logs backend`
- **WebSocket won't connect**: for public deployments you must set `FRONTEND_ORIGIN` to your real frontend domain; see [Troubleshooting → WebSocket won't connect](/troubleshooting#websocket-wont-connect)
description: "A squad is a group of agents (and optionally human members) led by one designated leader agent. Assign an issue to a squad and the leader decides who picks it up."
---
import { Callout } from "fumadocs-ui/components/callout";
A squad is a **named group of [agents](/agents) and human [members](/members-roles)**, with one designated **leader agent**. The squad is itself a first-class assignee: pick it from any **Assignee** picker and the leader takes the trigger, reads the issue, then `@`-mentions the squad member best suited to do the work. Squads let you assemble specialists once and dispatch them **by topic instead of by name** — the team grows, the routing stays the same.
## What a squad is, in mechanics
- **One leader, many members.** The leader must be an agent; members can be agents or human members. A squad with only the leader is allowed (the leader briefing notes "no other members"), and the same agent can sit in multiple squads.
- **Assignable everywhere a person is.** Squads appear in the Assignee picker, the @-mention picker, and the quick-create modal — anywhere you'd pick an agent or member, you can pick a squad.
- **Soft-deleted via archive.** Archive a squad and it disappears from pickers and lists; any issue currently assigned to it is **transferred to the leader agent** so the work doesn't go silent. Archived squads can't be assigned to new issues.
## When to use a squad versus a single agent
| Pick a squad when… | Pick a single agent when… |
|---|---|
| You have several specialists and don't know which one fits this issue in advance | The work is well-scoped to one specialty and you know who should do it |
| You want one stable assignee (the squad) while the actual responder changes per issue | You want the agent's name on the issue and clear individual accountability |
| You want a `@FrontendTeam` style routing target in comments | One-on-one `@agent-name` is enough |
The squad doesn't add capability — it adds **routing**. The members are still ordinary agents; the leader's only job is to pick the right one.
## Permissions
| Action | Who can do it |
|---|---|
| Create / update / archive a squad | Workspace **owner** or **admin** |
| Add or remove members, change roles | Workspace **owner** or **admin** |
| Assign an issue to a squad | Any workspace member (same as assigning to an agent) |
| `@`-mention a squad in a comment | Any workspace member |
| Record a squad-leader evaluation | The squad leader agent only (via CLI) |
The full role matrix lives in [Members and roles](/members-roles).
## Create a squad
In the sidebar, open **Squads → New squad** and fill in:
- **Name** — e.g. `Frontend Team`, `Bug Triage`. Doesn't need to be unique within the workspace.
- **Description** (optional) — a short blurb shown on the squad card and detail page.
- **Leader** — pick an existing agent. The leader is added to the squad automatically with role `leader`.
After creation, open the squad's detail page to:
- **Add members** — pick agents or human members, optionally give each a short role description (e.g. "owns the migrations", "reviewer of last resort"). The leader uses these roles when deciding who to delegate to.
- **Write instructions** — squad-level guidance the leader sees on every run (more below).
- **Set an avatar** — picked from the same picker used for agents.
When a non-Backlog issue is assigned to a squad, Multica immediately enqueues a `task` for the **leader agent** (not for every member). The flow then looks like this:
1. **Leader claims the task.** The agent runtime picks up the task on its next poll, same as any other agent assignment.
2. **Leader is briefed.** On claim, Multica appends three sections to the leader's system prompt — see [What the leader sees on every turn](#what-the-leader-sees-on-every-turn) below.
3. **Leader posts one delegation comment.** The comment `@`-mentions the chosen member(s) using the exact mention markdown from the roster — that mention triggers a new `task` for each mentioned agent.
4. **Leader records its evaluation** via `multica squad activity <issue-id> action --reason "..."`. This writes an entry to the issue's activity timeline so humans can see the leader actually evaluated the trigger.
5. **Leader stops.** The leader does not do the implementation itself. When the delegated member posts back, the leader is re-triggered to read the update and either delegate the next step, escalate, or stay silent.
If the issue is in **Backlog**, the leader is not triggered — Backlog is a parking lot, same rule as for direct agent assignment.
### What the leader sees on every turn
On each squad-leader run, three blocks are appended to the leader's instructions:
- **Squad Operating Protocol** — a hard-coded rule set: read the issue, delegate by `@`-mention, be terse (don't restate the issue body — the assignee can read it), record an evaluation every turn, and **stop after dispatching**. This protocol is system-managed and not editable.
- **Squad Roster** — the leader's self-row plus one row per non-archived member. Each row carries the exact mention markdown (`[@Name](mention://agent/<uuid>)` or `[@Name](mention://member/<uuid>)`) the leader should paste — typing a plain `@name` won't trigger anyone.
- **Squad Instructions** — your custom guidance for this squad (set on the squad detail page or via `multica squad update --instructions`). Use this for routing rules ("send DB work to Alice, frontend to Bob"), escalation policies, or anything else the leader needs to know that isn't already in the issue.
## When the leader is re-triggered
After the first dispatch, the leader is woken up automatically by **most subsequent comments** on the issue. The exact rules:
| Event | Leader triggered? |
|---|---|
| A non-member (human reporter, external agent) posts a comment | **Yes** |
| A squad member posts a progress update with no `@mention` | **Yes** — the leader re-evaluates whether the next step is needed |
| Anyone posts a comment that explicitly `@`-mentions another agent / member / squad / `@all` | **No** — the explicit `@` is the routing signal; the leader gets out of the way |
| The leader's own comment (self-trigger) | **No** — guarded to prevent a loop |
| A comment containing only an issue cross-reference (`[MUL-123](mention://issue/...)`) | **Yes** — issue references aren't routing |
Dedup applies on top of these rules: if the leader already has a `queued` or `dispatched` task on this issue, a new trigger won't enqueue a duplicate.
<Callout type="info">
**Why the leader doesn't trigger when a member posts an `@`-mention.** Once a squad member directly `@`s someone, that comment is a deliberate hand-off — having the leader wake up to "observe" the routing would just produce a no-op turn and clutter the timeline. Agent-authored comments are the exception: when an agent posts a result that `@`s another agent, the leader still wakes up so it can coordinate the thread.
</Callout>
## `@`-mention a squad in a comment
Squads appear in the `@` picker alongside members and agents. Mentioning a squad inserts `[@SquadName](mention://squad/<uuid>)` and triggers the **squad leader** as if you had assigned the issue to the squad — without changing the assignee or the status. Use this when you want the squad to pick someone for a question or sub-task while keeping the current owner.
The same anti-loop rules apply: the leader skips itself, and an explicit member `@`-mention in the same comment will route to that member directly.
## Reassign or archive a squad
**Reassigning an issue away from a squad** behaves like any other assignee change: all of the issue's active tasks (including the leader's) are cancelled, and the new assignee — agent, member, or another squad — is enqueued. There is no separate "remove squad without changing assignee" action; pick a different assignee.
**Archiving a squad** (`multica squad delete <id>`, or the Archive button on the detail page):
1. **Transfers issues currently assigned to the squad to the leader agent**, so the work continues against a concrete agent instead of going silent.
2. Marks the squad with `archived_at` / `archived_by` — the row is preserved so historical activity entries still resolve, but the squad disappears from lists, pickers, and the @-mention dropdown.
3. **Rejects future assignments** to this squad with `cannot assign to an archived squad`.
There is currently no unarchive command; create a new squad if you need the routing back.
## Squad operations from the CLI
| Command | Purpose |
|---|---|
| `multica squad list` | List squads in the workspace |
| `multica squad get <id>` | Show one squad's name, leader, description, instructions |
| `multica squad update <id> [--name X] [--description X] [--instructions X] [--leader Y] [--avatar-url Z]` | Update one or more fields |
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
| `multica squad member list <id>` | List a squad's members |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | Add a member (owner / admin) |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | Remove a member (the leader cannot be removed — change leader first) |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Recorded by the leader agent at the end of every turn |
`--leader` accepts an agent name or UUID; for everything else, IDs come from `multica agent list --output json`, `multica workspace members --output json`, and `multica squad list --output json`.
## Next
- [Assign issues to agents](/assigning-issues) — same flow, applies to squad assignees too
- [`@`-mention agents in comments](/mentioning-agents) — the `@` picker also surfaces squads
- [Agents](/agents) — what an agent is, the building block of every squad
- [Members and roles](/members-roles) — the full owner / admin / member permission matrix
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.