mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 05:49:12 +02:00
agent/lambda/56b67592
346 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5900d8b637 |
fix(issues): make start_date/due_date timezone-stable calendar days (#3618) (#3692)
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925) These fields are calendar days (the pickers offer no time-of-day), but were stored as TIMESTAMPTZ. A client serializing local midnight via toISOString() folded its timezone into the instant, so the day shifted by the local offset (GH #3618). Migrate the columns to DATE and parse/serialize date-only "YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the UTC day) so older clients keep working. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925) Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of toISOString(), and every read formats via the shared @multica/core/issues/date helpers with timeZone:"UTC" so the day never shifts with the viewer's offset. The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers, quick-set menu, list/board/detail/activity, and the mobile due-date picker. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925) Review follow-ups on #3692: - ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339 to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight instant and rejects ambiguous ones loudly. Adds util unit tests. - migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too. - Convert remaining date-change display sites to formatDateOnly: inbox detail label (web) and mobile activity + inbox labels (were new Date()+local format). - CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
10afd1af1b |
feat(server): introduce pkg/taskfailure classifier and switch in-flight failure_reason writes (MUL-2946) (#3693)
Lift MUL-1949's offline backfill failure_reason taxonomy into a shared in-flight classifier so the agent_task_queue.failure_reason column is written with refined values (provider_auth_or_access, context_overflow, provider_capacity_or_rate_limit, …) at write time rather than waiting on SQL backfill to re-classify after the fact. PR1 of the Grafana board plan in MUL-2328 — the upcoming PR2 reuses pkg/taskfailure.AllReasons() to pre-warm the Prometheus failure_reason label set. * server/pkg/taskfailure: new package with the canonical 21 Reason constants (7 platform-side + 14 agent_error.* sub-reasons), AllReasons() returning a defensive copy, IsAgentError() prefix check, and Classify(rawError) Reason mirroring the SQL CASE rules from MUL-1949 (db-boy's analysis). 100% statement coverage. * server/internal/daemon/daemon.go: route the 'agent_error' coarse fallback paths (StartTask error, runTask early-return error, CompleteTask permanent rejection, reportTaskResult default branch) and the executeAndDrain default error case (chained after classifyPoisonedError) through taskfailure.Classify so blocked / timeout / unknown-status results all carry a refined reason on the wire. * server/internal/service/task.go: FailTask classifies errMsg when the daemon-supplied failureReason is empty, eliminating the legacy COALESCE(.., 'agent_error') landing. * server/internal/daemon/poisoned.go: alias FailureReasonIterationLimit and FailureReasonAPIInvalidRequest to the canonical taskfailure constants. agent_fallback_message and codex_semantic_inactivity are pre-existing operational reasons not in the canonical 21 — kept as literals for now and revisited in a follow-up PR. Backfill SQL from MUL-1949 stays as the authoritative offline source of truth; this PR keeps the in-flight classifier in lock-step with the SQL CASE expression so historical and future rows share the same taxonomy. No behavior change for the platform-side reasons (queued_expired, runtime_offline, runtime_recovery, timeout, etc.) which already align with the canonical set. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f2f17e3355 |
Optimize chat message loading (#3685)
* Optimize chat message loading Co-authored-by: multica-agent <github@multica.ai> * Fix chat history cursor pagination Co-authored-by: multica-agent <github@multica.ai> * Fix chat session list remount key Co-authored-by: multica-agent <github@multica.ai> * fix(chat): fall back to legacy /messages when paged endpoint 404s Deployment-order compatibility: a backend deployed before the /messages/page endpoint existed returns 404 for the unknown route. The cursorless initial page now falls back to the legacy full-list /messages endpoint and wraps it in a single has_more:false page, so chat never white-screens regardless of which side deploys first. A 404 on a cursor request still propagates to avoid duplicating the full list. Co-Authored-By: Claude Opus 4.8 <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.8 <noreply@anthropic.com> |
||
|
|
fcb5099ec5 |
fix(agent): raise opencode model-discovery timeout to 15s (MUL-2888) (#3689)
Newer opencode (1.15+) syncs its hosted free-model catalog over the
network on `opencode models`, which can take ~6s. The previous 5s cap
killed the command, discoverOpenCodeModels returned an empty list, and
the daemon reported it as a successful empty result — so the runtime
showed online but the model picker was empty ("暂无可用模型").
Fixes #3627
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
e2720f7d33 |
feat: add opencode thinking variants
Adds OpenCode model variant discovery for thinking controls, passes saved thinking_level through opencode run --variant, and hardens verbose model parsing with fallback coverage. |
||
|
|
1e1a4f7845 |
fix(daemon): fix Copilot CLI invocation on Windows and strip shell quotes from custom args (MUL-2876)
Bug 1: detect copilot.cmd/.bat on Windows and invoke the sibling .ps1 directly via powershell -File, bypassing cmd.exe %* re-tokenisation that mangled the multi-line -p prompt. Shared rewriteCmdToPS1() now serves cursor, pi, and copilot. Bug 2: filterCustomArgs (shared by all agent backends) strips one outer layer of shell quotes via unshellQuoteArg() before processing, so shell-style custom args like --deny-tool='write' no longer reach the CLI with literal quotes. |
||
|
|
700cd97407 |
feat(workspace): add per-workspace logo upload (#2760)
Adds avatar_url column to workspace, threads it through the API +
WorkspaceAvatar component, and adds a click-to-upload editor in the
workspace settings tab. Mirrors the squad avatar pattern (migration 086);
UI strings use "logo" while the schema/code uses avatar_url for codebase
consistency with user.avatar_url and squad.avatar_url.
- migration 093: ALTER TABLE workspace ADD COLUMN avatar_url TEXT
- UpdateWorkspace SQL + handler accept avatar_url (auth gated to
owner/admin at the router via RequireWorkspaceRoleFromURL)
- WorkspaceAvatar renders <img> when avatar_url is set, falls back to
the initial-letter span otherwise
- workspace-tab.tsx adds a 16x16 click-to-upload logo editor at the
top of the general settings card, using useFileUpload + accept=
image/png,image/jpeg,image/webp (server stores under workspaces/{id}/)
- en + zh-Hans settings i18n strings added
Co-authored-by: Matt Voska <voska@users.noreply.github.com>
|
||
|
|
674be86add |
fix(tasks): cancel autopilot run_only & quick_create tasks (MUL-2827) (#3615)
CancelTaskByUser (POST /api/tasks/{taskId}/cancel) keyed cancellation off
issue_id / chat_session_id alone, so any task whose only source link was
autopilot_run_id (run_only autopilots) or quick_create context fell into the
dead else branch and 404'd with "task not found" — even though the task was
visible (and showed a cancel X) on the agent Activity tab.
Enforce tenancy uniformly through the task's owning agent instead: agent_id is
NOT NULL on every task row (ON DELETE CASCADE), and agents are workspace-scoped,
so GetAgentTaskInWorkspace (task JOIN agent ON workspace) is a single tenant
guard that works regardless of which optional source FK is set — including
orphan tasks whose autopilot_run_id was SET NULL after the autopilot was
deleted. Privacy layers on top: chat tasks stay creator-only, and every other
task mirrors the agent Activity / snapshot private-agent visibility gate via
canAccessPrivateAgent so the id-only endpoint is never more permissive than the
surface that exposes the task.
Tests cover run_only (same-ws success, cross-ws 404 no-mutation), quick_create,
retry clones, issue-task regression, chat non-creator 403, and private-agent
plain-member 403.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
d4b97dc44a |
fix(agent): drain claude stdout while writing prompt to stdin (#3490)
The claude backend wrote the full prompt to the child's stdin and closed it before starting the stdout reader goroutine. With --verbose --output-format stream-json the CLI emits a startup banner before reading its first stdin frame; with no reader draining stdout, the child blocks on its stdout write, never reads stdin, and our stdin Write blocks until the per-task context fires. The field symptom is tasks failing exactly at the 2 h per-task timeout with "write |1: The pipe has been ended." Move writeClaudeInput into its own goroutine so the prompt write and the stdout drain proceed concurrently. Guard stdin close with sync.Once (it can now be called from both the writer goroutine and, previously, the result handler). Join the write result at cmd.Wait() and surface a write failure as a "failed" status only when no result event arrived and no session was established, so a genuine startup death still reports the stderr tail. Add a regression test that re-execs the test binary as a fake claude which bursts 256 KiB to stdout before reading stdin, with a 128 KiB prompt pushed at stdin — both past any plausible OS pipe buffer — so a regression hangs until the test deadline instead of passing. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4ae4722ef0 |
fix(comments): preserve direct parent on replies (#3579)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
382cdd6a0b |
feat(agent): consume OpenCode mcp_config via OPENCODE_CONFIG_CONTENT (#3098)
Closes the runtime-side gap of #2106: previously `agent.mcp_config` was honored only by Claude Code (via `--mcp-config <file>`); for OpenCode the field was accepted by the API but silently ignored at execution time. ## Approach OpenCode has no `--mcp-config` flag. Project the agent's `mcp_config` into OpenCode via OPENCODE_CONFIG_CONTENT — OpenCode's general inline-config injection environment variable, which accepts any subset of OpenCode's config schema (model / agent / mode / plugin / mcp / …) and merges at "local" scope after the project-config loop. MCP is the only field this PR projects through that channel; if a future Multica field needs the same channel it would assemble a combined config slice before the env append. The env-var route was deliberate. An earlier draft of this PR wrote the translated MCP servers into <workdir>/opencode.json and removed the file on cleanup; review (#3098) flagged that the task workdir is reused across turns for the same (agent, issue), and any agent- or user-written model / tools / permission settings in opencode.json must survive across runs. OPENCODE_CONFIG_CONTENT avoids the workdir entirely — nothing is written to disk, no cleanup is needed, and the env entry dies with the spawned process. OPENCODE_CONFIG_CONTENT was added to OpenCode in v1.4.10 (2025-09); the official @opencode-ai/sdk uses the same env var to inject runtime config, so the surface is stable. Verified empirically against OpenCode 1.15.6 in our K8s runtime: `opencode debug config` returns the injected mcp slice deep-merged with the user's global config, and <workdir>/opencode.json is observably untouched. ## Translation surface `agent.mcp_config` accepts two shapes for portability: - Claude-style `{"mcpServers": {name: {url|command, ...}}}` is translated into OpenCode's native form: `type: "local"|"remote"`, `command` coerced to a string array, `env` renamed to `environment`. - Native OpenCode `{"mcp": {name: ...}}` accepts the three shapes OpenCode's schema permits and is strict-decoded against each: - McpLocalConfig: `{type:"local", command:[…], environment?, enabled?, timeout?}` - McpRemoteConfig: `{type:"remote", url:"…", headers?, oauth?, enabled?, timeout?}` - bare override: `{enabled: bool}` (toggle a server inherited from global / project config without redefining it) Decoding uses `json.DisallowUnknownFields` so any field outside the matching schema is rejected — matching OpenCode's `additionalProperties: false`. Without this, a malformed payload (e.g. `command: "node"` instead of `command: ["node"]`) would reach OpenCode verbatim and either silently disable the server or crash the CLI at startup. Field-level checks the strict decoder doesn't catch: - `timeout` must be a positive integer (rejects 0, negative, fractional) - `oauth` must be either an object (validated against McpOAuthConfig) or the literal `false`; primitives and `true` are rejected as ambiguous - `oauth.callbackPort` must be in 1..65535 when set ## Precedence Go's os/exec dedups `cmd.Env` by key keeping the LAST occurrence (Go 1.9+). Appending OPENCODE_CONFIG_CONTENT after `buildEnv(b.cfg.Env)` guarantees the daemon's value wins over any value the user happened to put in `agent.custom_env` — which matches the intended semantics (`mcp_config` is the authoritative daemon-managed field; `custom_env` is the escape hatch). When that override happens we surface a warning log so accidental clobbers are debuggable. ## Limitation (out of scope, accepted in review) OpenCode also deep-merges its **global** config (`~/.config/opencode/opencode.json`) into every session and exposes no flag to disable that. Operators who want strict per-agent isolation from the global layer can set: ```jsonc // agent.custom_env on the platform { "XDG_CONFIG_HOME": "/tmp/opencode-isolated" } ``` …pointing at any directory without an `opencode/` subdir. OpenCode then reads no global config and only honors what the daemon injects via OPENCODE_CONFIG_CONTENT. Verified with `opencode debug config`. ## Changes server/pkg/agent/opencode_mcp.go (new): - buildOpenCodeMCPConfigContent — translates raw mcp_config into the JSON string OpenCode accepts via OPENCODE_CONFIG_CONTENT, returns "" when there's nothing to inject so the caller can skip the env entry (avoids clobbering anything the user put in agent.custom_env.OPENCODE_CONFIG_CONTENT) - translateMCPConfigForOpenCode + helpers — Claude-style → OpenCode native shape - validateOpenCodeNativeMCPEntry + opencodeMCPLocal / opencodeMCPRemote / opencodeMCPEnabledOnly / opencodeMCPOAuth typed structs — strict-decode native-shape entries against the schema (DisallowUnknownFields), plus targeted post-decode assertions for timeout / oauth / callbackPort server/pkg/agent/opencode.go: - 12 lines of env injection in Execute(), placed AFTER buildEnv so the daemon's value wins via os/exec dedup - warning log when agent.custom_env duplicates the same key - no on-disk state, no rollback closure, no post-run cleanup — OPENCODE_CONFIG_CONTENT lives only in the spawned process env server/pkg/agent/opencode_mcp_test.go (new): - TestBuildOpenCodeMCPConfigContent_{Empty,Remote,Local,Native} - TestBuildOpenCodeMCPConfigContent_NativeAcceptsAllSchemaFields — covers each native variant round-tripping every optional field (local with env+timeout+enabled; remote with headers+oauth-object+ timeout+enabled; remote with oauth: false; bare {enabled} override) - TestBuildOpenCodeMCPConfigContent_RejectsMalformedNative — 31-case table covering every constraint on Bohan-J's review: command must be a string array, environment / headers values must be strings, oauth must be an object or false, timeout must be a positive integer, additionalProperties: false (per-shape allow-list checked via DisallowUnknownFields) - TestOpencodeBackendInjectsMCPConfigViaEnv — E2E happy path; fake opencode binary captures $OPENCODE_CONFIG_CONTENT, asserts the translated mcp slice is present AND <workdir>/opencode.json was NOT written - TestOpencodeBackendOmitsMCPEnvWhenEmpty — empty mcp_config does NOT inject the env, preserving any value the user set in agent.custom_env - TestOpencodeBackendOverridesUserOpenCodeConfigContent — daemon value wins via os/exec dedup keep-last apps/docs/content/docs/providers.{en,zh}.mdx: - flip OpenCode's MCP cell from ❌ to ✅ - reword the "MCP configuration: only Claude Code actually reads it" section so OpenCode is included; describe each tool's mechanism (Claude → `--mcp-config`, OpenCode → OPENCODE_CONFIG_CONTENT) apps/docs/content/docs/install-agent-runtime.{en,zh}.mdx: - update the Claude Code blurb (no longer "the only one") - expand the OpenCode blurb to mention mcp_config support - fix the now-broken /providers anchor Refs #2106 (TS types and per-agent UI for mcp_config are separate follow-ups, not in this PR). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
973a43923f |
fix(comments): revert since-delta to issue-wide, steer to parent thread first (#3535)
#3509/#3523 scoped the comment-trigger since-delta count to the triggering
thread, so an agent resuming a busy issue only saw "+N in this thread" and
lost visibility of new comments in other threads. Revert the count to
issue-wide (every thread), keeping the trigger-comment + agent-own
exclusions, and reshape the warm-path hint to:
- report the issue-wide new-comment volume,
- steer the agent to read the triggering (parent) thread FIRST
(`--thread <trigger> --since`, or `--tail 30` for full context),
- demote the issue-wide `--since` catch-up to an only-if-needed fallback
("don't read them all blindly").
Also fixes the now-stale "scoped to the triggering thread" wording in the
resumed-session no-delta hint (it's issue-wide zero now).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
9616d78e47 |
MUL-2785: optimize resumed comment reads (#3509)
* feat(comments): skip default thread read on resumed comment sessions Co-authored-by: multica-agent <github@multica.ai> * fix(comments): scope since delta to trigger thread Co-authored-by: multica-agent <github@multica.ai> * chore(comments): address thread delta review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
75b5be3f8e |
feat(comments): roots-only thread stats + summary projection for comment list (MUL-2809) (#3505)
* feat(comments): roots-only thread stats + summary projection for comment list Enrich the roots_only read so each root carries reply_count (recursive descendant count) and last_activity_at (MAX created_at over the subtree), letting an agent triage which thread to open without fetching any replies. Add an orthogonal summary=true projection (--summary) that clips each returned comment's content to a fixed budget and sets content_truncated, so an agent can scan a list cheaply before pulling a full body. It composes with every read mode (default, since, thread, recent, roots_only). New response fields are optional (omitempty) and only populated for the agent-facing query params, so the default response shape is unchanged for the desktop/web and existing CLI callers. Co-authored-by: multica-agent <github@multica.ai> * test(comments): cover roots_only + summary composition end-to-end The summary projection composing with roots_only is the spec's headline "table of contents" read, but it was only exercised at the CLI param- forwarding level — no handler test asserted that a roots_only response both clips content AND keeps reply_count / last_activity_at. A refactor moving the clip into a per-mode branch would silently break that composition with no failing test. Add TestListComments_RootsOnlySummaryComposes: a long root + a reply, read via roots_only=true&summary=true, asserting the root is clipped (content_truncated=true) while its subtree stats still surface. Co-authored-by: multica-agent <github@multica.ai> * refactor(comments): address review nits on roots stats + summary - ListRootComments[Since]ForIssue: scope the recursive membership walk to a selected_roots CTE (the @row_limit page, with the @since cut applied up front) so stats are only computed over the subtrees of the roots actually returned, instead of every thread in the issue. - summarizeContent: scan by rune and stop at the budget+1th rune instead of allocating a full []rune for the whole body, so a pathologically long comment costs only the budget under summary mode. Add a multi-byte (CJK) test to lock rune-boundary clipping. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c730e906b9 | feat(cli): add roots-only issue comment listing (MUL-2805) (#3288) | ||
|
|
3187bbf90c |
feat(comments): re-add since-delta + cold-start thread read + parent-root write normalization (#3494)
* feat(comments): since-delta new-comment hint + default-on comment session resume (#3432) * feat(db): add unresolved comment count + list filter queries Add CountUnresolvedComments (excludes the agent's own comments) and ListUnresolvedCommentsForIssue. Both are additive — existing callers stay on the unfiltered queries — so old clients are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): support unresolved-only comment listing Wire an additive `unresolved` query param into ListComments. Defaults off so an old CLI that never sends it gets unchanged behavior; only true/1 enable it. Rejects combining unresolved with thread/recent (whole-issue filter vs navigation models). Includes filter + count query tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): plumb unresolved count + thread root into claim, gate comment resume Populate trigger_parent_id (thread root of the trigger comment) and unresolved_count (excludes the agent's own comments) on comment-triggered claim responses. Both fields are omitempty so old daemons ignore them. Gate comment-triggered session resume behind MULTICA_RESUME_COMMENT_SESSION (default off): resumed comment turns can inherit the prior turn's "Done." final message, so this stays an explicit rollout switch. The runtime-match and poisoned-session guards still apply regardless of the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(daemon): inject unresolved-comments hint + resolve step into agent brief Add a shared BuildUnresolvedCommentsHint helper rendered on both the per-turn prompt and the CLAUDE.md workflow (kept in sync per PR #2816). It ships only the count and the relevant CLI call — never comment bodies — so the server stays cheap. Thread case points at --thread <root>; issue case points at --unresolved. Suppressed when the count is 0. Also add a workflow step telling the agent to `multica comment resolve <thread-root>` once a thread is fully handled, so the unresolved set converges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add comment list --unresolved and comment resolve command Add an --unresolved filter to `issue comment list` (wired to the server's unresolved param, rejected when combined with --thread/--recent) and a top-level `comment resolve <id>` command that POSTs to the existing /api/comments/{id}/resolve endpoint, letting an agent close threads it has fully handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(comments): since-delta new-comment hint + default-on comment resume Simplifies the comment-triggered agent flow down to what's actually needed: - New-comment awareness is now a pure time delta: the claim response carries new_comment_count + new_comments_since (anchored on the prior run's started_at, never completed_at so a long run can't miss comments). The per-turn prompt and CLAUDE.md workflow render one line — "N new comment(s) since your last run, --since <ts>" — via a shared BuildNewCommentsHint so the two surfaces can't drift. Cold start (no prior run) falls back to a plain read. - Comment-triggered tasks resume the prior session by default (same runtime), dropping the MULTICA_RESUME_COMMENT_SESSION rollout gate. The "Focus on THIS comment" prompt guard defends against inheriting the prior turn's "Done." marker; GetLastTaskSession still excludes poisoned sessions. - Drops the resolved-based machinery from the first draft: CountUnresolvedComments / ListUnresolvedCommentsForIssue queries, the `comment list --unresolved` flag, the `multica comment resolve` command, and the resolve workflow step. - Removes the verbose cursor-pagination paragraph from the comment prompt; the --thread/--recent/--since flags stay in the CLI/API, just no longer explained inline every turn. Compatibility: new claim fields are omitempty (old daemons ignore them). Comment resume is default-on and affects even old daemons, which already consume prior_session_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(comments): collapse reply parent_id to thread root on write Comment threads are a 2-level model (root + flat replies, like Linear/Slack), enforced today only by the UI and the agent path — the CreateComment handler stored whatever parent_id it was handed, and the agent-side flatten walked just one level, so a reply-to-a-reply could land at depth 3+. Add GetThreadRoot (a recursive walk to the parent_id=NULL root) and run both write paths (handler.CreateComment, service.createAgentComment) through it, so every stored reply's parent_id IS its thread root. Readers can now treat parent_id as the thread root without re-walking. The agent-drift guard still compares the raw parent_id to the trigger comment before normalization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(comments): cold-start reads triggering thread, warm keeps --thread pointer The since-delta rework dropped the thread-first read on the COLD path: a first-time agent fell back to the flat `comment list` dump (oldest-first, cap 2000), burying the trigger's context in ancient chatter. Point cold start at the triggering conversation instead via a shared BuildColdCommentsHint (`--thread <trigger> --tail 30` + a --recent pointer for cross-thread background). On the WARM path, --since is a pure time delta and can miss the triggering thread's pre-anchor history, so BuildNewCommentsHint now also emits a --thread pointer. Both surfaces (per-turn prompt + CLAUDE.md workflow) render via the shared helpers so they cannot drift (PR #2816 rule). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e1745d09ea |
MUL-2797 feat(agent): add Claude Opus 4.8 to model catalog & pricing (#3492)
Claude Code now ships Opus 4.8 (claude-opus-4-8). Add it to the three places that enumerate Claude models so the picker, thinking-level catalog, and usage cost estimates all recognize it: - claudeStaticModels(): list Claude Opus 4.8 (Sonnet 4.6 stays default) - claudeModelEffortAllow: Opus supports the full low..max set incl. xhigh - MODEL_PRICING: $5/$25 in, $0.50 cache read, $6.25 5m cache write — same current-gen Opus tier as 4.5/4.6/4.7, confirmed against platform.claude.com/docs/en/about-claude/pricing Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d90732750f |
Revert "feat(comments): since-delta new-comment hint + default-on comment ses…" (#3455)
This reverts commit
|
||
|
|
09f9c7e2ce |
MUL-2764 feat(agent): wire mcp_config through ACP runtimes (Hermes / Kimi / Kiro) (#3439)
* MUL-2764 feat(agent): wire mcp_config through ACP runtimes (Hermes / Kimi / Kiro) The MCP config Tab (#3419) already lets admins save mcp_config on an agent, and the daemon plumbs it through to `agent.ExecOptions.McpConfig` for every runtime. Claude and Codex consume it; the three ACP runtimes (Hermes / Kimi / Kiro) ignored the field and hardcoded an empty `mcpServers: []` in their `session/new` requests. Add `buildACPMcpServers` to translate the Claude-style `{"mcpServers": {"<name>": {...}}}` object-of-objects into the array shape ACP requires (`[{name, command, args, env: [{name,value}, ...]}, ...]` for stdio; `[{type, name, url, headers: [...]}, ...]` for http/sse), then pass the translated array on `session/new` (all three) and `session/load` (kiro resume). Malformed JSON fails the launch closed — same contract Codex's `renderCodexMcpServersBlock` uses — so users see a real error instead of silently running with no MCP servers. Individual unclassifiable entries (no command, no url) are skipped with a warning so one bad row can't take MCP down for the rest of the agent. Co-authored-by: multica-agent <github@multica.ai> * MUL-2764 fix(agent): wire mcp_config through ACP resume + gate http/sse on capability Addresses the two blockers Elon raised on #3439: 1. session/resume now carries mcpServers for Hermes and Kimi (Kiro's session/load already did). Per the ACP Session Setup spec the resume path re-attaches MCP servers, and without this a resumed task lost access to MCP tools that a fresh task on the same agent would have had. Pinned with new TestHermesResumeIncludesMcpServers and TestKimiResumeIncludesMcpServers integration tests that inspect the recorded wire request. 2. Added extractACPMcpCapabilities + filterACPMcpServersByCapability so http/sse MCP entries get dropped (with a daemon-log warning naming the entry) when the runtime's initialize response doesn't advertise mcpCapabilities.http / .sse. Sending those entries to a stdio-only runtime is a spec violation and reliably tanks session/new; now they get filtered and the rest of the session still starts. Stdio entries pass through unconditionally. Both backends wire the filter in right after initialize so session/new and session/resume see the same filtered list. Also added TestKiroLoadIncludesMcpServersFromConfig — Elon flagged that no test pinned "non-empty mcp_config actually reaches the wire" for Kimi/Kiro, so the wire assertions go in for all three runtimes. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5e78e5100a |
feat(comments): since-delta new-comment hint + default-on comment session resume (#3432)
* feat(db): add unresolved comment count + list filter queries Add CountUnresolvedComments (excludes the agent's own comments) and ListUnresolvedCommentsForIssue. Both are additive — existing callers stay on the unfiltered queries — so old clients are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): support unresolved-only comment listing Wire an additive `unresolved` query param into ListComments. Defaults off so an old CLI that never sends it gets unchanged behavior; only true/1 enable it. Rejects combining unresolved with thread/recent (whole-issue filter vs navigation models). Includes filter + count query tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): plumb unresolved count + thread root into claim, gate comment resume Populate trigger_parent_id (thread root of the trigger comment) and unresolved_count (excludes the agent's own comments) on comment-triggered claim responses. Both fields are omitempty so old daemons ignore them. Gate comment-triggered session resume behind MULTICA_RESUME_COMMENT_SESSION (default off): resumed comment turns can inherit the prior turn's "Done." final message, so this stays an explicit rollout switch. The runtime-match and poisoned-session guards still apply regardless of the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(daemon): inject unresolved-comments hint + resolve step into agent brief Add a shared BuildUnresolvedCommentsHint helper rendered on both the per-turn prompt and the CLAUDE.md workflow (kept in sync per PR #2816). It ships only the count and the relevant CLI call — never comment bodies — so the server stays cheap. Thread case points at --thread <root>; issue case points at --unresolved. Suppressed when the count is 0. Also add a workflow step telling the agent to `multica comment resolve <thread-root>` once a thread is fully handled, so the unresolved set converges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add comment list --unresolved and comment resolve command Add an --unresolved filter to `issue comment list` (wired to the server's unresolved param, rejected when combined with --thread/--recent) and a top-level `comment resolve <id>` command that POSTs to the existing /api/comments/{id}/resolve endpoint, letting an agent close threads it has fully handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(comments): since-delta new-comment hint + default-on comment resume Simplifies the comment-triggered agent flow down to what's actually needed: - New-comment awareness is now a pure time delta: the claim response carries new_comment_count + new_comments_since (anchored on the prior run's started_at, never completed_at so a long run can't miss comments). The per-turn prompt and CLAUDE.md workflow render one line — "N new comment(s) since your last run, --since <ts>" — via a shared BuildNewCommentsHint so the two surfaces can't drift. Cold start (no prior run) falls back to a plain read. - Comment-triggered tasks resume the prior session by default (same runtime), dropping the MULTICA_RESUME_COMMENT_SESSION rollout gate. The "Focus on THIS comment" prompt guard defends against inheriting the prior turn's "Done." marker; GetLastTaskSession still excludes poisoned sessions. - Drops the resolved-based machinery from the first draft: CountUnresolvedComments / ListUnresolvedCommentsForIssue queries, the `comment list --unresolved` flag, the `multica comment resolve` command, and the resolve workflow step. - Removes the verbose cursor-pagination paragraph from the comment prompt; the --thread/--recent/--since flags stay in the CLI/API, just no longer explained inline every turn. Compatibility: new claim fields are omitempty (old daemons ignore them). Comment resume is default-on and affects even old daemons, which already consume prior_session_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bae8a84abd |
MUL-2767 feat(agent): add Antigravity runtime backend (#3427)
* feat(agent): add Antigravity runtime backend Adds Google's Antigravity CLI (`agy`) as the 12th supported coding-tool runtime, alongside Claude / Codex / Cursor / Copilot / Gemini / Hermes / Kimi / Kiro / OpenCode / OpenClaw / Pi. The CLI emits plain assistant text on stdout (no structured event stream), so the backend streams stdout line-by-line as `MessageText` events and accumulates the same text as the final `Result.Output`. Session resumption uses `--conversation <id>`; because the conversation UUID is not echoed on stdout, the daemon routes `--log-file` to a temp file and recovers the id from the glog-formatted log lines. MUL-2767 Co-authored-by: multica-agent <github@multica.ai> * fix(agent): correct Antigravity capability contract from Elon review - ModelSelectionSupported now returns false for antigravity. `agy` has no --model flag and antigravityBackend deliberately drops opts.Model, so the UI must render a disabled "Managed by runtime" picker instead of an empty dropdown plus a silently-ignored manual-entry field. Also stop seeding AgentEntry.Model from MULTICA_ANTIGRAVITY_MODEL — the backend would silently ignore it. - Antigravity skills now write to {workDir}/.agents/skills/, the CLI's native workspace path (inherits Gemini CLI's layout per https://antigravity.google/docs/gcli-migration). Previously they went to the .agent_context/skills/ fallback that the CLI doesn't scan. Runtime brief moves antigravity into the native-discovery branch and local_skills.go points the user-level skill root at ~/.gemini/antigravity-cli/skills for Runtime → local skill import. - Doc + UI comment sync: providers matrix / install-agent-runtime / cloud-quickstart / agents-create / tasks (session-resume support) / skills / README all now list Antigravity in the right buckets, and the model-picker / model-dropdown comments cite antigravity (not the stale hermes reference) as the supported=false example. New tests: TestAntigravityModelSelectionUnsupported, TestInjectRuntimeConfigAntigravity (native discovery wording), TestWriteContextFilesAntigravityNativeSkills (.agents/skills/ landing, .agent_context/skills/ NOT written). Co-authored-by: multica-agent <github@multica.ai> * feat(provider-logo): swap inline placeholder for real Antigravity PNG Replaces the hand-drawn planet+arc placeholder with the official asset shipped from Downloads. Stored next to the component; bundlers (Next.js / electron-vite) resolve the PNG import to a URL string at build time. Added a small assets.d.ts so packages/views' tsc accepts PNG / SVG module imports — there was no prior asset usage in this package to register the declaration. --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d39da9f7f0 |
MUL-2764: feat(agents): add MCP config tab to agent detail page (#3419)
* MUL-2764: feat(agents): add MCP config tab to agent detail page
Backend already stores `mcp_config` and the daemon forwards it to the
runtime CLI via `--mcp-config`; this only adds the UI entry point.
The new tab presents a JSON editor that pretty-prints the existing
config, validates the buffer on every keystroke, and saves through the
existing `PUT /api/agents/{id}` path. Clearing the editor sends
`mcp_config: null`, which the handler reads as "wipe the column" and
the daemon falls back to the CLI's own default.
When the caller can't see secrets (agent actor, or a non-owner
non-admin member), the server already returns `mcp_config: null` with
`mcp_config_redacted: true`; the tab renders a read-only "configured
but hidden" state in that case so a non-privileged member cannot
silently overwrite an admin-owned config by saving an empty editor.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): MCP tab — preserve in-flight edits + warn non-Claude runtimes
- Fix stale-editor sync: compare the local draft against the *previous*
original via a ref, so a background agent refetch updates an untouched
editor instead of being silently ignored. Without this, a draft equal to
the OLD original was treated as user-edited after the prop changed, and
the next Save would write the old config back over a concurrent admin
edit.
- Surface a notice inside the tab when the agent's runtime provider is not
Claude — today's daemon only forwards mcp_config via Claude's
--mcp-config, so saving on e.g. a Codex agent was silent but ineffective.
- Tests for both: rerender resyncs an untouched editor, rerender preserves
an in-flight edit, warning renders on non-Claude / hides on Claude.
MUL-2764
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2764: feat(agents): codex MCP support + hide MCP tab on unsupported runtimes
- Backend: codex.go now translates agent.mcp_config (Claude-style
`{"mcpServers": {...}}`) into `-c mcp_servers.<name>=<inline-toml>`
flags for `codex app-server`, so MCP servers configured in the UI
reach Codex's per-task config layer. Bad mcp_config JSON downgrades
to a warn-and-skip so it can't break the agent launch.
- Frontend: AgentOverviewPane hides the MCP tab when the agent's
runtime provider doesn't read mcp_config — only `claude` and `codex`
are supported today, every other provider sees no MCP tab. The
previous in-tab warning is removed (no longer reachable).
- New shared helper `providerSupportsMcpConfig` lives in
`@multica/core/agents` so views and any future caller share one list
of MCP-aware providers.
- Tests: new go-side coverage for stdio + url + multi-server inputs,
TOML string escaping, malformed-input fallback, and arg ordering vs
custom_args; new views-side coverage for which providers surface the
MCP tab. En + zh-Hans copy and parity test refreshed.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2764: fix(agents): keep codex mcp_config secrets out of argv/logs
Move the agent's mcp_config from a `-c mcp_servers.<id>=<inline-toml>`
argv flag into a daemon-managed `[mcp_servers.*]` block inside the
per-task `$CODEX_HOME/config.toml`. mcp_servers.<id>.env is a documented
Codex config field and the UI already treats mcp_config as redacted for
non-admins; argv would have leaked those values into `ps aux` and the
`agent command` log line. The file is forced to 0600 to keep secrets in
the daemon owner's lane regardless of the seed file's mode.
Also drop user-supplied `-c/--config mcp_servers.*` entries from
custom_args. Codex `-c` is last-wins (verified against codex-cli 0.132.0),
so without filtering, a custom_args entry could silently shadow whatever
the MCP Tab saved.
Strip inherited `[mcp_servers.*]` tables from the per-task config.toml
when the agent has its own mcp_config, mirroring Claude's
`--strict-mcp-config`: avoids TOML "table already exists" errors on
name collisions and matches admin expectations that the MCP Tab is the
authoritative source for that task.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2764: fix(agents): codex mcp_config three-state semantics + custom_args compat
Address the third review pass:
1. Distinguish nil vs present-but-empty mcp_config. `{}` and
`{"mcpServers":{}}` now count as "admin saved an explicit (empty)
managed set" — strip inherited user `[mcp_servers.*]` and pin an
empty managed marker block. Only SQL NULL / JSON `null` map to
"absent" and fall back to the user's global `~/.codex/config.toml`.
This aligns Codex with the API's three-state contract (omit / null
/ object) and with Claude's `--strict-mcp-config` semantics.
2. Fail closed on `ensureCodexMcpConfig` errors and on managed
mcp_config without CODEX_HOME. Previous warn-and-launch would
silently inherit the user's global MCP servers and look identical
to a successful apply — exactly the surprise the MCP Tab is meant
to remove.
3. Only filter `-c mcp_servers.*` from `custom_args`/`extra_args`
when the agent has a managed mcp_config. Pre-MUL-2764 agents that
configured MCP via custom_args keep working; once an admin opts
in via the MCP Tab the daemon owns the `mcp_servers` namespace
and overrides are dropped (last-wins safety).
4. Update mcp_config locale intro to mention $CODEX_HOME/config.toml
instead of the now-removed `-c mcp_servers.*` argv path.
Tests:
- Split `TestEnsureCodexMcpConfigEmptyInputsAreNoop` into
`TestEnsureCodexMcpConfigAbsentLeavesUserTablesAlone` (nil/null)
and `TestEnsureCodexMcpConfigEmptyManagedSetStripsUserMcp` (`{}`,
`{"mcpServers":{}}`).
- Add `TestEnsureCodexMcpConfigEmptyManagedSetIdempotent` to pin
byte-identical reruns on the empty managed marker block.
- Add `TestHasManagedCodexMcpConfig` covering the eight relevant
inputs.
- Add `TestBuildCodexArgsPreservesCustomMcpOverridesWhenUnmanaged`
and `TestBuildCodexArgsDropsCustomMcpOverridesWhenManaged` to
pin the new gating.
- Add `TestCodexExecuteFailsClosedWhenMcpConfigInvalid` and
`TestCodexExecuteFailsClosedWhenManagedMcpButNoCodexHome` for the
Execute paths.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
2bda4065d0 |
MUL-2708: fix(agent): preserve multi-line Pi prompt on Windows by bypassing the .cmd shim (#3417)
Pi is installed on Windows via npm, which lays down `pi.cmd` → `pi.ps1`
→ `node_modules/@mariozechner/pi-coding-agent/dist/cli.js`. The daemon
spawns Pi with `exec.Command("pi", ...)`; PATHEXT resolves that to
`pi.cmd`, and cmd.exe expands `%*` in the shim by re-tokenising the
original command line, which truncates any argv containing newlines.
buildPiArgs passes the full prompt as the last positional argv, so the
multi-line system+user prompt is silently cut at the first newline
before it reaches the JS entrypoint. The session JSONL then records
only the first line ("You are running as a chat assistant for a Multica
workspace.") and Pi replies as if the user message were missing
(GitHub multica-ai/multica#3306).
Mirror the existing cursor-agent fix: when LookPath resolves Pi to a
.cmd/.bat launcher and a sibling pi.ps1 exists, invoke PowerShell with
`-File <ps1>` directly and forward each arg as a discrete token. This
keeps us on the official launch path while skipping the cmd.exe %*
re-expansion. Falls back to the original launcher when pi.ps1 or
PowerShell can't be located.
The Windows test asserts the rewrite produces the expected argv and
that the multi-line positional prompt survives unchanged.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
4864831721 |
MUL-2744: feat(auth): auto-renew daemon PAT in-place within 7-day window (#3360)
* MUL-2744: feat(auth): auto-renew daemon PAT in-place within 7-day window Daemons currently hold a 90-day PAT and have no renewal path: once the token's expires_at passes, every request 401s and the user has to find the silent failure in the daemon log and re-run `multica login`. This adds an in-place renewal: - New `POST /api/tokens/current/renew` (Auth-protected, mul_ only). The server checks remaining lifetime: ≥ 7 days is a no-op; < 7 days bumps expires_at to now + 90 days via a guarded UPDATE that makes concurrent renews idempotent (the WHERE expires_at < $2 clause means only one writer wins; the loser sees pgx.ErrNoRows and reports the already- extended value). No raw token rotation — the same secret stays in every CLI/daemon process sharing the config. - Daemon-side `tokenRenewalLoop`: fires once on startup (covers machine-was-off cases) and then every 3 days. With a 7-day server threshold this gives at least two renewal attempts before the window closes, so a single network blip can't push the token out. - 401 fallback: when the renew call comes back 401 (token already revoked/expired), the daemon logs a user-actionable WARN telling the operator to run `multica login` — instead of the current silent failure mode. Loop keeps running so the warning repeats until fixed. PAT cache (auth.AuthCacheTTL = 10m) doesn't need invalidation: the next miss after the UPDATE re-reads the row and re-caches with the bumped TTL automatically. Co-authored-by: multica-agent <github@multica.ai> * MUL-2744: fix(auth): renew PAT before first sync; CAS against renewal threshold Addresses the two issues Elon raised on #3360. Must-fix: if the PAT is already revoked/expired when the daemon starts, syncWorkspacesFromAPI 401s and Run returns before the background tokenRenewalLoop ever fires its initial renewal. The operator only sees a generic auth failure in the workspace-sync log with no hint that 'multica login' is the fix. Now the startup path runs an inline tryRenewToken first, surfacing the existing 401 WARN before anything else gets a chance to fail. Pulled the renew + first-sync pair into preflightAuth so the ordering invariant is enforced at one site and tests can exercise the failure modes without spinning up the full Run setup. Removed the redundant initial tryRenewToken from tokenRenewalLoop — startup now owns the first call. Nit: the previous WHERE clause on ExtendPersonalAccessTokenExpiry (expires_at < $2) did not actually make concurrent renews idempotent the way the comment claimed. Two callers race-computing $2 = now + 90d produce strictly-different values, and the second writer's $2 always exceeds the row the first writer just wrote, so the UPDATE re-matches and bumps again. Switched to a CAS against the renewal threshold (expires_at <= $renew_threshold_at, i.e. now + 7d): once writer A pushes expires_at past the threshold, writer B's UPDATE matches zero rows and the loser falls back to reporting the already-extended value as a no-op. Tests: - TestPreflightAuth_RenewsBeforeWorkspaceSyncOnExpiredToken locks in the call ordering — renew endpoint is hit before workspaces, and the re-login WARN appears even though both endpoints 401. - TestPreflightAuth_SyncProceedsWhenRenewIsNoOp covers steady-state startup: a renew=false no-op must still progress to workspace sync. - TestPreflightAuth_TransientRenewFailureDoesNotBlockStartup covers a 500 from the renew endpoint — startup must continue, no WARN. - TestRenewPAT_ParallelRenewExtendsExactlyOnce fires N=8 concurrent renews at one row and asserts exactly one returns renewed=true with the others reporting the same already-extended expires_at, plus the DB carries only that single bumped value. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f02bc56e70 |
fix(agent/cursor): remove obsolete 'chat' subcommand from argv (#3077) (#3092)
The current cursor-agent CLI no longer has a 'chat' subcommand. The positional 'chat' argument was silently treated as prompt text, leaking into the user message (e.g. 'chat <actual prompt>'). Remove 'chat' from buildCursorArgs so the generated argv matches the current cursor-agent CLI interface. Fixes #3077 |
||
|
|
bdb60acae9 |
fix: swimlane empty lanes in due to pagination (MUL-2724) (#3326)
* fix: Swimlane lazy load issues * wip * refactor * fix: Rebase issues * fix: rerender * refactor bactch and chunking |
||
|
|
2b5696703f |
MUL-2703: feat(autopilots): webhook event filters per trigger (MUL-2334 follow-up) (#3231)
* feat(autopilots): webhook event filters per trigger (MUL-2334 follow-up) Adds schema-backed event/action filtering to webhook triggers so operators can declare exactly which GitHub (or generic) events should spawn autopilot runs. Events outside the declared scope are recorded as ignored with reason 'event_filtered' — visible in the delivery log but without expensive run/task creation. Closes #3093 (supersedes the description-parsing approach from that PR). Backend: - Migration 108 adds event_filters JSONB to autopilot_trigger - sqlc queries updated for CREATE / UPDATE / LIST / GET - HandleAutopilotWebhook filters against trigger.event_filters before dispatch - Create/Update trigger handlers accept event_filters in the request body - Response shape includes event_filters so the UI can render it Frontend: - New WebhookEventFilterSection component in the autopilot dialog - Inputs for event name + comma-separated actions - i18n strings added (en + zh-Hans) Tests: - Unit tests for splitWebhookEvent and webhookEventAllowedByTriggerScope - Handler-level integration tests for filtered / allowed / no-filter paths co-authored-by: ZephaniaCN <agent/autopilot-webhook-filter> * fix: recognize gitlab/bitbucket/gitea as providers in splitWebhookEvent TestSplitWebhookEvent failed because only 'github' was recognized as a provider prefix. Extract isKnownProvider() to handle gitlab, bitbucket, and gitea as well. * fix(autopilots): address PR #3231 review for webhook event filters Must-fix from PR #3231 review: 1. event_filters now uses typed []WebhookEventFilter at the HTTP boundary instead of []byte. encoding/json was base64-encoding the field on the way out, so the UI could not .map() the response, and a real JSON array on the way in failed to decode. Response field also decodes the stored JSONB into a typed slice before serialising back. 2. UpdateAutopilotTriggerRequest.EventFilters is *[]WebhookEventFilter with tri-state PATCH semantics: nil pointer = leave alone, [] = clear, [...] = replace. The handler marshals an explicit empty slice to the JSONB literal `[]` so COALESCE overwrites instead of preserves. AutopilotDialog now PATCHes the webhook trigger when event_filters change in edit mode (previously the toast said "updated" while the backend was unchanged). 3. webhookEventAllowedByTriggerScope no longer short-circuits to false on the first event-name match whose actions don't line up. Earlier code silently shadowed any later filter that shared the same event name with disjoint actions. Robustness: validateWebhookEventFilters rejects empty event names / actions at write time, and the matcher fails closed on malformed stored bytes instead of widening the allowlist. Tests: handler tests now post real JSON arrays (the prior []byte path masked the contract bug). Adds round-trip / clear-with-[] / preserve- when-omitted / replace / invalid-filter / filters-on-schedule coverage, plus matcher tests for same-event multi-filter and malformed-deny. Migration renamed 108 → 110 to avoid colliding with main's 108_task_token (came in via the merge from main). |
||
|
|
31b58494cf |
feat(comments): align UpdateComment post-processing with CreateComment (#3337)
* feat(comments): align UpdateComment post-processing with CreateComment (#2965 follow-up) Part 1 — PR #2965 code review follow-ups: - Fix sqlc Column3 naming → AttachmentIds via sqlc.arg(attachment_ids) - Return 500 on ReplaceCommentAttachments failure instead of logging + 200 - Remove optional marker from onEdit attachmentIds (always passed) - Add optimistic update for attachments in useUpdateComment - Extract useEditAttachmentState hook from CommentRow/CommentCardImpl - Add integration tests for attachment replacement scenarios Part 2 — Edit-comment logic alignment: - Add ExpandIssueIdentifiers to UpdateComment (bare identifiers now expand) - Add handleEditMentionDiff: diff old vs new agent/squad mentions on edit, cancel tasks for removed mentions, enqueue tasks for added mentions, cancel + re-trigger when content changes but mentions are unchanged Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(sqlc): regenerate with v1.31.1 + add mention diff integration tests Fixes sqlc version downgrade (v1.31.1 → v1.30.0) that was introduced when the original PR was authored with a local v1.30.0 binary. Regenerated all sqlc output with v1.31.1 to match main. Adds integration tests for handleEditMentionDiff covering: edit adds mention → task enqueued, edit removes mention → task cancelled, edit changes content with same mentions → cancel + re-trigger. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(comments): simplify edit post-processing to cancel-all + re-trigger Replace handleEditMentionDiff (120-line mention diff) with a simpler model: when content changes, cancel all tasks triggered by this comment, then re-run the same three trigger paths as CreateComment (assignee, squad leader, mentions). Fixes gap where assignee/squad-leader tasks were not cancelled or re-triggered on edit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(comments): extract triggerTasksForComment to unify Create/Edit trigger paths Create and Edit duplicated the same three trigger paths (assignee, squad leader, mentioned agents). A fourth path would need changes in two places. Extract into a shared function so the composition is: Create: trigger() + unresolve() Edit: cancel() + trigger() Delete: cancel() 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 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
341ce7bfa5 |
feat: support local working directory for projects (MUL-2618 v1) (#3283)
* feat(project): add local_directory project_resource type (MUL-2662)
Adds a second project_resource type alongside github_repo so a project
can be pinned to an existing directory on a specific daemon (the v1 of
the local-working-directory flow tracked in MUL-2618). The ref schema is
{ local_path, daemon_id, label? }; local_path must be absolute and
daemon_id is required. The same (daemon_id, local_path) pair is allowed
on multiple projects by design — no UNIQUE constraint is added.
Implementation reuses the existing project_resource API surface: the new
type is wired through the validator switch with no migration, no new
events, and no daemon-handler changes (daemon already passes through
arbitrary resource types via ProjectResources). The CLI gains
--local-path / --daemon-id / --ref-label shortcuts so
`multica project resource add --type local_directory` mirrors the
existing `--type github_repo --url ...` ergonomics; the generic --ref
flag still works for both types.
Tests cover the full CRUD lifecycle, the same-path-across-projects
allowance, the same-path-same-project conflict, the validator rejections
(missing/blank/relative path, missing daemon_id, wrong payload type),
and the cross-platform isAbsoluteLocalPath helper.
Co-authored-by: multica-agent <github@multica.ai>
* feat(project): add update endpoint + label-shadow guard for project_resource (MUL-2662)
Addresses the Elon review on PR #3263:
- Add PUT /api/projects/{id}/resources/{resourceId} with sqlc query,
matching handler, CLI `project resource update`, and a new
EventProjectResourceUpdated WS event. resource_type stays immutable;
ref/label/position are all individually optional.
- Catch same-project (daemon_id, local_path) collisions where only the
embedded label differs — the row-level UNIQUE only matches the full
ref JSON, so a label typo would otherwise let the same working
directory bind twice.
- Tests cover the update lifecycle (label-only / ref / clear / 404 /
invalid path) and the label-shadow conflict on both create and
update; the in-place rename still succeeds because the conflict
scan ignores the row being edited.
Incidental: regenerating sqlc picked up a missing skills_local scan in
UpdateAgentCustomEnv that drifted in from #3200.
Co-authored-by: multica-agent <github@multica.ai>
* fix(project): close bundled-create label-shadow gap + merge resource_ref on CLI update (MUL-2662)
Two follow-ups from MUL-2662 review round 2:
- CreateProject inline resources path now dedupes local_directory entries on
(daemon_id, local_path) before opening the transaction. The DB-level
UNIQUE(project_id, resource_type, resource_ref) constraint only fires on a
full JSON match, so two rows with the same target but different `label`
would otherwise slip past. Standalone POST/PUT already cover this via
findLocalDirectoryConflict; bundled create was the missing surface.
- `multica project resource update` now seeds resource_ref from the existing
row before applying per-type shortcut flags, so `--default-branch-hint x`
on its own no longer constructs a payload missing `url` (which the server
400s on). Local_directory partial edits get the same merge behavior.
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): local_directory project_resource UI (MUL-2665) (#3273)
* feat(desktop): local_directory project_resource UI (MUL-2665)
First UI surface for the local-working-directory flow tracked in MUL-2618.
Lets users on the desktop pin a project to an existing folder on this
machine; web stays read-only since the per-daemon check can't be done in
the browser.
What's new for the renderer:
- ProjectResourcesSection grows a desktop-only "Add local directory"
button next to the existing GitHub-repo popover. Clicking it opens
Electron's native folder picker, validates the path through a new
IPC pair (existence + r/w), and submits a project_resource of
resource_type=local_directory with daemon_id pulled live from
daemonAPI.getStatus.
- LocalDirectoryRow renders the rename pencil + path tooltip, and
greys out when ref.daemon_id != this machine's daemon_id (with a
"only available on the machine that registered this directory"
tooltip). Delete stays enabled so users can drop stale registrations
from any device.
- LocalDirectoryHint sits above the issue-detail comment composer and
shows "Agent will work in-place at {label} ({path})" when the issue's
project has a local_directory matching this daemon. Hidden on web.
- TaskStatusPill picks up a new "waiting_for_directory_release" stage
that the daemon will publish when it dequeues a task but can't
acquire the path lock. The render is in place now so the daemon
sibling subtask can wire the status string without an additional UI
PR.
Plumbing:
- @multica/core/types gains LocalDirectoryResourceRef +
UpdateProjectResourceRequest, and the api client gets the matching
PUT method backed by the server endpoint that landed in
|
||
|
|
7d24a8594a | fix(comments): support edit-time attachment removal (#2965) | ||
|
|
311cf4d998 |
fix(agent): surface Codex app-server no-progress diagnostics (MUL-2688)
Refs #3262. |
||
|
|
26ff52385b |
fix: attribute Hermes usage to current model (MUL-2696)
Fix Hermes ACP usage attribution to current model when agent.model is unset. Also preserves cache-read token accounting and makes ACP model-list parsing more tolerant of snake_case payloads and Unknown display names. |
||
|
|
744b474199 |
revert(agent): remove per-agent local skill toggle (MUL-2603) (#3286)
* Revert "feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)" This reverts commit |
||
|
|
ae11f290b4 |
fix(server): gate GitHub auto-close on closing keywords (MUL-2680) (#3281)
* fix(server): gate GitHub auto-close on closing keywords (MUL-2680) Closes multica-ai/multica#3264. The PR webhook previously treated any mention of an issue identifier in a PR title/body/branch as a close intent, so a body of "Closes MUL-1. Follow up in MUL-2. Unblocks MUL-3." would advance all three issues to done on merge. The auto-link layer stays generous (mentions still link the PR), but advancing to done now requires an explicit "Closes/Fixes/Resolves MUL-X" keyword adjacent to the identifier in the title or body — bare title prefixes (`MUL-1: ...`) and branch-name references no longer auto-complete. MUL-2680 Co-authored-by: multica-agent <github@multica.ai> * fix(server): persist close_intent on issue↔PR link rows (MUL-2680) The first take of MUL-2680 gated auto-advance on `closingIdents[id]` from the current webhook event. That broke the multi-PR sibling case: a PR declaring `Closes MUL-X` could merge first while a link-only sibling stayed open, leaving the issue in_progress; when the sibling closed later, its webhook carried no closing keyword and the handler skipped re-evaluation, so the issue stayed stuck forever. Move close intent from per-event state to per-link state: - New `close_intent` column on `issue_pull_request` (migration 109), set monotonically — `LinkIssueToPullRequest` ORs the existing flag with the incoming one so a subsequent webhook re-fire without the keyword cannot clear it. - New `GetIssuePullRequestCloseAggregate` query returns open-count and merged-with-close-intent-count for an issue. The auto-advance gate now reads from this persisted aggregate, which is event-agnostic: any terminal linked-PR event re-evaluates and the verdict only depends on accumulated DB state. - Webhook handler links all mentioned identifiers first (writing close_intent for the ones declared with a keyword), then iterates the affected issues in a separate pass to re-evaluate. The 'only fires for keyword-declared identifiers in this event' gate is gone — replaced by `merged_with_close_intent_count > 0` against the link rows. Regression test `TestWebhook_LinkOnlySiblingMergeAfterCloseKeywordPR` walks the full open→merge→open→merge sequence Elon described and asserts the issue advances on the link-only sibling's merge. MUL-2680 Co-authored-by: multica-agent <github@multica.ai> * Fix GitHub close intent updates Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Eve <eve@multica-ai.local> |
||
|
|
a67bf81225 |
fix(agent): surface host OAuth token via env var on macOS isolation (MUL-2603) (#3267)
* fix(agent): surface host OAuth token via env var on macOS isolation (MUL-2603) Claude Code 2.x scopes the macOS keychain credentials entry by sha256(CLAUDE_CONFIG_DIR)[:8], so the MUL-2603 isolation path strands the child at "Not logged in" even after #3261 mirrored .claude.json: the child looks up `Claude Code-credentials-<scratch-hash>`, the host token is sitting in the no-suffix `Claude Code-credentials` entry. Read the host OAuth token from the keychain via /usr/bin/security and inject it as CLAUDE_CODE_OAUTH_TOKEN, which bypasses keychain lookup entirely. Linux/Windows continue to use the .credentials.json mirror (no-op there). Operator-pinned tokens and ANTHROPIC_API_KEY both take precedence over the keychain reader. Co-authored-by: multica-agent <github@multica.ai> * fix(agent): tighten empty-value auth gate, pin Claude CLI env-scrub assumption (MUL-2603) Empty-value gate - `ANTHROPIC_API_KEY=` inherited from a login shell that conditionally exports auth previously posed as an "operator pinned API-key auth" choice and disabled the keychain reader, stranding the isolated child at "Not logged in" even though no auth was actually selected. - Custom_env `CLAUDE_CODE_OAUTH_TOKEN=""` (stale agent config) had the same effect, plus would have shadowed a keychain-injected token in libc env lookups that pick the first match. - Both are now treated as noise: the empty entry is dropped from the child env and the keychain reader runs unchanged. Two new unit tests cover the os.Environ side (`...TreatsEmptyAnthropicAPIKeyAsUnpinned`, `...HonorsNonEmptyAnthropicAPIKey`) and the custom_env side (`...EmptyOAuthTokenInCustomEnvAsUnpinned`). Env-scrub boundary - Surfacing `CLAUDE_CODE_OAUTH_TOKEN` to the isolated child is only safe because Claude Code itself drops that variable from the env it hands to Bash / hook subprocesses, so a model-driven `printenv` can never echo the secret into the agent transcript. - Empirically verified against `claude` 2.1.121: printf '...test -n "$CLAUDE_CODE_OAUTH_TOKEN" && echo SET || echo UNSET...' \ | CLAUDE_CODE_OAUTH_TOKEN=sk-canary-XYZ \ MUL2603_CONTROL=control-value \ claude --print --output-format text \ --allow-dangerously-skip-permissions --allowedTools Bash returned `UNSET` for the OAuth token while the non-sensitive `MUL2603_CONTROL` control returned `CONTROL-SET`, proving the CLI scrubs only the auth env, not the env in general. - Pinned this assumption in a new skip-gated regression test (`TestClaudeCLIScrubsOAuthTokenFromBashSubprocess`) that boots the real CLI with a canary token; failing the test means upstream Claude Code stopped scrubbing and the passthrough must move off env vars before MUL-2603 can ship. Co-authored-by: multica-agent <github@multica.ai> * fix(agent): gate keychain passthrough on default host dir, harden scrub test (MUL-2603) Two follow-ups from the round-2 review on #3267: 1. Custom CLAUDE_CONFIG_DIR no longer pulls the default OAuth token. Claude Code 2.x maps each config dir to its own suffixed `Claude Code-credentials-<hash>` keychain entry, so an operator that pins a managed/custom CLAUDE_CONFIG_DIR via custom_env or the daemon-host env was getting the *daemon user's* default unsuffixed entry injected into the isolated child — silently crossing accounts, exactly the boundary mirrorHostClaudeJSONIfMissing already protects for `.claude.json`. buildClaudeEnvWith now threads the effective hostConfigDir through and only calls the reader when that dir is the default `$HOME/.claude`. The new gate has a unit-level truth table (TestIsDefaultHostClaudeConfigDir) plus a regression (TestBuildClaudeEnvIsolatedSkipsKeychainForCustomHostConfigDir) that makes a t.Fatal-armed reader prove the gate keeps the read off for custom dirs. 2. Scrub e2e now asserts the control prong and the proof-of-execution marker, not just "canary absent". The previous assertion would false-pass on a model refusal, paraphrase, or "Bash gets no env at all" upstream change. The strengthened version sets a non-secret MUL2603_CONTROL alongside the canary OAuth token and asserts (a) canary is NOT in the transcript, (b) CONTROL-SET IS in the transcript (env propagation works for non-secrets — proves a targeted scrub), (c) UNSET IS in the transcript (the Bash tool actually ran AND saw the OAuth var as empty/unset). Code comment in buildClaudeEnvWith and the test docstring now narrow the security contract to the Bash tool subprocess only; hook subprocess env-scrub is no longer claimed because it has not been verified. Co-authored-by: multica-agent <github@multica.ai> * test(agent): use per-run nonces in Claude scrub e2e to kill false-pass (MUL-2603) Elon's round-3 review flagged that TestClaudeCLIScrubsOAuthTokenFromBashSubprocess still false-passed: the proof markers "UNSET" / "CONTROL-SET" were literal strings in the prompt, so strings.Contains matched them even when the model only paraphrased the prompt without spawning Bash. Replace the hard-coded markers with two per-run random hex nonces passed *only* via env vars (MUL2603_UNSET_NONCE, MUL2603_CONTROL_NONCE). The prompt now references the variable names, not the values, so the nonces can land in the transcript only if a real Bash subprocess inherits the env vars and echoes them. A paraphrasing or refusing model cannot fake nonces it never saw. Also update the security-boundary comment in buildClaudeEnvWith to describe the nonce-based proof. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bf8a346cf0 |
feat(runtimes): cascade-archive agents on runtime delete (MUL-2667) (#3266)
* feat(runtimes): cascade-archive agents on runtime delete (MUL-2667) Replace the bare 409 "cannot delete runtime: it has active agents" with a structured response carrying the blocking agent list, and wire a cascade endpoint that archives those agents, cancels their tasks, pauses dangling autopilots and deletes the runtime in a single transaction. The unified DeleteRuntimeDialog opens directly in cascade mode when the runtime has bound agents, pivots from light to cascade if the strict DELETE refuses with runtime_has_active_agents, and re-prompts when the cascade refuses with runtime_delete_plan_changed (live agent set drifted while the dialog was open). The online-local self-healing rule is preserved at the affordance level (kebab hidden, Diagnostics button disabled with tooltip) and re-checked at confirm time as defence in depth. Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): close cascade race + i18n delete dialog (PR #3266 review) - Acquire FOR UPDATE on the runtime row at the top of the cascade tx so FK-validated agent INSERTs/UPDATEs that would point at this runtime block until commit, and lock each currently-active agent row via ListActiveAgentsByRuntimeForUpdate so a concurrent archive/move of an existing active row also blocks. - Switch the bulk archive from runtime-keyed (ArchiveAgentsByRuntime) to ID-keyed (ArchiveAgentsByIDs), narrowed to the user-confirmed expected_active_agent_ids set. Combined with the runtime row lock, this guarantees no agent outside the confirmed plan can be silently archived between plan-compare and archive even at read-committed. - Wire delete-runtime-dialog.tsx to runtimes locale via useT(); add detail.delete_dialog.{light,cascade} keys (EN with _one/_other plurals, zh-Hans _other) covering titles, descriptions, warning, notices, checkbox, buttons, table headers, presence labels, and toasts. Resolves the i18next/no-literal-string CI failure. - Locale parity test passes (51 tests). All 4 dialog test cases pass unmodified (EN copy preserves original wording). Full views vitest: 91 files / 792 tests green; full server go test: green. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
40da88fc16 |
fix(agent): mirror $HOME/.claude.json into isolated config dir (MUL-2661) (#3261)
PR #3200 introduced per-agent `skills_local=ignore` isolation that mirrors the host's Claude config dir into a per-task scratch dir, omitting `skills/` to keep broken local skills out of the CLI's discovery path. The mirror walks entries inside `hostConfigDir` (default: `$HOME/.claude/`), but Claude Code's default layout stores its main config — login state, project history — at `$HOME/.claude.json`, a *sibling* of `~/.claude/` rather than inside it. Once `CLAUDE_CONFIG_DIR=$ISOLATED` is set, the CLI looks for `$ISOLATED/.claude.json`, finds only `backups/.claude.json.backup.*` (those live inside `~/.claude/` and DO get mirrored), and exits with: Claude configuration file not found at: …/.claude.json Not logged in · Please run /login — so every agent with `skills_local=ignore` on a host using the default Claude layout dies on the first turn. Flipping the toggle back to "merge" restores the host CLAUDE_CONFIG_DIR and recovers the agent; that's the workaround Bohan flagged in MUL-2661. Fix: after the existing `mirrorHostClaudeExceptSkills`, run a new `mirrorHostClaudeJSONIfMissing` that pulls `$HOME/.claude.json` into the scratch dir as `.claude.json` when (a) the dest doesn't already have one and (b) the host source dir is the default `$HOME/.claude/`. The custom-CLAUDE_CONFIG_DIR path is left alone because a pinned custom dir is expected to be self-contained — silently borrowing `$HOME/.claude.json` from a different account would mask credential drift. The helper goes through `createFileLink`, so it inherits the same symlink → junction → hardlink → copy fallback chain the rest of the mirror uses on Windows-without-Developer-Mode hosts. Tests: - `TestMirrorHostClaudeJSONIfMissing_DefaultLayoutMirrorsParentFile` covers the happy path with an injected `homeDir`/`fileLink`. - `TestMirrorHostClaudeJSONIfMissing_AlreadyPresentNoop` asserts a pre-existing dest `.claude.json` (from a custom CLAUDE_CONFIG_DIR mirror) is not overwritten. - `TestMirrorHostClaudeJSONIfMissing_CustomHostDirSkipped` locks in the custom-host-dir gate. - `TestMirrorHostClaudeJSONIfMissing_MissingSourceNoop` documents the env-var-auth-only / fresh-install case. - `TestClaudeExecuteIsolatesProvidesClaudeJSONFromHome` is the end-to-end MUL-2661 regression: a fake `\$HOME` with the default split layout, `skills_local=ignore`, fake claude binary that prints whatever `.claude.json` reaches the scratch dir. Asserts the file rides through. Verified the test fails (with the documented MUL-2661 error message) when the new mirror call is removed. Verification: - `go test ./pkg/agent/...` green (full agent suite). - `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` clean. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
960befa56f |
feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603) (#3200)
* feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603)
Adds an agent-scoped `skills_local` switch ("ignore" default / "merge") so
shared agents stop inheriting the operator's user-global Claude skill
directory. A single broken local skill on one operator's machine was
crashing the Claude CLI before it ever read stdin — the daemon saw a
"broken pipe" with no recoverable signal (GitHub #3052).
- DB: migration 108 adds `agent.skills_local` (NOT NULL DEFAULT 'ignore'),
with sqlc CreateAgent/UpdateAgent updates and handler validation.
- Claude runtime: when the agent is in "ignore" mode the backend points
CLAUDE_CONFIG_DIR at an empty per-task scratch dir under the task cwd
(fallback: OS temp), strips any inherited override, and cleans up after
the run. Workspace skills under `{cwd}/.claude/skills/` still load.
"merge" preserves the legacy inherit-from-machine behavior; Codex and
other isolated backends are no-ops.
- UI: new Skills toggle in the Create Agent dialog and the Agent → Skills
tab, with EN/zh-Hans copy and SkillsLocalToggle shared between the two.
- Tests: unit coverage for the new env helper, isolation dir lifecycle,
full Claude execute paths (ignore + merge), and the handler tristate
contract. Existing skills-tab test updated for the new copy.
- Docs: updated `/skills` docs (EN + ZH) and added a 0.3.7 changelog entry
in the landing-page i18n.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): preserve claude login + validate skills_local input (MUL-2603)
Address Elon's review on PR #3200:
1. Skill isolation no longer drops the operator's Claude login. The
per-task scratch dir now mirrors every entry under `~/.claude/`
as symlinks except `skills/`, so `.credentials.json`, settings,
plugins, etc. reach the CLI exactly as on the host while the
user-global skills directory stays hidden. Without this, default
`ignore` would have broken every Claude agent on a non-API-key
host the moment migration 108 landed.
2. Internal CreateAgent callers (agent_template, onboarding_shim)
now set `SkillsLocal: "ignore"`. The Go zero value was about to
trip the migration-108 CHECK constraint and 500 template /
onboarding agent creation.
3. Create / update handler validation no longer normalizes garbage
to "ignore". The strict 400 path is now reachable on bad client
input; the drift-safe `normalizeSkillsLocal` stays on the read
side only.
UI copy + docs clarified that the toggle is Claude-only; other
runtimes ignore the setting.
Verification:
- `go test ./...` green (full suite locally).
- `pnpm --filter @multica/views exec vitest run agents/components/tabs/skills-tab.test.tsx` green.
- Handler DB-backed tests still skip locally without docker (same
as Elon's run) — CI will validate the create / update paths
against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): mirror effective claude config dir with windows fallback (MUL-2603)
Address Elon's second-round review on PR #3200:
1. The per-task scratch dir now mirrors the *effective* host Claude
config dir, not unconditionally `~/.claude/`. Precedence: agent
`custom_env` CLAUDE_CONFIG_DIR > parent process env > `~/.claude/`.
Without this, an operator who pinned Claude at a managed install
(custom env CLAUDE_CONFIG_DIR) would get the wrong credentials in
the scratch dir, because `buildClaudeEnv` strips that env before
handing it to the child. We resolve the source up front and feed
it to the mirror, so the override env still points at the right
bytes.
2. Mirror entries now go through platform-aware linkers. On Windows
without Developer Mode / admin, `os.Symlink` is denied, which
previously left the scratch dir empty and broke Claude Code auth
on default `ignore`. The new helpers try symlink first, then fall
back to a directory junction (`mklink /J`) for dirs or a hardlink
(same-volume content share) / copy for files. Mirrors the
execenv/codex_home_link_windows.go pattern.
3. Tests:
- `TestResolveHostClaudeConfigDir` locks in the custom_env >
parent_env > `~/.claude` precedence.
- `TestNewIsolatedClaudeConfigDirMirrorsCustomHostDir` confirms
the scratch dir picks up `.credentials.json` from a synthetic
custom host dir, proving the source resolution actually
propagates into the mirror.
- `TestNewIsolatedClaudeConfigDirEmptyHostIsNoop` documents the
env-var-auth-only case (no host source ⇒ empty scratch dir).
- `TestMirrorHostClaudeExceptSkillsWith_FallbackWhenSymlinkFails`
exercises the Windows-no-Developer-Mode path via the new
`mirrorHostClaudeExceptSkillsWith` seam, asserting credentials
and sub-dir children still reach the scratch dir after the
symlink stand-in fails.
- `TestMirrorHostClaudeExceptSkillsWith_PropagatesFirstLinkError`
confirms callers see the per-entry error when even fallback
fails (so the warn-log fires on broken Windows installs).
- `TestCopyFileRoundTrip` covers the last-resort copy fallback
and its EXCL no-overwrite contract.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` is the
end-to-end check: an agent with custom_env CLAUDE_CONFIG_DIR
reads its credentials from the pinned dir, not `~/.claude/`.
4. Docs: `apps/docs/content/docs/skills.{mdx,zh.mdx}` updated to
describe the effective-source resolution and the Windows
fallback chain so the docs match the runtime behaviour.
Verification:
- `go test ./...` green (full server suite locally, including
`pkg/agent` 23 cases covering the new + existing isolation
paths).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean, confirming the
Windows-tagged linker file builds.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): default skills_local to merge to preserve legacy behavior (MUL-2603)
Per Bohan's product decision on PR #3200, the per-agent host-skill toggle
defaults to "merge" — the pre-MUL-2603 inherit-from-machine behavior —
so existing personal workflows that rely on locally installed Claude
Skills keep working unchanged. Agent owners explicitly opt into "ignore"
when they need to harden a shared agent against a broken local skill on
one operator's machine (GitHub #3052).
Also audited all 11 runtimes for user-global skill discovery paths and
documented the scope of the toggle. Only Claude reads a user-global
`~/.claude/skills/`; Codex isolates via `CODEX_HOME`, the ACP backends
(Hermes / Kimi / Kiro) and the JSON-stream backends (Copilot / Cursor /
Gemini / Pi / OpenCode / OpenClaw) anchor discovery to the task workdir
and never read a user-global skill directory. UI copy and docs now say
"for runtimes that support it (currently Claude Code)" everywhere so
the scope is explicit.
Changes:
- Migration 108: column default flipped to 'merge'.
- Handler CreateAgent: missing field → "merge"; explicit "ignore" /
"merge" still validated, garbage still 400.
- normalizeSkillsLocal: drift-safe coercion now lands on "merge" for
anything that isn't the exact literal "ignore".
- agent_template.go / onboarding_shim.go: internal CreateAgent callers
send "merge" instead of "ignore" to match the new default.
- Claude runtime (`claude.go`): isolate-mode gate flipped from
`SkillsLocal != "merge"` to `SkillsLocal == "ignore"`, so "" (legacy
daemons / older clients) and "merge" both walk `~/.claude/` directly.
- Create Agent dialog + Skills tab: toggle defaults to on (merge); only
duplicate of an explicit "ignore" agent carries through. The
isolation opt-in is now `skills_local: "ignore"` when the user flips
off; "merge" is omitted from the request body.
- i18n (EN + zh-Hans): copy reframed — "On (default) — merged"; "Off —
ignored. Recommended for shared agents".
- Docs (`/skills`, `/guides/agents.zh`): describe new default and
enumerate which runtimes act on the toggle.
- Landing changelog 0.3.7: retitled "Per-Agent Local-Skill Toggle"; note
the on-by-default behavior + off-to-isolate framing.
- Tests:
- `TestClaudeExecuteIsolatesHostSkillsWhenIgnoreOptedIn` replaces the
old by-default isolation case (now requires explicit "ignore").
- New `TestClaudeExecuteDefaultModeKeepsHostConfigDir` locks in that
default ExecOptions preserve the host CLAUDE_CONFIG_DIR.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` now explicitly opts
into "ignore" mode.
- Handler tests: omitted → "merge"; explicit "ignore" round-trips;
preserve-existing test seeds "ignore" and asserts "merge" flip-back.
- `TestNormalizeSkillsLocal_DriftStaysSafe`: only literal "ignore"
maps to ignore; everything else → "merge".
- `skills-tab.test.tsx`: toggle ON by default; flip OFF when agent
opted into "ignore". Intro-text matcher anchored to a more specific
phrase so it no longer collides with the toggle hint copy.
Verification:
- `go test ./...` green (full server suite locally).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean (windows-tagged linker
file still builds).
- `pnpm typecheck` green across all packages and apps.
- `pnpm --filter @multica/views test` 88 files / 771 tests green.
- `pnpm --filter @multica/core test` 43 files / 390 tests green.
- Handler DB-backed tests still skip locally without docker; CI will
validate the create / update paths against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* chore(landing): drop 0.3.7 changelog entry from this PR (MUL-2603)
The landing-page release notes belong in a separate release-prep PR, not in the feature PR.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): propagate skills_local=ignore to codex user-skill seed (MUL-2603)
Make the per-agent skills_local toggle real for Codex too, not just Claude.
Previously the toggle was only consumed by the Claude backend, while the
daemon's execenv layer always seeded Codex's per-task CODEX_HOME with the
host machine's user-installed skills from ~/.codex/skills/. A shared Codex
agent with skills_local=ignore could still inherit a broken local skill
from one operator's machine.
Now: PrepareParams/ReuseParams carry SkillsLocal; hydrateCodexSkills
skips seedUserCodexSkills when SkillsLocal == "ignore" so the per-task
CODEX_HOME exposes only workspace skills to the codex CLI. Default
("merge", or empty from older servers/clients) preserves existing
inherit-from-machine behavior. UI / docs are updated to reflect the
contract honestly: Claude Code and Codex honor the toggle; other
runtimes (Hermes / Kimi / Kiro / Copilot / Cursor / Gemini / Pi /
OpenCode / OpenClaw) leave $HOME untouched and discover user-level
skills natively, so the toggle is a no-op for them today.
New tests: TestPrepareCodexSkillsLocalIgnoreSkipsUserSeed,
TestPrepareCodexSkillsLocalMergeSeedsUserSkills, and
TestReuseCodexSkillsLocalIgnoreSkipsUserSeed cover Prepare(ignore),
Prepare(merge), and the toggle-flip-on-reuse path.
Co-authored-by: multica-agent <github@multica.ai>
* docs(skills): scope skills_local toggle copy to Claude Code + Codex (MUL-2603)
Off-state hint and Skills tab intro now explicitly call out Claude Code +
Codex as the only runtimes that honor the toggle, with "other runtimes
ignore this setting" wired into both states (en + zh-Hans), so users on
non-Claude/Codex agents don't read "Off" as runtime-wide isolation.
Docs (skills.mdx, skills.zh.mdx, guides/agents.zh.mdx) stop describing
Hermes / Kimi / Gemini / Copilot / Cursor / Pi / OpenCode / OpenClaw / Kiro
as having native user-level skill discovery; the daemon simply does not
manage user-level skill discovery for those runtimes today, and the toggle
is a no-op regardless of where it is set.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
13f74e651a |
feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600) (#3209)
* feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600)
The agent resource shape (list / get / create / update / archive /
restore responses + WebSocket events) no longer carries `custom_env`
values. Reads/writes of env now flow exclusively through a dedicated
`/api/agents/{id}/env` endpoint that is owner/admin-only, rejects
agent-actor sessions, applies a "****" sentinel preserve guard on
PUT, and writes a persistent audit row per reveal/update.
Why
- `multica agent list --output json` historically returned plaintext
`custom_env` for owner/admin callers (the redaction gate gave only
members the masked map). Any agent token running on the workspace
inherits its owner's role and could read every other agent's
secrets just by listing.
- Patching list/get redaction alone (PR #3175 direction) left
symmetric leaks via mutation responses, WS events, the "reveal"
path itself (no actor-aware auth), and a `****` overwrite footgun
on UpdateAgent.
What changed
- Backend: drop `custom_env` from AgentResponse; add coarse
`has_custom_env` + `custom_env_key_count`. Strip env handling from
UpdateAgent (silently ignored if sent). Keep CreateAgent's
custom_env acceptance.
- Backend: new GET/PUT `/api/agents/{id}/env` handlers in
`internal/handler/agent_env.go`:
- resolveActor → 403 for agent actors (closes the lateral-movement
path).
- Owner/admin role gate via existing helper.
- PUT honours value == "****" as "preserve existing value".
- Both write to `activity_log` with `agent_env_revealed` /
`agent_env_updated` actions. Audit details record key names only,
never values.
- Daemon claim path (`ClaimAgentTask`) unchanged — `TaskAgentData`
still carries plaintext env for runtime injection.
- SQL: new `UpdateAgentCustomEnv` query; sqlc regenerated (v1.31.1).
- CLI: new `multica agent env get|set` subcommands. `--custom-env*`
flags removed from `multica agent update`; the no-fields error
now points to the new path.
- Frontend: drop env fields from `Agent` + `UpdateAgentRequest`; add
`getAgentEnv` / `updateAgentEnv` client methods; rewrite env-tab
to show "N variables configured" + explicit "Reveal & edit"
button, fetching values only on intentional reveal.
- Locales: parity-safe additions to en + zh-Hans.
- Docs: agents-create.{mdx,zh.mdx} reflect the new threat model and
endpoint.
- Mobile: schema drops `custom_env` / `custom_env_redacted`, adds
metadata fields.
Tests
- Handler tests pinned the new invariants: no env in list/get
responses, owner reveal happy-path + audit row, agent-actor 403,
`****` sentinel preserves real values, UpdateAgent silently
ignores `custom_env`, pure `mergeAgentEnv` cases.
- CLI tests pivot to the new flag surface: `agent update` MUST NOT
expose the env flags; `agent env set` MUST expose
--custom-env-stdin/--custom-env-file.
- Frontend test fixtures updated; pnpm typecheck / test / lint
pass cleanly.
This is a breaking API change. Scripts that read `custom_env` from
`/api/agents` must migrate to `GET /api/agents/{id}/env`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close actor-spoofing + audit fail-closed in env endpoints (MUL-2600)
Addresses Elon's review of #3209:
* Mint a task-scoped `mat_` token per claim, bound to (agent, task,
workspace, owner). Daemon injects it into the agent process in place
of its own credential. Auth middleware authoritatively rebuilds
X-User-ID / X-Agent-ID / X-Task-ID from the token row and sets
X-Actor-Source=task_token; that header is server-set only — incoming
values are stripped before any auth branch runs. resolveActor honors
the header so an agent that strips X-Agent-ID / X-Task-ID still
resolves as actor=agent.
* GetAgentEnv / UpdateAgentEnv are now fail-closed on audit-log
failures: GET refuses to return plaintext, PUT persists inside the
same tx as the audit row so they commit/roll back together.
* PUT /api/agents/{id} returns 400 when the body carries custom_env
instead of silently dropping it — directs callers to the audited env
endpoint.
* Agent actors never see mcp_config, even when the underlying member
is owner/admin; mutation broadcasts go through a redaction shim so
WS subscribers don't pick it up either.
* Fix backend test that asserted dense JSON (jsonb::text renders
whitespace) and frontend test that assumed a unique "Test User"
match.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close residual MUL-2600 gaps from review (MUL-2600)
Migration 108 FK now correctly references agent_task_queue(id) instead
of the non-existent agent_task table; the previous name blocked CI
backend migrations.
Task-token-authenticated requests can no longer be re-routed at a
different workspace by passing workspace_slug / workspace_id /
?workspace_id / a URL workspace param. ResolveWorkspaceIDFromRequest
and resolveWorkspaceUUID both short-circuit on X-Actor-Source=task_token
and return only the token-bound X-Workspace-ID; buildMiddleware adds a
defence-in-depth 403 if any URL-resolved workspace disagrees with the
token binding.
mcp_config no longer leaks back to agent actors through UpdateAgent /
CreateAgent / ArchiveAgent / RestoreAgent HTTP responses — the same
redactAgentResponseForActor helper that GetAgent/ListAgents use is now
applied to mutation responses too. WS broadcasts were already redacted
via broadcastAgentResponse.
FailTask and every TaskService cancel path (CancelTask /
CancelTasksForIssue / CancelTasksForAgent / CancelTasksByTriggerComment
/ BroadcastCancelledTasks) now eagerly DeleteTaskTokensByTask so the
mat_ token's 24h window doesn't outlive a terminated task. Failure is
non-fatal — the FK cascade and expiry remain durable guards.
Doc-only: clarify that PUT /api/agents/{id} now hard-rejects bodies
that carry custom_env (was previously "silently ignores").
Tests:
- middleware: TestResolveWorkspaceIDFromRequest gains a task_token
case asserting client-supplied slug/id/query cannot override the
bound workspace.
- handler: TestUpdateAgent_RedactsMcpConfigForAgentActor and
TestUpdateAgent_KeepsMcpConfigForMemberActor pin the mutation-
response redaction contract per actor type.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): match redacted mcp_config as JSON null, not Go nil (MUL-2600)
`AgentResponse.McpConfig` is `json.RawMessage` without `omitempty`, so
the redacted response serialises as `"mcp_config": null`. On decode,
`json.RawMessage` keeps the literal bytes `null` rather than collapsing
to Go nil, which made the assertion fire on a non-leak.
The product contract (field always present, distinguished from "no
config" via `mcp_config_redacted`) is intentional, so adjust the test
to check for "no secret-bearing content" instead of weakening the
contract via `omitempty`.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
cfc652aa5f |
fix(daemon): close stdin pipe in Pi adapter to deliver EOF (#2188) (#3118)
Pi reads its prompt from argv (positional, see buildPiArgs) and never expects interactive input, so the Pi backend previously left cmd.Stdin nil. Under systemd, the resulting /dev/null character device has been observed not to satisfy Pi's readable-side wait, leaving runs stuck in "working" forever (#2188). Attach an explicit StdinPipe and close it immediately after Start so the child sees an EOF on a FIFO, matching the pattern already used by the Claude, Codex, Hermes, Kiro, and Kimi backends. The fix is defensive on the daemon side because Pi is mid-refactor and is not accepting issues upstream; once Pi itself stops blocking on stdin, this close is still correct (a closed pipe is a no-op for a process that does not read it). Test asserts the structural invariant: a shell-stub `pi` inspects /proc/self/fd/0 and only emits a valid event stream when stdin is a FIFO. If a future change drops the StdinPipe and stdin reverts to /dev/null (char device), the stub exits non-zero and the test fails. |
||
|
|
6261ea45fd | Improve board and squad hover cards (#3188) | ||
|
|
5bc77f2953 | fix(pi): strip leaked tool markup safely (#2956) | ||
|
|
a6f19380b2 |
test(agent): use ForkLock helper to fix ETXTBSY flake in thinking tests (#3062)
Two thinking tests wrote fake CLI scripts via os.WriteFile and immediately execed them. Under t.Parallel() with the rest of pkg/agent, a sibling test's concurrent fork can inherit our still-open write fd, so Linux returns ETXTBSY at exec time (Go #22315). CI hit this on main as "TestRunCodexDebugModels_ArgvSeenByBinary: fork/exec ...: text file busy". Switch both call sites to the existing writeTestExecutable helper, which holds syscall.ForkLock across OpenFile→Write→Close so no concurrent fork can inherit the write fd. Same pattern the rest of the package already uses (kimi, kiro, codex, claude tests). |
||
|
|
1c91c2a3b2 |
security(db): scope DELETE/UpdateIssueStatus by workspace_id (defense-in-depth) (#3027)
* fix(security): scope DELETE/UpdateIssueStatus by workspace_id Add workspace_id to the WHERE clause of DeleteIssue, DeleteComment, DeleteProject, DeleteSkill, DeleteChatSession, and UpdateIssueStatus as SQL-layer defense-in-depth. Handler loaders (loadIssueForUser / loadSkillForUser / etc.) already enforce workspace membership today, so this is not patching a known live vuln. But the tenant invariant is currently a handler-layer guarantee — a future loader bypass or a new caller skipping the loader would be silently catastrophic. Making workspace_id part of the SQL identity collapses the trust surface to the schema itself: forging a sibling-workspace UUID becomes ErrNoRows instead of a cross-tenant write. Reference: incident #1661 (util.ParseUUID silent zero UUID returning 204 on a DELETE that matched zero rows) — same class of failure, prevented at a different layer. Scope: - 5 DELETE queries: issue, comment, project, skill, chat_session - 1 simple UPDATE: UpdateIssueStatus (2 narg, no SET ordering risk) - All callers updated (handlers, service, runtime sweeper fallback) Multi-narg UPDATE queries (UpdateIssue, UpdateProject, UpdateSkill, UpdateComment, UpdateChatSession*) are deferred to a follow-up to keep this change reviewable: each needs its narg pinning shifted and per-caller verification. sqlc was regenerated by hand (no local sqlc toolchain); CI's backend job is the authoritative compile check. * test(security): add workspace_scope_guard regression test Locks in the SQL-layer tenant guard added in this PR. For each of the 6 scoped queries (DeleteIssue, DeleteComment, DeleteProject, DeleteSkill, DeleteChatSession, UpdateIssueStatus), creates the resource in workspace A, invokes the query with a foreign workspace UUID, and asserts the row is untouched (0 rows affected with no error for :exec; pgx.ErrNoRows for :one). A future refactor that drops the workspace_id arg from any of these queries will now fail loudly instead of silently regressing. Includes a sanity sub-test that the in-workspace path still mutates, so a buggy guard that returns no-op for every call would not pass. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com> --------- Co-authored-by: Tom Qiao <tomqiaozc@users.noreply.github.com> Co-authored-by: Claude Opus 4 <noreply@anthropic.com> |
||
|
|
fedd0f1694 |
feat(issues): live agent activity chip + per-issue indicator + filter (#3058)
* feat(server): broadcast task:running event The dispatched → running transition was silent: only task:queued, task:dispatch, task:cancelled, task:completed and task:failed broadcast over WS. Any UI that distinguishes "queued" from "running" (e.g. the new issue-card agent activity indicator) would lag by up to the 30s agentTaskSnapshot staleTime on the most user-visible transition. StartTask now broadcasts task:running so the workspace snapshot invalidates immediately, keeping the agent activity UI live. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(issues): live agent activity chip + per-issue indicator + filter Surfaces "which agents are working on what, right now" in the Issues and My Issues views, with a one-click filter to narrow the list to issues that have a running agent task. Two visual surfaces: - **Workspace chip** in the header (left of Filter). Shows the brand-tinted avatar stack of agents currently running on visible issues. Click toggles a page-scoped filter; idle state renders a static "0 working" button with a hover-card placeholder. When the filter is active the chip pins to brand fill across hover and popover states (the Button outline variant otherwise repaints back to neutral). A muted "Viewing only working agents" hint sits to the left of the chip whenever the filter is on, so users notice the active state without having to hover. - **Per-issue indicator** on every board card and list row (top-right of the identifier line). Renders the avatar stack of agents in running or queued state on that issue, full-opacity ring at brand/70 when ≥1 is running, half-opacity stack when only queued. Returns null when nothing is in flight. Both surfaces open the same hover-card body that lists each active task with the agent avatar, status dot (composed via the existing availability + workload tokens), and a live-ticking duration. Adds a new "All" scope to /my-issues that unions assignee, creator, and involves_user_id via three parallel fetches deduped on the client — no backend changes for this part. The chip's count and the quick-filter both use the page's currently visible issue ids so they stay in sync with the active scope. State is per-user (Zustand + localStorage) and the agentRunningFilter is intentionally omitted from partialize — running state changes second-to-second and a stored toggle would land users in an unexplained empty list. WS task:running, already added in the preceding commit, drives real-time updates without polling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(issues): swap indicator ring pulse for shimmer text label Earlier iterations layered a brand ring with various opacity-pulse cadences around the per-issue avatar stack. Every tuning attempt was either invisible (transparent ring + faded pulse) or oppressive (a visible ring that flashed on a dense board). Moves the "alive" signal onto a small text label and reuses chat's existing `animate-chat-text-shimmer` utility — a soft light sweep across the glyphs that already powers the ChatGPT-style "thinking" cue in task-status-pill. Indicator now reads as a 12 px avatar stack + 10 px label: - Running → full-opacity avatars + shimmering localized "Working" - Queued → half-opacity avatars + muted static "Queued" - Idle → render nothing (unchanged) Avatars and the surrounding card stay completely still; only the few glyphs animate. The label is i18n-driven via the existing `status_running` / `status_queued` keys, so no locale changes are required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7984606eed |
feat(landing): add Contact Sales page and inquiry endpoint (MUL-2493) (#2988)
* feat(landing): add Contact Sales page and inquiry endpoint (MUL-2493) Adds a public `/contact-sales` marketing page with a needs-discovery form modelled on the design reference attached to MUL-2493 — first/last name, business email (with free-provider rejection), company name + size, country/region, intended use case, and a free-text goals field, plus the two consent checkboxes from the reference. Submissions hit a new public `POST /api/contact-sales` endpoint with per-IP rate limiting (Redis-backed via the existing RateLimit middleware, configurable through `RATE_LIMIT_CONTACT_SALES`) and a per-email hourly cap so a single business address can't be used as a flood channel after one valid pass. The inquiry is stored in a new `contact_sales_inquiry` table; analytics fires a `contact_sales_submitted` PostHog event with only the closed-enum dimensions (size, country, use case) — the free-text goals stay in the DB and are never broadcast. The page is linked from the landing header (md+) and the footer's Company column, in both English and Simplified Chinese. The reserved-slug list is updated so a workspace named `contact-sales` can't shadow the route. Co-authored-by: multica-agent <github@multica.ai> * fix(landing): canonicalize business email and tighten contact-sales form (MUL-2493) - Parse the submitted email with net/mail and run the free-email block-list against the canonical addr.Address, so a display-name form like `Ada <ada@gmail.com>` can no longer slip past the gate (the raw string had domain `gmail.com>`, which wasn't blocked). Adds regression tests covering the display-name bypass and the canonicalization helper. - Drop noValidate from the contact-sales form so the browser's native required / email / select checks fire before submit; the JS-side free-email warning still runs as a UX guard. - Update success copy ("respond within three business days") in EN and ZH plus the page metadata. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5bacfd9742 |
MUL-2526 feat: add member(user_id, workspace_id) index + upgrade sqlc to v1.31.1 (#3046)
- Add migration 106: CREATE INDEX CONCURRENTLY on member(user_id, workspace_id) - Rewrite ListWorkspaces to drive from member table with explicit fields - Regenerate all sqlc code with v1.31.1 (intentional version upgrade) Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fbd965e5bf |
feat(onboarding): v3 — thin server, frontend-orchestrated welcome (#3008)
* feat(onboarding): Multica Helper as general workspace assistant + blocking modal
Reshape Multica Helper from an onboarding-only guide into the workspace's
general-purpose AI assistant. The agent's permanent identity (injected as
`## Agent Identity` into every task's CLAUDE.md / AGENTS.md / GEMINI.md
via execenv.InjectRuntimeConfig) is rewritten to three sections that don't
overlap with what the brief already provides:
- Who I am (built-in workspace assistant, not onboarding-only)
- What Multica is + docs/source/issues URLs as knowledge sources
- What I can do (CLI = manifest, `multica --help` is the source of truth)
- Tone (concise, like a colleague, match user's language)
Bootstrap moves out of the in-flow Step 4. Runtime step now exits the
onboarding shell with no bootstrap call; a blocking OnboardingHelperModal
mounts inside the workspace layout (web + desktop) and gates purely on
`me.onboarded_at == null`. The user picks one of three starter prompts
(intro / assign / second_agent) and the modal calls
BootstrapOnboardingRuntime with a new optional `starter_prompt` field that
becomes the seeded onboarding issue's description.
Side effects required to make `onboarded_at == null` an honest signal:
- CreateWorkspace no longer marks onboarded (was atomic with CreateMember).
The "member exists ⟹ onboarded_at != null" invariant is intentionally
broken; guards (useDashboardGuard / desktop App.tsx) already tolerate
this — comments updated to reflect the new contract.
- AcceptInvitation still marks (invitee skips the modal in someone
else's workspace). Code comment added warning future removers.
- resolvePostAuthDestination flips to workspace-presence-first: a user
with a workspace lands in it regardless of `onboarded_at`, so the
modal can pick up an interrupted setup on relogin.
Other backend changes:
- `onboardingAssistantDescription` rewritten ("Built-in workspace assistant…")
- `onboardingAssistantInstructions` rewritten to the 3-section identity
- `bootstrapOnboardingRuntimeRequest.StarterPrompt` (optional, 2 KiB rune
cap, empty-falls-back-to onboardingIssueDescription)
Frontend changes:
- Delete `packages/views/onboarding/steps/step-teammate.tsx` (no longer a
persisted step)
- `ONBOARDING_STEP_ORDER` and `OnboardingStep` type drop `"teammate"`
- `handleRuntimeNext` exits via `onComplete(workspace, undefined)` — no
bootstrap, `onboarded_at` stays NULL so the modal fires
- Runtime step next-button copy → "Start exploring" / "开始探索"
- New `packages/views/workspace/onboarding-helper-modal.tsx`:
Base UI Dialog, dismissible=false, three localized cards, mutation
invalidates agents + issues queries then navigates to the seeded issue
- Mounted in both `apps/web/app/[workspaceSlug]/layout.tsx` and
`apps/desktop/src/renderer/src/components/workspace-route-layout.tsx`
Tests:
- Backend: TestBootstrapOnboardingRuntime_{With,No}StarterPrompt and
TestCreateWorkspace_DoesNotMarkOnboarded
- Frontend: onboarding-helper-modal.test.tsx covers all four gating
conditions, three-card behavior, mutation pending state, and the
"no close button" invariant
Compatibility:
- Already-onboarded users: zero impact (modal can't fire)
- Invitees: AcceptInvitation still marks → modal can't fire
- Skip-runtime path: BootstrapOnboardingNoRuntime still marks → modal can't fire
- Old desktop / web clients: legacy teammate-step path keeps working
(bootstrap accepts missing starter_prompt) — the new modal only fires
on the new frontend bundle
- Avatar SVG kept (asterisk variant) — no migration of existing Helper
agents, only newly-created Helpers pick up the new instructions/description
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(desktop): suppress OnboardingHelperModal while a WindowOverlay is open
On desktop, App.tsx auto-creates a tab pointing at the user's first
workspace as soon as workspaces.length flips from 0 → 1 (during onboarding
Step 2). The new tab mounts WorkspaceRouteLayout under the overlay,
which mounts OnboardingHelperModal. The modal's Portal renders to
document.body — appearing AFTER the WindowOverlay in DOM order, so its
z-50 wins and the modal floats in front of the still-active onboarding
Step 3 (runtime).
Suppress the modal whenever any WindowOverlay is active. When the overlay
closes (onComplete fires after the user finishes onboarding), the modal
re-evaluates `me.onboarded_at == null` and pops on its own.
Web is unaffected (onboarding flow lives at /onboarding, not under
/[workspaceSlug]/, so WorkspaceRouteLayout never mounts during the
onboarding flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(onboarding): add v2 refactor plan
Captures the design + 8-step implementation order for collapsing the
onboarding state machine: single mark-onboarded entry point, persisted
Step 3 user choice, dumb Modal, single install-runtime seed call site.
Includes old-user compatibility analysis (4 existing gates) and per-PR
risk/rollback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(db): persist Step 3 runtime choice on user record (MUL-onboarding-v2)
Adds onboarding_runtime_id UUID NULL + onboarding_runtime_skipped BOOLEAN
columns to "user" and the CHECK constraint enforcing the 3-state machine
(unset / picked-runtime / explicit-skip; the fourth combination is
forbidden). ON DELETE SET NULL on the FK so a deleted runtime degrades
to "unset" rather than dangling.
PatchUserOnboarding gains the two narg fields plus CASE expressions that
collapse the runtime/skipped pair atomically — a follow-up PATCH that
flips one side now clears the other in the same statement, instead of
preserving it via per-field COALESCE and tripping the CHECK constraint.
Backwards compatible for existing users: both new fields default to
(NULL, false), which is the "unset" leaf of the state machine, and four
upstream gates on me.onboarded_at != null already short-circuit the
new fields' readers for everyone who's already onboarded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server): collapse onboarding side effects to service layer
Introduces OnboardingService.MarkComplete and
WorkspaceContentService.{Ensure,Seed}InstallRuntimeIssue as the single
authorities for the two onboarding side effects that used to be
duplicated across four handlers:
- MarkUserOnboarded + claim starter_content_state +
optional install-runtime fallback seed: was inline in
BootstrapOnboardingRuntime, BootstrapOnboardingNoRuntime,
AcceptInvitation, and CompleteOnboarding.
- install-runtime issue seeding: was inline in CreateWorkspace and
AcceptInvitation as a "no runtime yet" fallback.
After this refactor:
- MarkUserOnboarded is called from exactly one place (the service).
- install-runtime issue is seeded from exactly one place (the service).
- CreateWorkspace deliberately does not seed — the new
/ensure-onboarding-content endpoint (also added here) lets the
workspace-entry init component request the seed on first mount, so
workspaces created but never opened don't accumulate stale issues.
- The PatchOnboarding handler now accepts the new runtime_id /
runtime_skipped fields and rejects (uuid, skipped=true) up front.
- UserResponse exposes the two new persisted fields so the frontend
can read them off `me` without an extra round-trip.
Handler-side tests added: TestPatchOnboarding_RuntimeChoiceSwitch (the
explicit cross-request switch path that the original COALESCE design
would have 500'd on) + TestPatchOnboarding_PreserveUntouched.
Old handler-local file no_runtime_issue.go is deleted; its content
moved to service/workspace_content.go with the helpers exported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): API + types for persisted onboarding runtime choice
User type / Zod schema gain onboarding_runtime_id (string | null) and
onboarding_runtime_skipped (boolean); EMPTY_USER + test fixture updated
to match. api.patchOnboarding accepts the new optional fields and the
new api.ensureOnboardingContent endpoint is wired so the workspace
shell can request the fallback seed.
Two new store helpers — recordOnboardingRuntimeChoice(runtimeId) and
recordOnboardingRuntimeSkipped() — replace the prior pattern of
Step 3 calling bootstrap directly. They PATCH the user's choice, sync
the auth store, and return. Mutually exclusive on the server side via
the CHECK constraint; the client just ships one intent at a time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(workspace): WorkspaceOnboardingInit single decision point + dumb Modal
Replaces OnboardingHelperModal's self-gating render path with a 4-branch
dispatcher that runs once on workspace-shell mount:
branch 0 me.onboarded_at != null → ensure install-runtime issue
fallback, render nothing
branch 1 me.onboarding_runtime_skipped → SkipBootstrapping component:
loading veil → bootstrap →
navigate. On failure shows
a Retry UI instead of
silently freezing the veil
branch 2 me.onboarding_runtime_id → render Modal with the
runtime id from `me` (no
internal list query)
branch 3 (none of the above) → useEffect navigate back to
/onboarding so the user
walks Step 3 again
The Modal itself is now a dumb component — receives `workspace` and
`runtimeId` as props, no internal gates, no runtimeListOptions query.
Tests rewritten to cover the props-driven render + pick-card paths;
the prior gating tests move into the new
workspace-onboarding-init.test.tsx alongside the M2 retry-on-failure
behaviour.
Mounted in both apps/web/app/[workspaceSlug]/layout.tsx and the desktop
workspace-route-layout. Desktop keeps its `!overlayActive` suppression
guard so the init doesn't portal-jump in front of an active
WindowOverlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): Step 3 records user choice instead of calling bootstrap
handleRuntimeNext now PATCHes the user's pick (recordOnboardingRuntime
{Choice,Skipped}) and navigates straight into the workspace shell. The
workspace-entry WorkspaceOnboardingInit reads the persisted choice off
`me` and runs the appropriate branch — Step 3 is pure intent capture
with zero side effects on its own.
PATCH must succeed before navigation: if it fails the user stays on
Step 3 with a toast, because navigating with no persisted intent would
land them in WorkspaceOnboardingInit's branch 3 "no decision yet" rescue
and trigger a redirect loop back to /onboarding.
The prior asymmetry (Connect deferred bootstrap to the workspace, Skip
ran bootstrap inline) is gone — both paths defer to the workspace
shell now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): v3 — thin server, frontend-orchestrated welcome
Collapse v2's persisted runtime-choice fields + 4-branch dispatcher +
OnboardingService/WorkspaceContentService stack down to a single rule:
`onboarded_at` is the only state field, layout hard-gates on it, and the
welcome experience after Step 3 is owned entirely by the frontend.
V3 flow
- Step 3 button: await POST /api/me/onboarding/complete (mark only) +
park a transient signal in `useWelcomeStore` + navigate
- Workspace layout: hard gate `onboarded_at == null` -> /onboarding
- `<WelcomeAfterOnboarding />` reads the welcome-store signal:
- runtime path: find-or-create Multica Helper via generic createAgent
with bilingual instructions from `templates/helper-instructions.ts`,
blocking modal with 3 starter cards, pick -> createIssue + navigate
- skip path: provision install-runtime (in_progress) -> agent-guide
(todo, body embeds install-runtime mention chip) -> follow-up comment
on install-runtime mentioning agent-guide; then pop celebration
modal with 🎉 emoji pop animation, 2 read-only preview cards, single
[Got it] CTA that navigates to install-runtime
Server cleanup
- Drop OnboardingService, WorkspaceContentService, v2 runtime-choice
columns/CHECK on user, EnsureOnboardingContent endpoint
- CompleteOnboarding/AcceptInvitation call qtx.MarkUserOnboarded
directly (no service indirection)
- BootstrapOnboardingRuntime / BootstrapOnboardingNoRuntime kept as a
deprecation shim in onboarding_shim.go for desktop < v3 during the
rollout window — handlers inlined to qtx.* calls, no service layer
Localization
- Persisted strings (issue titles/bodies, Helper instructions/
description, comment prefix) live as TS const `{en, zh}` maps in
`packages/views/onboarding/templates/` — i18n bundle staleness can no
longer write raw key paths into DB
- UI-rendered strings (modal copy, status chips, buttons) stay in
`packages/views/locales/{en,zh-Hans}/onboarding.json`
- Language picked from live `i18n.language` (not `me.language`, which is
null for new users until they pick a preference)
Race protection
- Module-level promise dedupe (`findOrCreateHelper`, `seedIssueDeduped`,
`postCommentDeduped`) so React StrictMode double-mount can't fire two
parallel API calls that the server would then 409
Cross-references between the two skip-path issues render via Multica's
mention-chip protocol `[<identifier>](mention://issue/<uuid>)` so they
match the styled IssueChip pills used elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): welcome-after-onboarding modal redesign + cross-user safety
Welcome modal polish (the post-Step-3 surface this branch already
introduced):
Runtime path
- Helper avatar replaces the bouncy 🎉 hero; tone-down animation to
fade. New copy: "Hi, welcome to Multica / I'm your first Agent
assistant" + capability hint sentence so users discover assignment +
chat from the first screen.
- Cards changed from "click = submit" to multi-select with the existing
border-primary + ring selection pattern used by compact-runtime-row;
bottom CTA "Assign N tasks to me →" appears only with N>0.
- New starter cards: intro / tour / welcome_page (the last one tells
Helper to paste an HTML welcome page into the issue comment — works
on any runtime regardless of fs access).
- Success state added between createIssue and navigation: 🎉 +
"All set!" + "Sit tight ☕ — your {agentName} is on it" + inbox/chat
hints, single [Got it] button.
- Title/prompt for starter cards now live in TS const
HELPER_STARTER_PROMPTS (persisted to DB — must not depend on i18n
bundle being loaded); subtitle stays in onboarding.json.
Skip path
- Body restructured into three independent ```md blocks (Name /
Description / Instructions) so each picks up the markdown renderer's
per-block copy button — no manual extraction.
- ZH body now embeds the ZH Helper Description + Instructions (was
Chinese-around-English-block).
- Follow-up comment uses Multica's mention-chip protocol
[identifier](mention://issue/uuid) so it renders as the styled
IssueChip pill.
- Issue titles bilingual with "Step 1 / Step 2" prefix.
Cross-user / cross-workspace safety (code review feedback)
- web onLogout + desktop handleDaemonLogout now call
useWelcomeStore.reset() so user B logging into the same browser
doesn't inherit user A's signal.
- WelcomeAfterOnboarding gates on
currentWorkspace.id === signal.workspaceId — prevents firing the
modal in workspace B when the signal was parked for workspace A
(desktop multi-tab, back/forward, deep-link).
- Module-level promise dedupes (pendingHelperSetup,
pendingIssueSeed, pendingCommentSeed) for the three API calls so
React 18+ StrictMode dev double-mount can't race-create duplicates.
Other small fixes carried in this commit
- Helper instructions / agent description / starter card titles all
read i18n.language (not me.language, which is null for new users
who haven't picked a UI language preference yet).
- Reverted welcome-emoji-pop animation to a small fade for the runtime
avatar (kept the bouncy variant for the skip 🎉 hero where the
celebration is the whole point).
- Removed the duplicate 🎉 from the skip modal title (kept the hero
one only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(views): i18n hardcoded "Close" in welcome FullScreenError
CI lint (i18next/no-literal-string) blocked on a literal "Close" string
inside `FullScreenError` — surfaced as a nit in the original code
review but missed in the merge. Add `error_close` to onboarding.json
(EN: "Close" / ZH: "关闭") and thread it through as a `closeLabel`
prop, matching the existing `retryLabel` plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
29c2a5d18f |
fix(daemon): reclaim stale dispatched claims (MUL-2485) (#2872)
* fix(daemon): reclaim stale dispatched claims * fix(daemon): widen stale claim reclaim window |
||
|
|
0c767c0052 |
feat(issues): per-issue metadata KV (MUL-2017) (#2845)
* feat(issues): per-issue metadata KV (MUL-2017)
Adds a small JSONB KV map to every issue for agent pipeline state (attempts,
PR number, pipeline status, ...). Keys match a narrow regex, values are
primitives (string / number / bool), capped at 50 keys per issue and 8KB
per blob. Defense-in-depth via two CHECK constraints (object shape + size).
All mutations are single-key atomic (jsonb_set / `- key`). `UpdateIssue`
intentionally does NOT touch metadata: a whole-blob overwrite would race
with concurrent agent writes.
GET /api/issues/:id/metadata
PUT /api/issues/:id/metadata/:key body: { "value": <primitive> }
DELETE /api/issues/:id/metadata/:key
Containment filter on list: GET /api/issues?metadata=<json-object> uses
PG `@>` against a `jsonb_path_ops` GIN index. Mirrored across ListIssues,
CountIssues, ListOpenIssues, and the hand-rolled ListGroupedIssues SQL so
CLI/API and UI grouped views stay consistent.
CLI: multica issue metadata {list,get,set,delete}
multica issue list --metadata key=value (repeatable, AND)
set has --type to override the default value-sniffing
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): metadata test bugs + wire realtime + read-only display (MUL-2017)
- Fix two failing handler tests blocking backend CI:
- reset decode target after delete so map merge does not mask removal
- url.PathEscape the key segment so spaces no longer panic NewRequest
- Wire issue_metadata:changed end to end so the detail / list / my-issues
caches stay in sync with set/delete events (other tabs, CLI writes).
- Add a read-only Metadata strip to the issue detail sidebar; hidden when
the issue has no keys so it stays quiet in the common case.
Co-authored-by: multica-agent <github@multica.ai>
* feat(runtime): teach agents to read/write issue metadata (MUL-2017)
Add an `## Issue Metadata` section to the runtime brief plus a
`metadata list` step on entry and a `metadata set`/`delete` step on
exit. Section only emits when the task carries an issue id (comment- or
assignment-triggered); chat / quick-create / run-only autopilot stay
clean so they don't fire failing CLI calls.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): bump metadata migration to 105 and drop attempts as example (MUL-2017)
main is now at 104_drop_runtime_timezone; the migrator picks
LatestVersion() by sorted filename, so a slot before the tail would
let DBs that have already run 099–104 think they're up-to-date while
the issue.metadata column is missing — runtime would then fail with
column does not exist. Renumbering to 105 puts the migration at the
tail and forces it to run.
Also drop attempts as a positive example across docs/code comments and
test fixtures — the runtime instruction prompt already lists it under
"What NOT to pin" (runtime bookkeeping). Replace with pr_number, which
is in the recommended-keys set, so docs/tests speak the same language
as the prompt.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|