* fix(daemon): normalize hostname by stripping .local mDNS suffix
Daemons started via different methods (standalone CLI vs desktop app
bundled binary) resolve the hostname differently on macOS — one gets
'computer' and the other 'computer.local'. This caused duplicate runtime
registrations for the same machine.
Stripping the .local suffix at the point of hostname resolution ensures
both always register under the same identifier.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(daemon): move empty-host fallback to after .local trim; fix Makefile @ prefix
- Reorder: TrimSuffix runs first, then empty-check, so a hostname of
just ".local" doesn't propagate as an empty daemon_id/device_name
- Add missing @ prefix on migrate command in Makefile so it isn't
echoed twice at startup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(daemon): allow startup with zero workspaces
The daemon used to fail fast with "no runtimes registered" when the
initial workspace sync returned zero workspaces. This masked a latent
bug: a newly-signed-up user has no workspaces yet, so the daemon would
crash immediately after login instead of waiting for the first
workspace to be created.
workspaceSyncLoop already polls every 30s (daemon.go:107, 365) to
discover new workspaces — the fail-fast check at startup was bypassing
this dynamic discovery. Remove the check so the daemon stays resident
and picks up the first workspace whenever it appears.
PR #1001 partially addressed this for the "server has workspaces but
local CLI config is empty" case. This finishes the job for the true
zero-workspace state, which until now was masked by the onboarding
wizard always creating a workspace before the daemon started.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(views): extract CreateWorkspaceForm for reuse
Modal and the upcoming /new-workspace page share the same form +
mutation + slug validation. Extract to a shared component so they
can't drift.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(views): add NoAccessPage for unknown or inaccessible workspace slugs
Rendered when the URL slug doesn't resolve to a workspace the user has
access to. Deliberately doesn't distinguish 404 vs 403 to avoid letting
attackers enumerate workspace slugs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(paths): add /new-workspace route and reserve slug on both sides
Adds paths.newWorkspace() builder, registers /new-workspace as a global
(pre-workspace) prefix, and reserves the "new-workspace" slug on both
frontend and backend (kept in sync per convention). Existing
"onboarding" reservation retained — removing it would desync FE/BE
and leaves no future fallback if an onboarding route is revived.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(migrations): audit no existing workspace uses 'new-workspace' slug
Migration 046 blocks deploy if any workspace in the DB has slug =
'new-workspace', which would shadow the new global workspace creation
route at /new-workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /new-workspace route on web and desktop
Renders the CreateWorkspaceForm as a full-page workspace creation flow,
used as the destination for first-time users with zero workspaces.
Replaces the 4-step onboarding wizard with a single form.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: show NoAccessPage on unknown workspace slug, hold null during active removal
Layouts render NoAccessPage when the URL slug doesn't resolve to an
accessible workspace — except when the slug previously resolved during
this layout instance's lifetime.
URL and cache are two asynchronous signals: there will always be a
short window where the URL still points at the old workspace but the
cache has already been invalidated (e.g. just after a delete/leave
mutation, or a realtime workspace:deleted event). Rendering
NoAccessPage during that window would flash "Workspace not available"
with recovery buttons in front of a user who just deleted the
workspace themselves — jarring and wrong.
useWorkspaceSeen classifies the two cases:
- slug was seen before, now gone → user's intent is changing (caller
is navigating away); render null, no flash
- slug never seen → user is genuinely looking at an inaccessible
workspace (stale bookmark, revoked access, link from a former
teammate); render NoAccessPage with recovery options
NoAccessPage deliberately does not distinguish 404 vs 403 to avoid
letting attackers enumerate workspace slugs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: redirect zero-workspace users to /new-workspace instead of /onboarding
Switches 8 call sites and the CLI:
- Web: login, auth callback, landing redirect-if-authenticated
- Desktop: routes.tsx IndexRedirect
- Shared: dashboard guard, invite page fallback, workspace-tab on delete,
realtime sync on workspace loss
- CLI: cmd_login.go waitForOnboarding now opens /new-workspace
Also adds /new-workspace to navigation store's lastPath exclusion list
so it doesn't get persisted as a 'last visited' page.
Adds a desktop App.tsx effect that restarts the daemon when workspace
count transitions 0 → ≥1, so first-workspace creation triggers
immediate daemon pickup rather than waiting up to 30s for the daemon's
workspaceSyncLoop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: remove onboarding flow
The 4-step onboarding wizard (workspace → runtime → agent → demo issues)
is replaced by:
- /new-workspace: a single-page workspace creation form (Phase 3)
- NoAccessPage: explicit feedback when a slug doesn't resolve (Phase 4)
- daemon zero-workspace bootstrap (Phase 1) so the daemon doesn't
crash before the user creates their first workspace
- desktop daemon restart on first workspace creation (Phase 5) for
instant pickup instead of the 30s workspaceSyncLoop tick
Deletions:
- packages/views/onboarding/ (OnboardingWizard + 4 step components + tests)
- apps/web/app/(auth)/onboarding/page.tsx
- apps/desktop/src/renderer/src/components/onboarding-gate.tsx (+test)
- OnboardingGate wrapper in desktop-layout.tsx
- OnboardingRoute + /onboarding route in desktop routes.tsx
- paths.onboarding() builder + /onboarding from GLOBAL_PREFIXES
- packages/views/package.json onboarding export
- /onboarding from navigation store's EXCLUDED_PREFIXES
Retained (intentional):
- 'onboarding' in RESERVED_SLUGS (both FE + BE) — kept for FE/BE sync
and future-proofing if /onboarding is ever revived
Also drops 4 demo issues that onboarding used to create on the new
workspace ('Say hello', 'Set up repo', etc.). New workspaces are now
fully empty; all list views already render empty-state UI correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: clean stale 'onboarding' references in comments and CLI helpers
Batch cleanup of references to the removed onboarding flow:
- 13 comment sites mentioning 'onboarding' updated to reflect the
new /new-workspace flow or removed where no longer accurate
- CLI waitForOnboarding renamed to waitForWorkspaceCreation (function
name + docstring); behavior unchanged
The 'onboarding' reserved slug entries (frontend + backend) are
intentionally retained — see prior commit rationale.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(views): extract shared NewWorkspacePage shell
The web (/new-workspace) and desktop (NewWorkspaceRoute) pages had
identical outer layout — same container, heading, and copy — with only
the onSuccess navigation primitive differing. That's exactly the
No-Duplication Rule pattern: extract the shared UI, inject the
platform-specific behavior.
The apps now only own the thin auth guard (web needs it, desktop
routes below WorkspaceRouteLayout already handle it) and the
onSuccess → navigate call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: remove rollback compat layer and tighten daemon restart trigger
Two cleanup items:
1. Drop localStorage['multica_workspace_id'] double-write in both
workspace layouts. That write was added as a rollback safety net
for the workspace-slug URL refactor (PR #1138) — the refactor has
since landed and stabilized, so the compat shim is no longer
needed. Per CLAUDE.md: don't keep compat layers beyond their
purpose.
2. Tighten the desktop daemon-restart trigger. The previous ref-based
logic fired a restart on any 0→1 workspace-count transition,
including account switches (user A logout → user B login). Scope
it precisely to 'this session started with zero workspaces and
just gained one' using a three-state ref (null=undecided,
true=empty-start, false=already-restarted-or-started-nonempty).
Account switches are already handled by daemon-manager.ts on
token change, so this avoids a redundant restart there.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): redirect to /login on logout and unauthenticated workspace visits
Two gaps previously left users stuck on blank workspace pages:
1. app-sidebar logout() cleared all state but never moved the URL. The
current path is /{workspaceSlug}/... which has no meaning without
auth; the workspace layout would then see user=null, render null
(via the hasBeenSeen short-circuit), and the user saw a blank page
thinking logout didn't work.
2. The workspace layouts (web + desktop) had no !user handling at all.
Any path that leaves user=null — token expiration, cross-tab logout,
or fresh visit to a workspace URL without a session — resulted in
the same blank screen.
Fix:
- app-sidebar.logout() explicitly push(paths.login()) after authLogout()
to cover the primary (user-initiated) logout path.
- Both workspace layouts get a defensive useEffect that redirects to
/login whenever auth has settled and user is null. Covers token
expiration, realtime logout, and any other silent session loss.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(runtime): derive runtime usage from task_usage only
The daemon used to scan each runtime's local CLI log directory every 5
minutes (Claude Code, Codex, OpenCode, OpenClaw, Hermes) and post daily
aggregates to /api/daemon/runtimes/{id}/usage. Those directories are
shared with the user's own local CLI sessions, so the user's personal
usage was being counted as Daemon-executed usage. Cursor and Gemini had
no scanner at all, so their runtime-level aggregates were always zero.
Switch GetRuntimeUsage to aggregate task_usage (already scoped to
Daemon-executed tasks) via agent_task_queue.runtime_id. Single source of
truth; Cursor/Gemini/Copilot get runtime usage for free; no reliance on
external CLI log formats.
Removes:
- server/internal/daemon/usage/ (all scanners)
- Daemon.usageScanLoop + providerToRuntimeMap
- Client.ReportUsage
- ReportRuntimeUsage handler + POST /api/daemon/runtimes/{id}/usage
- UpsertRuntimeUsage / GetRuntimeUsageSummary queries
- runtime_usage table (migration 046)
Refs: MUL-786
* fix(runtime): bucket daily usage by task_usage.created_at, not enqueue time
ListRuntimeUsage was aggregating by DATE(atq.created_at) and filtering
on atq.created_at. agent_task_queue.created_at is the enqueue timestamp,
which drifts from actual token-production time: a task queued at 23:58
and executed at 00:05 was attributed to yesterday; a task sitting in
the queue overnight was counted on the queue day.
The ?days=N cutoff also became a rolling window (now() - N) instead of
a calendar-day boundary, silently clipping the morning of the earliest
day returned.
Switch bucket + filter to task_usage.created_at (~= task completion /
usage-report time) and snap the since cutoff to start-of-day via
DATE_TRUNC.
Add a regression test covering both scenarios: cross-midnight task
attributes to the day tokens were reported, and the earliest day's
pre-cutoff rows are still included.
* feat(agent): add GitHub Copilot CLI backend
Integrate Copilot CLI as a new agent backend using the stable
`-p` JSONL mode (`--output-format json`), following the same
spawn-CLI-scan-JSONL pattern established by claude.go.
Backend (server/pkg/agent/copilot.go):
- Spawn `copilot -p <prompt> --output-format json --allow-all-tools --no-ask-user`
- Parse streaming JSONL events (system/assistant/user/result/log)
- Extract session ID for resume support (`--resume <id>`)
- Accumulate per-model token usage for billing
- Filter blocked args to prevent protocol-critical flag overrides
Daemon config:
- Probe MULTICA_COPILOT_PATH / MULTICA_COPILOT_MODEL env vars
- Copilot uses AGENTS.md (native discovery) and default skills path
Frontend:
- Add Copilot logo SVG and provider switch case
Tests: 14 unit tests covering arg building, event parsing, usage
accumulation, and edge cases. All Go + TS checks pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(daemon): add restart subcommand, make daemon uses it
- `daemon start` keeps original behavior: errors if already running
- `daemon restart` stops existing daemon then starts fresh
- `make daemon` now runs `daemon restart --profile local`
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(copilot): address review nits 1-5
- Nit 1: Add MinVersions["copilot"] = "1.0.0"
- Nit 2: Seed activeModel from session.start.data.selectedModel (falls
back to opts.Model, then "copilot"). First-turn tokens now get correct
model attribution.
- Nit 3: Handle assistant.reasoning/reasoning_delta → MessageThinking,
reasoningText in assistant.message → MessageThinking,
session.warning → MessageLog{warn}
- Nit 4: Extract handleCopilotEvent() method shared by production and
tests — no more duplicated switch body that can drift
- Nit 5: Deltas write to output buffer as defense-in-depth; if process
dies before assistant.message, output is non-empty
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent): add Cursor Agent CLI runtime support
Add cursor-agent as a new agent backend, following the same pattern as
existing providers. The implementation spawns cursor-agent CLI with
stream-json output, parses JSONL events into the unified Message type,
and supports session resume, usage tracking, and auto-approval (--yolo).
Changes:
- server/pkg/agent/cursor.go: cursorBackend implementation
- server/pkg/agent/cursor_test.go: unit tests for args, parsing, errors
- server/pkg/agent/agent.go: register "cursor" in New() factory
- server/internal/daemon/config.go: probe cursor-agent in PATH
- server/internal/daemon/execenv/context.go: cursor skill discovery path
- server/internal/daemon/execenv/runtime_config.go: AGENTS.md injection
- packages/views/.../provider-logo.tsx: cursor logo in UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): address PR review for cursor backend
1. Fix token usage double-counting: usage is now taken exclusively from
"result" events (session totals). Per-message usage in "assistant"
events is intentionally ignored. "step_finish" usage is only used as
fallback when no "result" usage is available.
2. Remove dead code: isCursorUnknownSessionError() and its regex were
defined but never called. Removed along with corresponding test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): add missing CustomArgs, SystemPrompt, MaxTurns, and debug logging to cursor backend
- Add cursorBlockedArgs and filterCustomArgs support for safe custom arg passthrough
- Add --system-prompt and --max-turns flag support to buildCursorArgs
- Add debug logging of command args before execution (consistent with all other backends)
- Move stdout-close goroutine inside main goroutine (consistent with claude.go pattern)
- Add tests for SystemPrompt/MaxTurns and CustomArgs filtering
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: make daemon uses local profile & update Cursor logo to official brand
- Makefile: make daemon now runs 'daemon start --profile local' for local dev
- Replace Cursor runtime logo with official brand SVG (removed background rect)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(agent): remove unsupported --system-prompt and --max-turns from cursor-agent
cursor-agent CLI does not support these flags. Instructions are already
injected via AGENTS.md and .cursor/skills/ files.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(agent): prevent step_finish + result usage double-counting in cursor
Split usage accumulation into separate stepUsage and resultUsage maps.
After stream ends, use resultUsage if available (session totals from
result event), otherwise fall back to stepUsage (sum of step_finish).
This prevents 2x counting when result.usage already includes totals.
Added table-driven test covering: result-only, step_finish-only,
step_finish+result (no double count), and multi-model scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(agent): fix misleading comment on cursor -p flag
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: yushen <ldnvnbl@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent): add Pi agent runtime support
Add Pi as a new agent runtime provider, following the established adapter
pattern. Pi CLI outputs JSONL events which are parsed for messages, tool
calls, and usage tracking.
Backend:
- New piBackend implementing the Backend interface (pi.go)
- Pi CLI discovery via MULTICA_PI_PATH env var or PATH lookup
- JSONL event stream parsing (agent_start, message_update, thinking_update,
tool_execution_start/end, agent_end)
- Usage scanner for ~/.pi/sessions/*.jsonl files
- Runtime config injection via AGENTS.md
- Skill injection to .pi/agent/skills/
Frontend:
- Pi provider logo (teal π icon)
- Pi label in transcript dialog
Docs:
- Updated all provider lists in README, CLI_INSTALL, and docs
* fix(agent): filter Pi usage scanner to agent_end events only
Address review feedback: restrict usage parsing to agent_end events
which contain cumulative totals, preventing potential inaccuracy if
Pi adds usage fields to other event types in the future.
* fix(agent): align Pi runtime with real CLI flags, event schema, and custom_args
- Flags: Pi's CLI uses `--mode json` (not `--output-format jsonl`), has no
`--yolo` (explicit `--tools` allowlist instead), takes the prompt as a
positional argument (not `-p <prompt>`), splits model as
`--provider <name> --model <id>`, and treats `--session` as a file path
that must exist before spawn.
- Event parsing: rewrite the stream event struct to match Pi's actual
JSON event schema (`message_update.assistantMessageEvent.delta`,
`turn_end.message.usage.{input,output,cacheRead,cacheWrite}`, etc.).
- Sessions: generate/persist session files under ~/.multica/pi-sessions/
and use the file path as the opaque SessionID returned to the daemon.
- Usage scanner: read assistant `message` events from the same session
files (Pi's session-file schema, distinct from the stdout stream).
- Custom args: consume `ExecOptions.CustomArgs` via `filterCustomArgs`
with a Pi-specific blocked set (`-p`, `--print`, `--mode`, `--session`)
so Pi matches the pattern shared by every other agent backend.
- TrimSpace incoming repoURL in ensureRepoReady to prevent unnecessary
server refreshes when CLI passes URLs with whitespace
- Add comment on reposVersion field clarifying it is stored for future
version-based skip optimization
- Add concurrency safety comment on syncWorkspacesFromAPI skip logic
- Add test for URL trimming fast-path behavior
* feat(agent): add custom CLI arguments support
Allow users to configure custom CLI arguments per agent that get
appended to the agent subprocess command at launch time. This enables
use cases like specifying different models (--model o3), max turns,
or other provider-specific flags without needing separate runtimes.
Changes:
- Add custom_args JSONB column to agent table (migration 041)
- Update API handler to accept/return custom_args in create/update
- Pass custom_args through claim endpoint to daemon
- Append custom_args to CLI commands for all agent backends
- Add ExecOptions.CustomArgs field in agent package
- Add Custom Args tab in agent detail UI
- Add --custom-args flag to CLI agent create/update commands
Closes MUL-802
* fix(agent): filter protocol-critical flags from custom_args
Add per-backend filtering of custom_args to prevent users from
accidentally overriding flags that the daemon hardcodes for its
communication protocol (e.g. --output-format, --input-format,
--permission-mode for Claude).
This follows the same pattern as custom_env's isBlockedEnvKey: we
only block the small, stable set of flags that would break the
daemon↔agent protocol — not every possible dangerous flag. Workspace
members are trusted for everything else.
Each backend defines its own blocked set:
- Claude: -p, --output-format, --input-format, --permission-mode
- Gemini: -p, --yolo, -o
- Codex: --listen
- OpenCode: --format
- OpenClaw: --local, --json, --session-id, --message
- Hermes: none (ACP is positional)
Includes unit tests for the filtering logic.
* fix(agent): address code review nits for custom_args
- Replace module-level `nextArgId` counter with `crypto.randomUUID()`
in custom-args-tab.tsx to avoid SSR ID conflicts
- Add unit tests for custom args passthrough and blocked-arg filtering
in both Claude and Gemini arg builders
Issue list JSON now includes total, limit, offset, has_more fields so agents
can detect truncated results and paginate. Also documents --limit/--offset in
the agent prompt and emphasizes mention format in Output section.
Closes MUL-837
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): restart local daemon when bundled CLI version differs
Desktop bundles a multica CLI binary at build time via bundle-cli.mjs.
If a local daemon is already running from a previous session with an
older CLI, the newly bundled version never takes effect until the user
manually restarts. Fix that on the login/auto-start path.
- Expose the daemon's CLI version on GET /health as cli_version (sourced
from cfg.CLIVersion, which is already set from the ldflag at daemon
startup in cmd_daemon.go).
- In the desktop main process, query the resolved CLI binary's version
once via `multica version --output json` and cache it for the process
lifetime.
- On daemon:auto-start, if the daemon is already running, compare the
two versions. Restart only when BOTH sides are known and the strings
differ — a restart kills in-flight agent tasks, so any uncertainty
(bundled CLI unknown, older daemon without cli_version field, read
failure) fails safe and leaves the daemon alone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(daemon): defer version-mismatch restart until active tasks drain
Previous iteration restarted the daemon immediately on a confirmed CLI
version mismatch, which would kill any agent tasks mid-execution. Gate
the restart on an active-task counter so in-flight work always finishes.
- Daemon: add `activeTasks atomic.Int64` on the Daemon struct,
increment/decrement it around handleTask, and expose it as
`active_task_count` on GET /health.
- Desktop: when a version mismatch is confirmed but active_task_count >
0, set a pendingVersionRestart flag instead of restarting. The 5s
pollOnce loop retries ensureRunningDaemonVersionMatches on each tick
and fires the restart the moment the count drops to 0.
- Eventual consistency: if the user keeps the daemon permanently busy,
the version stays out of date — that's a strictly better failure mode
than silently killing hour-long agent runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(daemon): cover version-check decision + /health counter exposure
Addresses the test-coverage gap from the second review.
- Go: extract the /health handler into a named method `(d *Daemon)
healthHandler(startedAt time.Time)` so it can be exercised via
httptest without spinning up a listener. Add health_test.go covering
cli_version + active_task_count field exposure and the increment /
decrement protocol used by pollLoop.
- Desktop: extract the pure version-check decision logic into
version-decision.ts (no electron, no I/O, no module state). The
ensureRunningDaemonVersionMatches wrapper now delegates the "what
should we do" decision to decideVersionAction and owns only the side
effects (logging, flag mutation, restartDaemon call).
- Desktop: bolt vitest onto apps/desktop (vitest.config.ts + catalog
devDep + test script) so main-process unit tests have a home. Add
version-decision.test.ts covering all four action branches and the
busy→idle drain transition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): bust CLI version cache on retry-install, lock wire-level JSON keys
Two polish items from review.
- daemon:retry-install now also clears cachedCliBinaryVersion. Previously
a retry that landed a newly-downloaded CLI at a different version
would false-negative on the next version check because the cached
version string was sticky for the process lifetime.
- TestHealthHandlerReportsCLIVersionAndActiveTaskCount now decodes into
a raw map[string]any and asserts the exact snake_case keys
(cli_version, active_task_count, status). The desktop TS client keys
on these literal strings, so a silent struct-tag rename must fail the
test. Typed struct round-trip kept as a separate value check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The daemon now automatically watches all workspaces the user belongs to,
fetched directly from the API. This removes the manual watch/unwatch
workflow, the config-based watched/unwatched lists, the /watch HTTP
endpoints, the CLI watch/unwatch commands, and the desktop app's watched
workspace UI and reconciliation logic.
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the CLI config has no watched workspaces (e.g. fresh desktop app
install), loadWatchedWorkspaces returns successfully but registers zero
runtimes. The runtime check immediately after fails with "no runtimes
registered" before workspaceSyncLoop gets a chance to discover
workspaces from the API.
Run one sync cycle inline when the watched list is empty so the daemon
can bootstrap itself without a pre-configured workspace list.
* feat(desktop): add daemon management panel with sidebar status bar
Integrate multica daemon lifecycle management into the desktop app so
users can start/stop/restart the daemon and view live logs without
leaving the UI. Session tokens are automatically synced to the CLI
config file, making daemon authentication transparent.
- daemon-manager.ts: Electron main process module for daemon lifecycle
(health polling, start/stop via CLI, token sync, log tail)
- Preload bridge: new daemonAPI with IPC for all daemon operations
- Sidebar bottomSlot: persistent daemon status indicator in sidebar
footer (desktop-only, injected via AppSidebar slot)
- Daemon panel Sheet: right-side drawer with status details, controls,
and real-time log viewer with auto-scroll and level coloring
- Token sync: on login and app startup, JWT is written to
~/.multica/config.json so daemon can authenticate seamlessly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): add P1+P2 daemon features — runtimes card, auto-start, settings
P1: Runtimes page Local Daemon card
- Add topSlot prop to shared RuntimesPage for platform injection
- DaemonRuntimeCard shows status, agents, uptime with Start/Stop/
Restart/Logs buttons (desktop-only, injected via slot)
P2: Auto-start and auto-stop
- Daemon auto-starts on app launch when user is authenticated
(controlled by autoStart preference, default: true)
- Daemon auto-stops on app quit (controlled by autoStop preference,
default: false — daemon keeps running in background by default)
- Preferences persisted to ~/.multica/desktop_prefs.json
P2: Daemon settings tab
- New "Daemon" tab in Settings > My Account section (desktop-only)
- Toggle auto-start and auto-stop behavior
- CLI installation status check with link to install guide
- SettingsPage gains extraAccountTabs prop for platform injection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): address PR review feedback on daemon management
Must-fix:
- before-quit handler now calls event.preventDefault(), awaits
stopDaemon(), then re-calls app.quit() so the daemon actually
stops before the app exits
- Add concurrency guard (operationInProgress lock) in daemon-manager
to reject overlapping start/stop/restart IPC calls
- Extract shared types (DaemonState, DaemonStatus, DaemonPrefs),
constants (STATE_COLORS, STATE_LABELS), and formatUptime to
apps/desktop/src/shared/daemon-types.ts — all renderer components
now import from this single source
Should-fix:
- Log viewer uses monotonic counter (LogEntry.id) instead of array
index as React key, preventing full re-renders on overflow
- All start/stop/restart handlers now show toast.error() with the
error message when the operation fails
- startLogTail retries up to 5 times with 2s delay when the log
file doesn't exist yet (handles first-run case)
Minor:
- Cache findCliBinary() result after first successful lookup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(logger): suppress ANSI color codes when stderr is not a TTY
Detect whether stderr is connected to a terminal and set tint's NoColor
option accordingly. Previously daemon.log files contained raw escape
sequences like \033[2m and \033[92m which made them unreadable in the
Desktop log viewer and any non-TTY sink (docker logs, systemd, etc).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(daemon): runtime watch/unwatch HTTP endpoints and denylist
Add GET/POST/DELETE /watch handlers on the daemon's health port so
clients (notably Desktop) can add or remove watched workspaces at
runtime without restarting the daemon or editing config.json. Each
handler updates in-memory state under d.mu and persists back to
~/.multica/profiles/<name>/config.json for survival across restarts.
- CLIConfig gains UnwatchedWorkspaces as an explicit opt-out denylist.
syncWorkspacesFromAPI skips entries in the denylist so a manual
unwatch isn't silently revived 30s later by the periodic sync.
- loadWatchedWorkspaces tolerates an empty config and returns nil
instead of erroring out, because Desktop starts daemons with a
fresh profile and relies on the sync loop / watch endpoint to
populate the list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): bundled CLI, per-backend profile, and watch UI
Make the Desktop app self-sufficient: it bundles its own multica
binary, manages its own daemon profile keyed by the backend URL, and
authenticates that daemon with a long-lived PAT it mints on first
login. The daemon panel gains a checkbox list of watched workspaces
and surfaces the active profile + server URL.
CLI bootstrap
- scripts/bundle-cli.mjs copies server/bin/multica into
apps/desktop/resources/bin/ before electron-vite dev and
electron-builder package. asarUnpack: resources/** already covers
this path, so the binary ships with the .app in prod.
- main/cli-bootstrap.ts adds an ensureManagedCli() fallback that
downloads the latest release from GitHub when no bundled binary
exists (first launch on a machine without developer tooling).
- daemon-manager.resolveCliBinary prefers bundled > managed > download
> PATH, so local iteration uses the freshly built binary.
Daemon profile
- resolveActiveProfile now derives a desktop-<host> profile name from
the target API URL and creates its config.json on demand. Never
reads or writes the user's hand-configured CLI profiles, avoiding
the "Desktop polluted my default profile" class of bug.
- syncToken detects a JWT input and exchanges it for a PAT via
POST /api/tokens; caches the resulting mul_* token in the profile
config so subsequent launches skip the round-trip.
- startDaemon / stopDaemon / log tail all operate on the resolved
profile; renderer sets the target URL via a new
daemon:set-target-api-url IPC.
Workspace watching
- daemon-manager exposes daemon:list-watched / daemon:watch-workspace /
daemon:unwatch-workspace IPCs backed by the daemon's new /watch
endpoints.
- App.tsx reconciles the user's workspace list against the daemon's
watched set whenever TanStack Query updates it — new workspaces are
registered instantly instead of waiting for the daemon's 30s sync,
and removed workspaces are unwatched.
- daemon-panel gains a "Watched Workspaces" section with per-workspace
checkboxes that call watch/unwatch directly. Opt-outs persist in the
profile's unwatched_workspaces denylist.
Lifecycle states + UI
- DaemonStatus gains `profile`, `serverUrl`, and an `installing_cli`
state. Panel shows Profile / Server info rows and a "Setting up…"
blurb during first-run CLI download; failure surfaces a Retry button.
- Status bar renders a spinner during installation and hides the Start
button until setup finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): register /onboarding route
The create-workspace modal navigates to /onboarding on success, but
the Desktop router only had flat routes (issues, projects, runtimes,
etc.) — resulting in an "Unexpected Application Error! 404 Not Found"
page after creating a new workspace.
Mirror the web app's wiring: render OnboardingWizard with onComplete
pushing to /issues, via the shared navigation adapter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(desktop): remove sidebar daemon status bar
Drop the bottom-left daemon indicator in favor of the DaemonRuntimeCard
at the top of the Runtimes page, which already shows the same info
plus full Start/Stop/Restart controls and the Logs entry point. A
single canonical place avoids fragmenting daemon status across the UI.
Also remove the now-unused `bottomSlot` prop from AppSidebar — Desktop
was the only consumer, Web never needed it, so keeping it would be
dead scaffolding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): daemon panel layout and close button
- Logs section now fills the remaining vertical space down to the
sheet bottom instead of being capped at h-64, which left a huge
empty area below it. Top section (status, actions, watched list)
keeps natural height as shrink-0; the watched list gets its own
max-h-48 scroll so a long list can't push Logs off screen.
- Replace the Sheet's built-in close button with an explicit
<button> wired directly to onOpenChange(false). The Base UI
Dialog.Close wrapped in Button via the render prop wasn't firing
on click in this panel; going straight through the controlled
state guarantees it responds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): make daemon panel clickable inside Electron drag region
The sheet opens at the top of the window, which visually overlaps the
TabBar's -webkit-app-region: drag zone. Even though the sheet portals
to document.body, Chromium computes drag regions over the final
composited pixels, so the sheet inherited "drag" and swallowed the
mouseup of every click (mousedown fired but click never resolved) —
including the X close button.
Mark the entire SheetContent popup with -webkit-app-region: no-drag
to subtract it from the drag region. This also fixes future buttons /
checkboxes inside the sheet that would have hit the same issue.
While here, move the close button into the SheetHeader as a flex
sibling of SheetTitle instead of an absolutely positioned overlay —
simpler layout and avoids any stacking-context weirdness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): clickable daemon runtime card row
The whole Local Daemon row now opens the sheet panel — icon, title,
and status line are all part of one click target. This replaces the
standalone "Logs" button, which was redundant now that clicking
anywhere on the row does the same thing.
The right-side action cluster (Start / Stop / Restart) wraps its
onClick in stopPropagation so pressing those buttons doesn't bubble
up and open the panel.
Keyboard access: Enter / Space on the focused row opens the panel,
with a focus-visible background for feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(runtimes): mark Desktop-launched daemons as managed
When the Multica Desktop app spawns the CLI it ships with, the
resulting daemon shares its binary with the Electron bundle — Desktop
is responsible for updating that binary on every release. Letting the
daemon self-update would just get clobbered on the next Desktop launch
and could brick the embedded binary mid-update.
Propagate a "launched_by" signal end-to-end so the UI can hide the
CLI self-update affordance (and the daemon refuses updates as a second
line of defense):
- Desktop's startDaemon spawns execFile with env MULTICA_LAUNCHED_BY=desktop.
- daemon.Config gains LaunchedBy; cmd_daemon reads the env var on boot.
- registerRuntimesForWorkspace includes launched_by in the request body.
- Server DaemonRegister folds launched_by into runtime.metadata (JSONB
— no migration needed).
- handleUpdate returns a "failed" status with an explanatory message
when LaunchedBy == "desktop", so even a bypass API call can't trigger
the self-update path.
- RuntimeDetail extracts metadata.launched_by and passes it to
UpdateSection, which swaps the Latest / → available / Update button
cluster for a muted "Managed by Desktop" label.
CLI-only users (brew install, direct tarball) keep the exact same
behavior — the env var is empty, the UI shows the update button,
the daemon still self-updates on request.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): harden daemon manager from PR review
- syncToken now takes userId and mints a fresh PAT on user switch,
restarting a running daemon so it picks up the new credentials.
A .desktop-user-id sidecar in each profile records the owner so a
previous user's cached PAT can't be reused on the next login.
- App.tsx wires onLogout on CoreProvider to daemonAPI.clearToken()
and daemonAPI.stop() so the cached PAT and live daemon don't
outlive the session.
- startLogTail replaced with a cross-platform watchFile
implementation (initial 32 KB window + poll for new bytes,
handles truncation). spawn("tail") was broken on Windows.
- writeProfileConfig now serializes through a promise chain to
prevent concurrent writes from corrupting config.json.
- startDaemon keeps the "starting" state until pollOnce confirms
/health, avoiding a running → stopped flash when the Go daemon
isn't yet listening after the supervisor returns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): verify downloaded CLI against checksums.txt
Download goreleaser's checksums.txt alongside the release archive,
parse the sha256 lookup, stream the archive through createHash, and
refuse to install on mismatch or missing entry. Closes the supply-
chain gap where auto-install would execute an unverified binary on
first launch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(desktop): lint and style cleanups from PR review
- eslint.config.mjs: add scripts/**/*.{mjs,js} override with
globals.node so bundle-cli.mjs lints clean (was erroring on
undefined process/console).
- daemon-panel.tsx: log level classes now use semantic tokens
(text-info, text-warning, text-destructive) instead of hardcoded
Tailwind colors; escape the apostrophe in the retry copy.
- daemon-settings-tab.tsx: import DaemonPrefs from shared/daemon-
types instead of redefining it.
- runtimes-page.tsx: fix indentation inside the new topSlot wrapper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: yushen <ldnvnbl@gmail.com>
When a comment-triggered task resumes an existing session, the agent
may mistake the new comment for a previous one and skip it. Add [NEW
COMMENT] tag to the prompt and reinforce in AGENTS.md workflow that
the agent must respond to THIS specific comment, not prior ones.
When an agent CLI process hangs (e.g. a tool call blocks on unreachable
I/O), the daemon's scanner blocks indefinitely on stdout, preventing the
Result from ever being sent. This causes tasks to stay in "running"
state permanently with no further events.
Three-layer fix:
1. Agent backends (claude, opencode, openclaw, gemini): add a watchdog
goroutine that closes the stdout/stderr pipe when the context is
cancelled, forcing the scanner to unblock. Also set cmd.WaitDelay
so Go force-closes pipes after 10s if the process doesn't exit.
2. daemon executeAndDrain: add an independent drain timeout (backend
timeout + 30s buffer) with context-aware select on both the message
channel and the result channel, so the daemon never blocks forever.
3. daemon ping path: add context-aware select so pings don't deadlock
if the agent backend stalls.
Closes#925
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): prevent duplicate runtime registration on profile switch
The daemon_id included a profile name suffix (e.g. "hostname-staging"),
so switching profiles created a new daemon_id that bypassed the UPSERT
dedup constraint, leaving orphaned runtime records in the database.
Three changes:
- Remove profile suffix from daemon_id — use stable hostname only.
The unique constraint (workspace_id, daemon_id, provider) already
prevents collisions within the same workspace.
- Auto-migrate agents from old offline runtimes to the newly registered
runtime during DaemonRegister (same workspace/provider/owner).
- Add TTL-based GC in the runtime sweeper to delete offline runtimes
with no active agents after 7 days.
Closes MUL-695
* fix(daemon): address code review issues on PR #906
1. Move gcRuntimes() to the main sweep loop — previously it was inside
sweepStaleRuntimes() after an early return, so it only ran when new
runtimes were marked stale. Now it runs every sweep cycle independently.
2. Fix DeleteStaleOfflineRuntimes to exclude runtimes with ANY agent
reference (not just active ones). The FK agent.runtime_id is ON DELETE
RESTRICT, so archived agents also block deletion.
3. Scope MigrateAgentsToRuntime to the same machine by matching
daemon_id LIKE '<current_daemon_id>-%'. This prevents cross-machine
agent migration when the same user has multiple devices.
When an agent is triggered via @mention (not as the issue assignee),
the generated CLAUDE.md had no explicit agent identity. The agent would
infer its identity from the issue's assignee field, causing it to skip
work intended for it.
Now CLAUDE.md always includes "You are: <agent-name> (ID: <agent-id>)"
so the agent knows exactly who it is regardless of the issue assignee.
Closes MUL-709
- cmd_daemon.go: use filepath.Join for PID/log file paths instead of string concat with "/"
- codex_home.go: use os.TempDir() instead of hardcoded "/tmp" for cross-platform fallback
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Windows, os.Symlink requires Developer Mode or admin privileges.
Extract symlink creation into platform-specific files: on non-Windows,
behavior is unchanged (os.Symlink). On Windows, try os.Symlink first,
then fall back to directory junctions (mklink /J) for dirs and file
copy for files.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Security: add isBlockedEnvKey() blocklist that rejects MULTICA_*
prefix and critical system vars (HOME, PATH, USER, SHELL, TERM,
CODEX_HOME) from custom_env injection
2. Observability: log warnings when json.Unmarshal fails on custom_env
(agentToResponse + claim endpoint)
3. UX: use stable auto-increment IDs for env entry React keys instead
of array index to prevent input focus/state issues on add/remove
Add per-agent custom_env configuration that gets injected into the agent
subprocess at launch time. This enables users to configure custom API
endpoints (ANTHROPIC_BASE_URL), API keys (ANTHROPIC_API_KEY), and cloud
provider modes (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX) without
requiring code changes.
Changes:
- Migration 040: add custom_env JSONB column to agent table
- Backend: custom_env in agent CRUD API + claim endpoint
- Daemon: merge custom_env into subprocess environment variables
- Frontend: env var editor in agent settings (key-value pairs with
visibility toggle for sensitive values)
Closes#816
Related: #807, #809
Main introduced executeAndDrain/mergeUsage refactor. Resolve by keeping
main's refactored structure and re-applying EnvRoot to the switch/case
in runTask. Rename newTestDaemon → newGCTestDaemon to avoid collision
with the helper added in daemon_test.go on main.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The repoCache.Sync() call in loadWatchedWorkspaces runs synchronous git
clone/fetch operations that can take minutes for large repos. Because
heartbeatLoop and pollLoop only start after loadWatchedWorkspaces returns,
the runtime's last_seen_at is never updated during the sync, causing the
server's sweeper to mark it offline after 45 seconds.
Move repo cache sync to a background goroutine so heartbeat and poll
loops start immediately after runtime registration.
Closes#825
The context cancel in pruneRepoWorktrees was called explicitly after
CombinedOutput inside a loop. Extract to a helper method so defer
cancel() works correctly (scoped to the function, not the loop).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move WriteGCMeta from runTask() to handleTask() so it runs after
task completion, not at start. Mid-task crashes leave orphan dirs
that get cleaned by GCOrphanTTL.
- Strengthen isBareRepo to check both HEAD and objects/ directory.
- Remove empty workspace directories after all task dirs are cleaned.
- Add 30s context timeout to git worktree prune to prevent hangs.
- Add comprehensive unit tests for shouldCleanTaskDir (8 scenarios),
cleanTaskDir, gcWorkspace empty-dir cleanup, isBareRepo, and
WriteGCMeta/ReadGCMeta roundtrip.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): add fallback for failed session resume
When the daemon tries to resume a prior session (--resume flag for
Claude, --session for OpenCode, session/resume RPC for Hermes) and the
session no longer exists, the agent fails immediately. This adds a
fallback that retries the execution with a fresh session instead of
marking the task as blocked.
Extracts the execute+drain logic into a reusable executeAndDrain method
to avoid code duplication between the initial attempt and the retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): narrow session resume retry and merge usage
Address review feedback:
1. Narrow retry trigger: only retry when result.SessionID == "" (no
session was established), not on any failure with PriorSessionID set
2. Merge token usage from both attempts so billing is accurate
3. Log errors when the retry itself fails to start
4. Add unit tests for mergeUsage, fallback behavior, and no-retry
when session was already established
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Isolation directories accumulate indefinitely because they're preserved
for session reuse but never cleaned up after the issue is closed.
This adds a background GC loop that periodically scans local workspace
directories and removes those whose issue is done/canceled and hasn't
been updated for 5 days (configurable via MULTICA_GC_TTL). Orphan
directories with no metadata are cleaned after 30 days.
Changes:
- Write .gc_meta.json (issue_id, workspace_id) at task completion
- Add GET /api/daemon/issues/{issueId}/gc-check endpoint for status queries
- Add gcLoop goroutine to daemon with configurable interval/TTL
- Prune stale git worktree references from bare repo caches each cycle
- New env vars: MULTICA_GC_ENABLED, MULTICA_GC_INTERVAL, MULTICA_GC_TTL,
MULTICA_GC_ORPHAN_TTL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a task is triggered by a comment, the agent prompt now includes
the comment content directly. This prevents the agent from ignoring
the comment when stale output files exist in a reused workdir.
Closes#805
Previously only Claude and Codex had log-scanning-level token usage
reporting (Flow B). This adds scanners for the remaining three runtimes:
- OpenCode: reads JSON message files from ~/.local/share/opencode/storage/message/
- OpenClaw: reads JSONL session files from ~/.openclaw/agents/*/sessions/
- Hermes: reads JSONL session files from ~/.hermes/sessions/
All three are registered in Scanner.Scan() and follow the same
(date, provider, model) aggregation pattern as existing scanners.
Allow agents to pipe comment content through stdin instead of the
--content flag, avoiding shell escaping issues with backticks, quotes,
and other special characters in markdown content.
Usage: cat <<'COMMENT' | multica issue comment add <id> --content-stdin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codex tasks running in workspace-write sandbox mode could not resolve
api.multica.ai because the hardcoded sandbox parameter in thread/start
overrode any config.toml settings, and the default sandbox policy blocks
network access.
Changes:
- Remove hardcoded `sandbox: "workspace-write"` from thread/start RPC —
let Codex read sandbox config from its own config.toml instead
- Auto-generate config.toml in per-task CODEX_HOME with
`sandbox_mode = "workspace-write"` and `network_access = true`,
preserving any existing user settings
- Fix Reuse() to restore CodexHome for Codex provider on workdir reuse
Closes#368
Registers `gemini` as a sixth supported agent provider alongside claude,
codex, opencode, openclaw, and hermes.
- Daemon config probes for `gemini` on PATH (MULTICA_GEMINI_PATH /
MULTICA_GEMINI_MODEL env overrides mirror the other providers).
- New agent.geminiBackend in pkg/agent/gemini.go: spawns
`gemini -p <prompt> --yolo -o text [-m <model>] [-r <session>]`,
reads stdout to completion, and returns a single MessageText plus
the standard Result struct (Status / Output / DurationMs).
- Execution environment writes a GEMINI.md file into the task workdir
(mirroring the existing CLAUDE.md / AGENTS.md injection for other
providers) so Gemini discovers the Multica runtime meta-skill
through its native mechanism.
Tests:
- pkg/agent/gemini_test.go — unit coverage for buildGeminiArgs
(baseline, model override, resume session, omit-when-empty).
- internal/daemon/execenv/TestInjectRuntimeConfigGemini — verifies
GEMINI.md is written and that CLAUDE.md/AGENTS.md are NOT.
Scope (intentional for v1):
- Text output only (`-o text`). Streaming tool events via
`--output-format stream-json` is a follow-up once we have a
reliable reproduction of Gemini's event schema.
- No MCP config plumbing. Gemini's `--allowed-mcp-server-names`
filter pairs well with the per-agent MCP work on feat/per-agent-mcp;
stacking the two can land as a follow-up.
- No token usage scraping (Gemini's accounting lives on the Google
Cloud side, not a local JSONL log like claude/codex).
- No session resume wiring beyond accepting the ExecOptions field —
the daemon does not yet persist Gemini session IDs because the text
output mode does not expose them.
Migration / env changes:
- New optional environment variables MULTICA_GEMINI_PATH and
MULTICA_GEMINI_MODEL. Default path is the string "gemini" (resolved
via PATH at daemon startup). If no Gemini install is detected, the
provider is simply absent from the runtime — no behavior change for
existing deployments.
Replaces the hardcoded assignment-triggered workflow in buildMetaSkillContent()
with a minimal version that defers to agent Skills and Identity. Keeps platform
capability docs and status management steps intact.
Fixes#669
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(daemon): add minimum Claude Code version check during runtime registration
The daemon now validates the detected agent CLI version against a
minimum requirement before registering a runtime. Claude Code requires
>= 2.0.0 (when --output-format stream-json and --permission-mode
bypassPermissions were introduced). Older versions are skipped with a
warning log, preventing silent failures.
Closes#569
* feat(daemon): add minimum Codex CLI version check (>= 0.100.0)
The `codex app-server --listen stdio://` flag was introduced in v0.100.0.
Older versions lack this flag and fail silently. Add codex to the
MinVersions map so the daemon skips outdated codex CLIs with a clear
warning, matching the existing Claude version check.
Refs #490
* feat(agent): add Hermes Agent Provider via ACP protocol
Integrate Hermes as a new agent backend using the ACP (Agent
Communication Protocol) JSON-RPC 2.0 over stdio — the same pattern
as the Codex provider but with ACP-specific methods.
- New hermesBackend spawns `hermes acp` and drives initialize →
session/new → session/prompt lifecycle
- Handles session/update notifications: agent_message_chunk,
agent_thought_chunk, tool_call, tool_call_update, usage_update
- Auto-approves tool executions via HERMES_YOLO_MODE env var
- Supports session resume, model override, system prompt injection
- Token usage extracted from PromptResponse and usage_update events
- Auto-detected at daemon startup via MULTICA_HERMES_PATH env var
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(ui): optimize runtime icons and fix create-agent dialog overflow
- Replace OpenClaw pixel-art icon (32 rects) with clean vector paths
- Add Hermes provider icon (NousResearch mascot, 48x48 webp data URI)
- Use provider-specific icons in runtime selector instead of generic Monitor
- Fix dialog overflow: add min-w-0 to grid item so truncate works
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): add required mcpServers param to Hermes ACP session/new
ACP SDK v0.11.2 requires mcpServers as a mandatory field in
NewSessionRequest. Without it, Pydantic validation fails with
"Invalid params" and the agent immediately errors out.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-task CODEX_HOME isolated session logs in per-task directories, making
them invisible from the global ~/.codex/sessions/ where users expect to
find them. Symlink the sessions directory back to the shared home so
Codex writes session logs to the global location while keeping skills
isolated per task.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bare cache used a mirror-style fetch refspec
(+refs/heads/*:refs/heads/*) which collided with worktree-locked
refs/heads/agent/<task> branches once those branches were pushed
back to origin as PRs. git fetch aborted with "refusing to fetch
into branch ... checked out at ...", the error was swallowed as a
warning, and every subsequent checkout reused the snapshot from
the original clone.
Fix:
- Clone / migrate bare caches to a remote-tracking layout
(+refs/heads/*:refs/remotes/origin/*) so fetched heads never
land in refs/heads/*.
- Resolve the base ref from refs/remotes/origin/HEAD with a
5-level fallback (verified origin/HEAD symref to origin/main
or origin/master to the bare HEAD bridged into origin/<same>
to single-entry origin/* scan to bare HEAD for legacy caches).
- Refuse to guess when refs/remotes/origin/* has multiple
candidates and none match a known fallback, so CreateWorktree
fails loudly instead of basing work on an arbitrary branch.
- Refresh refs/remotes/origin/HEAD after every successful fetch,
not just on the legacy migration path, so a cache that was
already modern picks up an upstream default-branch change.
- Verify the primary symref target actually exists so a phantom
refs/remotes/origin/HEAD from a broken set-head does not
surface a deleted branch.
- Detect legacy caches on the fly and rewrite refspec +
refs/remotes/origin/* + refs/remotes/origin/HEAD in place so
existing clones self-heal on next use.
- Serialize per-bare-repo mutation (both Sync and CreateWorktree)
with sync.Map-backed mutexes so concurrent fetch and worktree
add on the same repo cannot race on git's own lockfiles.
- Narrow the already-exists retry to actual branch-collision
errors so a path-collision no longer silently leaks a branch
into the bare repo.
Implement the Master Agent chat feature allowing users to chat with agents
directly from a floating window, separate from the issue-based workflow.
Backend:
- New chat_session and chat_message tables (migration 033)
- Make issue_id nullable on agent_task_queue for chat tasks
- REST API: create/list/get/archive sessions, send/list messages
- EnqueueChatTask in TaskService with session_id persistence
- WS events: chat:message, chat:done
- Daemon: chat task type with separate prompt builder
- ClaimTaskByRuntime populates chat context (session, message, repos)
Frontend:
- ChatSession/ChatMessage types + API client methods
- core/chat: TanStack Query options, mutations with optimistic updates, WS updaters
- features/chat: Zustand store, ChatFab (floating button), ChatWindow with
real-time streaming via task:message events
- Mounted in dashboard layout (bottom-right corner)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): update existing worktree to latest remote on reuse
When an agent receives a new task on the same issue, the execution
environment is reused and the repo worktree already exists on disk.
Previously, `multica repo checkout` would fail because `git worktree add`
cannot create a path that already exists — so the agent worked on stale
code from the prior task.
Now `CreateWorktree` detects existing worktrees and updates them:
fetch origin, reset working tree, then checkout a new branch from the
latest remote default branch. The previous task's branch is preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): propagate actual branch name and use correct ref in worktree reuse
- Return (string, error) from updateExistingWorktree so collision-retried
branch name propagates to WorktreeResult
- Use baseRef directly instead of origin/baseRef — bare clone refspec maps
remote branches to local refs, so remote-tracking refs may not exist
- Remove redundant fetch (worktree shares object store with bare clone)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>