mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-09 06:21:51 +02:00
agent/j/ci-split-mobile
3308 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5950c13860 |
ci(mobile-verify): broaden path filter to cover real mobile deps
The initial filter only watched `apps/mobile/**` and `packages/core/types/**`, but mobile imports runtime modules from many more `@multica/core/*` paths (agents, markdown, permissions, api/schemas, etc.). PRs that touched only those subtrees would skip main CI (via `--filter='!@multica/mobile'`) AND skip Mobile Verify — a coverage regression vs. the pre-split CI. Expand paths to: - `packages/core/**` (covers every importable subpath) - root install/turbo configs that affect mobile build: `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `turbo.json` Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5ee88b4fb5 |
ci: split mobile lint/typecheck out of frontend job
Mobile lint (~38s) + typecheck (~13s) ran on every web/desktop PR even though mobile has no vitest suite and main CLAUDE.md already promises a parallel mobile-verify workflow. Excluding @multica/mobile from the frontend turbo filter pulls those 50s off the critical path, and the new mobile-verify.yml runs them in parallel only when apps/mobile/** or packages/core/types/** changes. MUL-2729 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d15da372a9 | chore: gitignore agent runtime caches (.agent_context/, .multica/) | ||
|
|
329fc0139d | fix(create-project): anchor Source popover to top so it doesn't get pushed below the trigger when the modal is expanded | ||
|
|
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
|
||
|
|
2af2f7b3e4 |
fix(comments): defer input clearing until submit succeeds (#3344)
clearContent() and setIsEmpty() were called before await onSubmit(), causing permanent content and draft loss on network failure. Move both to the success path, consistent with attachment and draft cleanup. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7bc1aa7563 |
fix(daemon): detect Codex Desktop bundle CLI (#3332)
Co-authored-by: codex <codex@multica.local> |
||
|
|
b343e13ec4 |
fix(settings): remove orphan repo count from GitHub tab shortcut card (#3342)
The bare "N" under the Repositories shortcut had no label and was
adjacent to a sentence ("Repository URLs live in the Repositories
tab") that has no semantic link to a number, so users read it as a
typo. The card is a navigation shortcut, not a status panel — the
actual count is visible after clicking through.
MUL-2725
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
668fe99cce |
fix(cli): drop "Showing N comments." stderr preamble on issue comment list (#3341)
This was the only `list` subcommand that printed a human-readable count to stderr. Consumers that merge stdout/stderr (agent harnesses, CI `2>&1`) saw it interleaved with the JSON array on `--output json`, and in table mode it carried no information the table itself didn't. The `Next thread cursor` / `Next reply cursor` lines stay — they're real paging signals the agent runtime reads from stderr. Closes #3303 MUL-2709 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6261e2b6b2 |
fix(comments): clear editor immediately on submit to eliminate WS race glitch (#3331)
* fix(comments): clear editor immediately on submit to eliminate WS race visual glitch The comment editor stayed populated while WebSocket delivered the new comment faster than the HTTP response, causing a "duplicate comment" flash. Move clearContent/setIsEmpty before the await so the editor clears at click time. Also remove dead `submitting` state in useIssueTimeline (redundant with the input components' own guards) and dead `isTemp` logic in comment-card (no code path ever creates temp- prefixed entries). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(comments): preserve attachments on submit failure and fix CommentRow indentation 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> |
||
|
|
963ed5cd0e | feat(comments): allow selecting multiple attachments | ||
|
|
7d24a8594a | fix(comments): support edit-time attachment removal (#2965) | ||
|
|
730fb61f4a |
fix(views): keep sort label centered in viewport during board scroll (#3325)
The "Board ordered by" overlay used absolute positioning inside a scrollable container, causing it to drift with scroll content. Move the overlay outside the scroll area into a non-scrolling wrapper so it stays centered in the visible viewport. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e55f050b84 |
fix(i18n): clean up zh-Hans translation inconsistencies (#3308)
- Normalize nav/page/section titles to plural English (Issues/Skills/Tasks) per conventions.zh.mdx rules for section titles - Lowercase 'Issue' inside UI short phrase '我的 Issue' (UI short-phrase rule) - Translate concept words in GitHub settings (Connection/Features/Repositories/Done) - Translate 'Cloud Runtime' to '云端运行时' to match runtime→运行时 glossary Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fa2a0e57ec |
feat(views): swimlane supports parent / project / assignee grouping (MUL-2711) (#3311)
* feat(views): swimlane supports parent / project / assignee grouping (MUL-2711) The swimlane view was hard-coded to group by parent issue. This adds a display dropdown so users can pick parent (default), project, or assignee — analogous to how the board view exposes its grouping option. - Generalise the lane builder in swimlane-view.tsx behind a `LaneGroup` abstraction (matcher + per-grouping `moveUpdates` payload) so the drag-end handler no longer branches on grouping. Cell ids gain a `<grouping>:<rawId>` prefix and lane sortable ids include the grouping so dnd-kit cannot collide entries from different groupings. - Extend the view store with `swimlaneGrouping`, `swimlaneOrders` (one saved order per grouping), and a grouping-keyed `collapsedSwimlanes`. The persist `merge` defends against the old `string[]` shape so a pre-upgrade snapshot doesn't crash on first read. - Wire `setSwimlaneGrouping` into the issues display popover next to the existing board grouping control. Add en / zh-Hans copy for the three swimlane buckets (Parent issue / Project / Assignee) and the two new pinned lanes (No project / Unassigned). - Expand swimlane tests with parent / project / assignee smoke cases and update existing mocks to the new lane-id format. Add stable `useActorName` / `projectListOptions` mocks to avoid the set-state-in-effect loop that an unstable `getActorName` would trigger via the cells-rebuild memo. Co-authored-by: multica-agent <github@multica.ai> * feat(views): default swimlane grouping to assignee Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
df02fcf175 |
fix(cli): show real MEMBERS count in multica squad list (#3307)
The MEMBERS column was hardcoded to "-" in the table output, so every squad looked empty even though the backend already returns `member_count` (and `member_preview`) on each row. `squad get --output json` exposed the correct data, which is why the bug was cosmetic but confusing. Read `member_count` from the response and render it; fall back to "-" when missing or zero so empty squads stay visually distinct. Fixes #3304 (MUL-2706). Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
298f54c819 |
fix(agents): gate on_comment trigger with private-agent visibility (MUL-2702) (#3302)
Closes #3300. After #2359 added canAccessPrivateAgent to chat, @mention, ListAgents, GetAgent, history, edit, delete and issue assignment, one trigger path was missed: shouldEnqueueOnComment. Once an owner/admin assigned a private agent to an issue, the agent's UUID was "welded" onto that issue and any workspace member who could view the issue could dispatch a new task to it by posting a plain (non-@mention) comment — bypassing the visibility gate the #2359 work was supposed to enforce. Mirror the @mention path: plumb (authorType, authorID) from CreateComment into shouldEnqueueOnComment, load the assigned agent, and gate it with canAccessPrivateAgent before enqueueing. Add a Go regression test on the existing privateAgentTestFixture covering the plain-member, agent-owner, workspace-owner and agent-to-agent cases. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
311cf4d998 |
fix(agent): surface Codex app-server no-progress diagnostics (MUL-2688)
Refs #3262. |
||
|
|
735f18a4ef |
fix(agents): drop the import-hint callout on agent Skills tab (again) (#3301)
#3265 already removed this blue "Importing creates a workspace copy..." banner, but #3286 (the skills_local toggle revert) brought it back as collateral. Re-remove it — this tab isn't where skill imports happen (that lives behind Skills page → Add Skill → From Runtime), so the callout is pure noise here. Also flip the header row back to items-center now that the intro is once again the only thing in it. |
||
|
|
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. |
||
|
|
7e37461075 |
docs: add 2026-05-26 changelog entry (#3293)
* docs: add 2026-05-26 changelog entry Co-authored-by: multica-agent <github@multica.ai> * docs: refine 2026-05-26 changelog entry Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>v0.3.9 |
||
|
|
83ecad4789 |
MUL-2690 fix(deps): force @xmldom/xmldom to ^0.8.13 via pnpm overrides (#3290)
Pin @xmldom/xmldom to ^0.8.13 in `pnpm.overrides` so every transitive resolution (currently @expo/plist@0.5.3 and plist@3.1.0, both pulled through expo) ships a patched build. All four lockfile entries move from 0.8.12 to 0.8.13. Closes the four high-severity advisories pnpm audit reports against the prior 0.8.12 resolution: - GHSA-2v35-w6hq-6mfw — uncontrolled recursion in serialization (DoS) - GHSA-f6ww-3ggp-fr8h — XML injection via DocumentType serialization - GHSA-x6wf-f3px-wcqx — node injection via processing-instruction - GHSA-j759-j44w-7fr8 — node injection via comment serialization Using `pnpm.overrides` (not a root direct dep) keeps the transitive fix scoped to the dependency graph and avoids implying that the multica codebase consumes xmldom directly. Verification: `pnpm audit --prod --audit-level high` no longer lists any @xmldom/xmldom advisories on this branch. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
30a7da6d10 |
feat(list): drag-and-drop reordering + loadMore sort fix (#3284)
* feat(list): drag-and-drop reordering + shared drag utils - List view drag-and-drop: reorder within/across status groups, drop into collapsed groups, DraggableListRow with useSortable + checkbox protection - Extract shared drag utilities (computePosition, findColumn, buildColumns, makeKanbanCollision, issueMatchesGroup, getMoveUpdates) to drag-utils.ts - Reuse IssueDisplayControls in MyIssues header (dedup ~380 lines) - Radio check marks for grouping/sort dropdowns - Remove outer p-1 wrapper, unify board view to p-2 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(my-issues): move MyIssuesHeader inside ViewStoreProvider IssueDisplayControls calls useViewStore which requires the provider context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(header): restore swimlane option in view toggle dropdown Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
057cb2ff34 |
fix(issues): thread sort through load-more so appended pages render (MUL-2678) (#3279)
The recent server-side-sort change (#3228) keyed the issue-list cache by sort but did not update the load-more hooks: useLoadMoreByStatus used a prefix-match that could pick a stale cache variant, and neither hook forwarded sort to the API request. As a result, scroll-to-load-more fired its request, but the response was either appended to a cache no useQuery was subscribed to, or it appended rows in an unsorted order into a sorted bucket. Pass `sort` explicitly through Board/List/Swimlane and into the hooks. The hook now targets the full sorted key via setQueryData and forwards sort to the listIssues / listGroupedIssues calls so the appended page lines up with the existing items. Also adds focused tests for both load-more hooks: stale-sort cache is untouched, sort is forwarded to the API, and sort-less callers still hit the {} key path used by actor-issues-panel. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0b50c5a209 |
feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)
* feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) Only Claude Code and Codex runtimes actually enforce `skills_local` at exec time today — Claude isolates `~/.claude/skills/` via `CLAUDE_CONFIG_DIR`, Codex isolates `~/.codex/skills/` via per-task `CODEX_HOME`. Every other runtime currently stores the field but treats it as a no-op, which made the toggle in the Create Agent dialog and Skills tab misleading for those runtimes. Gate the toggle on `runtime.provider` so it only renders for the providers the daemon currently isolates. Centralise the supported-provider list as `isSkillsLocalSupportedProvider()` in `packages/core/agents` and reuse it from the create dialog and the Skills tab. The create dialog also drops `skills_local` from the payload when the selected runtime is unsupported, so a runtime swap can't leave a stale `ignore` opt-in pinned where it would never take effect. Docs (EN + ZH) updated to say the toggle is hidden — not just "a no-op" — for the unsupported runtimes. Co-authored-by: multica-agent <github@multica.ai> * docs(agents): align skills_local hint and type comment with claude+codex boundary Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
46f7ac6bac |
docs(self-host): document task_usage_hourly rollup requirement (MUL-2682) (#3280)
The Usage / Runtime dashboards read from `task_usage_hourly`, but the default self-host stack does not schedule `rollup_task_usage_hourly()` anywhere — the bundled pgvector/pgvector:pg17 image ships without pg_cron, and the backend does not run the rollup in-process. Fresh installs see the dashboard stay at zero forever (#3244), and upgrades from v0.3.4 → v0.3.5+ are blocked by migration 103's fail-closed guard (#3015). Document the three supported paths (external cron / systemd-timer / CronJob, Postgres with pg_cron, or backfill_task_usage_hourly for upgrades) across SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, the quickstart pages on the docs site, and add troubleshooting entries for both the silent-zero and the migration-guard failure modes. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
91506e7f7b |
refactor(cli): rename daemon status helper and align value column (MUL-2676) (#3275)
- Rename printDaemonStatusTable -> printDaemonStatusReport. The helper emits a key/value list, not a table; the old name implied a tabular layout that never existed and made the call site read wrong. - Align the value column dynamically off the widest key. Previously the spacing was hard-coded so the static rows (Version/Agents/Workspaces) all landed at column 14, but the dynamic "Daemon [profile]" label could outgrow that and push only its own value rightward, breaking vertical alignment as soon as a profile was active. - Add negative coverage for cli_version absent / empty (the real back-compat contract for older daemons paired with a newer CLI) and a test that asserts the value column lines up under a long profile label. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
382e294e8c |
Show CLI version in daemon status (#3212)
Co-authored-by: Coresen <158120130+iCoresen@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
65ed5744a6 |
feat: add issue view swimlane (MUL-2607) (#3149)
* feat: Add swimlane view * refactor * test: add tests * wip * wip * wip * refactor * refactor and fixes * refactor * fix: parent empty cells * chore: Remove meanless comments * remove meanless comments |
||
|
|
77c7267105 |
fix(server): trigger assignee on agent-driven backlog→active (MUL-2670) (#3270)
* fix(server): trigger assignee on agent-driven backlog→active (MUL-2670) The backlog→active transition was gated on `actorType == "member"`, which silently dropped agent-driven promotions and broke the documented serial sub-task workflow — a parent agent finishing Step 1 and promoting Step 2 from backlog→todo would never fire Step 2's assignee. Replace the member-only gate with a self-promotion guard. Agent actors now fire the same enqueue path as members; the only excluded case is an agent promoting an issue assigned to itself (which would self-loop on every run). Applied to both UpdateIssue and BatchUpdateIssues. Adds two integration tests covering the documented serial-chain case and the self-loop guard. Co-authored-by: multica-agent <github@multica.ai> * fix(server): scope backlog→active self-loop guard to the calling task's issue The previous agent-id-only guard over-blocked same-agent serial chains: if Agent A finished a task on issue I1 and promoted issue I2 from backlog→todo, the promotion was silently dropped whenever I2 was also assigned to A. Only the cross-agent handoff worked. Replace the actor-vs-assignee check with a task-vs-issue check: isAgentRunningOnIssue looks up the calling X-Task-ID and only blocks when that task's issue_id matches the issue being promoted (the true self-loop). Member actors and same-agent cross-issue promotions now fire, including via BatchUpdateIssues. Tests: - TestBacklogToTodoByAgentSameIssueDoesNotSelfTrigger (true self-loop) - TestBacklogToTodoByAgentSameAgentDifferentIssue (serial chain works) - TestBatchBacklogToTodoByAgentTriggersAssignee (batch path) - TestBacklogToTodoByAgentTriggersSquadLeader (squad branch) Co-authored-by: multica-agent <github@multica.ai> * test(server): seed running task in handler test helper to avoid collisions createHandlerTestTaskForAgentOnIssue inserted with status='queued', which broke two tests added by the same-issue self-loop guard: - TestBacklogToTodoByAgentSameIssueDoesNotSelfTrigger asserted `count(*) WHERE status='queued'` was 0, but the seeded task itself showed up in the count → got 1. - TestBacklogToTodoByAgentSameAgentDifferentIssue seeded a task for the same (issue_id, agent_id) as step1's auto-enqueued queued task, tripping idx_one_pending_task_per_issue_agent. X-Task-ID semantically belongs to a currently-running task. Inserting the seed with status='running' (and started_at=now()) keeps it outside both the unique index and the queued-count assertions, so the tests verify only what the handler does in response to the agent-driven backlog→active promotion. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
88fe6d754f |
docs(cli): list valid statuses in issue status --help (#3239)
multica issue status --help only documents <status> as a required positional. Users have to discover the valid set via trial-and-error (triggering 'Error: invalid status "X"; valid values: ...'). Add a Long description that lists the 7 valid statuses inline: backlog, todo, in_progress, in_review, done, blocked, cancelled. Pure docs change; no behavior changes. Co-authored-by: Wington Brito <4412238+wingtonrbrito@users.noreply.github.com> |
||
|
|
e6324aad6c |
fix(server/child-done): require sibling dependency check before promoting backlog (MUL-2618) (#3272)
The previous system-comment wording ("promote any waiting `backlog`
sub-issues") let a planner agent flip every backlog sibling to `todo` on
the first child-done signal, ignoring per-sibling stated dependencies.
Tighten the prompt so the agent must read each sibling's description,
only promote items whose dependencies are satisfied, and leave the
status alone (and comment to confirm) when the parent's higher-level
breakdown conflicts with what a sibling lists as a prerequisite.
This is the short-term mitigation; a structured `blocked_by` edge is
out of scope here and will be designed separately.
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> |
||
|
|
d8075a5775 |
fix(agents): tighten skills-tab intro and drop redundant import hint (#3265)
Three small UI cleanups on the agent Skills tab: - The blue "Importing creates a workspace copy that your team can edit and reuse" callout was visual clutter — drop it (and the Info icon import that it relied on). - The intro paragraph conflated two things: the workspace-skills concept (applies to every runtime) and the Allow-locally-installed-skills toggle (only honoured by Claude Code and Codex; verified — none of copilot/cursor/gemini/opencode/openclaw/hermes/pi/kimi/kiro read agent.SkillsLocal). Rewrite the intro to only describe the main concept; the toggle's own local_hint_on/off strings still carry the Claude/Codex caveat where it belongs. - The trimmed intro now fits one line, so flip the header row from items-start to items-center so the text sits on the same baseline as the "Add skill" button instead of clinging to its top edge. |
||
|
|
6bc3d14eb3 |
fix(daemon/execenv): refresh stale Codex config copies across env reuse (MUL-2646) (#3268)
* fix(daemon/execenv): refresh stale Codex config copies across env reuse (MUL-2646)
`copyFileIfExists` previously short-circuited whenever the per-task
`codex-home/{config.toml,config.json,instructions.md}` already existed,
so once the files were seeded at first Prepare they were never refreshed
again — even though `Reuse()` calls `prepareCodexHomeWithOpts` on every
resume. A user who rotated their Codex `~/.codex/config.toml` between
runs (e.g. switching the active `[model_providers.X]` `base_url`, or
pointing `env_key` at a freshly rotated API key) kept reading the stale
per-task copy on session resume. Codex then issued requests to the new
URL using the old key and the API rejected the token.
Treat any existing `dst` as something to drop and re-copy from the
current shared source, mirroring the symlink path that already refreshes
`auth.json` (#2126). The daemon-managed sandbox / multi-agent / memory
blocks are applied via marker-bracketed idempotent passes after the
copy, so a re-copy + re-ensure cycle preserves them.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon/execenv): drop per-task Codex copy when shared source removed (MUL-2646)
Extend the MUL-2646 fix to the deletion arm of "sync the shared source":
`syncCopiedFile` (renamed from `copyFileIfExists`) now also removes the
per-task `dst` when the shared `src` is absent. The prior version
short-circuited on missing src and left `config.toml` / `config.json` /
`instructions.md` from the previous Prepare lingering in the per-task
home — so a user who removed a provider by deleting `~/.codex/config.toml`,
or pulled `config.json` / `instructions.md` out of the shared home, would
keep replaying the stale copy on session resume.
For `config.toml` the subsequent `ensureCodex{Sandbox,MultiAgent,Memory}Config`
passes recreate the file with only the daemon-managed default blocks, so
removing the shared file cleanly drops every user-managed
`[model_providers.X]` / `model_provider` line. For `config.json` and
`instructions.md` there is no daemon default, so they disappear in
lockstep with the shared source.
Adds `TestPrepareCodexHome_DropsCopiedConfigWhenSharedSourceRemoved`
covering the new path, and extends the refresh-arm test to assert the
multi-agent / memory marker blocks are still present after the copy is
refreshed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
612ac8f28e |
feat(issues): server-side sort + fix drag position corruption (#3228)
* refactor(editor): split rich text styles * feat(issues): server-side sort + fix drag position corruption in non-manual sort Backend: ListIssues and ListGroupedIssues now accept `sort` and `direction` query params (position/priority/title/created_at/start_date/due_date). ListIssues converted from sqlc to hand-written SQL for dynamic ORDER BY. Priority sort uses CASE expression for semantic ordering. Frontend: query keys include sort so changing sort triggers server refetch. Client-side sortIssues() removed from board-view and list-view. Drag-and-drop: non-manual sort disables within-column reorder (prevents silent position corruption). Cross-column drag only updates status/assignee, preserves original position. Column overlay shows current sort during drag. Cache: query key split into prefix (list) for invalidation and full key (listSorted) for queryOptions. All optimistic update paths use prefix matching via getQueriesData to work with any active sort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(board): prevent drag flicker by settling columns until mutation refetch After drag-and-drop, the optimistic cache patch updates position values without reordering the bucket array. The useEffect that rebuilds columns from TQ data would overwrite the correct local drag order, causing cards to snap back then forward. Fix: isSettlingRef blocks column rebuilds between drag end and mutation onSettled. Also invalidate issueKeys.list on WS position changes so other windows refetch correctly sorted data instead of showing stale bucket order. Includes debug logs (to be removed after verification). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(board): stabilize drag-and-drop for non-manual sort modes Three behavioral fixes for board drag when sort != position: 1. Settling: isSettlingRef + settleVersion blocks column rebuilds between drag-end and mutation settle, preventing the optimistic cache patch (which updates position values without reordering the bucket array) from overwriting the correct local column state. 2. Non-manual cross-column: handleDragOver returns prev (no visual card movement — column highlight + sort label is sufficient). handleDragEnd uses overCol directly instead of findColumn on the card's current position (which would be the source column). Cards use useSortable({ disabled: { droppable: true } }) to suppress within-column insertion indicators. 3. Collision detection: when no card droppables exist (disabled in non-manual sort), return column droppables from pointerWithin instead of falling through to closestCenter, so isOver reflects the column the pointer is actually inside. Also: WS position changes now invalidate issueKeys.list so other windows refetch correctly sorted data. Insertion-position prediction intentionally omitted — PostgreSQL's en_US.utf8 collation (glibc) cannot be faithfully replicated in JavaScript (ICU/V8), and an inaccurate indicator is worse than none. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort): manual sort ignores direction param on both ends Manual sort (position) is user-defined order via drag-and-drop — reversing it has no product meaning. Backend: sort=position now skips the direction query param and always uses ASC. Both ListIssues and ListGroupedIssues handlers. Frontend: sort object omits sort_direction when sortBy is position. Direction toggle hidden in the display popover for manual mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(board): memo columns + stabilize references to reduce re-renders - BoardColumn, PaginatedBoardColumn, PaginatedAssigneeBoardColumn wrapped in memo() — only columns with changed props re-render - IssueAgentActivityIndicator wrapped in memo() — 111 snapshot subscribers no longer trigger full re-render on every WS task event - buildColumns rewritten from O(groups × issues) to single-pass O(n) - EMPTY_IDS constant replaces ?? [] fallbacks (stable reference) - EMPTY_CHILD_PROGRESS constant replaces new Map() default - BOARD_COL_WIDTH / BOARD_CARD_WIDTH constants shared between column and DragOverlay for consistent card dimensions - issueListOptions + issueAssigneeGroupsOptions use placeholderData: keepPreviousData so sort/filter changes don't flash a full-page skeleton - Loading skeleton scoped to content area only — header stays rendered during data transitions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: remove outdated server-side sort implementation plan Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
312ee29cb6 |
fix(views): always show assignee meta row on board cards when assignee property is enabled (#3250)
When no assignee was set, the entire meta row (assignee + dates + child progress) could disappear because showAssignee required both storeProperties.assignee AND issue.assignee_id. Now the row visibility depends only on storeProperties.assignee, and unassigned cards show "Unassigned" text with a clickable picker to assign. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ec1589f7b6 |
fix(deps): add eslint phantom dep detection + fix existing violations (MUL-2654) (#3249)
* fix(deps): add eslint phantom dep detection + fix existing violations (MUL-2654) Introduce eslint-plugin-import-x/no-extraneous-dependencies rule to prevent phantom deps from causing production build splits when pnpm creates peer-dep variants. Fix all existing phantom deps across the monorepo, unify catalog references, and enable desktop smoke CI on PRs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * revert(ci): remove desktop smoke PR trigger per user feedback The existing smoke workflow only verifies packaging completes — it does not actually start the app or check rendering. This means it wouldn't have caught the white-screen bug (which was a runtime issue, not a build failure). Adding it to PRs would slow CI without providing meaningful protection. The ESLint no-extraneous-dependencies rule is the actual prevention mechanism. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(deps): sync pnpm-lock.yaml for rehype-sanitize dep classification Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): move rehype-sanitize to deps + declare eslint-config (MUL-2654) - Move rehype-sanitize from devDependencies to dependencies (used in production Markdown.tsx) - Add @multica/eslint-config to devDependencies (imported by eslint.config.mjs but previously undeclared) 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> |
||
|
|
e33b893c3f |
feat(views): sticky group headers in list view (#3221)
* feat(views): add sticky positioning to list-view group headers Group headers now stay pinned at the top of the scroll viewport so users always know which status group they are looking at. Background changed from semi-transparent to opaque to prevent content bleeding through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): remove top padding from list-view scroll container for sticky headers The `p-2` padding on the scroll container caused an 8px gap above sticky group headers. Replace with `px-2 pb-2` to keep horizontal and bottom padding while allowing headers to stick flush to the top. Sync skeleton containers in issues-page and my-issues-page to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): use p-2 pt-0 instead of px-2 pb-2 for list-view scroll container Reporter preferred adding pt-0 to override the top padding from p-2, keeping the original p-2 shorthand intact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): opaque sticky header hover + cursor-pointer on trigger Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c5b4c45e41 |
fix(chat): unify expand and drag-to-max rendering (MUL-2653) (#3235)
* fix(chat): unify expand and drag-to-max rendering so both produce same dimensions (MUL-2653) Expand button used CSS `inset-3` (parent minus 24px each side) while drag-to-max used explicit 90%-of-parent pixel dimensions — different sizes for the same conceptual state. Expand also hid resize handles, preventing drag-back. Now both paths render with explicit width/height at bottom-right and resize handles stay visible in all states. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): animate width/height via framer-motion for smooth expand toggle (MUL-2653) Move width/height from style prop into animate prop so framer-motion interpolates size changes. Remove layout="position" which only tracked position. Drag uses duration:0 for instant feedback. 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> |
||
|
|
e351f89843 |
fix(desktop): declare phantom deps + dedupe react-query in renderer (MUL-2651) (#3232)
Packaged renderer was bundling two copies of @tanstack/react-query because apps/desktop imported it without declaring the dep, so Node resolution fell through to the hoisted root variant (react@19.2.0, pulled in by apps/mobile), while packages/core resolved to the catalog variant (react@19.2.3). Two physical paths → two QueryClientContexts → "No QueryClient set" white screen on launch. - Declare @tanstack/react-query, lucide-react, zustand as direct deps via catalog: so apps/desktop resolves to the same peer variant as packages/core/views. - Add @tanstack/react-query to renderer dedupe as a defense-in-depth bound against future peer drift. Verified: realpaths under apps/desktop, packages/core, packages/views all point to @tanstack+react-query@5.96.2_react@19.2.3; production renderer bundle now contains exactly one "use QueryClientProvider to set one" string (was 2) and no useQueryClient\$1 suffix. Co-authored-by: multica-agent <github@multica.ai>v0.3.8 |
||
|
|
bf1e375015 |
fix(docker): copy source.config.ts into web deps stage (MUL-2649) (#3227)
apps/web postinstall runs fumadocs-mdx, which reads apps/web/source.config.ts. The deps stage only copied package.json files, so `pnpm install --frozen-lockfile` failed with "Could not resolve /app/apps/web/source.config.ts" and blocked the GHCR multica-web image build in the v0.3.7 release. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
636fa1adb4 |
fix(issues): hide activity-block header while only recent entries are shown (#3226)
In the trailing activity block's default truncated state ("last 8 shown,
N older hidden"), we were rendering two stacked chevron rows: a "v N
activities" collapse header and a "> Show N more activities" reveal link.
Visually that looked like nested folds even though they're parallel
controls, and the header is redundant when the user just wants a glance
at recent activity.
Drop the header in the truncated default state. It reappears the moment
the user clicks "Show N more" — at that point they're seeing the full
block and a fold-back affordance becomes useful again. Blocks that fit
within the 8-entry limit (and non-trailing blocks, which never truncate)
keep their header as before.
|
||
|
|
441fa18db4 |
feat(issues): truncate trailing activity block to most recent 8 (MUL-2628) (#3219)
* feat(issues): truncate trailing activity block to most recent 6 (MUL-2628) The trailing activity block defaults to expanded, but a block with dozens of entries still drowns the comment area. Show only the most recent 6 by default; older entries fold behind an in-place "Show N more activities" toggle. Non-trailing blocks are unchanged — they still collapse whole. The "show older" choice is tracked per block id in a separate Set so it survives the block losing its trailing position (when a new comment lands after it) and survives a collapse/re-expand cycle. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): bump trailing activity block visible limit from 6 to 8 (MUL-2628) User feedback on the original PR: 6 felt slightly too tight. Bumped the trailing-block truncation threshold to 8 entries to give the "most recent activity" view a bit more headroom before older entries fold behind the "Show N more activities" toggle. Test count is unchanged; the existing trailing-block / non-trailing-block truncation cases were adjusted to exercise the new 8-entry boundary (10-entry trailing block → 2 hidden; 8-entry trailing block → none hidden; 10-entry non-trailing block → all visible after expand). 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>
|