Compare commits

..

77 Commits

Author SHA1 Message Date
Lambda
11da9b07da feat(issues): add 'Open in new tab' to the issue actions menu (MUL-2933)
Desktop-only kebab/context-menu item that opens the issue in a new tab
via the existing navigation.openInNewTab adapter primitive. Gated on
isDesktopShell() && navigation.openInNewTab so it never renders on web/
mobile, and hidden when the target path equals the current path so the
issue's own detail kebab doesn't offer a self-referential no-op (list
rows, on a different path, still show it).

Adds the open_in_new_tab string across en/ja/ko/zh-Hans.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 18:02:22 +08:00
Jiayuan Zhang
580ad5b492 fix(issues): validate and clamp limit/offset in ListIssues (MUL-2847) (#3585)
* fix(issues): validate and clamp limit/offset in ListIssues (MUL-2847)

ListIssues parsed the limit and offset query params but never validated
them, so:
  - GET /api/issues?limit=-1  -> HTTP 500 (Postgres rejects negative
    LIMIT with SQLSTATE 2201W)
  - GET /api/issues?limit=100000000 -> unbounded read in a single
    response
  - GET /api/issues?offset=-1 -> same 500

SearchIssues and ListGroupedIssues already apply v > 0 + an upper clamp
on limit and v >= 0 on offset. This brings ListIssues to the same
pattern: ignore non-positive limit (keep default 100), clamp to 100,
ignore negative offset (keep default 0). default == clamp == 100 keeps
existing callers' behavior identical and matches the upstream issue
suggestion.

TestListIssues_LimitValidation seeds 3 issues in a dedicated project
and pins the nine boundary cases (negative/zero/huge/non-numeric
limit, negative/non-numeric offset, the clamp boundary, and explicit
small/positive-offset sanity) plus two sanity checks that an explicit
small limit and a positive offset are honored.

Fixes MUL-2847 / upstream multica-ai/multica#3563.

Co-authored-by: multica-agent <github@multica.ai>

* test(issues): strengthen LimitClamp test and fix comments (MUL-2847)

Address review feedback from @Lambda and @Emacs on PR #3585:

1. The 3-row set in TestListIssues_LimitValidation can't distinguish
   'clamp fired' from 'clamp missing': with only 3 rows, limit=100000000
   returns 3 rows whether or not the clamp exists. Split the clamp
   behavior into a new TestListIssues_LimitClamp that seeds 101 issues
   and asserts len(issues) == 100 for limit=100/101/200/100000000, plus
   limit=50 honored below the clamp. Without the clamp line, the
   huge/above-clamp subtests would fail with len == 101.

2. Fix the misleading comment that claimed 'limit=0 -> same 500'.
   Postgres LIMIT 0 is valid SQL and returns zero rows. The guard
   exists for sibling-consistency (SearchIssues / ListGroupedIssues
   already treat v <= 0 as 'use default'), not to avoid a 500. Move
   the limit=0 case out of TestListIssues_LimitValidation since it's
   not 500-related; TestListIssues_LimitClamp's 'no limit returns
   default page of 100' subtest pins the default behavior anyway.

3. Add a subtest that pins the offset+clamp composition
   (limit=200&offset=50 against 101 rows = 51 rows), proving the
   clamp caps the page size while offset still indexes the full
   result set.

4. Fix gofmt: the original file's leading-bullet comment indentation
   was off by two spaces; gofmt -l now reports clean.

All 14 subtests across both functions pass; full ./internal/handler/
suite still passes (3.2s).

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:17:41 +08:00
Jiayuan Zhang
f539fdba83 feat(onboarding): backfill prompt for missing source attribution (MUL-2796) (#3550)
* feat(onboarding): backfill prompt for users missing source attribution

Adds a one-shot popup shown after login to already-onboarded users
whose `onboarding_questionnaire.source` was never recorded — either
they completed onboarding before the source step shipped, or they
clicked Skip on it. Reuses the existing 12-option StepSource UI and
the existing `PATCH /api/me/onboarding` endpoint, so no schema or
backend changes.

Web renders it as a route at /onboarding/source (sibling of the
reserved /onboarding); desktop dispatches it as a WindowOverlay per
the Route categories rule. Submit and explicit Skip are terminal;
the close X bumps a per-user localStorage counter and stops appearing
after 3 dismissals.

Emits source_backfill_shown / submitted / skipped / dismissed PostHog
events so the funnel can be tracked separately from first-time
onboarding.

For MUL-2796.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): preserve role/use_case and respect dismiss cap in source backfill

Round-2 fixes from Emacs's review of #3550:

1. PATCH wipe: `PATCH /api/me/onboarding` replaces the JSONB column
   wholesale (server/internal/handler/onboarding.go), so sending only
   the source slots was wiping role/use_case/version for exactly the
   historical users this targets. Read user.onboarding_questionnaire,
   overlay the source fields client-side via mergedQuestionnairePatch,
   and send the full shape. 7 unit cases cover the merge semantics.

2. Legacy single-string source: pre-multi-select rows wrote
   `source: "search"` as a bare string. needsSourceBackfill now treats
   that as already answered, matching mergeQuestionnaire (views) and
   stringOrSlice.UnmarshalJSON (server). Flipped the existing test and
   added empty-string + null coverage.

3. Dismiss cap honored in callback: the web auth callback was passing
   dismissCount=0, which would force-route capped users through
   /onboarding/source on every login (the route page would bounce them
   onward, but only after a blank detour and a re-fired
   `source_backfill_shown` event). Added readSourceBackfillDismissCount
   so the callback reads the same per-user localStorage bucket the
   prompt writes to. Test asserts a count of 3 bypasses the detour.

Co-authored-by: multica-agent <github@multica.ai>

* test(onboarding): clear source-backfill dismiss counter in callback test beforeEach

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): footer hint text matches the Submit button on the backfill prompt

The Source step's hint reads "Hit Continue when you're ready" because
its commit button is "Continue". The backfill view ships a "Submit"
button instead, so the inherited hint was misleading. Add a dedicated
`source_backfill.hint_ready` key across en / zh / ko and use it here.

Caught during browser E2E in the round-2 verification stack.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): magic-code login also detours through source backfill

The round-2 fix in PR #3550 only wired the source-backfill detour
into the OAuth `/auth/callback` post-success path. Magic-code login
goes through `/login` → `handleSuccess()` which calls
`resolveLoggedInDestination()` and pushes directly to the workspace,
so those users never reach `/onboarding/source`. Caught during the
local-env demo for Jiayuan.

Add `maybeSourceBackfillDetour` to the login page and apply it in
both the already-authenticated useEffect and the post-verify-code
handler. Predicate consults the same per-user localStorage bucket
the prompt writes to, so a user who hit the close-X cap on this
browser flows straight through.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(onboarding): source backfill is a workspace-mounted modal, not a route detour

Per UAT, the prompt should overlay the workspace as a Dialog with the
workspace visible behind a dimmed backdrop — the original brief and
reference screenshot both showed a modal. PR #3550 shipped a full-window
takeover (web /onboarding/source + desktop WindowOverlay) which Jiayuan
rejected.

This commit replaces the full-window view with a Dialog-based
`<SourceBackfillModal />` mounted once inside the shared `DashboardLayout`
(packages/views/layout). The modal self-mounts: it reads
`needsSourceBackfill(user, dismissCount)` and opens itself when the
predicate flips to true; X / ESC / outside-click all bump the per-user
localStorage cap and close.

Removed:
- apps/web/app/(auth)/onboarding/source/page.tsx (route)
- paths.sourceBackfill (no longer needed)
- callback page detour
- login page maybeSourceBackfillDetour
- desktop WindowOverlay type "source-backfill"
- desktop navigation interception of /onboarding/source
- desktop App.tsx dispatch effect
- pageview-tracker case
- views/onboarding `SourceBackfillView` + `readSourceBackfillDismissCount` exports

Preserved (semantics unchanged):
- `needsSourceBackfill` predicate (incl. legacy single-string source coercion)
- `mergedQuestionnairePatch` so role / use_case survive Submit / Skip
- PostHog events: source_backfill_shown / submitted / skipped / dismissed
- Per-user dismiss-count cap (3) in localStorage
- en / zh / ko i18n strings

Tests:
- 7 new tests for the modal in packages/views/onboarding/source-backfill-modal.test.tsx
- Adjusted apps/web/app/auth/callback/page.test.tsx: detour tests dropped,
  one assertion remains that onboarded users with missing source land in
  the workspace (the modal handles the rest)
- Full suite: 965 tests pass, typecheck + lint clean

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): mount source-backfill modal on the desktop workspace too

Desktop's WorkspaceRouteLayout never wraps DashboardLayout, so the
previous commit's modal mount only fired for web. Regression: desktop
users were not seeing the prompt at all.

Wire the same `<SourceBackfillModal />` next to `<WelcomeAfterOnboarding />`
inside `workspace-route-layout.tsx`, with the matching
`!overlayActive` suppression so the Dialog doesn't portal-jump above
an active pre-workspace WindowOverlay (onboarding / accept-invite /
new-workspace). Same component on both platforms — single source of
truth lives in packages/views/onboarding/source-backfill-modal.tsx.

Also drop the now-stale `source-backfill detour` comment in the web
callback test fixture (Emacs nit, non-blocking).

Co-authored-by: multica-agent <github@multica.ai>

* test(desktop): assert workspace-route-layout mounts source-backfill modal

Two structural tests pinning the round-4 fix:

- `mounts SourceBackfillModal when no WindowOverlay is active` —
  guards against the regression Emacs caught (modal silently absent
  on desktop because the previous round only wired DashboardLayout).
- `suppresses SourceBackfillModal while a WindowOverlay is active` —
  mirrors the existing `!overlayActive` rule that WelcomeAfterOnboarding
  already relies on so a portal-rendered Dialog can't visually outrank
  an active pre-workspace overlay.

Mocks the SourceBackfillModal with a marker component so the test
asserts mount/unmount without depending on the modal's own predicate
gate.

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): backfill modal Other toggles off; entrance settles after 700ms

UAT round-3 follow-ups from Jiayuan:

1. **Other can't be deselected**: the modal kept a parallel
   `pendingOther` flag set to true on every Other click, and
   `IconOtherOptionCard`'s row click was guarded with
   `if (!selected) onSelect()` — so a second click neither flipped
   pendingOther nor reached the parent toggle. Drop `pendingOther`
   (the `source.includes("other")` derivation is already authoritative)
   AND add an opt-in `allowToggleOff` prop to `IconOtherOptionCard`
   that lets the row toggle when already selected. The text input
   stops click propagation so typing never deselects.

2. **Rebase + absorb GitHub channel**: rebased onto origin/main which
   added `social_github` (PR #3612). Modal's option list now mirrors
   StepSource — GitHub slotted between YouTube and Other social,
   reusing the existing `GitHubIcon`.

3. **Soft entrance**: defer the dialog open by 700ms after the user
   lands on a workspace so the underlying view paints first and the
   modal feels like an inviting prompt rather than a hard block.
   Honour `prefers-reduced-motion: reduce` (open immediately for
   users who have opted out of incidental motion).

Tests:
- New `Other toggles off on the second click instead of getting stuck`
- New `renders the GitHub channel rebased from origin/main`
- New `defers the entrance by ~700ms when the user has not opted into
  reduced motion`
- Existing tests stamp `prefers-reduced-motion: reduce` in beforeEach
  so the dialog opens synchronously and they don't need to drive
  fake timers.

Full suite passes (969 tests).

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): backfill modal opens reliably + Other deselects via icon area

Three follow-up fixes after live UAT:

1. Strict-mode regression on entrance delay: the gate ref was being
   stamped when the effect *scheduled* the timer, so React Strict
   Mode's double-invoke cleared the first timer and then bailed on
   the second pass because the ref was already set, leaving the
   dialog forever closed. Stamp the ref only inside the timer
   callback (or synchronously when reduced-motion is on) so the
   second strict pass starts a fresh timer.

2. Other deselect: dropping `pendingOther` wasn't enough — the input
   that replaces the label when Other is selected was previously
   stopping click propagation, so a re-click on the row never
   reached the toggle. Remove `e.stopPropagation()` and instead let
   the row's onClick ignore clicks whose target IS the input
   (typing / focusing the input still doesn't deselect; clicks on
   the icon, padding, or border do).

3. Tests: drive the Other re-click via Playwright `click({position:
   {x:24,y:24}})` so the click lands on the icon area instead of the
   center of the input, matching real-user behaviour.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(onboarding): source picker is single-select primary source

Per Jiayuan's call after the survey of HDYHAU UX in PLG SaaS (Linear /
Vercel / Loom / Notion / Webflow / Stripe / Figma / Cursor / PostHog
mostly skip the question entirely; where it's asked the documented
default — Fairing / Recast / HockeyStack / Ruler Analytics — is to
capture the primary source so channel weights sum to 100% and ROI
math is defensible).

Modal + StepSource both pivot from multi-select to single-select
radio. Server schema is intentionally untouched: `source` stays
`string[]` for back-compat with v2 multi-select rows; the client
always sends a one-element array. Zero migration, zero data loss.

Frontend:
- `source-backfill-modal.tsx`: state pivots from a multi-element
  `source: Source[]` to a single `pickedSlug` derived from
  `source[0]`; click handler replaces the array instead of toggling.
  Cards switch to `mode="radio"`, the fieldset gets `role="radiogroup"`,
  the now-redundant `pendingOther` and `allowToggleOff` opt-in go
  away — radio mode means no toggle-off, so the original UAT bug
  ("Other can't be deselected") is structurally impossible.
- `step-source.tsx`: drop the `multiSelect` prop so it routes
  through `step-question.tsx`'s existing radio path (same one
  StepRole already uses). Picking a second option replaces the
  first; switching away from Other clears `source_other` so a stale
  value can't leak.
- `icon-option-card.tsx`: revert the `allowToggleOff` plumbing.

Tests:
- `source-backfill-modal.test.tsx`: drop the multi-select toggle-off
  assertion; add "picking a second option replaces the first" with
  explicit radio-role queries.
- `step-source.test.tsx`: rewrite multi-select tests as single-select
  (no more "stacks several picks" / "toggle off" cases); add
  "switching away from Other clears source_other".

Full suite (970 tests) green, typecheck + lint clean.

Co-authored-by: multica-agent <github@multica.ai>

* docs(onboarding): refresh stale multi-select comments around source

Comment-only follow-up to the single-select refactor in d14f9d09f.
Five docblocks still described `source` as multi-select; they now
correctly say single-select and explain the array shape is kept
purely for v2 back-compat with the JSONB column.

- packages/core/onboarding/types.ts — QuestionnaireAnswers docblock
- packages/core/onboarding/store.ts — PostHog mirror comment
- packages/views/onboarding/steps/step-question.tsx — header docblock,
  canContinue branch, and footer-hint comment (Source moves from the
  multi-select side to the single-select side; Use case stays as the
  remaining multi-select consumer)
- server/internal/handler/onboarding.go — questionnaireAnswers docblock
  and the stringOrSlice fall-back comment (the column "going multi-
  select" is no longer the current state; rename to "pre-array shape")
- server/internal/analytics/events.go — OnboardingQuestionnaireSubmitted
  docblock

No behaviour changes. Tests + Go build still green.

Co-authored-by: multica-agent <github@multica.ai>

* i18n(onboarding): add ja translations for source-backfill keys

The Japanese locale landed on main (PR #3538) after this branch
started, so my source-backfill round-2 keys (`common.close`,
`source_backfill.eyebrow / lede / submit / hint_ready`) never made
it into ja and the parity test fails in CI. Add them now with
translations that match the en/zh-Hans/ko wording and tone.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 10:07:36 +02:00
Qi Yijiazhen
91c1e51411 feat(editor): add / slash-command palette for invoking agent skills (#3159)
* feat(editor): add / slash-command palette for invoking agent skills

Adds a `/` trigger in the chat box that opens a popover listing the active
agent's skills. Selecting an item inserts a `[/label](slash://skill/<id>)`
token; the daemon extracts those IDs in `buildChatPrompt` and emits an
"Explicitly selected skills:" block using the canonical names from the
agent's skill registry — labels are display-only and never trusted.

Built on Tiptap's `Mention` extension so the suggestion lifecycle,
keyboard routing, and IME handling mirror the existing `@` mention UX.
Item list is sourced from the React Query workspace cache (no per-keystroke
fetch). Gated behind a new `enableSlashCommands` prop so only `chat-input`
opts in; other `ContentEditor` consumers (issue editor, comments) are
unaffected. Read-only markdown surfaces render the token as a `.slash-command`
pill via a custom link renderer + sanitize-schema/url-transform allowlists.

Closes #3108

* fix(i18n): add slash_command editor copy for ko/ja

The PR added slash_command popover empty-state keys to en + zh-Hans only;
locales/parity.test.ts requires every locale to cover every EN key, so ko
and ja failed CI. Add the two keys (no_skills_configured, no_results)
matching existing skill terminology (스킬 / スキル).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:57:42 +08:00
LinYushen
2348301d2b fix: gate private squad leader bypass (MUL-2860) (#3648)
* fix: gate private squad leader from being triggered by unauthorized members

Add canEnqueueSquadLeader helper that checks canAccessPrivateAgent before
allowing a squad leader to be enqueued. Gate all EnqueueTaskForSquadLeader
call sites:

1. enqueueSquadLeaderTask (comment trigger, assign trigger, backlog→todo)
2. triggerChildDoneSquad (child-done → parent squad leader)
3. autopilot.go (defensive comment; actor is always agent → always passes)

Also fix validateAssigneePair's squad branch to run canAccessPrivateAgent
on the squad leader, returning 403 'cannot assign to squad with private
leader' when the actor lacks access.

Thread actorType/actorID through notifyParentOfChildDone →
dispatchParentAssigneeTrigger → triggerChildDoneSquad so the child-done
path can enforce the private-leader gate.

Regression tests:
- Plain member blocked from create-issue to private-leader squad (403)
- Plain member blocked from update-issue to private-leader squad (403)
- Owner allowed to assign private-leader squad
- Plain member comment on squad-assigned issue doesn't trigger private leader
- Child-done by plain member doesn't trigger parent's private leader
- Agent actor can still trigger private leader via comment

Closes MUL-2860

Co-authored-by: multica-agent <github@multica.ai>

* fix: add private-leader gate to autopilot save + dispatch paths

- validateAutopilotAssignee squad branch: call canAccessPrivateAgent on
  the leader, returning 403 for unauthorized members at save time.
- service/autopilot.go: add canCreatorAccessPrivateLeader helper that
  mirrors the handler-level canAccessPrivateAgent logic (agent creators
  pass; member creators must be owner/admin or agent owner).
- Gate both dispatch paths (dispatchCreateIssue and dispatchRunOnly)
  with fail-closed check: if leader is private and creator lacks access,
  the run is skipped instead of triggering the private leader.

Regression tests:
- Plain member create autopilot to private-leader squad → 403
- Plain member update autopilot to private-leader squad → 403
- Owner create autopilot to private-leader squad → 201
- Owner-created autopilot dispatch → issue_created (positive)
- Legacy plain-member-created autopilot dispatch → skipped (fail-closed)

Co-authored-by: multica-agent <github@multica.ai>

* test: add run_only legacy private-leader squad dispatch regression test

Covers the dispatchRunOnly path explicitly, complementing the existing
create_issue dispatch test. Both dispatch branches now have direct test
coverage for the private-leader fail-closed gate.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 15:47:57 +08:00
Chener
fd1cdf1801 fix project progress cache invalidation (#3016)
Co-authored-by: chener <chener@M5Air.local>
2026-06-02 15:05:49 +08:00
Naiyuan Qing
e36f874c86 feat: add additive agent skill assignment (#3642)
* feat: add additive agent skill assignment

Co-authored-by: multica-agent <github@multica.ai>

* test: cover cross-workspace agent skill add

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 15:02:24 +08:00
Naiyuan Qing
d52c4f238f fix(desktop): contain renderer crashes (#3643)
* fix(desktop): contain renderer crashes

Co-authored-by: multica-agent <github@multica.ai>

* fix(desktop): filter renderer exit prompts

Co-authored-by: multica-agent <github@multica.ai>

* refactor(desktop): drop redundant page-level ErrorBoundary on issue detail

The whole-page <ErrorBoundary> wrapper duplicated the new route-level
errorElement (DesktopRouteErrorPage). Let render errors bubble to the
root route boundary so all detail routes are contained the same way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): add Close tab escape to route error page

Reload tab recreates the same crashing path and Go to issues is a dead
end when the issues route itself crashed. Add a Close tab action that
destroys the crashing router entirely and falls back to a sibling tab
(or a reseeded default), the only always-safe escape regardless of
which route crashed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:01:46 +08:00
LinYushen
d013a31db9 fix: escape special chars in image alt and file-card filename (MUL-2899) (#3644)
* fix: escape special chars in image alt and file-card filename during Markdown serialization

Filenames containing Markdown label characters ([, ], \, (, )) broke
the ![alt](url) and !file[name](url) syntax, causing raw Markdown to
render instead of the image/file card.

- Add shared escapeMarkdownLabel utility
- Apply escaping in file-card renderMarkdown
- Add renderMarkdown to ImageExtension for alt text escaping
- Add regression tests

Closes #3616

Co-authored-by: multica-agent <github@multica.ai>

* fix: address review — fix tokenizer regex, unescape labels, add regression tests

- Remove unused tokenizeFn (TS6133)
- Change file-card regex to (?:\\.|[^\]])* to handle escaped brackets
- Unescape labels in tokenize() and preprocessFileCards()
- Export ImageExtension for testability
- Rewrite tests: 3 describe blocks covering ImageExtension.renderMarkdown,
  file-card tokenizer round-trip, and preprocessFileCards (6 tests total)
- typecheck and vitest both pass

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 14:33:45 +08:00
Anderson Shindy Oki
1aa742053b i18n: add japanese locale (MUL-2893) (#3538)
* i18n: add japanese locale

* fix: spacing issues

* refactor

* fix(desktop): set <html lang> before paint to avoid JA Kanji font flash

Switch the documentElement.lang sync from useEffect to useLayoutEffect so
lang is committed before the first paint. Otherwise Japanese desktop users
saw one frame of Kanji rendered with the Chinese-first fallback stack before
the html[lang|="ja"] CJK override applied. Also fix the stale selector in the
HTML_LANG comment (html[lang^="ja"] -> html[lang|="ja"]).

Addresses review nits on MUL-2893.

Co-authored-by: multica-agent <github@multica.ai>

* fix(docs): tokenize the ideographic iteration mark in JA search

Add U+3005 (々) to the Japanese search tokenizer character class. It sits just
below the kana blocks, so words like 様々 / 日々 / 個々 previously dropped the
mark and split awkwardly, hurting recall.

Addresses a review nit on MUL-2893.

Co-authored-by: multica-agent <github@multica.ai>

* fix(i18n): restore ja locale parity after merging main

Merging main brought new EN strings into agents/chat/onboarding/settings/
squads that the ja bundle (authored against an older snapshot) lacked, breaking
the locales parity test. Add the Japanese translations for the new keys
(workspace logo upload, agents runtime filter, chat session-history stop
dialog, onboarding social_github, squad archived status) and drop the two
renamed chat window keys (active_group / archived_group) that EN removed in
favour of history_group.

Fixes the failing @multica/views parity.test.ts on the FE CI for MUL-2893.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 14:29:29 +08:00
marovole
baf8b215cb Fix workspace recovery for desktop and web (MUL-2894) (#3436)
* fix(workspace): recover from stale workspace state

* fix(workspace): apply review nits for recovery flow

- no-access-page: navigate via nav.replace so a browser Back doesn't
  land the user back on NoAccessPage with the dead slug
- no-access-page: refresh the stale cookie-clear comment — the recovery
  button no longer routes through `/`; the clear now guards other `/`
  entry points (manual nav, Back into `/`, fresh page load)
- tab-store: drop the redundant `as string | undefined` cast (the Set
  value is already string | undefined under TS 5.9)
- tab-store.test: cover the route-layout heal path (all stale groups
  dropped, then seed a fresh tab for a valid slug) and assert the
  dropped group's router is disposed

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 14:04:27 +08:00
Bohan Jiang
03961206ff docs(squad): correct stale "four status buckets" comments to five (#3640)
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 13:19:14 +08:00
Multica Eve
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.
2026-06-02 13:15:14 +08:00
Naiyuan Qing
a590dd9a22 fix: apply working filter on project issues (#3631)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 13:07:12 +08:00
YYClaw
a6b83fef41 fix(agents): surface archived status for retired agents (#3608)
Retired agents (agent.archived_at set) previously read as offline across
the agent dot, hover card, detail badge, and squad member list — a
leftover online runtime row could even make them look reachable. Add a
dedicated archived presence/status that wins over every runtime/task
signal so a retired agent never reads as live or merely offline.

- Add archived to AgentAvailability and SquadMemberStatusValue unions
- Short-circuit deriveAgentPresenceDetail before runtime/task scan
- Backend deriveSquadMemberStatus returns archived instead of offline
- Render gray Archive dot/label; skip workload + reassign affordances
- en/ko/zh-Hans locale strings
2026-06-02 13:03:15 +08:00
Jonathan Barket
7e13e695ef docs: update --mode note to include run_only (PR #2360) (#3628)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 12:39:51 +08:00
xiaoyue26
27945727af fix(realtime): invalidate chat/labels/invitations queries on WS reconnect (#3570)
Backfill the missing query invalidations (chat / labels / invitations) in invalidateWorkspaceScopedQueries, so those lists refresh on WS reconnect and workspace switch instead of showing stale data until a manual refresh.

Adds tests covering invalidation on ws-instance change and actor_type passing to event handlers.

MUL-2882
2026-06-02 12:32:03 +08:00
Naiyuan Qing
b72434da68 refactor(chat,issues): unify hover-swap row pattern + drop archived chat sessions (#3634)
* refactor(chat): rework chat history list

- Drop legacy archived sessions from the history dropdown. The
  soft-archive feature was removed, so status='archived' rows are dead
  data; exclude them instead of showing a collapsed "archived" group.
  Rename the section heading "Active" -> "Chat history".
- Swap hover row actions into the status column's slot instead of an
  absolute overlay: status is hidden on hover and actions take its
  place inline, while the title keeps flex-1. No mid-row gap, no
  overlap, no text bleed-through.
- Remove orphaned i18n keys (active_group, archived_group,
  archived_label) across en/zh-Hans/ko.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(issues): align execution log rows with the chat hover-swap pattern

- Drop the fixed w-20 status column that forced premature truncation of
  the trigger text and left a mid-row gap; status now sizes to content.
- Running tasks render only the spinner (sr-only label retained for a11y
  and tooltip); the redundant "Working" text is removed.
- Hover swaps status for actions in place (RowStatus hidden, RowActions
  inline) instead of an absolute gradient overlay. Applies to both
  active and past ("show past runs") rows via the shared RowShell /
  RowStatus / RowActions.

Known tradeoff: dropping the absolute+opacity slot also drops the
group-focus-within keyboard reveal, so cancel/retry are no longer
Tab-reachable. Matches the chat pattern; revisit if keyboard access for
row actions becomes a requirement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:55:02 +08:00
Jan De Dobbeleer
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.
2026-06-01 23:28:51 +08:00
Matt Voska
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>
2026-06-01 16:48:05 +02:00
Bohan Jiang
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>
2026-06-01 22:11:27 +08:00
Bohan Jiang
0f9d9d1494 fix(skills): align Go/TS frontmatter coercion for non-scalar values (#3614)
The Go SKILL.md frontmatter parser unmarshalled into a {Name,Description}
string struct, so a non-scalar value (a list/map written where a scalar
belongs) made the whole decode fail and dropped even a valid sibling
`name`. The TS parser instead kept the name and JSON-encoded the value,
so the file-viewer (TS) and the import path (Go) could disagree about
the same SKILL.md.

Decode into a generic map and coerce per key on the Go side, mirroring
the TS coercion (scalars -> literal form, sequences/mappings -> JSON), so
both sides produce identical results and a structured value never
discards a sibling key. Rename ParseFrontmatter -> ParseSkillFrontmatter
to remove the cross-language name clash with the TS parseFrontmatter
(which returns {frontmatter, body}), and drop the unused TS
parseSkillFrontmatter export.

Add parity tests for sequence/mapping values plus name-only,
description-only, leading-blank-line and triple-dash-in-body edge cases
on both sides.

Follow-up to #3543 / MUL-2842.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 19:42:20 +08:00
YOMXXX
801c201d4c fix(skills): parse multi-line YAML frontmatter in SKILL.md (#3495) (#3543)
Three independent line-based frontmatter parsers only handled
single-line `description: value`, so a YAML block scalar
(`description: |`) collapsed to the literal "|" and the rest of the
description was dropped before it ever reached the database.

Replace all three with real YAML decoders that understand block
scalars, folded scalars and quoted values:

- server/internal/skill: shared ParseFrontmatter via gopkg.in/yaml.v3,
  used by both the handler import path and daemon local-skill discovery
- packages/core/skills: shared parseFrontmatter via the yaml package
- file-viewer renders multi-line frontmatter values (whitespace-pre-wrap)

Both parsers fall back to empty values on malformed YAML, preserving the
previous non-fatal behaviour.
2026-06-01 19:35:01 +08:00
Multica Eve
03134e11a0 docs: add Skill search changelog (#3609)
* docs: add 2026-06-01 changelog

Co-authored-by: multica-agent <github@multica.ai>

* docs: refine Skill Command changelog copy

Co-authored-by: multica-agent <github@multica.ai>

* docs: correct Skill search changelog wording

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 19:20:48 +08:00
Jiayuan Zhang
57894f29b6 feat(onboarding): add GitHub to acquisition channels (#3612)
Add 'social_github' as a new attribution source option in the
onboarding 'How did you hear about Multica?' multi-select picker,
alongside the existing X / LinkedIn / YouTube options.

Includes:
- New 'social_github' value in the Source type union
- New GitHubIcon in the brand-icons component
- New option in step-source.tsx (placed next to other social picks)
- en/zh-Hans/ko i18n labels

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 12:21:38 +02:00
Naiyuan Qing
6f38891665 Improve mobile issue header controls (#3602)
* Improve mobile issue header controls

Co-authored-by: multica-agent <github@multica.ai>

* Fix desktop issue header control sizing

Co-authored-by: multica-agent <github@multica.ai>

* Fix issue scope tab test for mobile trigger

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): keep header controls horizontally scrollable

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 16:33:30 +08:00
Naiyuan Qing
d62d894f17 fix(issues): clean execution log active rows (#3603)
* fix(issues): clean execution log active rows

* fix(issues): use fixed execution log trailing slot

* fix(issues): isolate execution log row hover

* fix(issues): keep execution log status off trigger text

* fix(issues): simplify execution log row layout

* fix(issues): preserve execution log row accessibility
2026-06-01 16:13:36 +08:00
Naiyuan Qing
dd4d58f20e feat: add skill search CLI (#3601)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 15:19:42 +08:00
Naiyuan Qing
2b2888c23a Handle duplicate skill imports as structured results (#3599)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 14:45:16 +08:00
Naiyuan Qing
2bb2d13e22 fix(chat): limit running history actions (#3598) 2026-06-01 14:24:46 +08:00
Mohammed Helaiwa
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>
2026-06-01 13:52:41 +08:00
Naiyuan Qing
41a1ca58ad fix(chat): clean up history indicators (#3592) 2026-06-01 13:20:21 +08:00
Naiyuan Qing
3c8645e546 feat(cli): add squad member set-role (#3583)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 12:51:15 +08:00
Naiyuan Qing
a2313442f2 fix(chat): improve history row interactions (#3591) 2026-06-01 12:49:45 +08:00
Jiayuan Zhang
ad09baa045 feat(agents): add runtime machine filter to Agents tab (MUL-2846) (#3580)
* feat(agents): add runtime machine filter to Agents tab (MUL-2846)

Add a dropdown filter to the Agents tab toolbar that lets the user
narrow the list to agents bound to a specific runtime machine. The
filter reuses `buildRuntimeMachines` from the runtimes package so the
machine grouping (Local / Remote / Cloud) matches the Runtimes page
sidebar, and the per-machine agent counts respect the current scope
(Mine/All) so the numbers reflect what the user would see if they
clicked the row.

Only rendered in the Active view; the Archived view's toolbar is
unchanged. If the selected machine is GC'd while the user is on the
page (daemon stopped, runtime deleted), the filter auto-resets to
'All runtimes' instead of leaving the list empty. The no-matches state
now surfaces 'No agents on <machine>' when the machine filter is the
reason for zero results.

Adds new `runtime_filter` and `no_matches.runtime_filtered` /
`no_matches.search_runtime_filtered` i18n keys in en, zh-Hans, and
ko. 7 new unit tests in
`runtime-machine-filter-dropdown.test.tsx`.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agents): address code review on runtime machine filter

- Plumb localDaemonId / localMachineName / hasLocalMachine / currentUserId
  through AgentsPage → buildRuntimeMachines so the Local section and
  device-name consolidation match the Runtimes page on both web and
  Desktop. Adds a DesktopAgentsPage wrapper that bridges daemonAPI the
  same way DesktopRuntimesPage does.
- Make the 'All runtimes' badge use the in-scope total instead of
  summing per-machine counts, so an agent bound to a GC'd runtime
  doesn't silently vanish from the count.
- Move Date.now() out of the machines useMemo into a useState lazy
  init so the snapshot stays stable per mount.
- Drop unused i18n keys (all_description / this_machine / reset) from
  runtime_filter in en / zh-Hans / ko.
- Add a regression test for the All-runtimes badge divergence.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agents): machine-scoped availability counts + Base UI menu items

Follow-up to the previous code-review round (Emacs review at 1144b6023).

#1 (medium) — Availability counts now respect the selected machine.
Introduce an inScopeOnMachine memo (inScope narrowed by the selected
runtime machine, but NOT by availability chip or search) and use it as
the base for both availabilityCounts and the AvailabilityFilterRow's
totalCount, so the chips reflect 'agents on this machine' once a
machine is selected. filteredAgents is now derived from inScopeOnMachine
so the availability chip and search further refine within the machine
scope. The dropdown's 'All runtimes' badge still uses inScope.length —
it's the count the user would see if they cleared the filter, so it
should stay unfiltered.

#2 (low) — Dropdown rows now use DropdownMenuItem instead of raw <button>.
Replaces the bare <button> in RuntimeMachineFilterItem with the
shared DropdownMenuItem wrapper (Base UI Menu.Item). The rows are now
registered as proper menu items: keyboard navigation (arrow keys, Enter,
Space), typeahead, ARIA role='menuitem' semantics, and auto-close on
selection (closeOnClick: true) all work. Active styling is preserved
via data-active, and a data-highlighted variant on the inactive style
matches Base UI's keyboard-focus appearance.

Tests updated to use role-based queries (getByRole('menuitem')) and
add a regression that verifies the menu is properly registered with
Base UI.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: MiniMax M3 <M3@multica.local>
2026-06-01 10:17:56 +08:00
Naiyuan Qing
cb2aab2f5c feat(cli): list issue pull requests (#3581)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 09:44:59 +08:00
Naiyuan Qing
4ae4722ef0 fix(comments): preserve direct parent on replies (#3579)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 08:28:15 +08:00
Multica Eve
c9c269675c fix: align MCP support docs and UI gate (#3553)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-30 18:24:45 +08:00
fengchangguo-star
2cf8107fc8 feat(email): support implicit TLS (SMTPS/465) for SMTP relay (MUL-2768) (#3340)
* feat(email): support implicit TLS (SMTPS/465) for SMTP relay

The SMTP relay previously only did opportunistic STARTTLS: it dialed
plaintext and upgraded if the server advertised STARTTLS. Providers that
only offer implicit TLS on port 465 and do not advertise STARTTLS (e.g.
Aliyun enterprise mail) could not be used as a relay at all.

Add an SMTP_TLS env var:
  - unset / starttls (default): unchanged STARTTLS-upgrade behavior.
  - implicit / smtps / ssl: dial with tls.DialWithDialer (SMTPS).
Implicit TLS is auto-enabled when SMTP_PORT=465 and SMTP_TLS is unset, so
the common case works with no extra config. The startup log line now
reports the negotiated mode (starttls / implicit-tls).

Co-authored-by: multica-agent <github@multica.ai>

* feat(email): plumb SMTP_TLS through selfhost compose, warn on unknown values

The backend reads SMTP_TLS but docker-compose.selfhost.yml never forwarded
it, so SMTP_TLS=implicit on a non-standard port (or an explicit starttls
override on 465) silently did nothing inside the container. Add it to the
backend.environment block.

Also log a one-line warning when SMTP_TLS is set to an unrecognized value
(e.g. "tls"/"true"/"on"), which would otherwise fall through to STARTTLS
and fail to dial a 465 SMTPS port with no startup hint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* test(email): cover SMTP_TLS precedence and alias resolution

Table-driven test over NewEmailService asserting the implicit-TLS decision:
465 auto-enables implicit; explicit starttls on 465 overrides auto-detect;
implicit/smtps/ssl aliases (case-insensitive, whitespace-trimmed) force SMTPS
on any port; unknown values fall back to starttls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* docs: document SMTPS / SMTP_TLS support, drop "465 unsupported"

Port 465 implicit TLS is now supported, so the five places that said it was
unsupported are wrong. Replace those sentences, add an SMTP_TLS row to the
environment-variables tables (EN + ZH), and add a copy-pasteable SMTPS env
block to the auth-setup pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: guofengchang <guofengchang@cumulon.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:15:04 +08:00
Ivan Vinokurov
9aa8ba0191 fix(runtimes): self-host daemon setup URLs (MUL-2804) (#3474)
Expose self-host daemon setup URLs from /api/config at runtime so the Add computer dialog renders the operator's own server/app domains, while Multica Cloud defaults stay unchanged.

Fixes #3013.
2026-05-30 18:13:02 +08:00
feifeigood
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>
2026-05-30 18:08:21 +08:00
Naiyuan Qing
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>
2026-05-29 20:13:23 +08:00
Multica Eve
eda2150a97 fix(agents): show MCP tab for ACP runtimes (#3534)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 20:00:05 +08:00
Naiyuan Qing
8b37edbbb1 fix(issues): move agent live bar back into the activity section (#3533)
PR #3517 moved the AgentLiveCard out of the activity section to a
sticky bar above the editable title. This restores its original
placement (after LocalDirectoryHint, above the timeline) and reverts
the container margin from mb-4 back to mt-4 that suited the top slot.

Everything else from #3517 is kept: the single-container multi-agent
accordion, the lifted cancel-confirmation dialog, and the dropped
parked/running background variant.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:51:19 +08:00
Multica Eve
c7fdc77908 docs: add session resume and Korean support changelog (#3524)
* docs: add 2026-05-29 changelog

Co-authored-by: multica-agent <github@multica.ai>

* docs: clarify session resume changelog

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 19:33:33 +08:00
Bohan Jiang
cf4a93ac20 chore(deps): drop stray mobile deps from monorepo root (#3525)
PR #2337 (Multica for iOS) accidentally added expo / react /
react-native to the monorepo ROOT package.json dependencies — almost
certainly a `pnpm add` run from the repo root instead of
`--filter @multica/mobile`. The root isn't an app and imports none of
them; apps/mobile already declares all three itself (react pinned to
19.2.0 for Expo SDK 55, per apps/mobile/CLAUDE.md).

The stray root react@19.2.0 hoisted to top-level node_modules/react,
producing a React version skew against the catalog's 19.2.3 that the
rest of the tree (web / desktop / ui / views) uses. After removal the
hoisted top-level react resolves to 19.2.3 and mobile keeps its own
19.2.0 untouched. Lockfile shrinks ~411 lines as the root importer no
longer pulls the Expo/RN transitive tree.

Pure dependency hygiene — no source changes, no behavior change for any
app. Mobile build unaffected (verified its package.json still declares
expo/react/react-native/react-dom).
2026-05-29 17:30:07 +08:00
Multica Eve
d1c7d478e1 MUL-2785: clarify thread-scoped comment delta (#3523)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 17:16:58 +08:00
Bohan Jiang
9fe7c935a9 MUL-2817: docs(i18n): add Korean (ko) documentation translation (#3521)
* docs(i18n): translate documentation corpus to Korean

Add Korean (.ko.mdx) translations for all 32 navigable docs pages plus
meta.ko.json navigation, mirroring the English source. Product terms
(Issue→이슈, Agent→에이전트, Squad→스쿼드, Runtime→런타임, Skill→스킬,
Workspace→워크스페이스, etc.) follow the in-app Korean locale at
packages/views/locales/ko/. Roles (owner/admin/member) and issue status
enums stay lowercase English per the conventions glossary.

MUL-2817

Co-authored-by: multica-agent <github@multica.ai>

* feat(docs): serve Korean docs content, remove English-fallback stopgap

Now that the *.ko.mdx corpus exists, drop the temporary docsContentLang
ko→en shim and the static-params fallback-synthesis loop so /docs/ko/*
renders real Korean content. Korean is now a first-class locale whose
params come straight from source.generateParams(). Also align the docs
home hero copy (agent→에이전트) with the app and the translated body.

MUL-2817

Co-authored-by: multica-agent <github@multica.ai>

* docs(i18n): align residual Korean UI/product terms with the app

Address review: sweep the .ko.mdx corpus for product/UI terms left in
English and match the in-app Korean locale.

- skills page title Skills → 스킬
- UI nav paths localized: Settings → 설정, Runtimes → 런타임, Agents →
  에이전트, Projects → 프로젝트, Squads/New squad → 스쿼드/새 스쿼드,
  Usage → 사용량, Personal Access Tokens → API 토큰, Provider → 제공자,
  and the agent-create form labels (Name/Provider/Model/Instructions →
  이름/제공자/모델/지침)
- see-also links Issues/Workspaces/Environment variables and
  'Providers Matrix' → Korean
- kept as literals (verified): code blocks, the conventions i18n glossary
  data, 'Anthropic Agent Skills' (standard name), the Squad Operating
  Protocol/Roster/Instructions prompt-block names, the literal 'Project
  Context' prompt section, and Xcode's Settings path
- add a docsAlternates test asserting ko hreflang is emitted when a real
  *.ko.mdx page exists

MUL-2817

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 16:51:13 +08:00
LinYushen
e024348c1f fix(cli/login): accept mcn_ Cloud Node PATs alongside mul_ (MUL-2815) (#3518)
* fix(cli/login): accept mcn_ Cloud Node PATs alongside mul_ (MUL-2815)

multica login --token rejected anything not starting with mul_, so
users with a Multica Cloud Node PAT (mcn_ prefix) hit
"invalid token format: must start with mul_" even though the server
middleware verifies both kinds.

Replace the inline literal check with validateLoginTokenPrefix(), backed
by a small loginTokenPrefixes list ({mul_, auth.CloudPATPrefix}) so the
accepted set has one source of truth. Add unit-test coverage so adding
a new prefix in future is an obvious one-line edit.

Co-authored-by: multica-agent <github@multica.ai>

* fix(cli/login): mention mcn_ Cloud Node PATs in --token help and comments

Follow-up to 47e423c4: the login command now accepts mcn_ tokens but the
help string and surrounding comments still only documented mul_, so a user
running 'multica login --help' couldn't tell that mcn_ was supported.
Update the --token help string and the cobra Args / NoOptDefVal comments
to list both mul_... and mcn_... prefixes.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 15:55:09 +08:00
Naiyuan Qing
fc9cec8e87 feat(issues): sticky agent live bar above title with multi-agent accordion (#3517)
Move the per-issue "agent is working" bar out of the activity section to a
sticky bar at the top of the main content, above the editable title, and fix
the multi-agent layout.

- Single active task fills one bordered container as a directly-actionable
  row (Logs + Stop inline).
- Multiple active tasks collapse into the same container: a summary header
  (avatar stack + count) that expands the rows inline as a divided list,
  instead of stacking N detached banners.
- Lift cancel confirmation to AgentLiveCard so one dialog serves both the lone
  row and the expanded list (avoids a confirm dialog being torn down by an
  enclosing popup).
- Drop the redundant parked/running background variant; running vs queued is
  signalled by icon + label only.
- Reuse the existing agent_activity.hover_header plural for the summary; no new
  i18n keys.
- Update tests for single-inline vs multi-accordion behavior.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:44:40 +08:00
김보경/DAXTF
5aa4fb7487 MUL-2760: feat(i18n): add Korean locale support (#3369)
* feat: add korean locale support

* feat(i18n): localize Korean landing page

* fix(i18n): refine Korean landing copy

* fix(i18n): refine Korean translations

* fix(i18n): translate Korean landing subpages

* fix(i18n): route Korean landing docs links

* fix(i18n): add Korean use case content

* fix(i18n): polish Korean locale copy

* fix(i18n): improve Korean landing copy

* fix(onboarding): persist Korean helper artifacts

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): add use case locale fallback

Co-authored-by: multica-agent <github@multica.ai>

* Align Korean pull requests wording

Co-authored-by: multica-agent <github@multica.ai>

* fix(i18n): dedupe docs href helper

Co-authored-by: multica-agent <github@multica.ai>

* fix(i18n): localize changelog dates

Co-authored-by: multica-agent <github@multica.ai>

* fix(docs): prerender Korean fallback pages

Co-authored-by: multica-agent <github@multica.ai>

* fix(docs): align fallback hreflang metadata

Co-authored-by: multica-agent <github@multica.ai>

* fix(i18n): preserve Chinese CJK font fallback order

Co-authored-by: multica-agent <github@multica.ai>

* chore(onboarding): update localized comment wording

Co-authored-by: multica-agent <github@multica.ai>

* test(i18n): harden CJK font fallback assertions

Co-authored-by: multica-agent <github@multica.ai>

* fix(docs): keep Chinese font fallbacks first

Co-authored-by: multica-agent <github@multica.ai>

* test(i18n): harden locale fallback coverage

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 15:16:22 +08:00
Multica Eve
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>
2026-05-29 14:57:14 +08:00
Naiyuan Qing
645af40ed9 refactor(views): unify detail/list headers into shared BreadcrumbHeader (#3510)
* refactor(views): unify detail/list headers into shared BreadcrumbHeader

Replace four hand-rolled, divergent header styles (workspace-name root,
"/" separator, back-arrow, raw div) with one shared BreadcrumbHeader
component. The mental model is now identical everywhere: leading crumbs
are the thing's real containers and clicking one navigates up.

- New packages/views/layout/breadcrumb-header.tsx (segments/leaf/actions)
- Detail pages (issue, project, runtime, skill, autopilot, agent, squad)
  now render `{Section} › name`; org name removed as a breadcrumb root
- Issue breadcrumb shows the single most-direct container only (parent
  wins over project; they are orthogonal columns), never a fabricated
  chain; bare issue shows just its title
- Issue leaf (identifier + title) is now a clickable link to the issue
  detail page with a subtle hover:opacity-80
- Issues / My Issues list headers drop the workspace prefix, matching the
  icon + title style of the other list pages

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(views): update breadcrumb tests for unified header behavior

The header unification changed three observable behaviors the tests
asserted against:
- issue detail no longer renders the workspace name as a breadcrumb root
- bare issue shows only its (now clickable) title leaf, no ancestor crumbs
- the project "Unknown project" error placeholder was removed

Rewrite the two affected issue-detail tests to assert the new leaf-link
and no-project-crumb behavior, drop the obsolete Unknown-project test, and
update the issues-page header test to assert the workspace prefix is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:27:51 +08:00
Bohan Jiang
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>
2026-05-29 12:59:53 +08:00
Bohan Jiang
ca1ea5716e fix(server/child-done): trigger agent parent assignee on child done (MUL-2808) (#3507)
Remove the agent-path self-trigger guard in triggerChildDoneAgent so a child going done wakes its parent agent even when the same agent owns both — a serial sub-task handoff across two different issues, not a loop. Runaway re-triggering stays bounded by HasPendingTaskForIssueAndAgent. Squad path unchanged. Closes #3374.
2026-05-29 12:49:07 +08:00
Fangfei
c730e906b9 feat(cli): add roots-only issue comment listing (MUL-2805) (#3288) 2026-05-29 12:03:38 +08:00
Bohan Jiang
d63d2328ab fix(runtimes): consolidate out-of-band (WSL2) local daemon under LOCAL (MUL-2799) (#3497)
* fix(runtimes): consolidate out-of-band local daemon by host name

The desktop derived `localMachineName`/`localDaemonId` only from the daemon
it manages itself. When the real daemon runs out-of-band (e.g. in WSL2 on a
Windows host), the app never gets a device name, so the #3336 device-name
consolidation short-circuits on `!!localMachineName`: the local-mode runtime
falls into REMOTE and an empty "This machine" placeholder is synthesized.

Fix in two parts:
- Desktop exposes the host OS hostname (`os.hostname()`) via a new
  `daemon:get-host-name` IPC and uses it as the final fallback for
  `localMachineName`, independent of daemon state.
- Scope device-name consolidation to the current user's own local runtimes
  (`owner_id === currentUserId`). The runtime list is workspace-wide, so a
  host-name match alone could otherwise claim another member's identically
  named machine as "this machine".

Once the real runtime classifies as current it moves to LOCAL and the empty
placeholder is no longer synthesized.

MUL-2799

Co-authored-by: multica-agent <github@multica.ai>

* fix(runtimes): use OS-neutral "This device" label for the current machine

The current-machine badge was hard-coded to "This Mac" (en), so it rendered
incorrectly on Windows/Linux hosts. zh-Hans already used the neutral "本机".

MUL-2799

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-29 11:12:48 +08:00
Naiyuan Qing
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>
2026-05-29 10:38:37 +08:00
Bohan Jiang
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>
2026-05-29 10:28:30 +08:00
Bohan Jiang
270fb6aa73 MUL-2792 fix(agent): preserve skills in update/archive/restore response (#3464)
* MUL-2792 fix(agent): preserve skills in update/archive/restore response (#3459)

agentToResponse always initialises Skills as []; the mutation handlers
relied on the caller to refresh it, but only GetAgent and ListAgents
actually did. UpdateAgent / ArchiveAgent / RestoreAgent therefore
returned "skills": [] regardless of what the agent_skill junction table
contained.

The DB write path was never wrong — skills weren't actually deleted —
but the misleading response (and its matching agent:status / archived /
restored WS broadcast) scared users into manually re-running
`agent skills set` and risked scripted clients writing the empty set
back as truth.

Extract the existing GetAgent skill-reload block into attachAgentSkills
and call it from the three buggy handlers. Add regression tests that
attach skills, hit each mutation endpoint, and assert both the response
and the junction table.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): attach skills before env/template broadcasts (#3459)

Two follow-up sites flagged in PR #3464 review that shared the same
"agentToResponse zeroes Skills, callers forget to reload" pattern as the
mutation handlers:

- agent_env.go: the agent:status broadcast after UpdateAgentEnv used a
  bare agentToResponse, so subscribers saw skills wiped on every env
  rotation. HTTP body is AgentEnvResponse so the response itself is
  unaffected, but the WS event still misleads any cache that ingests it.
- agent_template.go: CreateAgentFromTemplate attaches imported and extra
  skills inside the tx, then builds the response/agent:created broadcast
  without reloading them — so callers (and any client tracking the
  create event) see the freshly created agent as skill-less despite the
  template having just imported them.

Both call sites now reuse attachAgentSkills introduced for UpdateAgent.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 19:06:57 +08:00
Bohan Jiang
27b473151b refactor(ui): move CODE_LIGATURE_CLASS to zero-dep code-style module (#3463)
Extracts CODE_LIGATURE_CLASS and CODE_LIGATURE_DESCENDANT_CLASS into
packages/ui/lib/code-style.ts. Non-markdown CLI command surfaces
(onboarding/cli-install-instructions, runtimes/connect-remote-dialog)
can now import the class strings without pulling in the shiki +
react-markdown + katex dependency graph via the markdown barrel.

CodeBlock and Markdown continue to consume the constants from the
new module; the markdown barrel no longer re-exports CODE_LIGATURE_CLASS.

MUL-2793

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 18:52:09 +08:00
Xiaohan Li
2662291cf2 fix(runtimes): disable ligatures in CLI command snippets (#3357) 2026-05-28 18:43:51 +08:00
Bohan Jiang
fa076d38f2 MUL-2778 feat(agent): wire mcp_config through OpenClaw runtime (#3450)
* MUL-2778 feat(agent): wire mcp_config through OpenClaw runtime

The MCP config tab (#3419) lets admins save mcp_config on an agent, and
recent work (#3439) plumbed it through the three ACP runtimes. OpenClaw
still ignored the field, leaving the Tab silently inert for any
OpenClaw-backed agent.

Translate the agent's Claude-style `{"mcpServers": {...}}` into the
per-task OpenClaw wrapper's `mcp.servers` block — OpenClaw resolves MCP
via its own config schema rather than ExecOptions, so the existing
OPENCLAW_CONFIG_PATH preparer is the right seam. Fail closed on
malformed JSON / entries missing `command` or `url`, matching the
fail-closed posture the preparer already uses for the agents.list step.
Null / absent mcp_config leaves the wrapper free of an `mcp` key so the
user's global mcp.servers flows through untouched; an explicit empty
managed set (`{}` / `{"mcpServers":{}}`) is honoured as "admin saved no
servers" mirroring `hasManagedCodexMcpConfig`.

Strict-mode replacement (drop user-only servers entirely) would require
OpenClaw to do a per-key replace rather than a deep merge at
`mcp.servers`; the comment documents that caveat rather than relying on
undocumented behaviour.

Also adds `openclaw` to `MCP_SUPPORTED_PROVIDERS` so the MCP Tab
actually surfaces in the agent overview pane, and pins the new
visibility case with a renderPane test.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-2778 fix(agent): make openclaw mcp_config strict-replace via sanitized snapshot

Elon flagged on #3450 that the previous wiring let user-only mcp.servers
leak through the wrapper's `$include` of the live user config: deep-merge
at `mcp.servers` keeps user-only names, and the strict-empty case
(`{ "mcpServers": {} }`) silently inherited user globals.

Switch the strict-replace path to write a sanitized snapshot of the
user's fully resolved config (via `openclaw config get --json`) with the
`mcp` block stripped, then have the wrapper `$include` the snapshot
instead of the live user file. With the user's `mcp` gone from the
$include resolution, the wrapper's `mcp.servers` is the only definition
the embedded OpenClaw sees — managed only, including the explicit empty
set.

The snapshot lives in envRoot at 0o600 alongside the wrapper so the GC
reaper sweeps it with the rest of the task scratch, and no extra
OPENCLAW_INCLUDE_ROOTS entry is needed (same-dir $include).

Fail-closed on `config get --json` errors so the daemon never silently
falls back to the leaky $include path. The inherit branch (null
mcp_config) still uses the live user file directly — no extra CLI
roundtrip and no snapshot is written.

New tests pin the contract Elon's review required:

- TestPrepareOpenclawConfigStrictReplacesUserMcpServers: user has
  global_one + shared, managed has shared + managed_only → wrapper has
  exactly {shared (managed value), managed_only}; global_one does NOT
  leak; snapshot file has the user's `mcp` stripped while preserving
  gateway / providers / API keys.
- TestPrepareOpenclawConfigStrictEmptyManagedSetDropsUserMcp: empty
  managed set drops user's global_one (both `{}` and
  `{"mcpServers":{}}` cases).
- TestPrepareOpenclawConfigNullMcpConfigKeepsUserInclude: null path
  inherits the live user config, writes no snapshot, makes no extra CLI
  call.
- TestPrepareOpenclawConfigFailsClosedOnResolvedConfigError: errors
  during `config get --json` surface; no stale wrapper or snapshot.
- TestPrepareOpenclawConfigManagedSetFreshInstall: fresh install with
  managed mcp_config skips the snapshot dance entirely.

Also tightens en + zh-Hans MCP Tab copy to mention OpenClaw goes via the
per-task wrapper, and to use OpenClaw's own `transport` field rather
than Claude's `type` for HTTP/SSE entries.

Co-authored-by: multica-agent <github@multica.ai>

* MUL-2778 fix(agent): narrow openclaw snapshot strip to mcp.servers only

Elon's third-round must-fix: the previous strict-replace snapshot deleted
the entire `mcp` block, which wiped out non-server settings under `mcp`
like `sessionIdleTtlMs`. Those are documented OpenClaw config keys
(https://docs.openclaw.ai/gateway/configuration-reference#mcp) outside
the MCP Tab's scope — the agent's saved mcp_config only manages server
definitions, so other `mcp.*` tuning the user set must survive.

Replace the blanket `delete(resolved, "mcp")` with a stripUserMcpServers
helper that:

- deletes only `mcp.servers` when `mcp` is an object
- drops the parent `mcp` key only when the object is empty after the
  strip (so we don't emit `mcp: {}` placeholders)
- leaves non-object `mcp` values untouched (we only know how to strip
  servers from the documented shape)

Pinned with TestPrepareOpenclawConfigStrictPreservesNonServerMcpKeys:
user resolved has both `mcp.sessionIdleTtlMs: 300000` and
`mcp.servers.global_one`; after the strict path runs the snapshot
keeps the TTL and drops the servers map, and the wrapper's
`mcp.servers` is exactly the managed set with no leak.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 18:43:02 +08:00
sunhwang
16336ad33c docs: ITT-236 clarify issue mention Markdown (#3365) 2026-05-28 18:40:40 +08:00
Multica Eve
063e9711df docs(changelog): add v0.3.11 release notes (#3449)
* docs(changelog): add v0.3.11 release notes

Co-authored-by: multica-agent <github@multica.ai>

* docs(changelog): refine v0.3.11 release notes

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 18:01:07 +08:00
Naiyuan Qing
d90732750f Revert "feat(comments): since-delta new-comment hint + default-on comment ses…" (#3455)
This reverts commit 5e78e5100a.
2026-05-28 17:52:59 +08:00
LinYushen
372a330707 fix(billing): wire test page through useT instead of silencing i18n rule (#3451)
The previous fix (#3446) for the i18next/no-literal-string CI failure
took the lazy route — added a file-level eslint-disable comment with
the rationale that the test page is slated for deletion when the real
billing UI ships, so investing in a translation namespace was wasted
churn.

That argument is weak in practice:

  * The test page has been around since #3442 and there is no
    concrete date for when the real billing UI lands. "Throwaway"
    surfaces tend to outlive their stated half-life.
  * File-level disables hide future violations of the same rule from
    review — anyone touching this file later inherits an opt-out
    they didn't ask for.
  * The translation namespace is genuinely cheap. The page has ~50
    distinct strings, all of which now have a single en.json/zh.json
    home that the locale-parity test guards.

This commit replaces the silencer with a real `billing` namespace:

  * packages/views/locales/en/billing.json + zh-Hans/billing.json —
    every literal English label from the page, plus interpolated
    sentences with i18next `{{var}}` placeholders. The zh-Hans
    bundle is a basic translation; not perfect prose, but the parity
    test passes and a Chinese reviewer testing the page won't see
    raw English mixed with native UI chrome.
  * packages/views/i18n/resources-types.ts — adds the namespace to
    `I18nResources` so `t($ => $.billing.x.y)` is type-checked.
  * packages/views/locales/index.ts — registers both bundles in
    `RESOURCES` so the parity test sees them.
  * packages/views/billing/billing-test-page.tsx — every JSX literal
    rewritten as `t(($) => $.path)`, including aria-label,
    interpolated sentences (balance meta line, transactions row meta,
    paging footer), and conditional branches (with-bonus vs
    without-bonus rendering goes through two distinct keys rather
    than two literals). The file-level eslint-disable is removed
    along with its multi-paragraph rationale comment.
  * formatDate now takes `t` as an argument so the en/zh "—" dash
    placeholder is itself translated; calling useT inside the util
    would have violated the rule of hooks (the function is called
    from conditional render branches).

Verification:
  * pnpm --filter @multica/views lint   — 0 errors (15 unrelated
    warnings, all pre-existing on main)
  * pnpm --filter @multica/views typecheck — clean
  * pnpm --filter @multica/views test    — 883/883 passing,
    including the locale-parity test (which fails the build if
    en and zh-Hans diverge)

When the real billing UI ships and this file is deleted, the
`billing` namespace JSONs and the `resources-types.ts` /
`locales/index.ts` entries get deleted with it — same blast radius
as the eslint-disable would have had, but with proper i18n along
the way.
2026-05-28 17:38:46 +08:00
Bohan Jiang
ee4ec3b76d MUL-2784 fix(daemon): cleanup sidecar tree (.agent_context / .multica / provider skills) after local_directory tasks (#3444)
* fix(daemon): cleanup .agent_context / .multica / provider skill sidecars after local_directory tasks (MUL-2784)

PR #3438 (MUL-2753) only restored CLAUDE.md / AGENTS.md / GEMINI.md to
their pre-task bytes; the sidecar tree writeContextFiles seeds
(.agent_context/, .multica/, .claude/skills/, .github/skills/,
.opencode/skills/, skills/, .pi/skills/, .cursor/skills/,
.kimi/skills/, .kiro/skills/, .agents/skills/, fallback
.agent_context/skills/) was explicitly deferred to this follow-up. In
local_directory mode the agent's workdir is the user's repo, so each
task accumulates one more layer of those directories in the user's
tree.

Plan A: track every file/dir Prepare creates inside workDir in a
sidecarManifest written to envRoot/.multica_sidecar_manifest.json
(daemon scratch — never in the user's workdir). On local_directory
teardown CleanupSidecars walks the manifest, removes the recorded
files, then rmdir-iterates the recorded directories in reverse.
Pre-existing files and directories are deliberately NOT recorded, so
a user-installed .claude/skills/my-own-skill/ sibling — or any
unrelated file the user keeps under .claude/, .github/, etc. — is
preserved bit-for-bit. Non-empty rmdir fails ENOTEMPTY and is
silently skipped, which is the signal that the user owns the
directory.

Daemon wiring lives next to the existing CleanupRuntimeConfig defer
in runTask: runtime brief first, sidecars second. Cloud-mode runs
still write a manifest for symmetry but never trigger the cleanup
(the GC loop wipes envRoot wholesale).

Tests (sidecar_manifest_test.go) cover the round-trip invariant per
the issue's acceptance criteria:

- empty workdir → Prepare → Cleanup → empty workdir, byte-exact, for
  every file-based provider (claude, codex, copilot, opencode,
  openclaw, hermes, pi, cursor, kimi, kiro, antigravity, gemini),
- user's .claude/skills/my-own-skill/ (and equivalents per
  provider) survives Cleanup intact,
- unrelated user files under .claude/, .github/, etc. survive,
- three repeated cycles do not accumulate any orphan state,
- project_resources branch (.multica/project/resources.json) is
  also reversible,
- recordWriteFile refuses to record pre-existing files,
- recordMkdirAll refuses to record pre-existing dirs,
- Cleanup is a no-op when the manifest file is missing.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): refuse to overwrite pre-existing sidecar paths; pick collision-free skill slugs (MUL-2784 review)

Addresses PR #3444 review (Elon):

**Must-fix #1**: recordWriteFile used to overwrite pre-existing target
files unconditionally and only skip the manifest record. That destroys
user bytes at write time AND leaves the corrupted contents in place at
cleanup time — the byte-exact contract the issue requires is violated
on both halves. Fixed by making recordWriteFile detect any pre-existing
entry (regular file, symlink, directory) via Lstat and return a
sentinel errPathPreExists without touching the path. The user's bytes
are preserved verbatim.

For per-skill collisions (user's .claude/skills/issue-review/ vs
Multica's "Issue Review"), writeSkillFiles now allocates a
collision-free sibling slug via allocateCollisionFreeSkillDir: first
attempt is the natural slug, then `<base>-multica`,
`<base>-multica-2`, …, bounded at 64 attempts. Provider-native
discovery still picks the skill up (every subdir under skillsParent is
a distinct skill) and the user's path stays bit-for-bit intact.

For Multica-only namespace files (.agent_context/issue_context.md,
.multica/project/resources.json), the writer swallows errPathPreExists
and continues — the runtime brief already carries every fact those
files would, so a collision degrades to brief-only mode rather than
destroying user content.

**Must-fix #2**: Added byte-exact collision matrix tests covering
every file-based provider (claude / codex / copilot / opencode /
openclaw / hermes / pi / cursor / kimi / kiro / antigravity / gemini):

- TestPrepareThenCleanupSidecarsSameSlugCollisionPerProvider: seeds
  user's `<provider>/skills/issue-review/SKILL.md` plus a private
  notes.md sibling, runs Prepare → Inject → Cleanup, asserts
  workdir snapshot is byte-identical to seed.
- TestPrepareThenCleanupSidecarsIssueContextCollisionPerProvider:
  seeds user's `.agent_context/issue_context.md`, asserts round-trip
  preserves it.
- TestPrepareThenCleanupSidecarsProjectResourcesCollisionPerProvider:
  same for `.multica/project/resources.json`.
- TestPrepareThenCleanupSidecarsMultiSkillCollisionFreeAllocation:
  end-to-end check that the Multica skill lands at the
  collision-free sibling and Cleanup removes only the Multica side.
- TestAllocateCollisionFreeSkillDir: directed unit test pinning the
  slug-bumping sequence.
- TestRecordWriteFileRefusesToOverwritePreExistingFile (was
  TestRecordWriteFileSkipsPreExistingFile): flipped to assert the
  user's bytes survive and errPathPreExists is returned.
- TestRecordWriteFileRefusesToOverwriteSymlinkOrDir: covers the
  Lstat path for non-file entries.

**Should-fix**: CleanupSidecars used to swallow ANY non-ENOENT rmdir
error as "user content present," silently dropping real I/O failures
(EACCES, EPERM, EBUSY). Now it re-reads the directory after a failed
rmdir via the new dirHasEntries helper — non-empty → silently skip
(ENOTEMPTY, the intended branch); empty → genuine error, captured
into firstErr and surfaced. Plus directed tests:

- TestCleanupSidecarsSurfacesRealRmdirErrors
- TestDirHasEntries

Local verification:
- go test ./internal/daemon/execenv/... — all green
- go test ./internal/daemon/... — all green
- go vet ./... — clean

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): surface original rmdir error when post-rmdir ReadDir also fails (MUL-2784 review)

Addresses remaining PR #3444 review blocker (Elon): dirHasEntries used
to return true when ReadDir failed with anything other than ENOENT,
which made CleanupSidecars treat every locked / faulted directory as
ENOTEMPTY and silently drop the original rmdir error. The v1 fix from
the previous round closed the EACCES-on-empty-dir branch but missed
the case where the chmod also blocks ReadDir — exactly the failure
mode the review called out.

Helper change: dirHasEntries now returns (hasEntries, ok bool):

  - (false, true)  — dir exists and is empty (or missing, race-safe)
  - (true,  true)  — dir has user content (the ENOTEMPTY branch)
  - (_,     false) — ReadDir failed (EACCES, ENOTDIR, EIO, …); the
                     caller cannot tell ENOTEMPTY from a real error
                     and MUST surface the original rmdir error

CleanupSidecars switches on (ok, hasEntries):

  - !ok               → surface the ORIGINAL rmdir error (not the
                        ReadDir failure — that's diagnostic plumbing
                        and would distract from the root cause)
  - ok && hasEntries  → swallow silently (intended ENOTEMPTY branch;
                        preserve user content)
  - ok && !hasEntries → surface the rmdir error (empty dir + EACCES /
                        EPERM / EBUSY → genuine cleanup failure)

Tests:

  - TestDirHasEntries: extended with a regular-file sub-case (ReadDir
    returns ENOTDIR) asserting (false, false). The v1 helper returned
    (true) here, hiding the bug.
  - TestCleanupSidecarsSwallowsMissingAndNonEmptyDirs: renamed from
    TestCleanupSidecarsSurfacesRealRmdirErrors. The old name claimed
    to test the surfacing path but never actually exercised it.
  - TestCleanupSidecarsSurfacesEACCESOnEmptyRecordedDir: chmod parent
    to 0o555 so rmdir(recorded) fails EACCES while ReadDir(recorded)
    still succeeds (empty). Asserts firstErr is non-nil and references
    both the recorded path and the rmdir branch. Skipped when running
    as root (chmod is bypassed for uid 0).
  - TestCleanupSidecarsSurfacesEACCESWhenReadDirFailsToo: the must-fix
    case — chmod parent 0o555 AND chmod recorded 0o000 so BOTH rmdir
    and ReadDir fail. The surfaced error must be the ORIGINAL rmdir
    failure, not the ReadDir one. Skipped on uid 0.

Local verification:
- go test ./internal/daemon/execenv/... — all green
- go test ./internal/daemon/... — all green
- go vet ./... — clean

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 17:22:47 +08:00
Bohan Jiang
9b280bc631 chore(billing): silence i18next/no-literal-string in dev-only test page (#3446)
billing-test-page.tsx (introduced in #3442) ships dozens of raw English
JSX labels and is explicitly documented as "not a finished UI — slated
for deletion when the real billing UI ships". Routing every label
through useT() and inventing throwaway translation keys would be
wasted churn that gets deleted in the same week the real UI lands.

Add a file-level eslint-disable so CI passes on every PR that touches
unrelated files. When the real billing UI replaces this page, this
file (and the disable) gets deleted together.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 17:14:52 +08:00
Bohan Jiang
90a737fc7e fix(daemon): retry terminal task callbacks on transient errors (MUL-2780) (#3443)
CompleteTask / FailTask used to be fire-once. A 1-second upstream 502
burst would drop the call, then the immediate fail-fallback also 502'd,
leaving the task stuck in `running` forever and showing the agent as
"still working" in the UI.

Add a bounded retry around the two terminal callbacks: 4s, 8s, 16s,
32s, 64s backoff schedule (5 retries, ~124s ceiling), retrying only
on transient errors (5xx, 408, 429, transport-level) and bailing
immediately on permanent 4xx. Also fix a latent bug where a transient
complete failure would silently downgrade a successful run to a fail:
the fallback now triggers only on permanent errors. Server-side
CompleteTask / FailTask are already idempotent on "already terminal",
so replays from a retry are safe even if the prior 502'd response was
actually persisted.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 17:06:41 +08:00
LinYushen
7bbca54e92 feat(billing): test page consuming /api/cloud-billing/* (#3442)
* feat(billing): test page consuming /api/cloud-billing/*

Stuffs every cloud-billing endpoint onto a single dev page so we can
verify the proxy + Stripe end-to-end flow without a designed UI.
Reachable at /<workspaceSlug>/billing — the page is account-level
data but lives under the workspace dashboard layout because that's
where the authenticated shell sits. No sidebar entry on purpose;
this is test-quality and meant to be deleted when the real billing
UI ships.

What's there:

  * Balance card (GET /balance)
  * Stripe-success polling banner — visible only when ?session_id=
    is in the URL (Stripe substitutes it into checkout_success_url
    on its way back). React Query refetchInterval polls every 2s
    until the topup status reaches credited / failed / canceled,
    then a 'Clear from URL' button calls navigation.replace(pathname).
  * Buy section: server-authoritative price tier buttons (GET
    /price-tiers) → POST /checkout-sessions → window.location to the
    Stripe URL. We do NOT hard-code amounts on the frontend; tier
    config lives in cloud's billing.price_tiers.
  * Stripe Billing Portal button (POST /portal-sessions). Opens in a
    new tab so the originating page stays put for easy verification.
    Documented behaviour: 400 is expected for users with no Stripe
    customer record yet.
  * Three lists: transactions / batches / topups.

Plumbing:

  * packages/core/types/billing.ts — interfaces mirroring the cloud
    response shapes. Status / source / tx_type fields are typed
    'string' rather than enum unions to match the schemas' z.string()
    parsing (same convention as CloudRuntimeNode); the canonical
    enum values are exported as separate type aliases for callers
    that want to switch on them.
  * packages/core/api/schemas.ts — 9 zod schemas + 7 EMPTY_ fallbacks,
    all .loose() so a non-breaking cloud-side field addition doesn't
    crash the parser.
  * packages/core/api/client.ts — 8 methods using parseWithFallback,
    matching the existing cloud-runtime shape.
  * packages/core/billing/{queries,mutations,index}.ts — React Query
    queryOptions + mutations. Notable choices: balance / lists are
    NOT keyed on workspace (account-level data), and the
    checkout-session polling stops automatically when status is
    terminal so we don't poll forever after a user closes the tab.
  * packages/core/package.json + packages/views/package.json — exports
    map updated for @multica/core/billing and @multica/views/billing.

Verification:

  * pnpm --filter @multica/core typecheck clean
  * pnpm --filter @multica/views typecheck — only pre-existing
    hast-util-to-html error in editor code (exists on main)
  * pnpm --filter @multica/core test — 412 passing
  * pnpm --filter @multica/views test — 877 passing, 1 failure
    (editor/readonly-content) is also pre-existing on main, not
    caused by this change

Out of scope: real production-quality billing UI; sidebar entry; i18n
strings; mobile app. This is a single test page; it gets replaced
when the real UI ships.

* fix(billing): refetch balance/lists when checkout polling reaches terminal

Closes the second-half of the Stripe-return race the previous commit
left dangling.

Symptom:
  After Stripe redirects back with ?session_id=..., the banner polls
  /checkout-sessions/{id} every 2s and the rest of the page (balance,
  transactions, batches, topups) is fetched once on mount. The
  webhook race means those four queries usually see pre-credit state
  — but the banner is the only thing that keeps polling, so once it
  reads 'credited' nothing else on the page knows. The user would
  see 'Final status: credited' next to a stale balance card until
  they manually refresh.

Fix:
  Add useInvalidateBillingDataAfterCredit() in @multica/core/billing —
  a hook returning a callback that flushes balance / transactions /
  batches / topups (NOT the checkout-session itself; its
  refetchInterval already terminated, refetching would just confirm
  the same value). The Stripe-success banner runs this callback in
  a useEffect keyed on terminal-status transition, so it fires
  exactly once when the polling lands.

  Strict scope is documented in the hook's JSDoc:
    - balance/transactions/batches: only change at the 'credited'
      transition (cloud writes ledger + batch + wallet in one DB tx)
    - topups: changes on every terminal transition
    - For 'failed' / 'canceled' we technically over-fetch the first
      three; three cheap round-trips, simplifies the call site, fine
      on a test page.

  Effect dep is . terminal flips false→true at most once
  per session id (the polling stops when terminal is true so the
  data won't change again). If the user lands here with a session
  that is already terminal (re-opened tab on a credited URL), the
  effect still fires on first data load and we still re-fetch —
  correct, the cached snapshot is just as stale in that case.

go build / pnpm typecheck / pnpm test clean (core 412 passing; only
pre-existing hast-util-to-html error in unrelated editor code on
views, same as on main).
2026-05-28 16:48:04 +08:00
Bohan Jiang
90ddfb04e2 feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777) (#3441)
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433)

When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces
returns 403 for every caller and the UI hides every "Create workspace"
affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This
closes the gap where ALLOW_SIGNUP=false still let any signed-in user open
an isolated workspace the platform admin couldn't see.

- server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace,
  workspace_creation_disabled in /api/config, Go tests.
- frontend: new workspaceCreationDisabled in configStore, hide sidebar
  entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding
  StepWorkspace to a "creation disabled, ask for invite" state when the
  flag is on, EN + zh-Hans locale strings.
- ops: .env.example, docker-compose.selfhost, helm values + configmap,
  SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs
  (EN + zh).

Co-authored-by: multica-agent <github@multica.ai>

* fix(onboarding): drive create path off workspaceCreationAllowed (#3433)

PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already
has a workspace, StepWorkspace still walked the resume copy (`headline_resume`
/ `lede_resume` mentioning "or start another") and `creatingActive` ignored
the flag, leaving a stale clickable create CTA possible if /api/config
arrived late.

Refactor StepWorkspace to derive a single `workspaceCreationAllowed`
boolean from the config store. It now drives:

- Initial `mode` state (defaults to "existing" when disabled + reusing so
  the CTA is pre-armed for the only valid action).
- `creatingActive` so the footer CTA cannot fall back into the create
  branch even mid-render.
- Eyebrow / headline / lede strings — adds
  `creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for
  the disabled + reusing variant.

Tests: cover the three reachable shapes — flag off + no existing, flag on
+ no existing, flag on + existing.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 16:42:08 +08:00
Bohan Jiang
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>
2026-05-28 16:29:49 +08:00
Bohan Jiang
03f70209c4 fix(daemon): preserve user CLAUDE.md / AGENTS.md / GEMINI.md in local_directory runs (#3438)
* fix(daemon): preserve user CLAUDE.md / AGENTS.md / GEMINI.md in local_directory runs (MUL-2753)

InjectRuntimeConfig previously called os.WriteFile unconditionally, which
truncated whatever file lived at the same path. For the local_directory
project_resource flow the workdir is the user's own repo, so the agent
silently destroyed any repo-level CLAUDE.md / AGENTS.md / GEMINI.md the
first time it ran in that directory, and the daemon's local-directory
cleanup explicitly skips the user's path so the file was never restored.

Write the brief inside a marker block instead:

  <!-- BEGIN MULTICA-RUNTIME (auto-managed; do not edit) -->
  ...brief...
  <!-- END MULTICA-RUNTIME -->

writeRuntimeConfigFile handles three states:

- file missing  -> create with just the marker block,
- file present, no marker block -> append the marker block at the end
  (preserves user-authored content above), and
- file present, marker block already there -> replace the block body in
  place so repeated runs don't grow the file unboundedly.

This is the short-term fix called out on MUL-2753. The sidecar question
(.agent_context/, .claude/skills/, .multica/project/resources.json) is
left for a follow-up — those files don't overwrite user content, just
litter the workdir.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): cleanup runtime config marker block after local_directory tasks (MUL-2753)

Address Elon's review on PR #3438:

1. Add `CleanupRuntimeConfig` and wire it into the daemon's task path so
   `local_directory` runs excise the marker block on the way out. Without
   it, a user's subsequent manual `claude` / `codex` / `gemini` run in
   the same directory picks up the previous task's stale brief (issue
   id, trigger comment id, reply rules) and acts on the wrong context.
   Cloud workspace runs skip the cleanup — their scratch workdir is
   wiped by the GC loop anyway.

2. If excising the block would leave the file empty / whitespace-only,
   the file is removed so we don't leave behind a stub the user has to
   delete by hand. Surviving user content is preserved byte-for-byte.

3. Harden the marker parser: search for the end marker strictly after
   the begin marker. The previous `strings.Index` pair mishandled two
   malformed cases —
     - a stray end marker before any begin (e.g. user pasted a
       documentation snippet showing the wire format) would cause
       every run to stack another block, growing the file unboundedly;
     - a half-block left by a previous crashed run would cause every
       subsequent run to append a fresh block beneath the half-block.
   The `locateMarkerBlock` helper now anchors the end search past the
   begin offset, and treats "begin found, no end after" as "block runs
   to EOF" so the next write replaces it cleanly.

Centralised the provider→filename mapping in `runtimeConfigPath` so
Inject and Cleanup can't drift past each other when a new provider is
added.

Tests cover: parser hardening (stray-end-before-begin idempotency,
half-block recovery), Cleanup happy path / file removal / no-op cases /
malformed half-block / per-provider mapping, and an end-to-end
inject→cleanup round trip that locks in byte-identical restoration of
the user's pre-injection file.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): byte-exact inject/cleanup round trip for runtime config (MUL-2753)

Address Elon's second-round review on PR #3438. The previous cleanup
relied on `TrimRight + "\n"` for trailing newlines and `TrimSpace == ""`
for file removal — both compensated for the inject path's "normalise
trailing newlines so there's always exactly `\n\n` before the block"
step, but they did so by mutating the user's bytes. The result was a
real diff on three boundary cases:

  - file ended without a newline (`rules`) → cleanup added one;
  - file ended with two or more newlines (`rules\n\n`) → cleanup
    collapsed to a single newline;
  - file pre-existed but was empty / whitespace-only → cleanup
    deleted it.

Reshape the contract so the bytes inject adds are the exact bytes
cleanup removes, with no user-byte mutation in between:

  - Define `runtimeManagedSeparator = "\n\n"` as a fixed managed
    separator that inject always inserts (unconditionally — including
    for files that already end in two or more newlines) between
    pre-existing user content and the marker block.
  - Inject's missing-file branch still writes the block alone (no
    separator); that absence is the marker Cleanup uses to identify
    "we created this file from scratch" and is the only condition
    under which Cleanup is allowed to `os.Remove` the file.
  - Cleanup detects `HasSuffix(pre, runtimeManagedSeparator)` and
    strips exactly those bytes; whatever remains is written back
    verbatim with no `TrimRight` / `TrimSpace`, so the pre-injection
    bytes survive exactly.

The replace-in-place branch is untouched — the managed separator
established by the first inject lives in pre and survives across
subsequent runs, so byte-exactness is preserved through arbitrary
inject→inject→cleanup chains.

Tests:

  - `TestInjectThenCleanupRoundTripByteExactBoundaries` parameterises
    9 seed shapes (missing file, empty, whitespace-only, no trailing
    newline, one trailing newline, two trailing newlines, many
    trailing newlines, CRLF line endings, no final newline with
    embedded blank lines) and asserts byte-identical round trip
    across two full cycles.
  - `TestInjectReplaceThenCleanupRestoresByteExact` covers the
    replace-in-place branch for the same boundary seeds.
  - `TestWriteRuntimeConfigFileAlwaysInsertsFixedManagedSeparator`
    pins the new invariant at the source: regardless of seed shape,
    inject emits `<seed><\n\n><marker block>` with no normalisation.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 16:15:07 +08:00
Bohan Jiang
72e0cb70aa fix(transcript): truncate workdir chip + click-to-copy (#3440)
Real workdir paths are routinely long enough to push every other chip
off the transcript-dialog metadata row, leaving the row scrolling or
wrapping awkwardly. Turn the chip into a fixed-width button:

- max-w-[16rem] + truncate so the path tail gets a clean ellipsis no
  matter the depth
- title attribute carries the full relative_work_dir for a hover peek
- click copies relative_work_dir to clipboard, icon flips to a green
  Check for 2s as feedback
- swap FolderTree icon for the simpler Folder mark

The pre-existing privacy invariant is preserved unchanged: only the
server-cleaned relative_work_dir reaches the DOM / title / clipboard;
the absolute task.work_dir still never leaves the server response.
2026-05-28 16:14:50 +08:00
LinYushen
3943358e67 feat(billing): proxy /api/cloud-billing/* + Stripe webhook to multica-cloud (#3434) 2026-05-28 16:05:19 +08:00
Naiyuan Qing
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>
2026-05-28 15:58:42 +08:00
488 changed files with 41500 additions and 2424 deletions

View File

@@ -40,11 +40,12 @@ JWT_SECRET=change-me-in-production
# MULTICA_APP_URL=http://localhost:3000
# Public URL the API is reachable at from the open internet (no trailing
# slash). Used to mint absolute webhook URLs for autopilot webhook
# triggers. Leave unset behind a same-origin reverse proxy or for plain
# localhost dev — the frontend will compose the URL from
# window.origin + webhook_path in that case. Headers are intentionally
# not used to derive this value, to avoid Host / X-Forwarded-Host
# spoofing when a self-hosted reverse proxy is not hardened.
# triggers and to show correct daemon setup commands in the web UI. Leave
# unset behind a same-origin reverse proxy or for plain localhost dev —
# the frontend will compose the URL from window.origin + webhook_path in
# that case. Headers are intentionally not used to derive this value, to
# avoid Host / X-Forwarded-Host spoofing when a self-hosted reverse proxy
# is not hardened.
MULTICA_PUBLIC_URL=
# Comma-separated CIDR list of reverse proxies whose X-Forwarded-For /
# X-Real-IP headers the per-IP webhook rate limiter is allowed to trust.
@@ -88,11 +89,18 @@ RESEND_FROM_EMAIL=noreply@multica.ai
# Takes priority over Resend when SMTP_HOST is set.
# Supports unauthenticated relay (leave SMTP_USERNAME empty) and authenticated SMTP.
# Set SMTP_TLS_INSECURE=true only for private CA or self-signed certificates.
# SMTP_TLS controls the TLS mode:
# - unset / "starttls" (default): plaintext connect, upgrade via STARTTLS.
# - "implicit" (aliases: "smtps", "ssl"): TLS handshake on connect (SMTPS).
# Required by providers that only offer port 465 and do not advertise
# STARTTLS (e.g. Aliyun enterprise mail). Auto-enabled when SMTP_PORT=465
# and SMTP_TLS is unset.
SMTP_HOST=
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
SMTP_TLS=
# Google OAuth
# The web login page reads GOOGLE_CLIENT_ID from /api/config at runtime, so
@@ -208,6 +216,14 @@ ALLOWED_EMAIL_DOMAINS=
# Optional: Only allow these exact email addresses (comma-separated)
ALLOWED_EMAILS=
# Set to "true" to disable workspace creation for every caller on this
# instance (#3433). Operators usually leave this unset, bootstrap the
# shared workspace, then flip this to "true" and restart so subsequent
# users join only via invitations and the entire deployment is visible to
# the platform admin. The web UI reads this from /api/config at runtime,
# so toggling requires a backend restart but not a frontend rebuild.
DISABLE_WORKSPACE_CREATION=
# ==================== Analytics (PostHog) ====================
# Product analytics events feed the acquisition → activation → expansion funnel.
# Leave POSTHOG_API_KEY empty for local dev / self-hosted instances; the server

View File

@@ -655,7 +655,7 @@ multica autopilot update <id> --description "New prompt"
multica autopilot delete <id>
```
`--mode` currently only accepts `create_issue` (creates a new issue on each run and assigns it to the agent). The server data model also defines `run_only`, but the daemon task path doesn't yet resolve a workspace for runs without an issue, so it's not exposed by the CLI. `--agent` accepts either a name or UUID.
`--mode` accepts `create_issue` (creates a new issue on each run and assigns it to the agent) or `run_only` (enqueues a direct agent task without creating an issue). `--agent` accepts either a name or UUID.
### Manual Trigger

View File

@@ -73,7 +73,7 @@ Open http://localhost:3000 in your browser. The Docker self-host stack defaults
- **Without email configured:** the verification code is generated server-side and printed to the backend container logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing on a single machine.
- **Deterministic local/private testing:** set `APP_ENV=development` and `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env`, then restart the backend. This fixed code is ignored when `APP_ENV=production`.
Changes to `ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` also take effect after restarting the backend / compose stack. The web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
Changes to `ALLOW_SIGNUP`, `DISABLE_WORKSPACE_CREATION`, and `GOOGLE_CLIENT_ID` also take effect after restarting the backend / compose stack. The web UI reads all three from `/api/config` at runtime, so no web rebuild is needed. See [Advanced Configuration → Signup Controls](SELF_HOSTING_ADVANCED.md#signup-controls-optional) for the recommended sequence to lock down workspace creation.
> **Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
@@ -268,7 +268,7 @@ The chart defaults to `APP_ENV=production` (set in `values.yaml` under `backend.
kubectl -n multica rollout restart deploy/multica-backend
```
`ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` likewise live under `backend.config.*` in `values.yaml`. After `helm upgrade`, the backend pod will roll automatically because the ConfigMap hash changes; the web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
`ALLOW_SIGNUP`, `DISABLE_WORKSPACE_CREATION`, and `GOOGLE_CLIENT_ID` likewise live under `backend.config.*` in `values.yaml` (as `allowSignup`, `disableWorkspaceCreation`, and `googleClientId`). After `helm upgrade`, the backend pod will roll automatically because the ConfigMap hash changes; the web UI reads all three from `/api/config` at runtime, so no web rebuild is needed.
> **Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.

View File

@@ -44,9 +44,10 @@ Use this option when your deployment cannot reach the public internet or you alr
| `SMTP_PORT` | SMTP port | `25` |
| `SMTP_USERNAME` | SMTP username (leave empty for unauthenticated relay) | - |
| `SMTP_PASSWORD` | SMTP password | - |
| `SMTP_TLS` | TLS mode. `implicit` (aliases `smtps`, `ssl`) forces SMTPS on connect; port `465` auto-enables it. Unset / `starttls` upgrades via STARTTLS | `starttls` |
| `SMTP_TLS_INSECURE` | Set `true` to skip TLS certificate verification (self-signed / private CA certs) | `false` |
STARTTLS is used automatically when advertised by the server. Port 465 (SMTPS / implicit TLS) is not currently supported - use ports 25 or 587 with STARTTLS.
STARTTLS is used automatically when advertised by the server. Port 465 (SMTPS / implicit TLS) is supported and auto-enables implicit TLS; set `SMTP_TLS=implicit` (aliases `smtps`, `ssl`) to force it on a non-standard port.
> **Note:** If neither Resend nor SMTP is configured, generated verification codes are printed to backend logs — copy them from there to log in. A fixed local testing code (e.g. `888888`) is **opt-in only**: set `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env` and keep `APP_ENV` non-production. The Docker self-host stack pins `APP_ENV=production`, so the shortcut is ignored there. **Never enable a fixed code on a publicly reachable instance.**
@@ -67,8 +68,20 @@ Changes take effect after restarting the backend / compose stack. The web UI rea
| `ALLOW_SIGNUP` | Set to `false` to disable new user signups on a private instance |
| `ALLOWED_EMAIL_DOMAINS` | Optional comma-separated allowlist of email domains |
| `ALLOWED_EMAILS` | Optional comma-separated allowlist of exact email addresses |
| `DISABLE_WORKSPACE_CREATION` | Set to `true` to make `POST /api/workspaces` return 403 for every caller — users can only join workspaces they were invited to |
Changes take effect after restarting the backend / compose stack. The web UI reads `ALLOW_SIGNUP` from `/api/config` at runtime, so no web rebuild is needed.
Changes take effect after restarting the backend / compose stack. The web UI reads `ALLOW_SIGNUP` and `DISABLE_WORKSPACE_CREATION` from `/api/config` at runtime, so no web rebuild is needed.
#### Locking down workspace creation
`ALLOW_SIGNUP=false` blocks new accounts from being created, but it does **not** block an already-signed-in user from creating another workspace via `POST /api/workspaces`. On a self-hosted instance where every issue/repo/agent must be visible to the platform admin, set `DISABLE_WORKSPACE_CREATION=true` to close that gap. The recommended bootstrap sequence is:
1. Start the instance with `DISABLE_WORKSPACE_CREATION=false` (the default).
2. Sign in as the admin and create the shared workspace.
3. Set `DISABLE_WORKSPACE_CREATION=true` and restart the backend. Optionally set `ALLOW_SIGNUP=false` at the same time if you also want to block new account creation.
4. Going forward, additional users join via invitation only — the "Create workspace" affordance is hidden in the UI and any direct API call returns 403.
> Note: setting `ALLOW_SIGNUP=false` blocks **all** new account creation, including users who already have a pending invitation. If you need invited users to be able to sign up but not create their own workspaces, keep `ALLOW_SIGNUP=true` (optionally combined with `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS`) and only flip `DISABLE_WORKSPACE_CREATION=true`.
### File Storage (Optional)

View File

@@ -15,7 +15,7 @@ import {
type StatsListener,
} from "fs";
import { join } from "path";
import { homedir } from "os";
import { homedir, hostname } from "os";
import type { DaemonStatus, DaemonPrefs } from "../shared/daemon-types";
import { ensureManagedCli, managedCliPath } from "./cli-bootstrap";
import { decideVersionAction } from "./version-decision";
@@ -864,6 +864,11 @@ export function setupDaemonManager(
ipcMain.handle("daemon:stop", () => withGuard(() => stopDaemon()));
ipcMain.handle("daemon:restart", () => withGuard(() => restartDaemon()));
ipcMain.handle("daemon:get-status", () => fetchHealth());
// The host's OS name, available regardless of daemon state. The Runtimes
// page uses it as a fallback identity for "this machine" when no
// app-managed daemon is reporting a device name (e.g. the daemon runs
// out-of-band in WSL2). See desktop-runtimes-page.tsx.
ipcMain.handle("daemon:get-host-name", () => hostname());
ipcMain.handle(
"daemon:sync-token",
(_event, token: string, userId: string) => syncToken(token, userId),

View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, nativeImage, Notification } from "electron";
import { app, BrowserWindow, dialog, ipcMain, nativeImage, Notification } from "electron";
import { homedir } from "os";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
@@ -13,6 +13,11 @@ import { installNavigationGestures } from "./navigation-gestures";
import { getAppVersion } from "./app-version";
import { loadRuntimeConfig } from "./runtime-config-loader";
import type { RuntimeConfigResult } from "../shared/runtime-config";
import {
createElectronReloadPrompt,
installRendererRecoveryHandlers,
type RendererRecoveryWindow,
} from "./renderer-recovery";
// Bundled icon used for dock/taskbar branding. macOS/Windows production
// builds let the OS pick up the icon from the .app bundle / .exe resources,
@@ -224,13 +229,6 @@ function createWindow(): void {
log(level, `${message} (${sourceId}:${lineNumber})`);
});
// Fires when the renderer process dies for any reason (OOM, crash,
// killed). `details.reason` is the discriminator: "crashed", "oom",
// "killed", "abnormal-exit", "launch-failed", etc.
mainWindow.webContents.on("render-process-gone", (_event, details) => {
log("process-gone", JSON.stringify(details));
});
// Fires when loadURL / loadFile can't reach its target (dev server
// not up yet, network blip, file missing). errorCode is a Chromium
// net error number; -3 = ABORTED is normal during HMR and skipped.
@@ -245,14 +243,15 @@ function createWindow(): void {
},
);
// Fires when the preload script throws before the renderer can boot.
// This is the one error class that NEVER reaches DevTools (preload
// runs before any window) — without this listener it's invisible.
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
log("preload-error", `path=${preloadPath} err=${error?.stack ?? error}`);
});
}
installRendererRecoveryHandlers(mainWindow as unknown as RendererRecoveryWindow, {
isDev: is.dev,
showReloadPrompt: createElectronReloadPrompt((options) =>
dialog.showMessageBox(mainWindow!, options),
),
});
installContextMenu(mainWindow.webContents);
installNavigationGestures(mainWindow);

View File

@@ -0,0 +1,112 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { installRendererRecoveryHandlers } from "./renderer-recovery";
type Handler = (...args: unknown[]) => void;
function makeWindow() {
const windowHandlers = new Map<string, Handler>();
const webContentsHandlers = new Map<string, Handler>();
const reload = vi.fn();
return {
window: {
on: vi.fn((event: string, handler: Handler) => windowHandlers.set(event, handler)),
isDestroyed: vi.fn(() => false),
webContents: {
on: vi.fn((event: string, handler: Handler) => webContentsHandlers.set(event, handler)),
reload,
},
},
windowHandlers,
webContentsHandlers,
reload,
};
}
describe("installRendererRecoveryHandlers", () => {
beforeEach(() => vi.clearAllMocks());
afterEach(() => vi.useRealTimers());
it("registers production reload prompts for renderer death and preload failure without auto reloading", async () => {
const fixture = makeWindow();
const showReloadPrompt = vi.fn(async () => "reload" as const);
installRendererRecoveryHandlers(fixture.window, { isDev: false, showReloadPrompt });
expect(fixture.webContentsHandlers.has("render-process-gone")).toBe(true);
expect(fixture.webContentsHandlers.has("preload-error")).toBe(true);
expect(fixture.windowHandlers.has("unresponsive")).toBe(true);
expect(fixture.windowHandlers.has("responsive")).toBe(true);
fixture.webContentsHandlers.get("render-process-gone")?.({}, { reason: "crashed" });
fixture.webContentsHandlers.get("preload-error")?.({}, "/preload.js", new Error("boom"));
expect(fixture.reload).not.toHaveBeenCalled();
await Promise.resolve();
expect(showReloadPrompt).toHaveBeenCalledTimes(2);
expect(fixture.reload).toHaveBeenCalledTimes(2);
});
it("does not prompt when the renderer exits cleanly", async () => {
const fixture = makeWindow();
const showReloadPrompt = vi.fn(async () => "reload" as const);
installRendererRecoveryHandlers(fixture.window, { isDev: false, showReloadPrompt });
fixture.webContentsHandlers.get("render-process-gone")?.({}, { reason: "clean-exit" });
await Promise.resolve();
expect(showReloadPrompt).not.toHaveBeenCalled();
expect(fixture.reload).not.toHaveBeenCalled();
});
it("cancels an unresponsive prompt when the window becomes responsive again", async () => {
vi.useFakeTimers();
const fixture = makeWindow();
const showReloadPrompt = vi.fn(async () => "reload" as const);
installRendererRecoveryHandlers(fixture.window, {
isDev: false,
showReloadPrompt,
unresponsivePromptDelayMs: 100,
});
fixture.windowHandlers.get("unresponsive")?.();
fixture.windowHandlers.get("responsive")?.();
await vi.advanceTimersByTimeAsync(100);
expect(showReloadPrompt).not.toHaveBeenCalled();
expect(fixture.reload).not.toHaveBeenCalled();
});
it("prompts for sustained unresponsive windows and only reloads after user confirmation", async () => {
vi.useFakeTimers();
const fixture = makeWindow();
const showReloadPrompt = vi.fn(async () => "dismiss" as const);
installRendererRecoveryHandlers(fixture.window, {
isDev: false,
showReloadPrompt,
unresponsivePromptDelayMs: 100,
});
fixture.windowHandlers.get("unresponsive")?.();
await vi.advanceTimersByTimeAsync(100);
expect(showReloadPrompt).toHaveBeenCalledWith({ kind: "unresponsive", context: {} });
expect(fixture.reload).not.toHaveBeenCalled();
});
it("keeps dev diagnostics non-prompting", async () => {
const fixture = makeWindow();
const showReloadPrompt = vi.fn(async () => "reload" as const);
installRendererRecoveryHandlers(fixture.window, { isDev: true, showReloadPrompt, log: vi.fn() });
fixture.webContentsHandlers.get("render-process-gone")?.({}, { reason: "crashed" });
await Promise.resolve();
expect(showReloadPrompt).not.toHaveBeenCalled();
expect(fixture.reload).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,135 @@
export type RendererRecoveryWindow = {
isDestroyed: () => boolean;
on: (event: "unresponsive" | "responsive", handler: () => void) => unknown;
webContents: {
on: (event: string, handler: (...args: any[]) => void) => unknown;
reload: () => void;
};
};
type ReloadPromptPayload = {
kind: "render-process-gone" | "preload-error" | "unresponsive";
context: Record<string, unknown>;
};
type ReloadPromptResult = "reload" | "dismiss";
type RendererRecoveryOptions = {
isDev: boolean;
showReloadPrompt: (payload: ReloadPromptPayload) => Promise<ReloadPromptResult>;
log?: (tag: string, ...args: unknown[]) => void;
unresponsivePromptDelayMs?: number;
};
export function installRendererRecoveryHandlers(
window: RendererRecoveryWindow,
{
isDev,
showReloadPrompt,
log = defaultDevLog,
unresponsivePromptDelayMs = 1500,
}: RendererRecoveryOptions,
) {
let unresponsivePromptTimer: ReturnType<typeof setTimeout> | null = null;
const maybePromptReload = (payload: ReloadPromptPayload) => {
if (isDev) return;
void showReloadPrompt(payload).then((result) => {
if (result === "reload" && !window.isDestroyed()) {
window.webContents.reload();
}
});
};
window.webContents.on("render-process-gone", (_event, details) => {
if (isDev) log("process-gone", JSON.stringify(details));
if (!isRecoverableRendererExit(details)) return;
maybePromptReload({ kind: "render-process-gone", context: { details } });
});
window.webContents.on("preload-error", (_event, preloadPath, error) => {
if (isDev) log("preload-error", `path=${preloadPath} err=${formatError(error)}`);
maybePromptReload({
kind: "preload-error",
context: { preloadPath, error: formatError(error) },
});
});
window.on("unresponsive", () => {
if (isDev || unresponsivePromptTimer) return;
unresponsivePromptTimer = setTimeout(() => {
unresponsivePromptTimer = null;
maybePromptReload({ kind: "unresponsive", context: {} });
}, unresponsivePromptDelayMs);
});
window.on("responsive", () => {
if (!unresponsivePromptTimer) return;
clearTimeout(unresponsivePromptTimer);
unresponsivePromptTimer = null;
});
}
export function createElectronReloadPrompt(
showMessageBox: (options: {
type: "warning";
buttons: string[];
defaultId: number;
cancelId: number;
title: string;
message: string;
detail: string;
}) => Promise<{ response: number }>,
) {
return async (payload: ReloadPromptPayload): Promise<ReloadPromptResult> => {
const result = await showMessageBox({
type: "warning",
buttons: ["Reload", "Dismiss"],
defaultId: 0,
cancelId: 1,
title: "Multica needs to reload",
message: rendererRecoveryMessage(payload.kind),
detail: rendererRecoveryDetail(payload),
});
return result.response === 0 ? "reload" : "dismiss";
};
}
function isRecoverableRendererExit(details: unknown) {
if (!details || typeof details !== "object") return false;
const reason = (details as { reason?: unknown }).reason;
return (
reason === "crashed" ||
reason === "oom" ||
reason === "abnormal-exit" ||
reason === "launch-failed" ||
reason === "integrity-failure"
);
}
function rendererRecoveryMessage(kind: ReloadPromptPayload["kind"]) {
switch (kind) {
case "render-process-gone":
return "The desktop renderer process stopped responding or crashed.";
case "preload-error":
return "The desktop preload script failed before the app could start.";
case "unresponsive":
return "The desktop window is not responding.";
}
}
function rendererRecoveryDetail(payload: ReloadPromptPayload) {
return [
"Reloading is the safest recovery path for this window.",
"",
`kind: ${payload.kind}`,
`context: ${JSON.stringify(payload.context)}`,
].join("\n");
}
function defaultDevLog(tag: string, ...args: unknown[]) {
process.stderr.write(`[renderer ${tag}] ${args.map(String).join(" ")}\n`);
}
function formatError(error: unknown) {
return error instanceof Error ? (error.stack ?? error.message) : String(error);
}

View File

@@ -95,6 +95,7 @@ interface DaemonAPI {
stop: () => Promise<{ success: boolean; error?: string }>;
restart: () => Promise<{ success: boolean; error?: string }>;
getStatus: () => Promise<DaemonStatus>;
getHostName: () => Promise<string>;
onStatusChange: (callback: (status: DaemonStatus) => void) => () => void;
setTargetApiUrl: (url: string) => Promise<void>;
syncToken: (token: string, userId: string) => Promise<void>;

View File

@@ -185,6 +185,8 @@ const daemonAPI = {
ipcRenderer.invoke("daemon:restart"),
getStatus: (): Promise<DaemonStatus> =>
ipcRenderer.invoke("daemon:get-status"),
getHostName: (): Promise<string> =>
ipcRenderer.invoke("daemon:get-host-name"),
onStatusChange: (callback: (status: DaemonStatus) => void) => {
const handler = (_: unknown, status: DaemonStatus) => callback(status);
ipcRenderer.on("daemon:status", handler);

View File

@@ -1,7 +1,7 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { CoreProvider } from "@multica/core/platform";
import { pickLocale } from "@multica/core/i18n";
import { pickLocale, type SupportedLocale } from "@multica/core/i18n";
import { useAuthStore } from "@multica/core/auth";
import { useWelcomeStore } from "@multica/core/onboarding";
import { workspaceKeys, workspaceListOptions } from "@multica/core/workspace/queries";
@@ -21,6 +21,18 @@ import { useDaemonIPCBridge } from "./platform/daemon-ipc-bridge";
import { createDesktopLocaleAdapter } from "./platform/i18n-adapter";
import { RESOURCES } from "@multica/views/locales";
// BCP-47 region tags for the <html lang> attribute, mirroring
// apps/web/app/layout.tsx HTML_LANG. index.html ships a static lang="en";
// we sync it to the resolved locale at boot so screen readers announce the
// right language AND the Japanese-scoped CJK font override in globals.css
// (`html[lang|="ja"]`) can take effect.
const HTML_LANG: Record<SupportedLocale, string> = {
en: "en",
"zh-Hans": "zh-CN",
ko: "ko-KR",
ja: "ja-JP",
};
function AppContent() {
const user = useAuthStore((s) => s.user);
@@ -179,6 +191,7 @@ function AppContent() {
return undefined;
}, [user, workspaceListFetched, wsCount, workspaces, hasOnboarded, qc]);
// Validate persisted tab state against the current user's workspace list,
// and pick an active workspace if none is set. Runs in useLayoutEffect
// (synchronously after render, before paint) rather than the render
@@ -303,6 +316,15 @@ export default function App() {
[locale],
);
// Keep <html lang> in sync with the resolved locale (index.html hardcodes
// "en"). Drives the lang-scoped Japanese CJK font override and a11y.
// useLayoutEffect (not useEffect) so lang is committed before the first
// paint — otherwise Japanese users would see one frame of Kanji rendered
// with the Chinese-first fallback stack before the override kicks in.
useLayoutEffect(() => {
document.documentElement.lang = HTML_LANG[locale];
}, [locale]);
// React to OS-level language changes detected by main on focus regain.
// Only act when the user is following the system signal (no explicit
// Settings choice) — otherwise their preference wins. Cross-device sync

View File

@@ -0,0 +1,54 @@
import { useEffect, useState } from "react";
import { AgentsPage } from "@multica/views/agents";
import type { DaemonStatus } from "../../../shared/daemon-types";
/**
* Desktop wrapper around the shared `AgentsPage`. Bridges the Electron
* `daemonAPI` (main-process daemon state) into the page so the runtime
* machine filter can render the Local section the same way the Runtimes
* page does — without these props the page falls back to grouping
* every local-mode runtime under "Remote" with a generic title, which
* breaks the "drill from a machine into its agents" promise of the
* filter.
*
* Mirrors `DesktopRuntimesPage`: we cache the last seen daemon
* identity so the Local row doesn't get reclassified as Remote when
* the daemon is stopped (which would null out `status.daemonId`), and
* we fall back to the OS hostname so the section label stays useful
* even when the app doesn't manage the running daemon (WSL2 etc.).
*/
export function DesktopAgentsPage() {
const [status, setStatus] = useState<DaemonStatus>({ state: "stopped" });
const [lastIdentity, setLastIdentity] = useState<{
daemonId: string | null;
deviceName: string | null;
}>({ daemonId: null, deviceName: null });
const [hostName, setHostName] = useState<string | null>(null);
useEffect(() => {
const apply = (s: DaemonStatus) => {
setStatus(s);
if (s.daemonId) {
setLastIdentity({
daemonId: s.daemonId,
deviceName: s.deviceName ?? null,
});
}
};
window.daemonAPI.getStatus().then(apply);
window.daemonAPI.getHostName().then((name) => setHostName(name || null));
return window.daemonAPI.onStatusChange(apply);
}, []);
return (
<AgentsPage
localDaemonId={status.daemonId ?? lastIdentity.daemonId}
localMachineName={status.deviceName ?? lastIdentity.deviceName ?? hostName}
// Desktop owns a local machine for the lifetime of the app, even
// while the daemon is stopped or hasn't registered yet. The shared
// page synthesizes a placeholder local row so the filter dropdown
// still has a Local option to pick in the empty window.
hasLocalMachine
/>
);
}

View File

@@ -28,6 +28,11 @@ export function DesktopRuntimesPage() {
daemonId: string | null;
deviceName: string | null;
}>({ daemonId: null, deviceName: null });
// The host's OS hostname, independent of any daemon. Used as the last
// fallback for the local machine name so consolidation still works when
// the app doesn't manage the running daemon (e.g. it lives in WSL2) and
// thus never reports a device name.
const [hostName, setHostName] = useState<string | null>(null);
useEffect(() => {
const apply = (s: DaemonStatus) => {
@@ -40,6 +45,7 @@ export function DesktopRuntimesPage() {
}
};
window.daemonAPI.getStatus().then(apply);
window.daemonAPI.getHostName().then((name) => setHostName(name || null));
return window.daemonAPI.onStatusChange(apply);
}, []);
@@ -51,7 +57,7 @@ export function DesktopRuntimesPage() {
return (
<RuntimesPage
localDaemonId={status.daemonId ?? lastIdentity.daemonId}
localMachineName={status.deviceName ?? lastIdentity.deviceName}
localMachineName={status.deviceName ?? lastIdentity.deviceName ?? hostName}
localMachineActions={<DaemonRuntimeActions />}
// Desktop owns a local machine for the lifetime of the app, even
// while the daemon is stopped or hasn't registered yet. The shared

View File

@@ -0,0 +1,98 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { createMemoryRouter, RouterProvider } from "react-router-dom";
const openModal = vi.fn();
const reloadActiveTab = vi.fn();
const closeActiveTab = vi.fn();
vi.mock("@multica/core/modals", () => ({
useModalStore: {
getState: () => ({ open: openModal }),
},
}));
vi.mock("@/stores/tab-store", () => ({
useTabStore: {
getState: () => ({ reloadActiveTab, closeActiveTab }),
},
}));
import { DesktopRouteErrorPage, formatRouteErrorReport } from "./route-error-page";
function Boom(): null {
throw new Error("route render exploded");
return null;
}
describe("DesktopRouteErrorPage", () => {
beforeEach(() => {
openModal.mockReset();
reloadActiveTab.mockReset();
closeActiveTab.mockReset();
vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("brands React Router route errors and offers tab reload", async () => {
const router = createMemoryRouter(
[{ path: "/", element: <Boom />, errorElement: <DesktopRouteErrorPage /> }],
{ initialEntries: ["/"] },
);
render(<RouterProvider router={router} />);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Something went wrong in this tab",
);
fireEvent.click(screen.getByRole("button", { name: /reload tab/i }));
expect(reloadActiveTab).toHaveBeenCalledTimes(1);
});
it("offers Close tab as the always-safe escape from a crashing route", async () => {
const router = createMemoryRouter(
[{ path: "/acme/issues/1", element: <Boom />, errorElement: <DesktopRouteErrorPage /> }],
{ initialEntries: ["/acme/issues/1"] },
);
render(<RouterProvider router={router} />);
fireEvent.click(await screen.findByRole("button", { name: /close tab/i }));
expect(closeActiveTab).toHaveBeenCalledTimes(1);
});
it("opens the existing feedback modal with a structured markdown report only after click", async () => {
const router = createMemoryRouter(
[{ path: "/acme/issues", element: <Boom />, errorElement: <DesktopRouteErrorPage /> }],
{ initialEntries: ["/acme/issues"] },
);
render(<RouterProvider router={router} />);
expect(openModal).not.toHaveBeenCalled();
fireEvent.click(await screen.findByRole("button", { name: /report error/i }));
expect(openModal).toHaveBeenCalledWith(
"feedback",
expect.objectContaining({
initialMessage: expect.stringContaining("kind: desktop_route_error"),
}),
);
});
it("documents the structured kind/context follow-up debt in the report template", () => {
const report = formatRouteErrorReport({
error: new Error("bad route"),
url: "app://desktop/acme/issues",
appInfo: { version: "1.2.3", os: "macos" },
trigger: "route-errorElement",
});
expect(report).toContain("kind: desktop_route_error");
expect(report).toContain("trigger: route-errorElement");
expect(report).toContain("TODO: promote kind/context to structured feedback fields");
});
});

View File

@@ -0,0 +1,140 @@
import { useMemo } from "react";
import { useLocation, useNavigate, useRouteError } from "react-router-dom";
import { AlertTriangle, RotateCw, Send, X } from "lucide-react";
import { Button } from "@multica/ui/components/ui/button";
import { useModalStore } from "@multica/core/modals";
import { useTabStore } from "@/stores/tab-store";
type DesktopAppInfo = {
version?: string;
os?: string;
};
export function formatRouteErrorReport({
error,
url,
appInfo,
trigger,
}: {
error: unknown;
url: string;
appInfo?: DesktopAppInfo;
trigger: string;
}) {
const normalized = normalizeError(error);
return [
"kind: desktop_route_error",
`trigger: ${trigger}`,
`url: ${url}`,
`app_version: ${appInfo?.version ?? "unknown"}`,
`runtime_os: ${appInfo?.os ?? "unknown"}`,
"",
"context:",
`- name: ${normalized.name}`,
`- message: ${normalized.message}`,
"",
"stack:",
"```",
normalized.stack ?? "<no stack>",
"```",
"",
"TODO: promote kind/context to structured feedback fields once the feedback API supports them.",
].join("\n");
}
export function DesktopRouteErrorPage() {
const error = useRouteError();
const location = useLocation();
const navigate = useNavigate();
const workspaceSlug = location.pathname.split("/").filter(Boolean)[0];
const safeRoute = workspaceSlug ? `/${workspaceSlug}/issues` : null;
const report = useMemo(
() =>
formatRouteErrorReport({
error,
url:
typeof window !== "undefined"
? `${window.location.origin}${location.pathname}${location.search}${location.hash}`
: location.pathname,
appInfo: typeof window !== "undefined" ? window.desktopAPI?.appInfo : undefined,
trigger: "route-errorElement",
}),
[error, location.hash, location.pathname, location.search],
);
const message = normalizeError(error).message;
return (
<div
role="alert"
className="flex h-full min-h-[20rem] flex-col items-center justify-center gap-4 p-8 text-center"
>
<div className="rounded-full bg-destructive/10 p-3 text-destructive">
<AlertTriangle className="h-6 w-6" aria-hidden="true" />
</div>
<div className="space-y-2">
<h2 className="text-lg font-semibold">Something went wrong in this tab</h2>
<p className="max-w-lg text-sm text-muted-foreground">
A route-level renderer error was contained before it could take down the
desktop shell. Reload this tab, or send the report if it keeps happening.
</p>
<p className="max-w-lg truncate text-xs text-muted-foreground">{message}</p>
</div>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => useTabStore.getState().reloadActiveTab()}
>
<RotateCw className="mr-2 h-4 w-4" aria-hidden="true" />
Reload tab
</Button>
{safeRoute ? (
<Button type="button" variant="outline" onClick={() => navigate(safeRoute, { replace: true })}>
Go to issues
</Button>
) : null}
<Button
type="button"
variant="outline"
onClick={() => useTabStore.getState().closeActiveTab()}
>
<X className="mr-2 h-4 w-4" aria-hidden="true" />
Close tab
</Button>
<Button
type="button"
onClick={() =>
useModalStore.getState().open("feedback", {
initialMessage: report,
})
}
>
<Send className="mr-2 h-4 w-4" aria-hidden="true" />
Report error
</Button>
</div>
</div>
);
}
function normalizeError(error: unknown): { name: string; message: string; stack?: string } {
if (error instanceof Error) {
return {
name: error.name || "Error",
message: error.message || "Unknown route error",
stack: error.stack,
};
}
if (typeof error === "string") {
return { name: "Error", message: error };
}
return { name: "Error", message: "Unknown route error", stack: safeJson(error) };
}
function safeJson(value: unknown) {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}

View File

@@ -0,0 +1,147 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { render } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
// vi.hoisted shared state for all the stores / hooks the layout consumes.
const state = vi.hoisted(() => ({
user: null as { id: string } | null,
isAuthLoading: false,
overlay: null as { type: string } | null,
workspace: null as { id: string; slug: string } | null,
listFetched: true,
wsList: [] as { id: string; slug: string }[],
workspaceSeen: true,
modalRenders: 0,
modalAriaLabel: "source-backfill-modal-marker",
}));
vi.mock("@multica/core/auth", () => {
const useAuthStore = (selector: (s: typeof state) => unknown) => {
if (selector.toString().includes("isLoading"))
return state.isAuthLoading;
return state.user;
};
return { useAuthStore };
});
vi.mock("@multica/core/platform", () => ({
setCurrentWorkspace: vi.fn(),
}));
vi.mock("@multica/core/workspace", async () => {
const actual = await vi.importActual<typeof import("@multica/core/workspace")>(
"@multica/core/workspace",
);
return {
...actual,
workspaceBySlugOptions: () => ({
queryKey: ["workspace-by-slug"],
queryFn: async () => state.workspace,
}),
workspaceListOptions: () => ({
queryKey: ["workspace-list"],
queryFn: async () => state.wsList,
}),
};
});
vi.mock("@multica/core/paths", async () => {
const actual = await vi.importActual<typeof import("@multica/core/paths")>(
"@multica/core/paths",
);
return {
...actual,
WorkspaceSlugProvider: ({ children }: { children: React.ReactNode }) => (
<>{children}</>
),
paths: {
...actual.paths,
login: () => "/login",
},
};
});
vi.mock("@multica/views/workspace/use-workspace-seen", () => ({
useWorkspaceSeen: () => state.workspaceSeen,
}));
vi.mock("@multica/views/workspace/welcome-after-onboarding", () => ({
WelcomeAfterOnboarding: () => null,
}));
vi.mock("@multica/views/layout", () => ({
WorkspacePresencePrefetch: () => null,
}));
// The point of this whole test: assert the desktop layout mounts the
// SourceBackfillModal. We stub the real component with a marker that
// renders only when the layout actually rendered it (and not e.g.
// suppressed by overlayActive).
vi.mock("@multica/views/onboarding", () => ({
SourceBackfillModal: () => {
state.modalRenders += 1;
return <div data-testid={state.modalAriaLabel} />;
},
}));
vi.mock("@/stores/tab-store", () => ({
useTabStore: Object.assign(() => null, {
getState: () => ({ validateWorkspaceSlugs: vi.fn() }),
}),
}));
vi.mock("@/stores/window-overlay-store", () => {
const useWindowOverlayStore = (selector: (s: typeof state) => unknown) =>
selector(state);
return { useWindowOverlayStore };
});
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WorkspaceRouteLayout } from "./workspace-route-layout";
function renderLayout() {
const qc = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
});
// Seed the workspace queries so the gate inside the layout passes
// synchronously — the real hook reads from cache.
qc.setQueryData(["workspace-by-slug"], state.workspace);
qc.setQueryData(["workspace-list"], state.wsList);
return render(
<QueryClientProvider client={qc}>
<MemoryRouter initialEntries={["/acme/issues"]}>
<Routes>
<Route path=":workspaceSlug/*" element={<WorkspaceRouteLayout />}>
<Route path="*" element={<div data-testid="outlet" />} />
</Route>
</Routes>
</MemoryRouter>
</QueryClientProvider>,
);
}
beforeEach(() => {
state.user = { id: "u1" };
state.isAuthLoading = false;
state.overlay = null;
state.workspace = { id: "ws-1", slug: "acme" };
state.listFetched = true;
state.wsList = [{ id: "ws-1", slug: "acme" }];
state.workspaceSeen = true;
state.modalRenders = 0;
});
describe("WorkspaceRouteLayout", () => {
it("mounts SourceBackfillModal when no WindowOverlay is active", () => {
const { queryByTestId } = renderLayout();
expect(queryByTestId(state.modalAriaLabel)).not.toBeNull();
expect(state.modalRenders).toBeGreaterThan(0);
});
it("suppresses SourceBackfillModal while a WindowOverlay is active", () => {
state.overlay = { type: "new-workspace" };
const { queryByTestId } = renderLayout();
expect(queryByTestId(state.modalAriaLabel)).toBeNull();
expect(state.modalRenders).toBe(0);
});
});

View File

@@ -11,6 +11,7 @@ import { useAuthStore } from "@multica/core/auth";
import { useWorkspaceSeen } from "@multica/views/workspace/use-workspace-seen";
import { WelcomeAfterOnboarding } from "@multica/views/workspace/welcome-after-onboarding";
import { WorkspacePresencePrefetch } from "@multica/views/layout";
import { SourceBackfillModal } from "@multica/views/onboarding";
import { useTabStore } from "@/stores/tab-store";
import { useWindowOverlayStore } from "@/stores/window-overlay-store";
@@ -104,6 +105,13 @@ export function WorkspaceRouteLayout() {
* Modal — unless the store signal has already been consumed, in
* which case the hook renders null. */}
{!overlayActive && <WelcomeAfterOnboarding />}
{/* Source-attribution backfill: same Dialog the web shell mounts
* inside DashboardLayout. Desktop's WorkspaceRouteLayout doesn't
* wrap DashboardLayout, so the modal has to be wired in directly
* here. Same overlay-suppression rule as WelcomeAfterOnboarding —
* a portal-rendered Dialog at z-50 would otherwise sit above an
* active pre-workspace overlay. */}
{!overlayActive && <SourceBackfillModal />}
</WorkspaceSlugProvider>
);
}

View File

@@ -0,0 +1,58 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const chineseFonts = ["PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"];
const koreanFonts = ["Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR"];
const japaneseFonts = ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP"];
function expectChineseFontsBeforeKoreanFonts(source: string) {
const chineseIndexes = chineseFonts.map((font) => source.indexOf(font));
const koreanIndexes = koreanFonts.map((font) => source.indexOf(font));
expect(chineseIndexes).not.toContain(-1);
expect(koreanIndexes).not.toContain(-1);
for (const chineseIndex of chineseIndexes) {
for (const koreanIndex of koreanIndexes) {
expect(chineseIndex).toBeLessThan(koreanIndex);
}
}
}
// Japanese Kanji share the Han Unicode block with Chinese, so the global
// Chinese-first stack must stay Chinese-first (no zh regression) while a
// Japanese-first CJK stack is scoped to html[lang|="ja"]. App.tsx syncs
// document.documentElement.lang so the selector matches at runtime.
function expectJapaneseScopedOverride(source: string) {
expect(source).toContain('html[lang|="ja"]');
const japaneseIndexes = japaneseFonts.map((font) => source.indexOf(font));
expect(japaneseIndexes).not.toContain(-1);
const firstJapanese = Math.min(...japaneseIndexes);
const lastChinese = Math.max(
...chineseFonts.map((font) => source.lastIndexOf(font)),
);
expect(firstJapanese).toBeLessThan(lastChinese);
}
describe("CJK font fallback order", () => {
it("keeps desktop Chinese font fallbacks before Korean font fallbacks", () => {
const desktopCss = readFileSync(
resolve(process.cwd(), "src/renderer/src/globals.css"),
"utf8",
);
expectChineseFontsBeforeKoreanFonts(desktopCss);
});
it("scopes the Japanese-first CJK stack to html[lang|='ja']", () => {
const desktopCss = readFileSync(
resolve(process.cwd(), "src/renderer/src/globals.css"),
"utf8",
);
expectJapaneseScopedOverride(desktopCss);
});
});

View File

@@ -6,16 +6,15 @@
@custom-variant dark (&:is(.dark *));
/* Font stack: Inter for Latin UI text + system Chinese fonts for zh content.
/* Font stack: Inter for Latin UI text + system CJK fonts for localized content.
Web app uses the same stack via next/font/google in apps/web/app/layout.tsx —
keep the CJK fallback tail in sync across both files. The Inter primary family
differs by design: next/font produces `__Inter_xxx` (with a synthetic size-adjusted
fallback face to prevent FOUT layout shift); desktop uses fontsource's "Inter Variable".
Both resolve to Inter glyphs, so rendering is identical in practice.
Currently covers English + Simplified Chinese. When ja/ko i18n lands, extend
the tail with Hiragino Kaku Gothic ProN / Yu Gothic / Apple SD Gothic Neo / Malgun Gothic.
Per-character fallback: Latin chars render with Inter, Chinese chars with
PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans CJK SC (Linux).
Per-character fallback: Latin chars render with Inter, CJK chars render with
the platform-native Chinese/Korean fallback when needed. Chinese fonts must stay
before Korean fonts so zh users do not receive Korean Hanja glyph shapes.
Mono font has no explicit CJK fallback: CJK chars in code blocks are inherently
non-aligned with a mono grid (Chinese is proportional), so listing CJK fonts
@@ -23,14 +22,35 @@
the rare mixed case correctly. */
:root {
--font-sans: "Inter Variable", "Inter", -apple-system, BlinkMacSystemFont,
"Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC",
sans-serif;
"Segoe UI", "PingFang SC", "Microsoft YaHei",
"Noto Sans CJK SC", "Apple SD Gothic Neo", "Malgun Gothic",
"Noto Sans CJK KR", sans-serif;
--font-serif: "Source Serif 4 Variable", "Source Serif 4", "Iowan Old Style",
"Apple Garamond", Baskerville, "Times New Roman", serif;
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
monospace;
}
/* Japanese-scoped CJK override. Japanese Kanji share the Han Unicode block
with Chinese, and CSS font-fallback order is not changed by <html lang> —
so the global Chinese-first stack above would give Japanese users Chinese
glyph shapes for shared ideographs. We keep the global stack Chinese-first
(no regression for zh users) and promote Japanese fonts ahead of the
Chinese/Korean families only when the locale is Japanese. App.tsx syncs
document.documentElement.lang to the active locale so this selector
matches. Mirrors the lang-scoped override in apps/web/app/layout.tsx.
`[lang|="ja"]` is the BCP-47 language-range selector: it matches exactly
`ja` or `ja-<region>` (App.tsx sets `ja-JP`), never unrelated subtags
such as `jam`. */
html[lang|="ja"] {
--font-sans: "Inter Variable", "Inter", "Hiragino Sans",
"Hiragino Kaku Gothic ProN", "Yu Gothic", "YuGothic", "Meiryo",
"Noto Sans CJK JP", "Noto Sans JP", -apple-system,
BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei",
"Noto Sans CJK SC", "Apple SD Gothic Neo", "Malgun Gothic",
"Noto Sans CJK KR", sans-serif;
}
@source "../../../../../packages/ui/**/*.tsx";
@source "../../../../../packages/core/**/*.{ts,tsx}";
@source "../../../../../packages/views/**/*.{ts,tsx}";

View File

@@ -1,7 +1,6 @@
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { IssueDetail } from "@multica/views/issues/components";
import { ErrorBoundary } from "@multica/ui/components/common/error-boundary";
import { useWorkspaceId } from "@multica/core/hooks";
import { issueDetailOptions } from "@multica/core/issues/queries";
import { useDocumentTitle } from "@/hooks/use-document-title";
@@ -14,9 +13,8 @@ export function IssueDetailPage() {
useDocumentTitle(issue ? `${issue.identifier}: ${issue.title}` : "Issue");
if (!id) return null;
return (
<ErrorBoundary resetKeys={[id]}>
<IssueDetail issueId={id} />
</ErrorBoundary>
);
// Render errors bubble to the root route errorElement (DesktopRouteErrorPage),
// which contains the crash inside the tab content pane. No page-level boundary
// here — a whole-page wrapper duplicates the route-level error UI.
return <IssueDetail issueId={id} />;
}

View File

@@ -21,16 +21,16 @@ import { AutopilotsPage } from "@multica/views/autopilots/components";
import { MyIssuesPage } from "@multica/views/my-issues";
import { SkillsPage } from "@multica/views/skills";
import { DesktopRuntimesPage } from "./components/desktop-runtimes-page";
import { AgentsPage } from "@multica/views/agents";
import { DesktopAgentsPage } from "./components/desktop-agents-page";
import { SquadsPage, SquadDetailPage as SquadDetailPageView } from "@multica/views/squads/components";
import { InboxPage } from "@multica/views/inbox";
import { SettingsPage } from "@multica/views/settings";
import { useT } from "@multica/views/i18n";
import { ErrorBoundary } from "@multica/ui/components/common/error-boundary";
import { Download, Server } from "lucide-react";
import { DaemonSettingsTab } from "./components/daemon-settings-tab";
import { UpdatesSettingsTab } from "./components/updates-settings-tab";
import { WorkspaceRouteLayout } from "./components/workspace-route-layout";
import { DesktopRouteErrorPage } from "./components/route-error-page";
/**
* Wraps `SettingsPage` so the desktop-only extra tabs can pull their labels
@@ -109,6 +109,7 @@ function PageShell() {
export const appRoutes: RouteObject[] = [
{
element: <PageShell />,
errorElement: <DesktopRouteErrorPage />,
children: [
{ index: true, element: null },
{
@@ -118,11 +119,7 @@ export const appRoutes: RouteObject[] = [
{ index: true, element: <Navigate to="issues" replace /> },
{
path: "issues",
element: (
<ErrorBoundary>
<IssuesPage />
</ErrorBoundary>
),
element: <IssuesPage />,
handle: { title: "Issues" },
},
{
@@ -171,7 +168,7 @@ export const appRoutes: RouteObject[] = [
element: <SkillDetailPage />,
handle: { title: "Skill" },
},
{ path: "agents", element: <AgentsPage />, handle: { title: "Agents" } },
{ path: "agents", element: <DesktopAgentsPage />, handle: { title: "Agents" } },
{
path: "agents/:id",
element: <AgentDetailPage />,

View File

@@ -259,6 +259,47 @@ describe("useTabStore actions", () => {
expect(s.activeWorkspaceSlug).toBeNull();
});
it("validateWorkspaceSlugs seeds the first valid workspace when no group exists", () => {
const store = useTabStore.getState();
store.validateWorkspaceSlugs(new Set(["acme", "butter"]));
const s = useTabStore.getState();
expect(s.activeWorkspaceSlug).toBe("acme");
expect(s.byWorkspace.acme.tabs).toHaveLength(1);
expect(s.byWorkspace.acme.tabs[0].path).toBe("/acme/issues");
});
it("validateWorkspaceSlugs reactivates an existing valid group before seeding", () => {
const store = useTabStore.getState();
store.switchWorkspace("acme");
const existingTabId = useTabStore.getState().byWorkspace.acme.tabs[0].id;
useTabStore.setState({ activeWorkspaceSlug: null });
store.validateWorkspaceSlugs(new Set(["acme"]));
const s = useTabStore.getState();
expect(s.activeWorkspaceSlug).toBe("acme");
expect(s.byWorkspace.acme.tabs).toHaveLength(1);
expect(s.byWorkspace.acme.tabs[0].id).toBe(existingTabId);
});
it("validateWorkspaceSlugs seeds a fresh tab for a valid slug after dropping all stale groups", () => {
const store = useTabStore.getState();
// The only persisted group points at a workspace the user has lost access
// to — the stale-tab heal path WorkspaceRouteLayout drives.
store.switchWorkspace("stale");
const staleRouter = useTabStore.getState().byWorkspace.stale.tabs[0].router;
store.validateWorkspaceSlugs(new Set(["acme"]));
const s = useTabStore.getState();
expect(Object.keys(s.byWorkspace)).toEqual(["acme"]);
expect(s.activeWorkspaceSlug).toBe("acme");
expect(s.byWorkspace.acme.tabs).toHaveLength(1);
expect(s.byWorkspace.acme.tabs[0].path).toBe("/acme/issues");
// The dropped stale group's router must be disposed, not leaked.
expect(staleRouter.dispose).toHaveBeenCalled();
});
it("reset wipes the whole store", () => {
const store = useTabStore.getState();
store.switchWorkspace("acme");

View File

@@ -86,6 +86,16 @@ interface TabStore {
updateTab: (tabId: string, patch: Partial<Pick<Tab, "path" | "title" | "icon">>) => void;
/** Patch history tracking of a tab. Finds across groups. */
updateTabHistory: (tabId: string, historyIndex: number, historyLength: number) => void;
/** Recreate the active tab's router at the same path after a route-level crash. */
reloadActiveTab: () => void;
/**
* Close the active tab. The always-safe escape from a route-level crash:
* unlike reloadActiveTab (recreates the same crashing path) or navigating
* to a "safe" route (which may itself be the route that crashed), closing
* destroys the crashing router entirely and falls back to a sibling tab
* (or a reseeded default if it was the last tab).
*/
closeActiveTab: () => void;
/**
* Reorder within the active workspace's group only. Clamped so a tab can
* never cross the pinned / unpinned boundary — a drag that would move a
@@ -475,6 +485,38 @@ export const useTabStore = create<TabStore>()(
});
},
reloadActiveTab() {
const { activeWorkspaceSlug, byWorkspace } = get();
if (!activeWorkspaceSlug) return;
const group = byWorkspace[activeWorkspaceSlug];
if (!group) return;
const index = group.tabs.findIndex((t) => t.id === group.activeTabId);
if (index < 0) return;
const current = group.tabs[index];
const nextTabs = [...group.tabs];
nextTabs[index] = {
...current,
router: createTabRouter(current.path),
historyIndex: 0,
historyLength: 1,
};
set({
byWorkspace: {
...byWorkspace,
[activeWorkspaceSlug]: { ...group, tabs: nextTabs },
},
});
window.setTimeout(() => current.router.dispose(), 0);
},
closeActiveTab() {
const { activeWorkspaceSlug, byWorkspace, closeTab } = get();
if (!activeWorkspaceSlug) return;
const group = byWorkspace[activeWorkspaceSlug];
if (!group) return;
closeTab(group.activeTabId);
},
moveTab(fromIndex, toIndex) {
if (fromIndex === toIndex) return;
const { activeWorkspaceSlug, byWorkspace } = get();
@@ -557,6 +599,24 @@ export const useTabStore = create<TabStore>()(
changed = true;
}
if (!nextActive) {
nextActive = Object.keys(nextByWorkspace)[0] ?? null;
if (nextActive) changed = true;
}
if (!nextActive) {
const fallbackSlug = validSlugs.values().next().value;
if (fallbackSlug) {
const fresh = defaultTabFor(fallbackSlug);
nextByWorkspace[fallbackSlug] = {
tabs: [fresh],
activeTabId: fresh.id,
};
nextActive = fallbackSlug;
changed = true;
}
}
if (!changed) return;
set({ byWorkspace: nextByWorkspace, activeWorkspaceSlug: nextActive });
},

View File

@@ -11,6 +11,7 @@ import type { Metadata } from "next";
import { docsAlternates } from "@/lib/site";
import { i18n, type Lang } from "@/lib/i18n";
import { DocsLocaleProvider, LocaleLink } from "@/components/locale-link";
import { docsSlugStaticParams } from "@/lib/static-params";
function asLang(lang: string): Lang {
return (i18n.languages as readonly string[]).includes(lang)
@@ -22,11 +23,11 @@ export default async function Page(props: {
params: Promise<{ lang: string; slug: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug, params.lang);
const lang = asLang(params.lang);
const page = source.getPage(params.slug, lang);
if (!page) notFound();
const MDX = page.data.body;
const lang = asLang(params.lang);
return (
<DocsPage toc={page.data.toc}>
@@ -42,14 +43,15 @@ export default async function Page(props: {
}
export function generateStaticParams() {
return source.generateParams().filter((p) => p.slug.length > 0);
return docsSlugStaticParams(source.generateParams());
}
export async function generateMetadata(props: {
params: Promise<{ lang: string; slug: string[] }>;
}): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug, params.lang);
const lang = asLang(params.lang);
const page = source.getPage(params.slug, lang);
if (!page) notFound();
return {

View File

@@ -11,18 +11,13 @@ import { i18n, type Lang } from "@/lib/i18n";
import { uiTranslations, localeLabels } from "@/lib/translations";
import { DocsSettings } from "@/components/docs-settings";
// Inter (Latin UI face) is exposed under `--font-inter`. The full `--font-sans`
// stack — Inter + the per-locale CJK fallback chain, including the Japanese-first
// override scoped to `<html lang="ja">` — is composed in static CSS in
// ./global.css (CSP-safe, no inline <style>). Mirrors apps/web/app/layout.tsx.
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
fallback: [
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"PingFang SC",
"Microsoft YaHei",
"Noto Sans CJK SC",
"sans-serif",
],
variable: "--font-inter",
});
const geistMono = Geist_Mono({

View File

@@ -17,8 +17,42 @@ function tokenizeCJK(raw: string): string[] {
return tokens;
}
// Japanese mixes Hiragana, Katakana and Kanji; the English regex strips them
// all, and the zh tokenizer only keeps Han (Kanji), dropping kana entirely.
// Tokenize each kana/Kanji codepoint on its own and keep Latin/digit runs
// whole — same character-level recall strategy as tokenizeCJK, extended to
// the Hiragana (\u3040-\u309f) and Katakana (\u30a0-\u30ff) blocks, plus the
// ideographic iteration mark \u3005 which sits just below the kana blocks and
// recurs in common words (e.g. the JP for "various", "daily", "individual").
function tokenizeJapanese(raw: string): string[] {
const tokens: string[] = [];
const regex = /[\u3005\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff]|[A-Za-z0-9]+/g;
const lower = raw.toLowerCase();
let match: RegExpExecArray | null;
while ((match = regex.exec(lower)) !== null) {
tokens.push(match[0]);
}
return tokens;
}
export const { GET } = createFromSource(source, {
localeMap: {
ko: {
components: {
tokenizer: {
language: "english",
},
},
},
ja: {
components: {
tokenizer: {
language: "english",
normalizationCache: new Map(),
tokenize: tokenizeJapanese,
},
},
},
zh: {
components: {
tokenizer: {

View File

@@ -0,0 +1,57 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const chineseFonts = ["PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"];
const koreanFonts = ["Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR"];
const japaneseFonts = ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP"];
function expectChineseFontsBeforeKoreanFonts(source: string) {
const chineseIndexes = chineseFonts.map((font) => source.indexOf(font));
const koreanIndexes = koreanFonts.map((font) => source.indexOf(font));
expect(chineseIndexes).not.toContain(-1);
expect(koreanIndexes).not.toContain(-1);
for (const chineseIndex of chineseIndexes) {
for (const koreanIndex of koreanIndexes) {
expect(chineseIndex).toBeLessThan(koreanIndex);
}
}
}
// Japanese Kanji share the Han Unicode block with Chinese, so the docs
// Japanese-first CJK stack must be scoped to html[lang|="ja"] (zh/en keep
// Chinese-first) and order Japanese fonts before the Chinese families.
function expectJapaneseScopedOverride(source: string) {
expect(source).toContain('html[lang|="ja"]');
const japaneseIndexes = japaneseFonts.map((font) => source.indexOf(font));
expect(japaneseIndexes).not.toContain(-1);
const firstJapanese = Math.min(...japaneseIndexes);
const lastChinese = Math.max(
...chineseFonts.map((font) => source.lastIndexOf(font)),
);
expect(firstJapanese).toBeLessThan(lastChinese);
}
describe("CJK font fallback order", () => {
it("keeps docs Chinese font fallbacks before Korean font fallbacks", () => {
const cssSource = readFileSync(
resolve(process.cwd(), "app/global.css"),
"utf8",
);
expectChineseFontsBeforeKoreanFonts(cssSource);
});
it("scopes the Japanese-first CJK stack to html[lang|='ja']", () => {
const cssSource = readFileSync(
resolve(process.cwd(), "app/global.css"),
"utf8",
);
expectJapaneseScopedOverride(cssSource);
});
});

View File

@@ -6,6 +6,36 @@
@source "../../../packages/ui/**/*.{ts,tsx}";
/* ---------------------------------------------------------------------------
* Font stack. `--font-inter` is the next/font Inter family (+ synthetic
* size-adjusted fallback), set on <html> by inter.variable in app/[lang]/layout.tsx.
* `--font-sans` is composed here in static CSS so it can be overridden per
* `<html lang>` and stays CSP-safe (no inline <style>). Tailwind's `font-sans`
* utility resolves `var(--font-sans)`. Mirrors apps/web/app/globals.css.
*
* Default (en / zh / ko): Latin → Inter, CJK → Chinese then Korean. Chinese MUST
* stay before Korean so zh users don't get Korean Hanja glyph shapes.
* ------------------------------------------------------------------------- */
:root {
--font-sans: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Apple SD Gothic Neo",
"Malgun Gothic", "Noto Sans CJK KR", sans-serif;
}
/* Japanese: Kanji share the Han Unicode block with Chinese and CSS fallback
order is not affected by `<html lang>`, so promote a Japanese-first CJK chain
only for Japanese docs (`<html lang="ja">`). `[lang|="ja"]` is the BCP-47
language-range selector — matches exactly `ja` or `ja-<region>`, never
unrelated subtags like `jam`. Inter still leads for Latin. */
html[lang|="ja"] {
--font-sans: var(--font-inter), "Hiragino Sans", "Hiragino Kaku Gothic ProN",
"Yu Gothic", "YuGothic", "Meiryo", "Noto Sans CJK JP", "Noto Sans JP",
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", "Noto Sans CJK SC", "Apple SD Gothic Neo", "Malgun Gothic",
"Noto Sans CJK KR", sans-serif;
}
/* ---------------------------------------------------------------------------
* Multica Docs — editorial visual identity (v2)
*

View File

@@ -0,0 +1,127 @@
---
title: エージェントの作成と構成
description: エージェントを作成するために必要な最小限のフィールドと、すべての任意設定 — システム指示、環境変数、公開範囲、同時実行制限、アーカイブ。
---
import { Callout } from "fumadocs-ui/components/callout";
[エージェント](/agents)を作成するのに必要なのは 2 つだけです。**名前**と **[AI コーディングツール](/providers)の選択**です。それ以外はすべて任意です — システム指示、モデル、環境変数、CLI 引数、公開範囲、同時実行制限 — デフォルト値でも問題なく動作します。まず動かしてから後で調整しましょう。すべてのフィールドはいつでも変更できます。
## エージェントを作成する
前提条件: 使用中のマシンにサポートされている [AI コーディングツール](/providers)が少なくとも 1 つインストールされておりClaude Code、Codex など)、[デーモン](/daemon-runtimes)が実行中であること。まだそこまで準備できていない場合は、[Cloud クイックスタート](/cloud-quickstart)または[セルフホストクイックスタート](/self-host-quickstart)から始めてください。
準備が整ったら、ワークスペースの **Agents** ページに移動して **+ New** をクリックするか、CLI を使用します。
```bash
multica agent create
```
このフォームには必須フィールドが 2 つだけあります。**name**(ワークスペース内で一意であること)と **runtime**= AI コーディングツールの選択)です。それ以外のすべてのフィールドは、以下でセクションごとに扱います。
## AI コーディングツールを選ぶ
各ランタイムは特定の AI コーディングツールを基盤としています。Multica はそのうち 12 個をサポートします。最も一般的な選択肢は次のとおりです。
| ツール | 適している場合 |
|---|---|
| **Claude Code** | Anthropic の公式ツールで、最も完成度の高い機能セットを提供します。**最初の選択として最適です** |
| **Codex** | OpenAI 製で、主流の代替手段です |
| **Cursor** | Cursor エディターのエコシステムを使うユーザー |
| **Copilot** | GitHub アカウントの権限を活用するチーム |
| **Gemini** | Google エコシステムのユーザー |
残りの 7 個Antigravity、Hermes、Kimi、Kiro CLI、OpenCode、Pi、OpenClawと、各ツールの完全な機能比較表セッション再開、MCP、スキル注入パス、モデル選択は、[AI コーディングツール比較](/providers)で扱います。
## システム指示を書く
**システム指示**`instructions`)はすべてのタスクの先頭に追加され、エージェントがどんな役割を担い、どんなルールに従うべきかを伝えます。
```text
You're a frontend code-review agent. When an issue comes in, read the diff first. Focus only on:
- Styling issues (tailwind class names, box model)
- Accessibility (a11y)
Don't change code — leave suggestions in a comment.
```
空のままにすると(デフォルト)、エージェントは追加の制約なしに、基盤となる AI コーディングツールのネイティブな動作を使用します。
## モデルを選ぶ
ほとんどの AI コーディングツールはモデル選択をサポートしています(例えば Claude Code では Sonnet と Opus のどちらかを選べます)。空のままにするとツール自体のデフォルト値が使われ、明示的に 1 つを選ぶとそのモデルが実行されます。各ツールがサポートするモデルは、[AI コーディングツール比較](/providers)にまとめられています。
モデルの変更は**新しいタスクにのみ適用されます**。すでにディスパッチされたタスクは、ディスパッチ時点で固定されたモデルで実行を続けます。
## カスタム環境変数 (custom_env)
**カスタム環境変数**`custom_env`)を使うと、タスク実行時に追加の環境変数を注入できます。代表的な用途は API キーの設定やアップストリームエンドポイントの切り替えです。
```
ANTHROPIC_API_KEY = sk-...
ANTHROPIC_BASE_URL = https://my-proxy.example.com
```
システムにとって重要な変数は上書きできません。`PATH`、`HOME`、`USER`、`SHELL`、`TERM`、`CODEX_HOME`、そして `MULTICA_*` で始まるすべてのキーは、デーモンが静かに無視します(警告ログは残しますが、エラーは発生しません)。
<Callout type="warning">
**`custom_env` の値は Multica サーバーのデータベースに平文で保存されます。** エージェントの list/get レスポンスには環境変数の値がまったく含まれなくなり、不透明な個数だけが返されます。実際の値を読み取るには、ワークスペースの owner または admin が、専用で監査される `GET /api/agents/{id}/env` エンドポイントCLI: `multica agent env get <id>`)を呼び出す必要があります。タスクを実行中のエージェントは、ホストの owner 資格情報を使って他のエージェントの環境変数を明らかにすることはできません。このエンドポイントはエージェントアクターのセッションを拒否します。
**価値の高いシークレットは `custom_env` に入れないでください**本番データベースのパスワード、root レベルのトークンなど)。エージェントには**権限範囲が限定された専用の資格情報**(読み取り専用 API キー、単一スコープの PATを使用し、定期的にローテーションしてください。データベースのバックアップと DB 監査は、依然として意味のある露出面として残ります。
</Callout>
## カスタム CLI 引数 (custom_args)
**カスタム CLI 引数**`custom_args`は、AI コーディングツールのコマンドラインに 1 つずつ順に付け足される文字列配列です。
```json
["--max-turns", "100", "--append-system-prompt", "always respond in Chinese"]
```
最終的なコマンドは次のように生成されます。
```bash
claude --model <model> --max-turns 100 --append-system-prompt "always respond in Chinese" [...]
```
引数はシェルを介さずそのまま渡されるため(注入リスクなし)、特定のフラグが認識されるかどうかは AI コーディングツール自体に依存します。この部分はツールによって大きな差があります。
<Callout type="tip">
`custom_env` と `custom_args` には厳格な上限はありませんが、実際には**それぞれ 10 個以内に抑えてください**。多すぎるとコマンドラインが長くなり、起動が遅くなり、メンテナンスも難しくなります。
</Callout>
## 公開範囲
- **ワークスペース**`workspace` — ワークスペースのすべてのメンバーが割り当てできます
- **非公開**`private` — ワークスペースの owner、admin、またはエージェントの作成者だけが割り当てできます
新しいエージェントはデフォルトで `private` です。
**非公開だからといって隠されるわけではありません** — すべてのメンバーが一覧で非公開エージェントの名前と説明を見ることができ、ただし機微な構成は読み取れません(環境変数の値はエージェントの list/get レスポンスに決して現れず、MCP 構成は owner 以外のユーザーにはマスキングされます)。詳しい意味は[エージェント → 誰がエージェントを割り当てられるか](/agents#who-can-assign-an-agent)を参照してください。
## 同時実行制限
**同時実行制限**`max_concurrent_tasks`)は、このエージェントが一度に並列で実行できるタスク数を制御します。デフォルト値は **6** です。上限に達した新しいタスクは拒否されず、キューで待機します。
これは 2 段階の制限のうち「エージェント層」にすぎません。デーモン自体がより広い上限(デフォルト値 20を適用し、2 つのうちより厳しい方が優先されます。詳しくは[デーモンとランタイム → 並列で何個のタスクを実行できるか](/daemon-runtimes#how-many-tasks-can-run-in-parallel)にあります。
この値を変更しても**すでに実行中のタスクはキャンセルされず**、次に処理されるタスクからのみ適用されます。
## ドメインの専門性をつなぐ: スキル
作成したエージェントには**スキル**をアタッチできます — タスク実行時に AI コーディングツールへ自動的に届けられる**ナレッジパック**`SKILL.md` + 補助ファイルです。新しいスキルを作成したり、GitHub または ClawHub からインポートしたり、マシン上の既存のスキルディレクトリからスキャンしたりできます。[スキル](/skills)を参照してください。
## アーカイブと復元
もう使わないエージェントは**アーカイブ**できます — 日常的な画面からは消えますが、履歴データ(実行したタスク、投稿したコメント)はすべてそのまま保持されます。いつでも**復元**して再び作業に投入できます。
<Callout type="warning">
**アーカイブは、そのエージェントに属する未完了のすべてのタスクを即座にキャンセルします** — 実行中、ディスパッチ済み、キュー待ちのタスクがすべて `cancelled` としてマークされ、続行されません。進行中の重要なタスクがある場合は、アーカイブする前に最後まで完了させてください。
</Callout>
アーカイブ済みのエージェントには新しいタスクを割り当てられません。
## 次のステップ
- [スキル](/skills) — エージェントにナレッジパックをアタッチする
- [AI コーディングツール比較](/providers) — 12 個のツール全体の機能比較表
- [エージェントへのイシューの割り当て](/assigning-issues) — 新しく作ったエージェントを作業に投入する

View File

@@ -0,0 +1,127 @@
---
title: 에이전트 생성 및 구성
description: 에이전트를 생성하는 데 필요한 최소 필드와 모든 선택적 설정 — 시스템 지침, 환경 변수, 공개 범위, 동시 실행 제한, 보관.
---
import { Callout } from "fumadocs-ui/components/callout";
[에이전트](/agents)를 생성하는 데는 단 두 가지만 필요합니다. **이름** 하나와 **[AI 코딩 도구](/providers) 선택** 하나입니다. 나머지는 모두 선택 사항입니다 — 시스템 지침, 모델, 환경 변수, CLI 인자, 공개 범위, 동시 실행 제한 — 기본값으로도 문제없이 작동합니다. 먼저 실행해 보고 나중에 조정하세요. 모든 필드는 언제든지 변경할 수 있습니다.
## 에이전트 생성
전제 조건: 사용 중인 기기에 지원되는 [AI 코딩 도구](/providers)가 최소 하나는 설치되어 있고(Claude Code, Codex 등) [데몬](/daemon-runtimes)이 실행 중이어야 합니다. 아직 여기까지 준비되지 않았다면 [Cloud 빠른 시작](/cloud-quickstart)이나 [자체 호스팅 빠른 시작](/self-host-quickstart)부터 시작하세요.
준비가 끝나면 워크스페이스의 **에이전트** 페이지로 이동해 **+ New**를 클릭하거나 CLI를 사용하세요.
```bash
multica agent create
```
이 폼에는 필수 필드가 두 개뿐입니다. **이름**(워크스페이스 내에서 고유해야 함)과 **런타임**(= AI 코딩 도구 선택)입니다. 나머지 모든 필드는 아래에서 섹션별로 다룹니다.
## AI 코딩 도구 선택
각 런타임은 특정 AI 코딩 도구를 기반으로 합니다. Multica는 그중 12개를 지원합니다. 가장 일반적인 선택지는 다음과 같습니다.
| 도구 | 적합한 경우 |
|---|---|
| **Claude Code** | Anthropic의 공식 도구로, 가장 완성도 높은 기능 집합을 제공합니다. **첫 선택으로 가장 좋습니다** |
| **Codex** | OpenAI 제품으로, 주류 대안입니다 |
| **Cursor** | Cursor 에디터 생태계 사용자 |
| **Copilot** | GitHub 계정 권한을 활용하는 팀 |
| **Gemini** | Google 생태계 사용자 |
나머지 7개(Antigravity, Hermes, Kimi, Kiro CLI, OpenCode, Pi, OpenClaw)와 각 도구의 전체 기능 비교표(세션 재개, MCP, 스킬 주입 경로, 모델 선택)는 [AI 코딩 도구 비교](/providers)에서 다룹니다.
## 시스템 지침 작성
**시스템 지침**(`instructions`)은 모든 작업 앞에 추가되어, 에이전트가 어떤 역할을 맡고 어떤 규칙을 따라야 하는지 알려줍니다.
```text
You're a frontend code-review agent. When an issue comes in, read the diff first. Focus only on:
- Styling issues (tailwind class names, box model)
- Accessibility (a11y)
Don't change code — leave suggestions in a comment.
```
비워 두면(기본값) 에이전트는 추가 제약 없이 기반이 되는 AI 코딩 도구의 기본 동작을 사용합니다.
## 모델 선택
대부분의 AI 코딩 도구는 모델 선택을 지원합니다(예를 들어 Claude Code는 Sonnet과 Opus 중에서 고를 수 있습니다). 비워 두면 도구 자체의 기본값이 사용되고, 명시적으로 하나를 선택하면 그 모델이 실행됩니다. 각 도구가 지원하는 모델은 [AI 코딩 도구 비교](/providers)에 정리되어 있습니다.
모델 변경은 **새 작업에만 적용됩니다**. 이미 디스패치된 작업은 디스패치 시점에 고정된 모델로 계속 실행됩니다.
## 사용자 지정 환경 변수 (custom_env)
**사용자 지정 환경 변수**(`custom_env`)를 사용하면 작업 실행 시점에 추가 환경 변수를 주입할 수 있습니다. 대표적인 용도는 API 키 설정이나 업스트림 엔드포인트 전환입니다.
```
ANTHROPIC_API_KEY = sk-...
ANTHROPIC_BASE_URL = https://my-proxy.example.com
```
시스템에 핵심적인 변수는 재정의할 수 없습니다. `PATH`, `HOME`, `USER`, `SHELL`, `TERM`, `CODEX_HOME`, 그리고 `MULTICA_*`로 시작하는 모든 키는 데몬이 조용히 무시합니다(경고 로그는 남기지만 오류는 발생하지 않습니다).
<Callout type="warning">
**`custom_env`의 값은 Multica 서버 데이터베이스에 평문으로 저장됩니다.** 에이전트 list/get 응답은 더 이상 환경 변수 값을 전혀 포함하지 않으며, 불투명한 개수만 반환합니다. 실제 값을 읽으려면 워크스페이스 owner 또는 admin이 전용으로 감사되는 `GET /api/agents/{id}/env` 엔드포인트(CLI: `multica agent env get <id>`)를 호출해야 합니다. 작업을 실행 중인 에이전트는 호스트의 owner 자격 증명을 이용해 다른 에이전트의 환경 변수를 드러낼 수 없습니다. 이 엔드포인트는 에이전트 액터 세션을 거부합니다.
**가치가 높은 secret은 `custom_env`에 넣지 마세요**(운영 데이터베이스 비밀번호, root 수준 토큰 등). 에이전트에는 **권한 범위가 제한된 전용 자격 증명**(읽기 전용 API 키, 단일 스코프 PAT)을 사용하고 정기적으로 교체하세요. 데이터베이스 백업과 DB 감사는 여전히 의미 있는 노출 표면으로 남아 있습니다.
</Callout>
## 사용자 지정 CLI 인자 (custom_args)
**사용자 지정 CLI 인자**(`custom_args`)는 AI 코딩 도구의 명령줄에 하나씩 차례로 덧붙는 문자열 배열입니다.
```json
["--max-turns", "100", "--append-system-prompt", "always respond in Chinese"]
```
최종 명령은 다음과 같이 만들어집니다.
```bash
claude --model <model> --max-turns 100 --append-system-prompt "always respond in Chinese" [...]
```
인자는 셸을 거치지 않고 있는 그대로 전달되므로(주입 위험 없음), 특정 플래그가 인식되는지 여부는 AI 코딩 도구 자체에 달려 있습니다. 이 부분은 도구마다 상당한 차이가 있습니다.
<Callout type="tip">
`custom_env`와 `custom_args`에는 엄격한 상한이 없지만, 실제로는 **각각 10개 이내로 유지하세요**. 너무 많으면 명령줄이 길어지고 시작이 느려지며 유지 관리도 어려워집니다.
</Callout>
## 공개 범위
- **워크스페이스**(`workspace`) — 워크스페이스의 모든 멤버가 할당할 수 있습니다
- **비공개**(`private`) — 워크스페이스 owner, admin, 또는 에이전트 생성자만 할당할 수 있습니다
새 에이전트는 기본적으로 `private`입니다.
**비공개라고 해서 숨겨지는 것은 아닙니다** — 모든 멤버가 목록에서 비공개 에이전트의 이름과 설명을 볼 수 있으며, 다만 민감한 구성은 읽을 수 없습니다(환경 변수 값은 에이전트 list/get 응답에 절대 나타나지 않으며, MCP 구성은 owner가 아닌 사용자에게는 마스킹됩니다). 자세한 의미는 [에이전트 → 누가 에이전트를 할당할 수 있나요](/agents#who-can-assign-an-agent)를 참고하세요.
## 동시 실행 제한
**동시 실행 제한**(`max_concurrent_tasks`)은 이 에이전트가 한 번에 병렬로 실행할 수 있는 작업 수를 제어합니다. 기본값은 **6**입니다. 상한에 도달한 새 작업은 거부되지 않고 대기열에서 대기합니다.
이것은 두 단계 제한 중 "에이전트 계층"에 불과합니다. 데몬 자체가 더 넓은 상한(기본값 20)을 적용하며, 둘 중 더 빡빡한 쪽이 우선합니다. 자세한 내용은 [데몬과 런타임 → 병렬로 몇 개의 작업을 실행할 수 있나요](/daemon-runtimes#how-many-tasks-can-run-in-parallel)에 있습니다.
이 값을 변경해도 **이미 실행 중인 작업은 취소되지 않으며**, 다음에 처리될 작업부터만 적용됩니다.
## 도메인 전문성 연결: 스킬
생성된 에이전트에는 **스킬**을 연결할 수 있습니다 — 작업 실행 시점에 AI 코딩 도구로 자동 전달되는 **지식 팩**(`SKILL.md` + 보조 파일)입니다. 새 스킬을 만들거나, GitHub 또는 ClawHub에서 가져오거나, 기기에 있는 기존 스킬 디렉터리에서 스캔할 수 있습니다. [스킬](/skills)을 참고하세요.
## 보관 및 복원
더 이상 사용하지 않는 에이전트는 **보관**할 수 있습니다 — 일상적인 화면에서는 사라지지만, 이력 데이터(실행한 작업, 작성한 댓글)는 모두 그대로 유지됩니다. 언제든지 **복원**하여 다시 작업에 투입할 수 있습니다.
<Callout type="warning">
**보관은 해당 에이전트에 속한 완료되지 않은 모든 작업을 즉시 취소합니다** — 실행 중, 디스패치됨, 대기 중인 작업이 모두 `cancelled`로 표시되며 계속 진행되지 않습니다. 진행 중인 중요한 작업이 있다면 보관하기 전에 끝까지 완료되도록 두세요.
</Callout>
보관된 에이전트에는 새 작업을 할당할 수 없습니다.
## 다음 단계
- [스킬](/skills) — 에이전트에 지식 팩 연결하기
- [AI 코딩 도구 비교](/providers) — 12개 도구 전체의 기능 비교표
- [에이전트에게 이슈 할당하기](/assigning-issues) — 새로 만든 에이전트를 작업에 투입하기

View File

@@ -0,0 +1,49 @@
---
title: エージェント
description: "エージェントは Multica ワークスペースの一級メンバーです — イシューを割り当てられ、コメントを投稿し、@ でメンションされることができます。人間との核心的な違いは、エージェントは自分から作業を始め、通知を受け取らない点です。"
---
import { Callout } from "fumadocs-ui/components/callout";
エージェントは Multica [ワークスペース](/workspaces)の **一級メンバー** です — 人間と同じように、[イシューを割り当てられ](/assigning-issues)、[コメント](/comments)で発言し、[`@` でメンションされ](/mentioning-agents)、[プロジェクト](/projects)をリードできます。核心的な違いはこれです。すべてのエージェントの背後には、あなたのマシンで動作する [AI コーディングツール](/providers)があります。エージェントにタスクを割り当てると、特に促さなくても **数秒以内に自分から作業を始めます** — 急かす必要も、オフラインになることもなく、24時間いつでも利用できます。
## エージェントができること
エージェントは人間と同じ「メンバー」の表面を使っており、UI ではほとんど区別されません。
- **[イシューを割り当てられる](/assigning-issues)** — 担当者に設定された瞬間、自動的に作業を始めます
- **[`@` でメンションされる](/mentioning-agents)** — コメントに `@agent-name` と書くと、目覚めてそのコメントを読みます
- **[コメント](/comments)を投稿する** — イシューの下で進捗を報告し、人々に返信します
- **[プロジェクト](/projects)をリードする** — 人間と同じように、プロジェクトリードに設定できます
- **自分で[イシュー](/issues)を開く** — タスクを実行している間に関連する問題を見つけると、直接新しいイシューを作成できます
協業ビューから見ると、エージェントはただのワークスペースのメンバーです — 人間と同じメンバー一覧に名前が並び、通常はその前に小さなロボットアイコンが付きます。
## 人間との違い
いくつかの重要な違いは、実際にエージェントを使い始めて初めて見えてきます。
- **自分から始めます** — イシューを割り当てたり `@` でメンションしたりすると、Multica が即座にそのタスクをエージェントのランタイムにディスパッチします。人間のようにメッセージを見て応答するまで待つことはありません。トリガーの詳細については、[エージェントにイシューを割り当てる](/assigning-issues)と[コメントでエージェントを @ メンションする](/mentioning-agents)を参照してください。
- **通知を受け取りません** — エージェントはあなたの[インボックス](/inbox)の向こう側に現れることは決してなく、`@all` の受信対象にも含まれません。エージェントは「メッセージを読む受信者」ではなく「タスクを実行するためにトリガーされる作業の単位」です。
- **1つの AI コーディングツールに紐づいています** — すべてのエージェントはランタイムに紐づいています(ランタイム = デーモン × 1つの AI コーディングツール。[デーモンとランタイム](/daemon-runtimes)を参照)。ツールがオフラインだとエージェントは作業できず、新しいタスクはランタイムが戻るまで待機します。
- **アーカイブできます** — もう使わないエージェントをアーカイブすると日常的なビューから消えます。いつでも好きなときに復元できます。アーカイブすると、現在実行中のタスクはすべてキャンセルされます。
## 誰がエージェントを割り当てられるか
エージェントを作成するとき、誰がそのエージェントをイシューに割り当てたりプロジェクトリードに設定したりできるかを制御する **可視性visibility** を選択します。
- **Workspace** — ワークスペースの任意のメンバーが割り当てられます
- **Private** — ワークスペースの owner、admin、またはエージェントの作成者だけが割り当てられます
新しいエージェントはデフォルトで **private** です。ワークスペース全体で利用できるようにするには、作成時に可視性を `workspace` に設定するか、後でエージェントの設定で変更してください。役割と権限の完全なマトリクスについては、[メンバーと役割](/members-roles)を参照してください。
<Callout type="info">
**private は「誰が割り当てられるかを制限する」という意味であって、「他の全員から隠す」という意味ではありません。** ワークスペースのすべてのメンバーは、エージェント一覧で private エージェントの名前と説明を見ることができます — 見えないのは設定の詳細だけですカスタム環境変数、MCP 設定、その他の機密フィールドはマスクされます。「1人だけに見える」ようにしたい場合、現時点では実現できません。
</Callout>
## 次のステップ
- [エージェントの作成と構成](/agents-create) — エージェントを作る方法
- [スキル](/skills) — エージェントに知識パックを添付する
- [スクワッド](/squads) — 適切なエージェントが適切なイシューを担当するよう、リーダーの下にエージェントをグループ化する
- [デーモンとランタイム](/daemon-runtimes) — エージェントが実際に動作するために必要なもの

View File

@@ -0,0 +1,49 @@
---
title: 에이전트
description: "에이전트는 Multica 워크스페이스의 일급 멤버입니다 — 이슈를 할당받고, 댓글을 달고, @로 멘션될 수 있습니다. 사람과의 핵심 차이는, 에이전트는 스스로 작업을 시작하며 알림을 받지 않는다는 점입니다."
---
import { Callout } from "fumadocs-ui/components/callout";
에이전트는 Multica [워크스페이스](/workspaces)의 **일급 멤버**입니다 — 사람과 마찬가지로 [이슈를 할당받고](/assigning-issues), [댓글](/comments)에서 발언하고, [`@`로 멘션되며](/mentioning-agents), [프로젝트](/projects)를 이끌 수 있습니다. 핵심 차이는 이것입니다. 모든 에이전트 뒤에는 여러분의 기기에서 실행되는 [AI 코딩 도구](/providers)가 있습니다. 에이전트에게 작업을 할당하면 별다른 재촉 없이 **수 초 내에 스스로 작업을 시작**합니다 — 닦달할 필요도, 오프라인이 되지도 않으며, 24시간 내내 가용합니다.
## 에이전트가 할 수 있는 일
에이전트는 사람과 동일한 "멤버" 표면을 사용하며, UI에서는 거의 구분되지 않습니다.
- **[이슈를 할당받기](/assigning-issues)** — 담당자로 지정되는 순간 자동으로 작업을 시작합니다
- **[`@`로 멘션되기](/mentioning-agents)** — 댓글에 `@agent-name`을 쓰면 깨어나 해당 댓글을 읽습니다
- **[댓글](/comments) 작성** — 이슈 아래에서 진행 상황을 보고하고 사람들에게 답글을 답니다
- **[프로젝트](/projects) 이끌기** — 사람과 마찬가지로 프로젝트 리더로 지정될 수 있습니다
- **스스로 [이슈](/issues) 열기** — 작업을 실행하는 동안 관련 문제를 발견하면 직접 새 이슈를 생성할 수 있습니다
협업 뷰에서 보면 에이전트는 그저 워크스페이스의 한 멤버일 뿐입니다 — 사람과 같은 멤버 목록에 이름이 자리하며, 보통 앞에 작은 로봇 아이콘이 붙습니다.
## 사람과 다른 점
몇 가지 핵심 차이는 실제로 에이전트를 사용하기 시작해야 비로소 드러납니다.
- **스스로 시작합니다** — 이슈를 할당하거나 `@`로 멘션하면 Multica가 즉시 해당 작업을 에이전트의 런타임에 디스패치합니다. 사람처럼 메시지를 보고 응답할 때까지 기다리지 않습니다. 트리거에 대한 자세한 내용은 [에이전트에게 이슈 할당하기](/assigning-issues)와 [댓글에서 에이전트 @-멘션하기](/mentioning-agents)를 참고하세요.
- **알림을 받지 않습니다** — 에이전트는 여러분의 [인박스](/inbox) 건너편에 결코 나타나지 않으며, `@all`의 수신 대상에도 포함되지 않습니다. 에이전트는 "메시지를 읽는 수신자"가 아니라 "작업을 실행하도록 트리거되는 작업 단위"입니다.
- **하나의 AI 코딩 도구에 묶여 있습니다** — 모든 에이전트는 런타임에 묶여 있습니다(런타임 = 데몬 × 하나의 AI 코딩 도구. [데몬과 런타임](/daemon-runtimes) 참고). 도구가 오프라인이면 에이전트는 작업할 수 없으며, 새 작업은 런타임이 돌아올 때까지 대기합니다.
- **보관할 수 있습니다** — 더 이상 사용하지 않는 에이전트를 보관하면 일상적인 뷰에서 사라지며, 원하면 언제든지 복원할 수 있습니다. 보관하면 현재 실행 중인 작업은 모두 취소됩니다.
## 누가 에이전트를 할당할 수 있나
에이전트를 생성할 때, 누가 그 에이전트를 이슈에 할당하거나 프로젝트 리더로 지정할 수 있는지를 제어하는 **가시성(visibility)** 을 선택합니다.
- **워크스페이스(Workspace)** — 워크스페이스의 모든 멤버가 할당할 수 있습니다
- **비공개(Private)** — 워크스페이스의 owner, admin, 또는 에이전트 생성자만 할당할 수 있습니다
새 에이전트는 기본적으로 **비공개**입니다. 전체 워크스페이스에서 사용할 수 있게 하려면, 생성 시 가시성을 `workspace`로 설정하거나 이후에 에이전트 설정에서 변경하세요. 전체 역할-권한 매트릭스는 [멤버와 역할](/members-roles)을 참고하세요.
<Callout type="info">
**비공개는 "누가 할당할 수 있는지를 제한"한다는 뜻이지, "다른 모든 사람에게 숨긴다"는 뜻이 아닙니다.** 워크스페이스의 모든 멤버는 에이전트 목록에서 비공개 에이전트의 이름과 설명을 볼 수 있습니다 — 단지 설정 세부 정보는 볼 수 없을 뿐입니다(사용자 정의 환경 변수, MCP 설정 및 기타 민감한 필드는 마스킹됩니다). "단 한 사람에게만 보이게" 하고 싶다면, 현재로서는 불가능합니다.
</Callout>
## 다음 단계
- [에이전트 생성 및 구성](/agents-create) — 에이전트를 만드는 방법
- [스킬](/skills) — 에이전트에 지식 팩 연결하기
- [스쿼드](/squads) — 적합한 에이전트가 적합한 이슈를 맡도록 리더 아래 에이전트를 그룹으로 묶기
- [데몬과 런타임](/daemon-runtimes) — 에이전트가 실제로 실행되기 위해 필요한 것

View File

@@ -0,0 +1,83 @@
---
title: エージェントにイシューを割り当てる
description: イシューをエージェントに渡すと、作業が終わるまで公式の担当者として引き継ぎます — 完全なコンテキストを持ち、イシューのステータスやフィールドを変更できます。
---
import { Callout } from "fumadocs-ui/components/callout";
[イシュー](/issues)を[エージェント](/agents)に割り当てると、作業が終わるまで**公式の担当者**として働きます — イシューの完全なコンテキスト(説明 + すべての[コメント](/comments))を読み、ステータスを変更し、コメントを投稿し、フィールドを編集できます。これは Multica の 4 つのトリガー経路の中で**最も一般的で、最も重い**方式です。同じフローは[スクワッド](/squads)を担当者として受け付けることもできます — その場合、Multica は代わりにスクワッドの**リーダーエージェント**をトリガーします。
| 経路 | 使う場面 | イシューの変更 | コンテキスト | 優先度 | 自動リトライ |
|---|---|---|---|---|---|
| **割り当て** | エージェントに所有権を渡す | 担当者を変更 | イシュー + すべてのコメント | イシューから継承 | ✓ |
| [**@メンション**](/mentioning-agents) | ちょっと見てもらうために呼び込む | 変更なし | イシュー + トリガーコメント | イシューから継承 | ✓ |
| [**チャット**](/chat) | イシューと無関係な 1 対 1 の会話 | イシューは関与しない | 現在の会話履歴 | 固定で medium | ✓ |
| [**オートパイロット**](/autopilots) | スケジュールまたは手動の自動化 | モードによる | モードによる | オートパイロットが設定 | ✗ |
「自動リトライ」とは、インフラ障害(ランタイムのオフライン、タイムアウト)後のリトライを指します。エージェント側のビジネスエラー(たとえばモデルがエラーを報告する場合)はリトライされません。詳しくは [**タスク**](/tasks)を参照してください。
## UI から割り当てる
イシュー詳細ページで、**担当者**ピッカーをクリックしてください。ワークスペースのすべてのメンバー、アーカイブされていないすべてのエージェント、アーカイブされていないすべての[スクワッド](/squads)が一覧表示されます。エージェント(またはスクワッド)を選ぶと、イシューはすぐに割り当てられます。
いくつかのルールがあります。
- **ワークスペースエージェント**はどのメンバーでも割り当てられます。**プライベートエージェント**はその owner またはワークスペースの admin のみが割り当てられます。
- **オンラインのランタイムを持つ**エージェントにのみ割り当てられます — 誰も実行していないエージェントはピッカーで利用不可と表示されます。
- イシューのステータスが **Backlog** のとき、割り当てても**エージェントはトリガーされません** — Backlog は一時保管所であり、イシューを Todo または In Progress に移して初めてエージェントがキューに入ります。
## CLI から割り当てる
コマンドラインでの同等の操作です。
```bash
multica issue assign MUL-42 --to alice
multica issue assign MUL-42 --to-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
```
`--to` はメンバーのユーザー名またはエージェント名(あいまい一致)を受け付けます。名前が重複するとき — たとえばエージェント `J` の隣に `Cursor - J` がある場合 — は、代わりに `--to-id <uuid>` を渡してください。このとき `multica workspace member list --output json` の `user_id`(メンバー)または `multica agent list --output json` の `id`エージェントを使います。UUID 一致は厳密かつ曖昧さがないため、スクリプトや CLI を駆動するエージェントに適しています。`--to` と `--to-id` は同時に使えません。
割り当て解除:
```bash
multica issue assign MUL-42 --unassign
```
## 割り当て後に起こること
Backlog ではないイシューがエージェントに割り当てられると、Multica はすぐにバックグラウンドで次のことを行います。
1. イシューから継承した優先度で `queued` 状態の `task` をキューに入れ、エージェントが存在するランタイムへルーティングします。
2. エージェントのデーモンが次のポーリング時に `task` を取得し、`dispatched` に遷移させます。
3. エージェントが作業を開始すると `task` が `running` に移ります。完了すると `completed` または `failed` になります。
4. 実行中、エージェントはイシューのステータスを変更し、コメントを投稿し、フィールドを編集できます — これらの操作はエージェントの ID で表示されます。
**エージェントがオフラインの場合**、`task` はキューで待機します — **5 分後に `runtime_offline` の理由でタイムアウトして失敗します**。リトライ可能なソース(割り当て、@メンション、チャットについては、Multica が自動的に再度キューに入れます。完全なリトライルールは [**タスク**](/tasks)を参照してください。
割り当てると、エージェントはイシューに自動的に購読されます — ただし Multica では**エージェントはインボックス通知を受け取りません**(メンバーのみ受け取ります)。この購読は内部的な記録管理にすぎず、ユーザーに見える副作用はありません。
## 再割り当てまたは割り当て解除
担当者をエージェント A からエージェント B に変更すると、
1. **A が進行中だったものはすべてキャンセルされます** — `queued`、`dispatched`、`running` 状態のすべての `task` が `cancelled` と表示されます。
2. **B にはすぐに新しい `task` がキューに入ります**(イシューが Backlog でなく、B にオンラインのランタイムがある場合)。
<Callout type="warning">
**再割り当てはこのイシューのすべてのアクティブな `task` をキャンセルします — 以前の担当者のものだけではありません。** 別のエージェントが @メンションによってこのイシューで作業中の場合、その `task` も一緒にキャンセルされます。現在のところ、単一のエージェントの `task` だけを個別にキャンセルする UI 操作はありません。
</Callout>
割り当て解除(`--unassign` またはピッカーで「none」を選択は、すべてのアクティブな `task` 項目を `cancelled` と表示し、**新しい項目をキューに入れません**。既存の購読は自動的にクリアされません — 以前の担当者は購読リストに残ります(ただし依然としてインボックス通知は受け取りません)。
## イシューごとエージェントごとにアクティブな `task` が 1 つだけの理由
**単一のエージェントは、同じイシューで任意の時点に最大 1 つの `queued` または `dispatched` の `task` しか持てません。** データベースレベルの一意インデックスとクレームロジックがこれを強制します — 重複したキュー登録と、同時実行が互いを上書きすることを防ぎます。
しかし**異なるエージェントは同じイシューで並列に作業できます** — たとえばエージェント A が担当者で、エージェント B が @メンションされた場合、2 つの `task` 項目がそれぞれ自分のランタイムで実行されながら共存できます。完全な直列・並列ルールは [**タスク**](/tasks)を参照してください。
## 次へ
- [**コメントでエージェントを @メンションする**](/mentioning-agents) — 担当者とステータスを変えない、より軽いトリガー
- [**スクワッド**](/squads) — エージェントのグループに割り当て、リーダーに誰が引き受けるかを決めさせる
- [**チャット**](/chat) — イシューと無関係な 1 対 1 の会話
- [**オートパイロット**](/autopilots) — エージェントがスケジュールに沿って自動的に作業を開始するようにする

View File

@@ -0,0 +1,83 @@
---
title: 에이전트에게 이슈 할당하기
description: 이슈를 에이전트에게 넘기면 작업이 끝날 때까지 공식 담당자로 인계받습니다 — 전체 컨텍스트를 갖고 이슈 상태와 필드를 변경할 수 있습니다.
---
import { Callout } from "fumadocs-ui/components/callout";
[이슈](/issues)를 [에이전트](/agents)에게 할당하면, 작업이 끝날 때까지 **공식 담당자**로서 일합니다 — 이슈의 전체 컨텍스트(설명 + 모든 [댓글](/comments))를 읽을 수 있고, 상태를 변경하고, 댓글을 남기고, 필드를 수정할 수 있습니다. 이것은 Multica의 네 가지 트리거 경로 중 **가장 일반적이고 가장 무거운** 방식입니다. 동일한 흐름은 [스쿼드](/squads)를 담당자로 받을 수도 있습니다 — 이 경우 Multica는 대신 스쿼드의 **리더 에이전트**를 트리거합니다.
| 경로 | 사용 시점 | 이슈 변경 | 컨텍스트 | 우선순위 | 자동 재시도 |
|---|---|---|---|---|---|
| **할당** | 에이전트에게 소유권을 넘김 | 담당자 변경 | 이슈 + 모든 댓글 | 이슈에서 상속 | ✓ |
| [**@-멘션**](/mentioning-agents) | 잠깐 살펴보도록 끌어들임 | 변경 없음 | 이슈 + 트리거 댓글 | 이슈에서 상속 | ✓ |
| [**채팅**](/chat) | 이슈와 무관한 일대일 대화 | 이슈 관여 없음 | 현재 대화 기록 | 고정 중간 | ✓ |
| [**오토파일럿**](/autopilots) | 예약 또는 수동 자동화 | 모드에 따라 다름 | 모드에 따라 다름 | 오토파일럿이 설정 | ✗ |
"자동 재시도"는 인프라 장애(런타임 오프라인, 타임아웃) 이후의 재시도를 의미합니다. 에이전트 쪽의 비즈니스 오류(예: 모델이 오류를 보고하는 경우)는 재시도되지 않습니다. 자세한 내용은 [**작업**](/tasks)을 참고하세요.
## UI에서 할당하기
이슈 상세 페이지에서 **담당자** 선택기를 클릭하세요. 워크스페이스의 모든 멤버, 보관되지 않은 모든 에이전트, 보관되지 않은 모든 [스쿼드](/squads)가 목록에 표시됩니다. 에이전트(또는 스쿼드)를 선택하면 이슈가 즉시 할당됩니다.
몇 가지 규칙이 있습니다.
- **워크스페이스 에이전트**는 어떤 멤버든 할당할 수 있습니다. **프라이빗 에이전트**는 owner 또는 워크스페이스 admin만 할당할 수 있습니다.
- **온라인 런타임이 있는** 에이전트에게만 할당할 수 있습니다 — 아무도 실행하고 있지 않은 에이전트는 선택기에서 사용 불가로 표시됩니다.
- 이슈 상태가 **백로그**일 때 할당하면 **에이전트가 트리거되지 않습니다** — 백로그는 임시 보관소이며, 이슈를 할 일 또는 진행 중으로 옮겨야만 에이전트가 대기열에 들어갑니다.
## CLI에서 할당하기
명령줄에서의 동등한 작업입니다.
```bash
multica issue assign MUL-42 --to alice
multica issue assign MUL-42 --to-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
```
`--to`는 멤버 사용자 이름 또는 에이전트 이름(퍼지 매칭)을 받습니다. 이름이 겹칠 때 — 예를 들어 에이전트 `J` 옆에 `Cursor - J`가 있을 때 — 대신 `--to-id <uuid>`를 전달하세요. 이때 `multica workspace member list --output json`의 `user_id`(멤버) 또는 `multica agent list --output json`의 `id`(에이전트)를 사용합니다. UUID 매칭은 엄격하고 모호하지 않으므로, 스크립트나 CLI를 구동하는 에이전트에게 적합합니다. `--to`와 `--to-id`는 함께 쓸 수 없습니다.
할당 해제:
```bash
multica issue assign MUL-42 --unassign
```
## 할당 이후에 일어나는 일
백로그가 아닌 이슈가 에이전트에게 할당되면, Multica는 즉시 백그라운드에서 다음을 수행합니다.
1. 이슈에서 상속한 우선순위로 `queued` 상태의 `task`를 대기열에 넣고, 에이전트가 있는 런타임으로 라우팅합니다.
2. 에이전트의 데몬이 다음 폴링 시 `task`를 가져가 `dispatched`로 전환합니다.
3. 에이전트가 작업을 시작하면 `task`가 `running`으로 이동합니다. 완료되면 `completed` 또는 `failed`가 됩니다.
4. 실행 중에 에이전트는 이슈의 상태를 변경하고, 댓글을 남기고, 필드를 수정할 수 있습니다 — 이러한 동작은 에이전트의 신원으로 표시됩니다.
**에이전트가 오프라인인 경우**, `task`는 대기열에서 기다립니다 — **5분 후 `runtime_offline` 사유로 타임아웃되어 실패합니다**. 재시도 가능한 소스(할당, @-멘션, 채팅)에 대해서는 Multica가 자동으로 다시 대기열에 넣습니다. 전체 재시도 규칙은 [**작업**](/tasks)을 참고하세요.
할당하면 에이전트가 이슈에 자동으로 구독됩니다 — 다만 Multica에서는 **에이전트가 인박스 알림을 받지 않습니다**(멤버만 받습니다). 이 구독은 내부 기록 관리일 뿐이며 사용자에게 보이는 부작용은 없습니다.
## 재할당 또는 할당 해제
담당자를 에이전트 A에서 에이전트 B로 변경하면:
1. **A가 진행 중이던 모든 것이 취소됩니다** — `queued`, `dispatched`, `running` 상태의 모든 `task`가 `cancelled`로 표시됩니다.
2. **B에게 즉시 새 `task`가 대기열에 들어갑니다**(이슈가 백로그가 아니고 B에게 온라인 런타임이 있는 경우).
<Callout type="warning">
**재할당은 이 이슈의 모든 활성 `task`를 취소합니다 — 이전 담당자의 것만이 아닙니다.** 다른 에이전트가 @-멘션 때문에 이 이슈에서 작업 중이라면, 그 `task`도 함께 취소됩니다. 현재로서는 단일 에이전트의 `task`만 따로 취소하는 UI 동작이 없습니다.
</Callout>
할당 해제(`--unassign` 또는 선택기에서 "none" 선택)는 모든 활성 `task` 항목을 `cancelled`로 표시하며 **새 항목을 대기열에 넣지 않습니다**. 기존 구독은 자동으로 정리되지 않습니다 — 이전 담당자는 구독 목록에 남아 있습니다(다만 여전히 인박스 알림은 받지 않습니다).
## 이슈당 에이전트당 활성 `task`가 하나뿐인 이유
**단일 에이전트는 같은 이슈에서 어느 시점에든 최대 하나의 `queued` 또는 `dispatched` `task`만 가질 수 있습니다.** 데이터베이스 수준의 고유 인덱스와 클레임 로직이 이를 강제합니다 — 중복 대기열 등록과 동시 실행이 서로를 덮어쓰는 것을 방지합니다.
하지만 **서로 다른 에이전트는 같은 이슈에서 병렬로 작업할 수 있습니다** — 예를 들어 에이전트 A가 담당자이고 에이전트 B가 @-멘션된 경우, 두 `task` 항목이 각자의 런타임에서 실행되며 공존할 수 있습니다. 전체 직렬/동시 실행 규칙은 [**작업**](/tasks)을 참고하세요.
## 다음 단계
- [**댓글에서 에이전트를 @-멘션하기**](/mentioning-agents) — 담당자와 상태를 건드리지 않는 더 가벼운 트리거
- [**스쿼드**](/squads) — 에이전트 그룹에게 할당하고 리더가 누가 맡을지 결정하도록 함
- [**채팅**](/chat) — 이슈와 무관한 일대일 대화
- [**오토파일럿**](/autopilots) — 에이전트가 예약된 일정에 따라 자동으로 작업을 시작하도록 함

View File

@@ -0,0 +1,166 @@
---
title: ログインとサインアップの構成
description: メール + 認証コードログイン、Google OAuth、サインアップ許可リスト、ローカルテストコードを構成します。
---
import { Callout } from "fumadocs-ui/components/callout";
import { Mermaid } from "@/components/mermaid";
Multica は 2 つのログイン方式をサポートしています。**メール + 認証コード**(デフォルト)と **Google OAuth**(オプション)です。ログインに成功すると、サーバーは 30 日間有効な JWT クッキーを発行します。このページでは、各方式の構成方法、誰がサインアップできるかを制限する方法、そしてセルフホストのデプロイで最も陥りやすい落とし穴を 1 つ取り上げます。
以下で参照する環境変数の一覧は[環境変数](/environment-variables)を参照してください。トークンの使い方とライフサイクルの詳細は[認証とトークン](/auth-tokens)を参照してください。
## メール + 認証コードログインの仕組み
ユーザーがログインページでメールを入力します → サーバーが 6 桁のコードを送信します → ユーザーがコードを入力します → サーバーがコードを検証します → JWT クッキーが発行されます。標準的なフローです。2 つの送信バックエンドがサポートされているので、デプロイ環境に合うほうを選んでください。
### オプション A: Resendクラウド / 公開インターネットのデプロイに推奨)
1. [Resend](https://resend.com/) アカウントを作成し、ドメインを認証します
2. API キーを作成します
3. 環境変数を設定します:
```bash
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
RESEND_FROM_EMAIL=noreply@yourdomain.com # must be a domain verified in Resend
```
4. サーバーを再起動します
### オプション B: SMTP relayセルフホスト / オンプレミスのデプロイ用)
デプロイ環境から `api.resend.com` に到達できない場合や、すでに内部メール relayMicrosoft Exchange、Postfix、オンプレミスの SendGrid など)がある場合に使用してください。両方が設定されている場合は `SMTP_HOST` が `RESEND_API_KEY` より優先されます。`SMTP_HOST` が空でなければ、`RESEND_API_KEY` も併せて構成されていても、サーバーは常に SMTP を経由するため、認証メールと招待メールが内部ネットワークの外に出ることは決してありません。
SMTP 経路は、ほとんどのオンプレミスメールサーバー(特に Microsoft Exchange の receive connectorが公開する 3 つの relay モードをサポートします。
| モード | ポート | 認証 | TLS |
|---|---|---|---|
| 匿名内部 relay | `25` | なし — IP / サブネットで送信を信頼 | 伝送経路上はなし(内部セグメント専用) |
| 認証付き送信submission | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS、自動アップグレード |
| 暗黙的 TLSSMTPS | `465` | — | **まだサポートされていません** — ポート 25 または 587 を使用してください |
**ポート 25 の匿名 Exchange relay** — 認証情報なしで信頼されたサブネットからのメールを受け入れる、典型的な「internal SMTP relay」/ Exchange 匿名 receive connector:
```bash
SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
```
**ポート 587 の認証付き送信** — サービスアカウントを必要とする relay 用。サーバーが STARTTLS のサポートを通知すると自動的にアップグレードされます:
```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
起動時に、サーバーは選択したプロバイダーを出力します。例えば `EmailService: SMTP relay exchange.internal.example.com:25 from=noreply@example.com`(または `Resend API` / `DEV mode`)のように表示されます。パスワードがログに記録されることは決してありません。再起動後に SMTP の行が見えない場合は `SMTP_HOST` がプロセスに届いていないので、コンテナ環境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)を確認してください。
**どちらも設定しない場合**: サーバーはエラーを出しませんが、**送信されるはずだったすべてのメールがサーバーの stdout にのみ書き出されます**。ローカル開発には便利ですが(ログからコードをコピーできます)、プロダクションではブラックホールになります。
## 固定ローカルテストコード
<Callout type="warning">
**公開アクセス可能なインスタンスでは固定の認証コードを有効にしないでください。**
非プロダクションのインスタンスがデフォルトで `888888` を受け入れていた従来の動作は削除されました。明示的に構成しない限り、`888888` の入力は他の誤ったコードと同じように扱われます。
メールバックエンドをまったく構成していないResend も SMTP もない)ローカル開発では、サーバーログに出力される生成されたコードを使用してください。決定論的なローカル / プライベートの自動化が必要な場合は、`MULTICA_DEV_VERIFICATION_CODE` を `888888` のような 6 桁の値に設定し、`APP_ENV` を非プロダクションに保ってください:
```bash
APP_ENV=development
MULTICA_DEV_VERIFICATION_CODE=888888
```
このショートカットは `APP_ENV=production` のときは無視されます。
</Callout>
プロダクションのデプロイでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにし、`APP_ENV=production` に設定してください。`make selfhost` / `docker-compose.selfhost.yml` でデプロイする場合、`APP_ENV` はデフォルトで `production` です。
## Google OAuth の構成
オプションです。構成しないとメール + 認証コードのみが利用可能で、構成するとログインページに「Sign in with Google」ボタンが追加されます。
1. [Google Cloud Console](https://console.cloud.google.com/) で OAuth 2.0 クライアントを作成します
2. **Authorized redirect URIs** を Multica フロントエンドのアドレスに `/auth/callback` を加えた値に設定します。例:
```text
https://multica.yourdomain.com/auth/callback
```
3. クライアント ID とクライアント secret を取得したら、3 つの環境変数を設定します:
```bash
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
GOOGLE_REDIRECT_URI=https://multica.yourdomain.com/auth/callback
```
4. サーバーを再起動します。
**ランタイムで反映されます**: フロントエンドは `/api/config` を通じてランタイムにこれらの設定を読み込みます — 変更後にサーバーを再起動すると、フロントエンドはリビルドや再デプロイなしで新しい値を取得します。
<Callout type="warning">
**リダイレクト URI は Google Console と `GOOGLE_REDIRECT_URI` の両方で完全に一致している必要があります** — プロトコル(`http` と `https`)、末尾のスラッシュ、ポートを含みます。少しでも一致しないと Google は OAuth フロー全体を拒否し、ユーザーに表示されるエラーは `redirect_uri_mismatch` です。
</Callout>
## 誰がサインアップできるかを制限する
3 つの環境変数が優先順位に従って組み合わされます。
<Mermaid chart={`
graph TD
Start[New user first sign-in] --> A{Email in<br/>ALLOWED_EMAILS?}
A -- Yes --> Allow[Allow signup]
A -- No --> B{Domain in<br/>ALLOWED_EMAIL_DOMAINS?}
B -- Yes --> Allow
B -- No --> C{Any allowlist<br/>non-empty?}
C -- Yes --> Block[Reject]
C -- No --> D{ALLOW_SIGNUP<br/>= true?}
D -- Yes --> Allow
D -- No --> Block
`} />
**既存のユーザーはいつでも再ログインできます** — サインアップ許可リストは**初回サインアップ**にのみ適用され、戻ってくるユーザーは妨げられません。
- **`ALLOWED_EMAILS`**(最高優先度) — 明示的なメール許可リスト、カンマ区切り。**空でない場合、リストにあるメールのみがサインアップできます。**
- **`ALLOWED_EMAIL_DOMAINS`** — ドメイン許可リスト、カンマ区切り(例: `company.io,partner.com`)。
- **`ALLOW_SIGNUP`** — マスタースイッチ、デフォルト `true`。`false` に設定するとサインアップが完全に無効になります。
<Callout type="warning">
**3 つの層は OR ではなく AND のセマンティクスです。** よくある誤った直感は、`ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true` が「company.io に加えて他の全員を許可する」という意味だと考えることです。そうでは**ありません**。いずれかの層に空でない値があると、**それに一致しないメールはただちに拒否され**、`ALLOW_SIGNUP=true` はそれを無効にできません。
実際に「全員を許可」するには、3 つの変数をすべて空のままにしてください(または `ALLOW_SIGNUP=true` を維持してください)。
</Callout>
**典型的な構成**:
| 目的 | 構成 |
|---|---|
| 内部専用、`company.io` の従業員のみ | `ALLOWED_EMAIL_DOMAINS=company.io` |
| 内部 + 少数の外部コラボレーター | `ALLOWED_EMAIL_DOMAINS=company.io` + コラボレーターのアドレスを `ALLOWED_EMAILS` に追加 |
| セルフサービスのサインアップを完全に無効化、招待のみ | `ALLOW_SIGNUP=false` |
| 開放型サインアップ(プロダクションには非推奨) | 3 つすべて空 |
## サインアップを無効にしても人を招待できますか?
**すでに Multica アカウントを持っている人のみ可能です。** 招待の受諾はサインアップ許可リストをチェックしません — 招待された人がすでにサインアップ済み(例えば別のワークスペースで)であれば、招待リンクをクリックしてログインすれば受諾できます。
**しかし一度もサインアップしていない人は招待で救うことはできません。** 受諾する前にまずログインする必要があり、ログインの最初のステップ(認証コードの要求)はサインアップ許可リストのチェックを通過します。`ALLOW_SIGNUP=false` であるか、そのメールが `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` にない場合、**サインアップを完了できず**、したがって招待を受諾することもできません。
まだサインアップしていない外部コラボレーターを招待するには: そのメールを `ALLOWED_EMAILS` に一時的に追加し、その人がサインアップして招待を受諾するのを待ってから、エントリを削除してください。
招待の作成と使用方法については[メンバーとロール](/members-roles)を参照してください。
## 次に
- [環境変数](/environment-variables) — このページで使用するすべての変数の完全な定義
- [認証とトークン](/auth-tokens) — JWT / PAT / デーモントークンの分類と使い方
- [トラブルシューティング](/troubleshooting) — 認証コードが届かない、OAuth `redirect_uri_mismatch`、サインアップ拒否

View File

@@ -0,0 +1,166 @@
---
title: 로그인 및 회원가입 구성
description: 이메일 + 인증 코드 로그인, Google OAuth, 회원가입 허용 목록, 로컬 테스트 코드를 구성합니다.
---
import { Callout } from "fumadocs-ui/components/callout";
import { Mermaid } from "@/components/mermaid";
Multica는 두 가지 로그인 방식을 지원합니다. **이메일 + 인증 코드**(기본값)와 **Google OAuth**(선택). 로그인에 성공하면 서버가 30일 수명의 JWT 쿠키를 발급합니다. 이 페이지에서는 각 방식을 구성하는 방법, 누가 회원가입할 수 있는지 제한하는 방법, 그리고 자체 호스팅 배포에서 가장 빠지기 쉬운 함정 하나를 다룹니다.
아래에서 참조하는 환경 변수 목록은 [환경 변수](/environment-variables)를 참고하세요. 토큰 사용법과 수명 주기 세부 사항은 [인증 및 토큰](/auth-tokens)을 참고하세요.
## 이메일 + 인증 코드 로그인의 작동 방식
사용자가 로그인 페이지에서 이메일을 입력합니다 → 서버가 6자리 코드를 보냅니다 → 사용자가 코드를 입력합니다 → 서버가 코드를 검증합니다 → JWT 쿠키가 발급됩니다. 표준 흐름입니다. 두 가지 전송 백엔드가 지원되므로 배포 환경에 맞는 쪽을 선택하세요.
### 옵션 A: Resend (클라우드 / 공용 인터넷 배포에 권장)
1. [Resend](https://resend.com/) 계정을 만들고 도메인을 인증합니다
2. API 키를 생성합니다
3. 환경 변수를 설정합니다:
```bash
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
RESEND_FROM_EMAIL=noreply@yourdomain.com # must be a domain verified in Resend
```
4. 서버를 재시작합니다
### 옵션 B: SMTP relay (자체 호스팅 / 온프레미스 배포용)
배포 환경에서 `api.resend.com`에 접근할 수 없거나 이미 내부 메일 relay(Microsoft Exchange, Postfix, 온프레미스 SendGrid 등)가 있는 경우에 사용하세요. 둘 다 설정된 경우 `SMTP_HOST`가 `RESEND_API_KEY`보다 우선합니다. `SMTP_HOST`가 비어 있지 않으면 `RESEND_API_KEY`도 함께 구성되어 있더라도 서버는 항상 SMTP를 통하므로, 인증 및 초대 메일이 내부 네트워크를 벗어나는 일이 결코 없습니다.
SMTP 경로는 대부분의 온프레미스 메일 서버(특히 Microsoft Exchange의 receive connector)가 노출하는 세 가지 relay 모드를 지원합니다:
| 모드 | 포트 | 인증 | TLS |
|---|---|---|---|
| 익명 내부 relay | `25` | 없음 — IP / 서브넷으로 제출을 신뢰 | 전송 경로상 없음(내부 세그먼트 전용) |
| 인증된 제출(submission) | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS, 자동 업그레이드 |
| 암묵적 TLS (SMTPS) | `465` | — | **아직 지원하지 않음** — 포트 25 또는 587을 사용하세요 |
**포트 25의 익명 Exchange relay** — 자격 증명 없이 신뢰된 서브넷에서 오는 메일을 받아들이는 일반적인 "internal SMTP relay" / Exchange 익명 receive connector:
```bash
SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
```
**포트 587의 인증된 제출** — 서비스 계정이 필요한 relay용. 서버가 STARTTLS 지원을 알리면 자동으로 업그레이드됩니다:
```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
시작 시 서버는 선택한 제공자를 출력합니다. 예를 들어 `EmailService: SMTP relay exchange.internal.example.com:25 from=noreply@example.com`(또는 `Resend API` / `DEV mode`)와 같이 표시됩니다. 비밀번호는 절대 로그에 기록되지 않습니다. 재시작 후 SMTP 줄이 보이지 않는다면 `SMTP_HOST`가 프로세스에 도달하지 못한 것이므로, 컨테이너 환경(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)을 확인하세요.
**둘 다 설정하지 않으면**: 서버는 오류를 내지 않지만, **전송되어야 했던 모든 이메일이 서버의 stdout에만 기록됩니다**. 로컬 개발에는 편리하지만(로그에서 코드를 복사하면 됩니다), 프로덕션에서는 블랙홀이 됩니다.
## 고정 로컬 테스트 코드
<Callout type="warning">
**공용으로 접근 가능한 인스턴스에서는 고정 인증 코드를 활성화하지 마세요.**
프로덕션이 아닌 인스턴스가 기본적으로 `888888`을 받아들이던 기존 동작은 제거되었습니다. 명시적으로 구성하지 않는 한 `888888`을 입력하는 것은 다른 잘못된 코드와 동일하게 처리됩니다.
이메일 백엔드를 전혀 구성하지 않은(Resend도 SMTP도 없는) 로컬 개발에서는 서버 로그에 출력되는 생성된 코드를 사용해야 합니다. 결정적인 로컬/사설 자동화가 필요하다면 `MULTICA_DEV_VERIFICATION_CODE`를 `888888` 같은 6자리 값으로 설정하고 `APP_ENV`를 프로덕션이 아닌 값으로 유지하세요:
```bash
APP_ENV=development
MULTICA_DEV_VERIFICATION_CODE=888888
```
이 단축키는 `APP_ENV=production`일 때 무시됩니다.
</Callout>
프로덕션 배포에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두고 `APP_ENV=production`으로 설정해야 합니다. `make selfhost` / `docker-compose.selfhost.yml`로 배포하는 경우 `APP_ENV`는 기본적으로 `production`입니다.
## Google OAuth 구성
선택 사항입니다. 구성하지 않으면 이메일 + 인증 코드만 사용할 수 있고, 구성하면 로그인 페이지에 "Google로 로그인" 버튼이 추가됩니다.
1. [Google Cloud Console](https://console.cloud.google.com/)에서 OAuth 2.0 클라이언트를 생성합니다
2. **승인된 리디렉션 URI**(Authorized redirect URIs)를 Multica 프런트엔드 주소에 `/auth/callback`을 더한 값으로 설정합니다. 예:
```text
https://multica.yourdomain.com/auth/callback
```
3. 클라이언트 ID와 클라이언트 secret을 얻은 후 세 개의 환경 변수를 설정합니다:
```bash
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
GOOGLE_REDIRECT_URI=https://multica.yourdomain.com/auth/callback
```
4. 서버를 재시작합니다.
**런타임에 적용됨**: 프런트엔드는 `/api/config`를 통해 런타임에 이 설정을 읽습니다. 변경 후 서버를 재시작하면 프런트엔드가 다시 빌드하거나 다시 배포할 필요 없이 새 값을 가져옵니다.
<Callout type="warning">
**리디렉션 URI는 Google Console과 `GOOGLE_REDIRECT_URI` 양쪽에서 정확히 일치해야 합니다** — 프로토콜(`http` vs `https`), 끝의 슬래시, 포트까지 포함합니다. 조금이라도 일치하지 않으면 Google이 전체 OAuth 흐름을 거부하며, 사용자에게 표시되는 오류는 `redirect_uri_mismatch`입니다.
</Callout>
## 누가 회원가입할 수 있는지 제한하기
세 개의 환경 변수가 우선순위에 따라 조합됩니다:
<Mermaid chart={`
graph TD
Start[New user first sign-in] --> A{Email in<br/>ALLOWED_EMAILS?}
A -- Yes --> Allow[Allow signup]
A -- No --> B{Domain in<br/>ALLOWED_EMAIL_DOMAINS?}
B -- Yes --> Allow
B -- No --> C{Any allowlist<br/>non-empty?}
C -- Yes --> Block[Reject]
C -- No --> D{ALLOW_SIGNUP<br/>= true?}
D -- Yes --> Allow
D -- No --> Block
`} />
**기존 사용자는 언제든 다시 로그인할 수 있습니다** — 회원가입 허용 목록은 **최초 회원가입**에만 적용되며, 돌아오는 사용자는 막지 않습니다.
- **`ALLOWED_EMAILS`** (최고 우선순위) — 명시적 이메일 허용 목록, 쉼표로 구분합니다. **비어 있지 않으면 목록에 있는 이메일만 회원가입할 수 있습니다.**
- **`ALLOWED_EMAIL_DOMAINS`** — 도메인 허용 목록, 쉼표로 구분합니다(예: `company.io,partner.com`).
- **`ALLOW_SIGNUP`** — 마스터 스위치, 기본값 `true`. `false`로 설정하면 회원가입이 완전히 비활성화됩니다.
<Callout type="warning">
**세 계층은 OR가 아니라 AND 의미입니다.** 흔한 잘못된 직관은 `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true`가 "company.io에 더해 다른 모든 사람을 허용"한다는 것입니다. 그렇지 **않습니다**. 어느 계층이든 비어 있지 않은 값이 있으면 **그에 일치하지 않는 이메일은 곧바로 거부되며**, `ALLOW_SIGNUP=true`는 그것을 무효로 만들지 못합니다.
실제로 "모두 허용"하려면 세 변수를 모두 비워 두세요(또는 `ALLOW_SIGNUP=true`를 유지하세요).
</Callout>
**일반적인 구성**:
| 목표 | 구성 |
|---|---|
| 내부 전용, `company.io` 직원만 | `ALLOWED_EMAIL_DOMAINS=company.io` |
| 내부 + 소수의 외부 협업자 | `ALLOWED_EMAIL_DOMAINS=company.io` + 협업자 주소를 `ALLOWED_EMAILS`에 추가 |
| 셀프서비스 회원가입을 완전히 비활성화, 초대 전용 | `ALLOW_SIGNUP=false` |
| 개방형 회원가입(프로덕션에는 권장하지 않음) | 셋 다 비움 |
## 회원가입을 비활성화해도 사람을 초대할 수 있나요?
**이미 Multica 계정이 있는 사람만 가능합니다.** 초대 수락은 회원가입 허용 목록을 확인하지 않습니다. 초대받은 사람이 이미 회원가입한 상태라면(예: 다른 워크스페이스에서), 초대 링크를 클릭하고 로그인하면 수락할 수 있습니다.
**하지만 한 번도 회원가입하지 않은 사람은 초대로 구제할 수 없습니다.** 수락하기 전에 먼저 로그인해야 하고, 로그인의 첫 단계(인증 코드 요청)는 회원가입 허용 목록 검사를 거칩니다. `ALLOW_SIGNUP=false`이거나 그들의 이메일이 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS`에 없으면 **회원가입을 완료할 수 없으며**, 따라서 초대도 수락할 수 없습니다.
아직 회원가입하지 않은 외부 협업자를 초대하려면: 그들의 이메일을 `ALLOWED_EMAILS`에 임시로 추가하고, 그들이 회원가입하고 초대를 수락하기를 기다린 다음 항목을 제거하세요.
초대를 만들고 사용하는 방법은 [멤버 및 역할](/members-roles)을 참고하세요.
## 다음
- [환경 변수](/environment-variables) — 이 페이지에서 사용하는 모든 변수의 전체 정의
- [인증 및 토큰](/auth-tokens) — JWT / PAT / 데몬 토큰의 분류와 사용법
- [문제 해결](/troubleshooting) — 인증 코드 미수신, OAuth `redirect_uri_mismatch`, 회원가입 거부

View File

@@ -37,7 +37,7 @@ The SMTP path supports the three relay modes most on-premise mail servers (notab
|---|---|---|---|
| Anonymous internal relay | `25` | none — submission is trusted by IP / subnet | none on the wire (internal segment only) |
| Authenticated submission | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS, upgraded automatically |
| Implicit TLS (SMTPS) | `465` | — | **not supported yet** — use port 25 or 587 |
| Implicit TLS (SMTPS) | `465` | optional (`SMTP_USERNAME` + `SMTP_PASSWORD`) | TLS handshake on connect — auto-enabled on port `465`, or force on a non-standard port with `SMTP_TLS=implicit` |
**Anonymous Exchange relay on port 25** — the typical "internal SMTP relay" / Exchange anonymous receive connector that accepts mail from a trusted subnet without credentials:
@@ -61,7 +61,18 @@ SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
At startup the server prints which provider it picked — for example `EmailService: SMTP relay exchange.internal.example.com:25 from=noreply@example.com` (or `Resend API` / `DEV mode`). The password is never logged. If you don't see the SMTP line after restart, `SMTP_HOST` didn't reach the process — check the container env (`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`).
**Implicit TLS (SMTPS) on port 465** — for providers that only offer SMTPS and don't advertise STARTTLS (e.g. Aliyun / Tencent enterprise mail). Port `465` auto-enables implicit TLS; `SMTP_TLS=implicit` (aliases: `smtps`, `ssl`) forces it on a non-standard SMTPS port:
```bash
SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465 # implicit TLS auto-enabled on 465
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit # optional on 465; required on a non-standard SMTPS port
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
At startup the server prints which provider it picked, including the negotiated TLS mode — for example `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` or `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…` (or `Resend API` / `DEV mode`). The password is never logged. If you don't see the SMTP line after restart, `SMTP_HOST` didn't reach the process — check the container env (`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`).
**What happens if you set neither**: the server doesn't error, but **every email that should have been sent is written to the server's stdout only**. Handy for local development (copy the code from the logs); in production it's a black hole.

View File

@@ -37,7 +37,7 @@ SMTP 路径覆盖大多数本地邮件服务器(特别是 Microsoft Exchange
|---|---|---|---|
| 匿名内部 relay | `25` | 无 —— 按 IP / 子网信任 | 链路上无 TLS仅限内网段 |
| 认证提交submission | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS自动升级 |
| 隐式 TLSSMTPS | `465` | —— | **暂不支持** —— 请用 25 或 587 |
| 隐式 TLSSMTPS | `465` | 可选(`SMTP_USERNAME` + `SMTP_PASSWORD` | 连上即 TLS 握手 —— 端口 `465` 自动启用;非标准端口设 `SMTP_TLS=implicit` 强制开启 |
**匿名 Exchange relay端口 25** —— 经典的 "internal SMTP relay" / Exchange 匿名 receive connector按可信子网放行不要求凭据
@@ -61,7 +61,18 @@ SMTP_TLS_INSECURE=false # 仅在私有 CA / 自签证书时改成 true
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
启动时 server 会打印当前选择的 provider比如 `EmailService: SMTP relay exchange.internal.example.com:25 from=noreply@example.com`(或 `Resend API` / `DEV mode`),密码不会出现在日志里。重启后没看到 SMTP 这行,说明 `SMTP_HOST` 没进到进程,确认下容器环境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)。
**隐式 TLSSMTPS端口 465** —— 适用于只提供 SMTPS、不广告 STARTTLS 的服务商(例如阿里云 / 腾讯企业邮箱)。端口 `465` 会自动启用隐式 TLS非标准 SMTPS 端口设置 `SMTP_TLS=implicit`(别名:`smtps`、`ssl`)即可强制开启:
```bash
SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465 # 465 自动启用隐式 TLS
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit # 465 上可省略;在非标准 SMTPS 端口上必填
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
启动时 server 会打印当前选择的 provider 和协商出的 TLS 模式,比如 `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` 或 `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…`(或 `Resend API` / `DEV mode`),密码不会出现在日志里。重启后没看到 SMTP 这行,说明 `SMTP_HOST` 没进到进程,确认下容器环境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)。
**两种都不配**server 不报错,但所有本该发出去的邮件**只打到 server 的 stdout**。本地开发方便(你从日志抄验证码),生产环境等于黑洞。

View File

@@ -0,0 +1,80 @@
---
title: 認証とトークン
description: Multica には 3 種類のトークンがあります — ブラウザ、CLI、デーモンにそれぞれ 1 つずつ。どの場面でどれを使うかを解説します。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica には 3 種類のトークンがあり、それぞれが 1 つのコンテキストに対応します。ブラウザの Web UI、コマンドラインとスクリプト、そしてデーモンです。3 つとも同じあなたを表しますが、スコープと有効期間が異なります。
## 3 つのトークン
| トークン | 形式 | 使われる場所 | 有効期間 |
|---|---|---|---|
| **JWT クッキー** | `multica_auth` クッキー (HttpOnly) | Web ブラウザ | 30 日 |
| **個人アクセストークン (PAT)** | `mul_` プレフィックス | CLI、スクリプト、直接の API 呼び出し | デフォルトでは期限なし。API で作成する際に `expires_in_days` を渡せます |
| **デーモントークン** | `mdt_` プレフィックス | デーモンとサーバー間の通信 | デーモン自体が管理 |
日常的な利用では、最初の 2 つだけを直接扱うことになります。**[デーモン](/daemon-runtimes)トークン**は `multica daemon login` が自動的に作成・更新するため、気にする必要はありません。
## 各トークンがアクセスできるもの
| API ルート | JWT クッキー | PAT | デーモントークン |
|---|---|---|---|
| `/api/user/*` (ユーザーレベルの操作) | ✓ | ✓ | ✗ |
| `/api/workspaces/:id/*` (ワークスペースレベル) | ✓ | ✓ | ✗ |
| `/api/daemon/*` (デーモン専用) | ✗ | ✓ | ✓ |
| WebSocket `/ws` (リアルタイムプッシュ) | ✓ (クッキー) | ✓ (最初のメッセージで認証) | ✗ |
**PAT はほぼすべてにアクセスできます** — これは「完全なあなた」を表します。デーモントークンはデーモンに必要なこと、つまりタスクを取得して結果を報告することしかできません。
**どちらも `/api/daemon/*` にアクセスできますが、スコープが異なります。** PAT は**ユーザー全体**を表し、一度認証されると、あなたが所属するすべてのワークスペースを見ることができます。デーモントークンは作成時点で単一のワークスペースに固定され、そのワークスペースのリソースにしかアクセスできません。本番環境では、デーモンはデーモントークンで実行してください。手軽さのために PAT を使う近道を選ばないでください。そうしないと、デーモンに必要な以上にはるかに大きな権限を与えてしまいます。
## ログイン
### メール + 認証コード
1. メールアドレスを入力すると、サーバーが 6 桁のコードを送信します。
2. コードを入力すると、サーバーが JWT クッキーを発行ブラウザするか、PAT に交換CLIします。
<Callout type="warning">
**セルフホストの運用者は注意してください**: 公開デプロイでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにしておいてください。固定のローカルテストコードを有効にすると、`APP_ENV` が production 以外の間は、コードをリクエストできる人なら誰でもその値でサインインできてしまいます。[セルフホスト認証の構成](/auth-setup)を参照してください。
</Callout>
### Google OAuth
**Sign in with Google** をクリックして、標準の OAuth コールバックを通過してください。セルフホストには `GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET`、そしてリダイレクト URI を構成する必要があります — [セルフホスト認証の構成](/auth-setup)を参照してください。
## PAT の作成、表示、失効
PAT の**作成**は 2 つの方法で行えます。
- **Web UI**: 設定 → 個人アクセストークン → 新しいトークン
- **CLI**: `multica login` は、まだローカル PAT がない場合に自動的に 1 つ作成します
<Callout type="warning">
**完全な PAT は作成時に正確に 1 回だけ表示されます。** 更新したりダイアログを閉じたりした後は、二度と見ることができません。
Multica はデータベースに PAT のハッシュだけを保存します — サーバーでさえ元の値を取得できません。すぐにコピーして保存してください。紛失した場合の唯一の手段は、失効させて新しく作り直すことです。
</Callout>
既存の PAT の**表示**(名前、作成時刻、最終使用時刻 — 完全なトークンは**含みません**)は、設定 → 個人アクセストークンにあります。
PAT の**失効**: 一覧で Revoke をクリックしてください。失効はすぐに反映されます — その PAT で送られる次のリクエストは 401 で拒否されます。
## ログアウトはローカルトークンを削除するだけ
`multica auth logout` を実行するか、Web UI でログアウトをクリックすると、
- **ローカルトークンが消去されます** — CLI は `~/.multica/config.json` から PAT を削除し、ブラウザはクッキーを削除します。
- **PAT はサーバー上では依然として有効です** — ログアウトする前に誰かがあなたの PAT を入手していた場合(たとえば別のマシンにコピーしていた場合)、その人は**依然としてそれを使用できます**。
<Callout type="warning">
**PAT が漏洩したと疑われる場合は、単にログアウトするだけにしないでください。** 設定 → 個人アクセストークンに進み、そのトークンを**失効**させてください。失効だけが、漏洩したトークンを即座に無効化します。
</Callout>
## 次のステップ
- [CLI コマンドリファレンス](/cli) — すべての CLI コマンドの認証は自動です
- [セルフホスト認証の構成](/auth-setup) — セルフホスト時にメール、OAuth、サインアップ許可リストを構成する方法
- [デーモンとランタイム](/daemon-runtimes) — デーモントークンがどこから来るのか

View File

@@ -0,0 +1,80 @@
---
title: 인증과 토큰
description: Multica에는 세 종류의 토큰이 있습니다 — 브라우저, CLI, 데몬에 각각 하나씩. 어떤 상황에 무엇을 쓰는지 설명합니다.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica에는 세 종류의 토큰이 있으며, 각각 하나의 컨텍스트에 대응합니다: 브라우저 Web UI, 명령줄과 스크립트, 그리고 데몬입니다. 세 가지 모두 동일한 당신을 나타내지만, 범위와 수명이 다릅니다.
## 세 가지 토큰
| 토큰 | 형식 | 사용되는 곳 | 수명 |
|---|---|---|---|
| **JWT 쿠키** | `multica_auth` 쿠키 (HttpOnly) | 웹 브라우저 | 30일 |
| **개인 액세스 토큰 (PAT)** | `mul_` 접두사 | CLI, 스크립트, 직접 API 호출 | 기본적으로 만료 없음. API로 생성할 때 `expires_in_days`를 전달할 수 있습니다 |
| **데몬 토큰** | `mdt_` 접두사 | 데몬-서버 통신 | 데몬 자체가 관리 |
일상적인 사용에서는 앞의 두 가지만 직접 다루게 됩니다. **[데몬](/daemon-runtimes) 토큰**은 `multica daemon login`이 자동으로 생성하고 갱신하므로, 신경 쓸 필요가 없습니다.
## 각 토큰이 접근할 수 있는 것
| API 라우트 | JWT 쿠키 | PAT | 데몬 토큰 |
|---|---|---|---|
| `/api/user/*` (사용자 수준 작업) | ✓ | ✓ | ✗ |
| `/api/workspaces/:id/*` (워크스페이스 수준) | ✓ | ✓ | ✗ |
| `/api/daemon/*` (데몬 전용) | ✗ | ✓ | ✓ |
| WebSocket `/ws` (실시간 푸시) | ✓ (쿠키) | ✓ (첫 메시지로 인증) | ✗ |
**PAT는 거의 모든 것에 접근할 수 있습니다** — 이는 "완전한 당신"을 나타냅니다. 데몬 토큰은 데몬에 필요한 일, 즉 작업을 가져오고 결과를 보고하는 것만 할 수 있습니다.
**둘 다 `/api/daemon/*`에 접근할 수 있지만, 범위가 다릅니다.** PAT는 **사용자 전체**를 나타내며 — 일단 인증되면 당신이 속한 모든 워크스페이스를 볼 수 있습니다. 데몬 토큰은 생성 시점에 단일 워크스페이스에 고정되며 해당 워크스페이스의 리소스에만 접근할 수 있습니다. 프로덕션에서는 데몬을 데몬 토큰으로 실행하세요. 편의를 위해 PAT를 사용하는 지름길을 택하지 마세요. 그렇지 않으면 데몬에 필요한 것보다 훨씬 많은 권한을 부여하게 됩니다.
## 로그인
### Email + 인증 코드
1. 이메일을 입력하면 서버가 6자리 코드를 보냅니다.
2. 코드를 입력하면 서버가 JWT 쿠키를 발급(브라우저)하거나 PAT로 교환(CLI)합니다.
<Callout type="warning">
**자체 호스팅 운영자는 주의하세요**: 공개 배포에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두세요. 고정된 로컬 테스트 코드를 활성화하면, `APP_ENV`가 production이 아닌 동안 코드를 요청할 수 있는 누구나 그 값으로 로그인할 수 있습니다. [자체 호스팅 인증 구성](/auth-setup)을 참고하세요.
</Callout>
### Google OAuth
**Sign in with Google**을 클릭하고 표준 OAuth 콜백을 거치세요. 자체 호스팅에는 `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, 그리고 리디렉션 URI를 구성해야 합니다 — [자체 호스팅 인증 구성](/auth-setup)을 참고하세요.
## PAT 생성, 조회, 취소
PAT **생성**은 두 가지 방법으로 할 수 있습니다:
- **Web UI**: 설정 → API 토큰 → 새 토큰
- **CLI**: `multica login`은 아직 로컬 PAT가 없으면 자동으로 하나를 생성합니다
<Callout type="warning">
**전체 PAT는 생성될 때 정확히 한 번만 표시됩니다.** 새로 고침하거나 대화상자를 닫은 후에는 다시 볼 수 없습니다.
Multica는 데이터베이스에 PAT의 해시만 저장합니다 — 서버조차 원본을 가져올 수 없습니다. 즉시 복사하여 저장하세요. 분실하면 유일한 방법은 취소하고 새로 생성하는 것입니다.
</Callout>
기존 PAT **조회**(이름, 생성 시각, 마지막 사용 시각 — 전체 토큰은 **포함하지 않음**)는 설정 → API 토큰에 있습니다.
PAT **취소**: 목록에서 Revoke를 클릭하세요. 취소는 즉시 적용됩니다 — 그 PAT로 보낸 다음 요청은 401로 거부됩니다.
## 로그아웃은 로컬 토큰만 삭제합니다
`multica auth logout`을 실행하거나 Web UI에서 로그아웃을 클릭하면:
- **로컬 토큰이 지워집니다** — CLI는 `~/.multica/config.json`에서 PAT를 제거하고, 브라우저는 쿠키를 삭제합니다.
- **PAT는 서버에서 여전히 유효합니다** — 로그아웃하기 전에 누군가 당신의 PAT를 입수했다면(예를 들어 다른 기기에 복사했다면), 그들은 **여전히 그것을 사용할 수 있습니다**.
<Callout type="warning">
**PAT가 유출되었다고 의심되면, 단순히 로그아웃하지 마세요.** 설정 → API 토큰로 가서 그 토큰을 **취소**하세요. 취소만이 유출된 토큰을 즉시 무효화합니다.
</Callout>
## 다음 단계
- [CLI 명령어 레퍼런스](/cli) — 모든 CLI 명령어의 인증은 자동입니다
- [자체 호스팅 인증 구성](/auth-setup) — 자체 호스팅 시 이메일, OAuth, 가입 허용 목록을 구성하는 방법
- [데몬과 런타임](/daemon-runtimes) — 데몬 토큰이 어디서 오는지

View File

@@ -0,0 +1,239 @@
---
title: オートパイロット
description: エージェントが cron スケジュールやインバウンド webhook で作業を開始したり、UI や CLI で一度だけ手動でトリガーしたりできるようにします。
---
import { Callout } from "fumadocs-ui/components/callout";
オートパイロットは、[エージェント](/agents)が**スケジュールに従って自動的に作業を開始**できるようにします — cron 式とタイムゾーンを設定すると、あなたが何もトリガーしなくても Multica が自ら [`task`](/tasks) をディスパッチします。定期点検、繰り返しのレポート、夜間のクリーンアップ作業など、「常設指示standing order」の形の作業に適しています。残りの 3 つのトリガー経路([割り当て](/assigning-issues)、[@-メンション](/mentioning-agents)、[チャット](/chat) — いずれもあなた自身が起点となる方式)と比べたとき、オートパイロットの核心的な違いは**時間駆動**であることです。
## オートパイロットを構成する
ワークスペースの**オートパイロット**ページで新しいオートパイロットを作成します。次の項目を設定します。
- **名前Name** — 表示名
- **エージェントAgent** — 実行をディスパッチする対象
- **優先度Priority** — 生成される `task` に継承されます(イシューの優先度と同じ意味)
- **説明 / プロンプトDescription / prompt** — 実行のたびにエージェントが受け取る作業説明
- **実行モードExecution mode** — 以下を参照
- **トリガーTriggers** — `schedule`cron + タイムゾーン)または `webhook` のうち少なくとも 1 つ
## 実行モードを選ぶ
オートパイロットには 2 つの実行モードがあります。**「イシュー作成」モードから始めてください。**
- **イシュー作成モードCreate issue mode**`create_issue` — デフォルトであり、**推奨**されます。各トリガーはまずワークスペースにイシューを作成し(タイトルには現在、単一のプレースホルダー `{{date}}` のみがサポートされ、これは `YYYY-MM-DD` 形式の UTC 日付に補間されます。それ以外の `{{...}}` トークンは作成時点で拒否されるため、タイプミスがイシュータイトルにリテラル文字列として静かに紛れ込むことを防ぎます)、通常の割り当てフローを通じてそのイシューをエージェントに割り当てます。すべての作業は、手動で割り当てたイシューと同じ履歴、コメント、ステータスを持った状態でイシューボードに上がります。
- **実行専用モードRun-only mode**`run_only` — イシュー作成をスキップし、`task` を直接キューに入れます。この実行はボードには表示されません — オートパイロットの実行履歴でのみ確認できます。
## スケジュールに従って実行する
すべてのオートパイロットには少なくとも 1 つの `schedule` トリガーが必要です。Cron は**標準の 5 フィールド形式**(分 時 日 月 曜日)を使用し、最小単位は **1 分**です(秒単位はありません)。タイムゾーンは IANA 形式(例: `Asia/Shanghai`で、cron 式がどのタイムゾーンで解釈されるかを決定します。
いくつかの例:
- `0 9 * * 1-5`, `Asia/Shanghai` — 平日の北京時間午前 9 時
- `*/30 * * * *`, `UTC` — 30 分ごと
- `0 3 * * *`, `UTC` — 毎日 UTC 午前 3 時
Multica サーバーは**30 秒**ごとに期限が来たトリガーをスキャンします — **実際の発火時刻は最大 30 秒まで遅れる可能性があり**、秒単位で正確ではありません。発火時刻のあたりでサーバーが再起動された場合、起動時に逃したトリガーを追いつきます(何も失われませんが、すぐに発火します)。
## 一度だけ手動でトリガーする
オートパイロットのデバッグ中に cron を待たないためには、手動でトリガーしてください。
- UI: オートパイロット詳細ページで「Run now」をクリック
- CLI:
```bash
multica autopilot trigger <autopilot-id>
```
手動トリガーは `schedule` トリガーとまったく同じ実行フローを通り — 実行レコードの `source` フィールドのみが `manual` とマークされます。
## webhook からトリガーする
オートパイロットはインバウンドの HTTP webhook でも発火できます。オートパイロット詳細ページで **Webhook** トリガーを追加すると、Multica は次のような形の一意の URL を生成します。
```
https://<your-multica-host>/api/webhooks/autopilots/awt_…
```
その URL に任意の JSON を POST してください — Multica は `source = webhook` で実行を記録し、本文を実行の `trigger_payload` として保存し、schedule トリガーとまったく同じ方法でエージェントをディスパッチします。
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"event":"demo.received","eventPayload":{"message":"hello"}}'
```
**イシュー作成モード**では、インバウンドの payload が新しいイシューの説明に追記され、エージェントがインラインで読めるようになります。**実行専用モード**では、payload はデーモンがエージェントに渡す実行コンテキストの一部になります。
### Payload の形
独自のエンベロープenvelopeを送れます。
```json
{ "event": "github.pull_request.opened", "eventPayload": { } }
```
…または任意の JSON オブジェクト / 配列を送ることもできます。Multica はこれを内部エンベロープに正規化します。
```json
{
"event": "<inferred>",
"eventPayload": <your body>,
"request": { "receivedAt": "<rfc3339>", "contentType": "application/json" }
}
```
`event` フィールドを指定しない場合、Multica は一般的なヘッダーと本文フィールドからこれを推論します(`X-GitHub-Event` + 本文 `action`、`X-Gitlab-Event`、`X-Event-Type`、本文の `event`/`type`/`action`)。どれも一致しない場合、イベントは `webhook.received` になります。
GitHub のようなソースを構成するときは、content type を `application/json` に設定してください — フォームエンコードされた webhook payload は受け付けられません。
### イベントフィルター
新しい webhook トリガーはインバウンドの POST ごとに発火します。単一用途の URL には問題ありませんが、多数のイベントタイプをファンアウトするソースGitHub が代表的です — 単一のリポジトリ webhook 一つが `push`、`pull_request`、`workflow_run`、`check_suite` などを配信できますにはイズになります。webhook トリガーの**イベントフィルターEvent filters**セクションを使うと、実際に実行をディスパッチするイベントを制限でき、それ以外のすべては `status = ignored`、`reason = event_filtered` で配信履歴に記録され、実行もイシューも作成されません。
各行は 1 つのルールです。**イベント名event name**と、任意でカンマ区切りの **action** リストで構成されます。Multica は**いずれか 1 つ**の行でも一致すれば webhook を許可します。セクションを空のままにすると、すべてを受け付けます(フィルタリング以前の動作)。
例:
| イベント名 | Actions | 一致対象 |
| -------------- | ------------------- | ------------------------------------------------------------------------ |
| `workflow_run` | `completed, failed` | `action: completed` または `action: failed` の `workflow_run` イベントのみ |
| `workflow_run` | _(空)_ | action に関係なくすべての `workflow_run` イベント |
| `push` | _(空)_ | すべての `push` イベント |
#### イベント名と action の出所
Multica は次の順序でインバウンドリクエストから `event` 名と `action` を導き出します — **最初に一致したものが優先されます**。
**1. 本文エンベロープBody envelope。** 本文が文字列の `event` フィールドを持つ JSON オブジェクトであれば、その値がそのままイベント名になります。任意の `eventPayload` オブジェクトは、自身の `action` / `state` / `conclusion` / `status` フィールドから action 候補を提供します。
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"event":"trigger","eventPayload":{"action":"true"}}'
# inferred: event = trigger, action candidate = true
```
**2. ヘッダーHeaders。** 本文エンベロープがない場合、Multica は次のよく知られたプロバイダーヘッダーを読みます。
- `X-GitHub-Event: <event>` — (存在する場合)最上位の本文 `action` フィールドと組み合わされて `github.<event>.<action>` を形成します。
- `X-Gitlab-Event: <event>` — `gitlab.<event>` になります。
- `X-Event-Type: <event>` — そのまま通過します。
```bash
# GitHub-style: header gives the event name, body gives the action.
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# inferred: event = github.workflow_run.completed
# → matches a filter row of workflow_run / completed
# Generic event-type header — no body fields needed.
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-Event-Type: trigger.true' \
-H 'Content-Type: application/json' \
-d '{}'
# inferred: event = trigger.true → matches trigger / true
```
**3. 本文フォールバックBody fallback。** 本文エンベロープも既知のヘッダーもない場合、Multica は次の順序で最上位の本文文字列フィールドにフォールバックします: `event` → `type` → `action`。
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"type":"trigger","action":"true"}'
# inferred: event = trigger (from `type`), action candidate = true
```
**4. デフォルトDefault。** 上記のいずれも一致しない場合、イベントは `webhook.received` で、action 候補はありません。
**action 候補、全リスト。** イベントが決定されると、Multica は以下のすべての値を可能な action 一致対象として考慮します。
- イベントが `provider.event.<action>` の形のときのイベント名のサフィックス(例: `github.workflow_run.completed` → `completed`)。
- 本文フィールド `action`、`state`、`conclusion`、`status` — **JSON 文字列のときのみ該当します**。ブール値(`{"action": true}`)や数値は資格がないため、`event=trigger, action=true` を期待するフィルターは `{"trigger": true}` の本文とは決して一致しません。`true` は文字列ではなく bool だからです。
**よくある落とし穴。** `Event name: trigger` / `Actions: true` のようなフィルター行は、「本文に `trigger: true` があれば発火せよ」という意味では**ありません** — イベントフィルターは任意の本文フィールドではなく、*推論されたイベントと action* に一致させます。これにヒットさせるには、`X-Event-Type` で `trigger.true` を送るか(または上に示した本文エンベロープを使ってください)。保存されたフィルター行の周囲の空白(`" workflow_run "`)はそのまま保存され、決して一致しないため — 保存する前に trim してください。
#### クイックテスト
フィルターを構成したら、`curl` で両方の分岐を確認できます。
```bash
# Allowed — header drives event=workflow_run, body drives action=completed
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# → 200 {"status":"accepted", ...}
# Filtered — same event, action not in allowlist
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"in_progress"}'
# → 200 {"status":"ignored","reason":"event_filtered"}
```
### URL は bearer secret です
生成された URL **そのものが**認証情報です。それを持っている人は誰でもオートパイロットを発火できます。トークンのように扱ってください。
- **公開のイシュースレッド、スクリーンショット、チャット履歴に貼り付けないでください。**
- **漏洩したら交換してください** — トリガー行で「Rotate URL」をクリックするか、`multica autopilot trigger-rotate-url <autopilot-id> <trigger-id>` を実行してください。古い URL はただちに動作を停止します。
- 強力なソース認証が必要なソースの場合は、トリガーごとの HMAC 署名検証を待ってください。この v1 URL は bearer 方式のみをサポートします。
- 現時点では、オートパイロットを閲覧できるワークスペースメンバーであればその webhook URL を読めます — 役割ごとのより厳格な secret の可視性は後続作業です。
### ステータスコードの意味
Multica は正常な no-op の結果に対して `status` フィールド付きで `200 OK` を返すため、プロバイダーの webhook 再試行メカニズムが URL を叩き続けることはありません。
- `{"status":"accepted","run_id":"…","autopilot_id":"…","trigger_id":"…"}` — 実行がディスパッチされました。
- `{"status":"skipped","run_id":"…","reason":"agent runtime is offline at dispatch time"}` — 割り当て先のランタイムがオフラインで、`skipped` の実行として記録されます。
- `{"status":"ignored","reason":"trigger_disabled"}` — トリガーが無効になっています。
- `{"status":"ignored","reason":"autopilot_paused"}` — オートパイロットが一時停止しています。
- `{"status":"ignored","reason":"autopilot_archived"}` — オートパイロットがアーカイブされています。
2xx 以外の応答は実際の失敗を扱います。
- `400` — 無効な JSON、スカラー本文、空の本文。
- `404` — 不明なトークン(`{"error":"webhook not found"}`)。
- `413` — payload が 256 KiB を超えました。
- `429` — トークンごとのレート制限超過(デフォルトは 60 req/min
### セルフホスト: 公開 URL を構成する
サーバーに `MULTICA_PUBLIC_URL` が設定されている場合(例: `https://multica.example.com`)、トリガー応答に絶対パスの `webhook_url` が含まれ、UI にはすぐにコピーできる URL が表示されます。設定しない場合、UI はクライアントの API origin から URL を構成します — デスクトップと同一オリジンの Web には問題ありませんが、カスタムのセルフホストリバースプロキシには適しません。Multica は、誤って構成されたリバースプロキシが攻撃者の制御するホストを指す webhook URL をサーバーに発行させて欺くことができないよう、`Host` / `X-Forwarded-Host` ヘッダーから公開ホストを導出しないよう意図的に設計されています。
## 実行履歴を見る
すべてのトリガーは**実行レコードrun record**を生成し、オートパイロット詳細ページの「History」タブで確認できます。
- トリガーソース(`schedule` / `manual` / `webhook`
- 開始時刻、完了時刻
- ステータス(`issue_created` / `running` / `completed` / `failed` / `skipped`
- 連携したイシュー(イシュー作成モード)または `task`(実行専用モード)
- 失敗理由(失敗またはスキップした場合)
## オートパイロットが失敗したらどうなるか
<Callout type="warning">
**オートパイロットの失敗は自動的に再試行されず、インボックス通知も送られません。** 失敗は実行履歴に `failed` のエントリを残すだけで — 割り当てや @-メンションのようなシステムレベルの再キューイングもなく、誰にも通知が行きません。オートパイロットが定期的な場合、**次の cron 発火が新しい実行をトリガー**しますが、失敗した作業が自動的に再実行されることはありません。
オートパイロットが重要な場合は、独自のモニタリングを設計してください — 例えば、エージェントに成功時にコメントを残させ、コメントの欠落に気づくことで失敗を検出する、といった具合です。
</Callout>
自動再試行がない理由: オートパイロットはすでに定期的であるため、システムレベルの再試行を追加すると次の予定実行の上に重なり、重複した実行を生み出します。スケジューリングを完全に cron に任せることで、すっきりと保てます。
## まだ提供されていない機能
**API 種類のトリガーはまだ接続されていません。** トリガースキーマは `api` 種類を予約していますが、それを発火させるイングレスルートはありません。UI は既存の行に Deprecated バッジを表示し、コピー / 交換の操作は提供しません。トリガーごとの HMAC 署名検証、IP 許可リスト、プロバイダー固有のイベントプリセットは後続作業として追跡されており、v1 URL は bearer 方式のみをサポートします。
## 次へ
- [**エージェントにイシューを割り当てる**](/assigning-issues) — イシューをエージェントに一回限りで引き渡す
- [**コメントでエージェントを @-メンションする**](/mentioning-agents) — コメントからエージェントを呼んで一度見てもらう
- [**チャット**](/chat) — イシューの外での一対一の会話

View File

@@ -0,0 +1,239 @@
---
title: 오토파일럿
description: 에이전트가 cron 스케줄, 인바운드 webhook으로 작업을 시작하거나, UI나 CLI로 한 번 수동 트리거하게 합니다.
---
import { Callout } from "fumadocs-ui/components/callout";
오토파일럿은 [에이전트](/agents)가 **스케줄에 따라 자동으로 작업을 시작**하게 합니다 — cron 표현식과 타임존을 설정하면, 여러분이 아무것도 트리거하지 않아도 Multica가 알아서 [`task`](/tasks)를 디스패치합니다. 정기 점검, 반복 보고서, 야간 정리 작업 등 "상시 지시(standing order)" 형태의 작업에 잘 맞습니다. 나머지 세 가지 트리거 경로([할당](/assigning-issues), [@-멘션](/mentioning-agents), [채팅](/chat) — 모두 여러분이 직접 시작하는 방식)와 비교했을 때, 오토파일럿의 핵심 차이는 **시간 기반**이라는 점입니다.
## 오토파일럿 구성하기
워크스페이스의 **오토파일럿** 페이지에서 새 오토파일럿을 만듭니다. 다음 항목을 설정합니다.
- **이름(Name)** — 표시 이름
- **에이전트(Agent)** — 실행을 디스패치할 대상
- **우선순위(Priority)** — 생성되는 `task`에 상속됩니다(이슈 우선순위와 동일한 의미)
- **설명 / 프롬프트(Description / prompt)** — 매 실행마다 에이전트가 받는 작업 설명
- **실행 모드(Execution mode)** — 아래 참고
- **트리거(Triggers)** — `schedule`(cron + 타임존) 또는 `webhook` 중 최소 하나
## 실행 모드 선택하기
오토파일럿에는 두 가지 실행 모드가 있습니다. **"이슈 생성" 모드부터 시작하세요.**
- **이슈 생성 모드(Create issue mode)**(`create_issue`) — 기본값이며 **권장**됩니다. 각 트리거는 먼저 워크스페이스에 이슈를 생성한 다음(제목에는 현재 단일 플레이스홀더 `{{date}}` 하나만 지원되며, 이는 `YYYY-MM-DD` 형식의 UTC 날짜로 보간됩니다. 그 외의 `{{...}}` 토큰은 생성 시점에 거부되므로, 오타가 이슈 제목에 리터럴 문자열로 조용히 들어가는 일을 막습니다), 일반 할당 흐름을 통해 그 이슈를 에이전트에게 할당합니다. 모든 작업은 수동으로 할당한 이슈와 동일한 히스토리, 댓글, 상태를 가진 채 이슈 보드에 올라갑니다.
- **실행 전용 모드(Run-only mode)**(`run_only`) — 이슈 생성을 건너뛰고 `task`를 곧바로 대기열에 넣습니다. 이 실행은 보드에 표시되지 않으며 — 오토파일럿의 실행 히스토리에서만 확인할 수 있습니다.
## 스케줄에 따라 실행하기
모든 오토파일럿에는 최소 하나의 `schedule` 트리거가 필요합니다. Cron은 **표준 5필드 형식**(분 시 일 월 요일)을 사용하며, 최소 단위는 **1분**입니다(초 단위는 없음). 타임존은 IANA 형식(예: `Asia/Shanghai`)이며, cron 표현식이 어느 타임존으로 해석될지를 결정합니다.
몇 가지 예시입니다.
- `0 9 * * 1-5`, `Asia/Shanghai` — 평일 베이징 시간 오전 9시
- `*/30 * * * *`, `UTC` — 30분마다
- `0 3 * * *`, `UTC` — 매일 UTC 오전 3시
Multica 서버는 **30초**마다 만료된 트리거를 스캔합니다 — **실제 발화 시각은 최대 30초까지 지연될 수 있으며**, 초 단위로 정확하지는 않습니다. 발화 시각 즈음에 서버가 재시작되면, 시작 시 놓친 트리거를 따라잡습니다(아무것도 유실되지 않지만 즉시 발화됩니다).
## 수동으로 한 번 트리거하기
오토파일럿을 디버깅하는 동안 cron을 기다리지 않으려면 수동으로 트리거하세요.
- UI: 오토파일럿 상세 페이지에서 "Run now" 클릭
- CLI:
```bash
multica autopilot trigger <autopilot-id>
```
수동 트리거는 `schedule` 트리거와 완전히 동일한 실행 흐름을 거치며 — 실행 레코드의 `source` 필드만 `manual`로 표시됩니다.
## Webhook으로 트리거하기
오토파일럿은 인바운드 HTTP webhook으로도 발화할 수 있습니다. 오토파일럿 상세 페이지에서 **Webhook** 트리거를 추가하면, Multica가 다음과 같은 형태의 고유 URL을 생성합니다.
```
https://<your-multica-host>/api/webhooks/autopilots/awt_…
```
그 URL로 아무 JSON이나 POST하세요 — Multica는 `source = webhook`로 실행을 기록하고, 본문을 실행의 `trigger_payload`로 저장한 다음, schedule 트리거와 정확히 동일한 방식으로 에이전트를 디스패치합니다.
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"event":"demo.received","eventPayload":{"message":"hello"}}'
```
**이슈 생성 모드**에서는 인바운드 payload가 새 이슈의 설명에 덧붙여져 에이전트가 인라인으로 읽을 수 있습니다. **실행 전용 모드**에서는 payload가 데몬이 에이전트에게 넘기는 실행 컨텍스트의 일부가 됩니다.
### Payload 형태
직접 만든 봉투(envelope)를 보낼 수 있습니다.
```json
{ "event": "github.pull_request.opened", "eventPayload": { } }
```
…또는 임의의 JSON 객체/배열을 보낼 수도 있습니다. Multica는 이를 내부 봉투로 정규화합니다.
```json
{
"event": "<inferred>",
"eventPayload": <your body>,
"request": { "receivedAt": "<rfc3339>", "contentType": "application/json" }
}
```
`event` 필드를 제공하지 않으면, Multica는 일반적인 헤더와 본문 필드로부터 이를 추론합니다(`X-GitHub-Event` + 본문 `action`, `X-Gitlab-Event`, `X-Event-Type`, 본문의 `event`/`type`/`action`). 어느 것도 일치하지 않으면 이벤트는 `webhook.received`가 됩니다.
GitHub 같은 소스를 구성할 때는 content type을 `application/json`으로 설정하세요 — 폼 인코딩된 webhook payload는 허용되지 않습니다.
### 이벤트 필터
새 webhook 트리거는 인바운드 POST마다 발화합니다. 단일 용도 URL에는 괜찮지만, 여러 이벤트 타입을 팬아웃하는 소스(GitHub가 대표적입니다 — 단일 저장소 webhook 하나가 `push`, `pull_request`, `workflow_run`, `check_suite` 등을 전달할 수 있습니다)에는 시끄럽습니다. webhook 트리거의 **이벤트 필터(Event filters)** 섹션을 사용하면 실제로 실행을 디스패치할 이벤트를 제한할 수 있으며, 그 외의 모든 것은 `status = ignored`, `reason = event_filtered`로 전달 히스토리에 기록되고 실행이나 이슈는 생성되지 않습니다.
각 행은 하나의 규칙입니다. **이벤트 이름(event name)**과 선택적으로 쉼표로 구분한 **action** 목록으로 구성됩니다. Multica는 **어느 한** 행이라도 일치하면 webhook을 허용합니다. 섹션을 비워 두면 모든 것을 수락합니다(필터링 이전 동작).
예시:
| 이벤트 이름 | Actions | 일치 대상 |
| -------------- | ------------------- | ------------------------------------------------------------------------ |
| `workflow_run` | `completed, failed` | `action: completed` 또는 `action: failed`인 `workflow_run` 이벤트만 |
| `workflow_run` | _(비어 있음)_ | action에 상관없이 모든 `workflow_run` 이벤트 |
| `push` | _(비어 있음)_ | 모든 `push` 이벤트 |
#### 이벤트 이름과 action의 출처
Multica는 다음 순서로 인바운드 요청에서 `event` 이름과 `action`을 도출하며 — **첫 번째로 일치하는 것이 우선합니다**.
**1. 본문 봉투(Body envelope).** 본문이 문자열 `event` 필드를 가진 JSON 객체이면, 그 값이 곧 이벤트 이름입니다. 선택적인 `eventPayload` 객체는 자신의 `action` / `state` / `conclusion` / `status` 필드에서 action 후보를 제공합니다.
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"event":"trigger","eventPayload":{"action":"true"}}'
# inferred: event = trigger, action candidate = true
```
**2. 헤더(Headers).** 본문 봉투가 없을 때, Multica는 다음의 잘 알려진 제공자 헤더를 읽습니다.
- `X-GitHub-Event: <event>` — (있는 경우) 최상위 본문 `action` 필드와 결합해 `github.<event>.<action>`을 형성합니다.
- `X-Gitlab-Event: <event>` — `gitlab.<event>`가 됩니다.
- `X-Event-Type: <event>` — 그대로 통과합니다.
```bash
# GitHub-style: header gives the event name, body gives the action.
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# inferred: event = github.workflow_run.completed
# → matches a filter row of workflow_run / completed
# Generic event-type header — no body fields needed.
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-Event-Type: trigger.true' \
-H 'Content-Type: application/json' \
-d '{}'
# inferred: event = trigger.true → matches trigger / true
```
**3. 본문 폴백(Body fallback).** 본문 봉투도 알려진 헤더도 없으면, Multica는 다음 순서로 최상위 본문 문자열 필드로 폴백합니다: `event` → `type` → `action`.
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"type":"trigger","action":"true"}'
# inferred: event = trigger (from `type`), action candidate = true
```
**4. 기본값(Default).** 위 어느 것도 일치하지 않으면, 이벤트는 `webhook.received`이고 action 후보는 없습니다.
**action 후보, 전체 목록.** 이벤트가 결정되면, Multica는 아래의 모든 값을 가능한 action 일치 대상으로 고려합니다.
- 이벤트가 `provider.event.<action>` 형태일 때의 이벤트 이름 접미사(예: `github.workflow_run.completed` → `completed`).
- 본문 필드 `action`, `state`, `conclusion`, `status` — **JSON 문자열일 때만 해당됩니다**. 불리언(`{"action": true}`)이나 숫자는 자격이 없으므로, `event=trigger, action=true`를 기대하는 필터는 `{"trigger": true}` 본문과 절대 일치하지 않습니다. `true`는 문자열이 아니라 bool이기 때문입니다.
**흔한 함정.** `Event name: trigger` / `Actions: true` 같은 필터 행은 "본문에 `trigger: true`가 있으면 발화하라"는 뜻이 **아닙니다** — 이벤트 필터는 임의의 본문 필드가 아니라 *추론된 이벤트와 action*에 일치시킵니다. 이를 적중시키려면 `X-Event-Type`로 `trigger.true`를 보내거나(또는 위에 표시된 본문 봉투를 사용하세요). 저장된 필터 행에서 주변 공백(`" workflow_run "`)은 그대로 저장되어 절대 일치하지 않으므로 — 저장하기 전에 trim하세요.
#### 빠른 테스트
필터를 구성한 후에는 `curl`로 두 분기를 모두 확인할 수 있습니다.
```bash
# Allowed — header drives event=workflow_run, body drives action=completed
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# → 200 {"status":"accepted", ...}
# Filtered — same event, action not in allowlist
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"in_progress"}'
# → 200 {"status":"ignored","reason":"event_filtered"}
```
### URL은 bearer secret입니다
생성된 URL **자체가** 자격 증명입니다. 그것을 가진 사람은 누구나 오토파일럿을 발화할 수 있습니다. 토큰처럼 다루세요.
- **공개된 이슈 스레드, 스크린샷, 채팅 히스토리에 붙여넣지 마세요.**
- **유출되면 교체하세요** — 트리거 행에서 "Rotate URL"을 클릭하거나 `multica autopilot trigger-rotate-url <autopilot-id> <trigger-id>`를 실행하세요. 기존 URL은 즉시 작동을 멈춥니다.
- 강력한 소스 인증이 필요한 소스의 경우, 트리거별 HMAC 서명 검증을 기다리세요. 이 v1 URL은 bearer 방식만 지원합니다.
- 현재로서는 오토파일럿을 볼 수 있는 워크스페이스 멤버라면 그 webhook URL을 읽을 수 있습니다 — 역할별로 더 엄격한 secret 가시성은 후속 작업입니다.
### 상태 코드 의미
Multica는 정상적인 no-op 결과에 대해 `status` 필드와 함께 `200 OK`를 반환하므로, 제공자의 webhook 재시도 메커니즘이 URL을 계속 두드리지 않습니다.
- `{"status":"accepted","run_id":"…","autopilot_id":"…","trigger_id":"…"}` — 실행이 디스패치되었습니다.
- `{"status":"skipped","run_id":"…","reason":"agent runtime is offline at dispatch time"}` — 수신 대상의 런타임이 오프라인이며, `skipped` 실행으로 기록됩니다.
- `{"status":"ignored","reason":"trigger_disabled"}` — 트리거가 비활성화되어 있습니다.
- `{"status":"ignored","reason":"autopilot_paused"}` — 오토파일럿이 일시 정지되어 있습니다.
- `{"status":"ignored","reason":"autopilot_archived"}` — 오토파일럿이 보관되어 있습니다.
2xx가 아닌 응답은 실제 실패를 다룹니다.
- `400` — 유효하지 않은 JSON, 스칼라 본문, 빈 본문.
- `404` — 알 수 없는 토큰(`{"error":"webhook not found"}`).
- `413` — payload가 256 KiB를 초과했습니다.
- `429` — 토큰별 속도 제한 초과(기본값 60 req/min).
### 자체 호스팅: 공개 URL 구성하기
서버에 `MULTICA_PUBLIC_URL`이 설정되어 있으면(예: `https://multica.example.com`), 트리거 응답에 절대 경로 `webhook_url`이 포함되고 UI에는 복사 가능한 URL이 바로 표시됩니다. 설정하지 않으면 UI는 클라이언트의 API origin으로부터 URL을 구성합니다 — 데스크톱과 동일 출처 웹에는 괜찮지만, 커스텀 자체 호스팅 리버스 프록시에는 적합하지 않습니다. Multica는 잘못 구성된 리버스 프록시가 공격자가 제어하는 호스트를 가리키는 webhook URL을 서버가 발행하도록 속이지 못하게, `Host` / `X-Forwarded-Host` 헤더로부터 공개 호스트를 도출하지 않도록 의도적으로 설계되었습니다.
## 실행 히스토리 보기
모든 트리거는 **실행 레코드(run record)**를 생성하며, 오토파일럿 상세 페이지의 "History" 탭에서 볼 수 있습니다.
- 트리거 소스(`schedule` / `manual` / `webhook`)
- 시작 시각, 완료 시각
- 상태(`issue_created` / `running` / `completed` / `failed` / `skipped`)
- 연결된 이슈(이슈 생성 모드) 또는 `task`(실행 전용 모드)
- 실패 사유(실패하거나 건너뛴 경우)
## 오토파일럿이 실패하면 어떻게 되나요
<Callout type="warning">
**오토파일럿 실패는 자동으로 재시도되지 않으며 인박스 알림도 보내지 않습니다.** 실패는 실행 히스토리에 `failed` 항목을 남길 뿐 — 할당이나 @-멘션처럼 시스템 수준에서 다시 대기열에 넣는 일도 없고, 누구에게도 알림이 가지 않습니다. 오토파일럿이 주기적이라면 **다음 cron 발화가 새 실행을 트리거**하지만, 실패한 작업이 자동으로 다시 실행되지는 않습니다.
오토파일럿이 중요하다면 직접 모니터링을 설계하세요 — 예를 들어 에이전트가 성공 시 댓글을 남기게 하고, 댓글이 누락된 것을 알아채 실패를 잡아내는 식입니다.
</Callout>
자동 재시도가 없는 이유: 오토파일럿은 이미 주기적이므로, 시스템 수준의 재시도를 추가하면 다음 예약된 실행 위에 겹쳐 중복 실행을 만들어 냅니다. 스케줄링을 전적으로 cron에 맡기면 깔끔하게 유지됩니다.
## 아직 제공되지 않는 기능
**API 종류의 트리거는 아직 연결되어 있지 않습니다.** 트리거 스키마는 `api` 종류를 예약해 두었지만, 그것을 발화하는 인그레스 라우트가 없습니다. UI는 기존 행에 Deprecated 배지를 표시하며 복사/교체 기능을 제공하지 않습니다. 트리거별 HMAC 서명 검증, IP 허용 목록, 제공자별 이벤트 프리셋은 후속 작업으로 추적되고 있으며, v1 URL은 bearer 방식만 지원합니다.
## 다음
- [**에이전트에게 이슈 할당하기**](/assigning-issues) — 이슈를 에이전트에게 일회성으로 넘기기
- [**댓글에서 에이전트 @-멘션하기**](/mentioning-agents) — 댓글에서 에이전트를 불러 한번 살펴보게 하기
- [**채팅**](/chat) — 이슈 밖에서의 일대일 대화

View File

@@ -0,0 +1,63 @@
---
title: チャット
description: どのイシューにも属さない、エージェントとの一対一の会話 — 完全にサンドボックス化されています。エージェントはイシューを見たり変更したりできず、他の誰もこの会話を見ることはできません。
---
import { Callout } from "fumadocs-ui/components/callout";
**チャットはあなたと[エージェント](/agents)との一対一の会話です** — [イシュー](/issues)ボードから外に出るものです。エージェントはどのイシューも見られず、どのイシューも変更できず、会話全体は**完全に非公開**です([ワークスペース](/workspaces)内の他の誰も、admin を含めて、この会話を見ることはできません)。エージェントとアプローチを議論したり、ブレインストーミングをしたり、どのイシューにも属さない質問をしたりするのに適しています。
## エージェントを @-メンションするだけではだめなのですか?
[@-メンション](/mentioning-agents)はエージェントをイシューのコンテキスト**の中へ引き入れます** — エージェントはイシューの説明とすべての過去のコメントを読み、イシューを変更できます。チャットはこれを逆転させます。**あなたをイシューの外へ引き出します** — エージェントはこの単一の会話のみを見られ、どのイシューの存在も認識せず、イシューを変更する入口もありません。
2 つの判断基準:
- 特定のイシューのコンテキストに基づくフィードバックがほしいとき → [@-メンション](/mentioning-agents)
- どのイシューとも無関係なトピックを議論したいとき(または他の誰にも議論を見られたくないとき) → チャット
## 会話を始める
サイドバーから**チャット**を開き、エージェントを選んで、新しい会話を始めてください。インターフェースはどのメッセージングアプリとも似ています。メッセージを送るとエージェントが返信します。各メッセージはバックグラウンドで実行をトリガーするため(キューに入れられた `task`)、返信には数秒かかることがあります。
## チャットでエージェントができることとできないこと
エージェントは会話の中で**完全にサンドボックス化された**モードで実行されます。
**できること:**
- 現在のメッセージに含まれる質問に答える
- 構成された[スキル](/skills)と MCP を使う
- 自身の作業ディレクトリでファイルを読み書きする
- イシューコンテキストを必要としない `multica` CLI コマンドを呼び出す(例: 基本的なワークスペース情報の照会)
**できないこと:**
- **どのイシューも見ること** — エージェントが受け取るプロンプトにはイシュー ID がなく、`multica issue list` のようなコマンドは空の結果を返します
- **どのイシューも変更すること** — イシューコンテキストがなければ、権限チェックによって API 呼び出しがブロックされます
- **他の会話を見ること** — 会話は完全に隔離されています
- **誰かや任意のエージェントを @-メンションすること** — チャットは他者に通知する経路のない非公開の空間です
## 複数ターンのコンテキストが保持される仕組み
チャットは**プロバイダーセッションの再開**を通じて複数ターンのコンテキストを維持します — エージェントは最初の返信でプロバイダーセッションを確立し(例: Claude セッション)、そのセッション ID が保存されます。次のメッセージでは、タスクのディスパッチがその ID を渡し直すため、エージェントは毎回履歴を読み直すことなく**中断したところから再開**します。
もし**1 つのターンが失敗した**場合、Multica はセッション ID を確立していた以前のタスク(そのタスクが成功したか失敗したかにかかわらず)を探し、再開を試みます — 途中で一度失敗したからといって、会話全体の記憶が失われることはありません。
注: すべてのプロバイダーが実際にセッション再開を実装しているわけではありません — サポート状況は[**プロバイダーマトリックス**](/providers)を参照してください。
## 会話をアーカイブする
もう見たくない会話はアーカイブできます — 会話一覧で右クリックするか、詳細ページの「アーカイブ」ボタンを使ってください。アーカイブ後は次のようになります。
- 会話がアクティブな一覧から消えます(「アーカイブ済み」ビューで引き続き見つけられます)
- 過去のメッセージ、セッション ID、作業ディレクトリはすべて保持されます — 何も削除されません
<Callout type="warning">
**アーカイブ後には「復元」ボタンがありません。** 現在、アーカイブされた会話を再びアクティブな状態に戻す入口はありません。後でそのスレッドを続けたい場合は、新しい会話を始める必要があります。アーカイブされた会話の内容を再び見るには、「アーカイブ済み」ビューを開いて履歴を読んでください。
</Callout>
## 次へ
- [**オートパイロット**](/autopilots) — エージェントがスケジュールに従って自動的に作業を開始できるようにします
- [**エージェントにイシューを割り当てる**](/assigning-issues) — トピックをイシューボードに戻します

View File

@@ -0,0 +1,63 @@
---
title: 채팅
description: 어떤 이슈에도 속하지 않는, 에이전트와의 일대일 대화 — 완전히 샌드박스화되어 있습니다. 에이전트는 이슈를 보거나 변경할 수 없으며, 다른 누구도 이 대화를 볼 수 없습니다.
---
import { Callout } from "fumadocs-ui/components/callout";
**채팅은 당신과 [에이전트](/agents) 사이의 일대일 대화입니다** — [이슈](/issues) 보드에서 벗어나는 것입니다. 에이전트는 어떤 이슈도 보지 못하고 어떤 이슈도 변경할 수 없으며, 대화 전체가 **완전히 비공개**입니다([워크스페이스](/workspaces) 내의 다른 누구도, admin을 포함해서, 이 대화를 볼 수 없습니다). 에이전트와 접근 방식을 논의하거나, 브레인스토밍을 하거나, 어떤 이슈에도 속하지 않는 질문을 하기에 적합합니다.
## 그냥 에이전트를 @-멘션하면 안 되나요?
[@-멘션](/mentioning-agents)은 에이전트를 이슈의 컨텍스트 **안으로 끌어들입니다** — 에이전트는 이슈 설명과 모든 과거 댓글을 읽고, 이슈를 변경할 수 있습니다. 채팅은 이를 뒤집습니다. **당신을 이슈 밖으로 끌어냅니다** — 에이전트는 이 단일 대화만 볼 수 있고, 어떤 이슈의 존재도 인지하지 못하며, 이슈를 수정할 진입점도 없습니다.
두 가지 판단 기준:
- 특정 이슈의 컨텍스트에 기반한 피드백을 원할 때 → [@-멘션](/mentioning-agents)
- 어떤 이슈와도 무관한 주제를 논의하고 싶을 때(또는 다른 누구에게도 논의를 보이고 싶지 않을 때) → 채팅
## 대화 시작하기
사이드바에서 **채팅**을 열고, 에이전트를 선택한 다음, 새 대화를 시작하세요. 인터페이스는 여느 메시징 앱과 비슷합니다. 메시지를 보내면 에이전트가 답장합니다. 각 메시지는 백그라운드에서 실행을 트리거하므로(대기열에 들어간 `task`), 답장에는 몇 초가 걸릴 수 있습니다.
## 채팅에서 에이전트가 할 수 있는 일과 할 수 없는 일
에이전트는 대화 안에서 **완전히 샌드박스화된** 모드로 실행됩니다.
**할 수 있는 일:**
- 현재 메시지에 담긴 질문에 답하기
- 구성된 [스킬](/skills)과 MCP 사용하기
- 자신의 작업 디렉터리에서 파일 읽기 및 쓰기
- 이슈 컨텍스트가 필요 없는 `multica` CLI 명령 호출하기(예: 기본 워크스페이스 정보 조회)
**할 수 없는 일:**
- **어떤 이슈도 보기** — 에이전트가 받는 프롬프트에는 이슈 ID가 없으며, `multica issue list` 같은 명령은 빈 결과를 반환합니다
- **어떤 이슈도 변경하기** — 이슈 컨텍스트가 없으면 권한 검사에 의해 API 호출이 차단됩니다
- **다른 대화 보기** — 대화는 완전히 격리되어 있습니다
- **누구도, 어떤 에이전트도 @-멘션하기** — 채팅은 다른 사람에게 알릴 경로가 없는 비공개 공간입니다
## 여러 턴에 걸친 컨텍스트가 보존되는 방식
채팅은 **제공자 세션 재개**를 통해 여러 턴에 걸친 컨텍스트를 유지합니다 — 에이전트는 첫 답장에서 제공자 세션을 설정하고(예: Claude 세션), 그 세션 ID가 저장됩니다. 다음 메시지에서는 작업 디스패치가 그 ID를 다시 전달하므로, 에이전트는 매번 기록을 다시 읽지 않고도 **중단했던 지점부터 이어서 재개**합니다.
만약 **한 턴이 실패하면**, Multica는 세션 ID를 설정했던 이전 작업(그 작업이 성공했든 실패했든)을 찾아 재개를 시도합니다 — 중간에 한 번 실패한다고 해서 대화 전체의 기억이 사라지지는 않습니다.
참고: 모든 제공자가 실제로 세션 재개를 구현하는 것은 아닙니다 — 지원 현황은 [**제공자 매트릭스**](/providers)를 참고하세요.
## 대화 보관하기
더 이상 보고 싶지 않은 대화는 보관할 수 있습니다 — 대화 목록에서 우클릭하거나 상세 페이지의 "보관" 버튼을 사용하세요. 보관 후에는:
- 대화가 활성 목록에서 사라집니다("보관됨" 보기에서 여전히 찾을 수 있습니다)
- 과거 메시지, 세션 ID, 작업 디렉터리가 모두 보존됩니다 — 아무것도 삭제되지 않습니다
<Callout type="warning">
**보관 후에는 "복원" 버튼이 없습니다.** 현재 보관된 대화를 다시 활성 상태로 되돌리는 진입점이 없습니다. 나중에 그 스레드를 계속 이어가고 싶다면 새 대화를 시작해야 합니다. 보관된 대화의 내용을 다시 보려면 "보관됨" 보기를 열고 기록을 읽어 보세요.
</Callout>
## 다음
- [**오토파일럿**](/autopilots) — 에이전트가 일정에 따라 자동으로 작업을 시작하도록 하세요
- [**에이전트에게 이슈 할당하기**](/assigning-issues) — 주제를 이슈 보드로 다시 가져오세요

View File

@@ -0,0 +1,147 @@
---
title: CLI コマンドリファレンス
description: すべてのトップレベル Multica CLI コマンドを 1 ページにまとめた概要です。完全な使い方は `multica <command> --help` を実行してください。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica CLI は、Web UI でできるほぼすべての操作をそのまま提供します([イシュー](/issues)の作成、[エージェント](/agents)の割り当て、[デーモン](/daemon-runtimes)の起動など)。このページでは、すべてのトップレベルコマンドを 1 行の説明とともに一覧します。フラグや例の完全な一覧は `multica <command> --help` を実行してください。
## 認証する
CLI を初めて使うときにこのコマンドを実行して、**パーソナルアクセストークンPAT**を取得します。
```bash
multica login
```
ブラウザが自動的に開きます。Web アプリで承認すると、CLI が PAT`mul_` プレフィックス付き)を `~/.multica/config.json` に保存します。これ以降のすべてのコマンドはこの PAT で認証されます。
<Callout type="tip">
CI やヘッドレス環境では、ブラウザフローをスキップできます。Web アプリの **Settings → Personal Access Tokens** で PAT を作成し、`multica login --token <mul_...>` で直接渡してください。
</Callout>
トークンの種類による違いについては、[認証とトークン](/auth-tokens)を参照してください。
## 認証とセットアップ
| コマンド | 用途 |
|---|---|
| `multica login` | ログインして PAT を保存 |
| `multica auth status` | 現在のログイン状態、ユーザー、ワークスペースを表示 |
| `multica auth logout` | ローカルの PAT を削除 |
| `multica setup cloud` | Multica Cloud のワンショットセットアップ(ログイン + デーモンのインストール) |
| `multica setup self-host` | セルフホストバックエンドのワンショットセットアップ |
## ワークスペースとメンバー
| コマンド | 用途 |
|---|---|
| `multica workspace list` | アクセスできるすべてのワークスペースを一覧 |
| `multica workspace get <slug>` | 1 つのワークスペースの詳細を表示 |
| `multica workspace member list` | 現在のワークスペースのメンバーを一覧 |
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | ワークスペースのメタデータを更新admin/owner。長いフィールドは `--description-stdin` / `--context-stdin` を使用できます。 |
## イシューとプロジェクト
<Callout type="info">
`list` 系のコマンド(`multica issue list`、`autopilot list`、`project list` など)は、デフォルトで短く**そのままコピー&ペーストできる** ID を出力します。イシューは `MUL-123` のようなイシューキー、それ以外のリソースは短い UUID プレフィックスです。以下の後続コマンドの `<id>` 引数は短い ID と完全な UUID のどちらも受け取るため、一般的な流れは `multica issue list` → キーをコピー → `multica issue get MUL-123` となります。正式な UUID が必要なときは `list` コマンドに `--full-id` を渡してください。
</Callout>
| コマンド | 用途 |
|---|---|
| `multica issue list` | イシューを一覧(コピー&ペーストできるイシューキーを出力) |
| `multica issue get <id>` | 単一のイシューを表示(イシューキーまたは UUID を受け取る) |
| `multica issue create --title "..."` | 新しいイシューを作成 |
| `multica issue update <id> ...` | イシューを更新(ステータス、優先度、担当者など) |
| `multica issue assign <id> --agent <slug>` | エージェントに割り当て(即座にタスクをトリガー) |
| `multica issue status <id> --set <status>` | ステータス変更のショートカット |
| `multica issue search <query>` | キーワード検索 |
| `multica issue runs <id>` | イシュー上のエージェント実行を表示 |
| `multica issue rerun <id>` | イシューの現在のエージェント担当者向けに新しいタスクを再キューイング |
| `multica issue comment <id> ...` | ネスト: コメントの表示 / 投稿 |
| `multica issue subscriber <id> ...` | ネスト: 購読 / 購読解除 |
| `multica project list/get/create/update/delete/status` | プロジェクトの CRUD |
## エージェントとスキル
| コマンド | 用途 |
|---|---|
| `multica agent list` | ワークスペースのエージェントを一覧 |
| `multica agent get <slug>` | エージェントの構成を表示 |
| `multica agent create ...` | エージェントを作成 |
| `multica agent update <slug> ...` | エージェントを更新 |
| `multica agent archive <slug>` | アーカイブ |
| `multica agent restore <slug>` | アーカイブ済みのエージェントを復元 |
| `multica agent tasks <slug>` | エージェントのタスク履歴を表示 |
| `multica agent skills ...` | ネスト: スキルのアタッチ / デタッチ |
| `multica skill list/get/create/update/delete` | スキルの CRUD |
| `multica skill import ...` | GitHub、ClawHub、またはローカルマシンからスキルをインポート |
| `multica skill files ...` | ネスト: スキルのファイルを管理 |
## スクワッド
| コマンド | 用途 |
|---|---|
| `multica squad list` | ワークスペースのスクワッドを一覧 |
| `multica squad get <id>` | 単一のスクワッドを表示 |
| `multica squad create --name "..." --leader <agent>` | スクワッドを作成owner / admin |
| `multica squad update <id> ...` | 名前、説明、指示、リーダー、またはアバターを更新 |
| `multica squad delete <id>` | アーカイブ(ソフト削除) — 割り当て済みのイシューをリーダーに移管 |
| `multica squad member list/add/remove <squad-id>` | スクワッドメンバーを管理 |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | スクワッドリーダーエージェントがターンごとに評価を記録するために使用 |
完全なモデルについては[スクワッド](/squads)を参照してください。
## オートパイロット
| コマンド | 用途 |
|---|---|
| `multica autopilot list` | ワークスペースのすべてのオートパイロットを一覧 |
| `multica autopilot get <id>` | 単一のオートパイロットを表示 |
| `multica autopilot create ...` | オートパイロットを作成 |
| `multica autopilot update <id> ...` | 更新 |
| `multica autopilot delete <id>` | 削除 |
| `multica autopilot runs <id>` | 実行履歴を表示 |
| `multica autopilot trigger <id>` | 手動で実行をトリガー |
## デーモンとランタイム
| コマンド | 用途 |
|---|---|
| `multica daemon start` | デーモンを起動(デフォルトはバックグラウンド。`--foreground` を追加するとフォアグラウンドで実行) |
| `multica daemon stop` | デーモンを停止 |
| `multica daemon restart` | デーモンを再起動 |
| `multica daemon status` | デーモンがオンラインかどうかと同時実行数を確認 |
| `multica daemon logs` | デーモンのログを表示 |
| `multica runtime list` | 現在のワークスペースのランタイムを一覧 |
| `multica runtime usage` | リソース使用量を表示 |
| `multica runtime activity` | 最近のアクティビティログ |
| `multica runtime update <id> ...` | ランタイムの構成を更新 |
## その他
| コマンド | 用途 |
|---|---|
| `multica repo checkout <url>` | エージェントが使用できるようにリポジトリをローカルにクローン |
| `multica config` | ローカルの CLI 構成を表示または編集 |
| `multica version` | CLI のバージョンを出力 |
| `multica update` | CLI を最新のリリースにアップグレード |
| `multica attachment download <id>` | イシューまたはコメントから添付ファイルをダウンロード |
## 完全なフラグを確認する
すべてのコマンドが `--help` をサポートしています。
```bash
multica issue create --help
multica agent update --help
```
v2 では、各コマンドごとに専用の詳細なリファレンスページを提供する予定です。
## 次のステップ
- [認証とトークン](/auth-tokens) — PAT vs. JWT vs. デーモントークン
- [デーモンとランタイム](/daemon-runtimes) — `daemon` コマンドが内部でどう動作するか
- [エージェントの作成と構成](/agents-create) — `multica agent create` のすべてのオプション

View File

@@ -0,0 +1,147 @@
---
title: CLI 명령어 레퍼런스
description: "모든 최상위 Multica CLI 명령어를 한 페이지로 정리한 개요입니다. 전체 사용법은 `multica <command> --help`를 실행하세요."
---
import { Callout } from "fumadocs-ui/components/callout";
Multica CLI는 Web UI에서 할 수 있는 거의 모든 작업을 그대로 제공합니다([이슈](/issues) 생성, [에이전트](/agents) 할당, [데몬](/daemon-runtimes) 시작 등). 이 페이지는 모든 최상위 명령어를 한 줄 설명과 함께 나열합니다. 전체 플래그와 예제는 `multica <command> --help`를 실행하세요.
## 인증하기
CLI를 처음 사용할 때 이 명령을 실행해 **개인 액세스 토큰(personal access token, PAT)**을 발급받으세요:
```bash
multica login
```
브라우저가 자동으로 열립니다. 웹 앱에서 승인하면 CLI가 PAT(`mul_` 접두사)를 `~/.multica/config.json`에 저장합니다. 이후 모든 명령은 이 PAT로 인증됩니다.
<Callout type="tip">
CI나 headless 환경에서는 브라우저 플로우를 건너뛰세요. 웹 앱의 **설정 → API 토큰**에서 PAT를 만든 뒤 `multica login --token <mul_...>`로 직접 전달하면 됩니다.
</Callout>
토큰 유형 간의 차이는 [인증과 토큰](/auth-tokens)을 참고하세요.
## 인증과 설정
| 명령어 | 용도 |
|---|---|
| `multica login` | 로그인하고 PAT 저장 |
| `multica auth status` | 현재 로그인 상태, 사용자, 워크스페이스 표시 |
| `multica auth logout` | 로컬 PAT 삭제 |
| `multica setup cloud` | Multica Cloud용 원샷 설정(로그인 + 데몬 설치) |
| `multica setup self-host` | 자체 호스팅 백엔드용 원샷 설정 |
## 워크스페이스와 멤버
| 명령어 | 용도 |
|---|---|
| `multica workspace list` | 접근할 수 있는 모든 워크스페이스 나열 |
| `multica workspace get <slug>` | 워크스페이스 하나의 상세 정보 표시 |
| `multica workspace member list` | 현재 워크스페이스의 멤버 나열 |
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | 워크스페이스 메타데이터 업데이트(admin/owner). 긴 필드는 `--description-stdin` / `--context-stdin`을 사용할 수 있습니다. |
## 이슈와 프로젝트
<Callout type="info">
`list` 계열 명령(`multica issue list`, `autopilot list`, `project list` 등)은 기본적으로 짧고 **바로 복사해 붙여 넣을 수 있는** ID를 출력합니다. 이슈는 `MUL-123` 같은 이슈 키이고, 나머지 리소스는 짧은 UUID 접두사입니다. 아래 후속 명령의 `<id>` 인자는 짧은 ID와 전체 UUID를 모두 받으므로, 일반적인 흐름은 `multica issue list` → 키 복사 → `multica issue get MUL-123`입니다. 정식 UUID가 필요할 때는 `list` 명령에 `--full-id`를 전달하세요.
</Callout>
| 명령어 | 용도 |
|---|---|
| `multica issue list` | 이슈 나열(복사해 붙여 넣을 수 있는 이슈 키 출력) |
| `multica issue get <id>` | 단일 이슈 표시(이슈 키 또는 UUID를 받음) |
| `multica issue create --title "..."` | 새 이슈 생성 |
| `multica issue update <id> ...` | 이슈 업데이트(상태, 우선순위, 담당자 등) |
| `multica issue assign <id> --agent <slug>` | 에이전트에게 할당(즉시 작업을 트리거) |
| `multica issue status <id> --set <status>` | 상태 변경 단축 명령 |
| `multica issue search <query>` | 키워드 검색 |
| `multica issue runs <id>` | 이슈의 에이전트 실행 표시 |
| `multica issue rerun <id>` | 이슈의 현재 에이전트 담당자에게 새 작업을 다시 큐에 넣기 |
| `multica issue comment <id> ...` | 중첩: 댓글 보기 / 작성 |
| `multica issue subscriber <id> ...` | 중첩: 구독 / 구독 취소 |
| `multica project list/get/create/update/delete/status` | 프로젝트 CRUD |
## 에이전트와 스킬
| 명령어 | 용도 |
|---|---|
| `multica agent list` | 워크스페이스의 에이전트 나열 |
| `multica agent get <slug>` | 에이전트의 구성 표시 |
| `multica agent create ...` | 에이전트 생성 |
| `multica agent update <slug> ...` | 에이전트 업데이트 |
| `multica agent archive <slug>` | 보관 |
| `multica agent restore <slug>` | 보관된 에이전트 복원 |
| `multica agent tasks <slug>` | 에이전트의 작업 기록 표시 |
| `multica agent skills ...` | 중첩: 스킬 연결 / 분리 |
| `multica skill list/get/create/update/delete` | 스킬 CRUD |
| `multica skill import ...` | GitHub, ClawHub, 또는 로컬 기기에서 스킬 가져오기 |
| `multica skill files ...` | 중첩: 스킬의 파일 관리 |
## 스쿼드
| 명령어 | 용도 |
|---|---|
| `multica squad list` | 워크스페이스의 스쿼드 나열 |
| `multica squad get <id>` | 단일 스쿼드 표시 |
| `multica squad create --name "..." --leader <agent>` | 스쿼드 생성(owner / admin) |
| `multica squad update <id> ...` | 이름, 설명, 지침, 리더, 또는 아바타 업데이트 |
| `multica squad delete <id>` | 보관(소프트 삭제) — 할당된 이슈를 리더에게 이관 |
| `multica squad member list/add/remove/set-role <squad-id>` | 스쿼드 멤버 관리 및 역할 직접 업데이트 |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 스쿼드 리더 에이전트가 매 턴마다 평가를 기록할 때 사용 |
전체 모델은 [스쿼드](/squads)를 참고하세요.
## 오토파일럿
| 명령어 | 용도 |
|---|---|
| `multica autopilot list` | 워크스페이스의 모든 오토파일럿 나열 |
| `multica autopilot get <id>` | 단일 오토파일럿 표시 |
| `multica autopilot create ...` | 오토파일럿 생성 |
| `multica autopilot update <id> ...` | 업데이트 |
| `multica autopilot delete <id>` | 삭제 |
| `multica autopilot runs <id>` | 실행 기록 표시 |
| `multica autopilot trigger <id>` | 수동으로 실행 트리거 |
## 데몬과 런타임
| 명령어 | 용도 |
|---|---|
| `multica daemon start` | 데몬 시작(기본은 백그라운드; `--foreground`를 추가하면 포그라운드 실행) |
| `multica daemon stop` | 데몬 중지 |
| `multica daemon restart` | 데몬 재시작 |
| `multica daemon status` | 데몬의 온라인 여부와 동시 실행 수 확인 |
| `multica daemon logs` | 데몬 로그 보기 |
| `multica runtime list` | 현재 워크스페이스의 런타임 나열 |
| `multica runtime usage` | 리소스 사용량 표시 |
| `multica runtime activity` | 최근 활동 로그 |
| `multica runtime update <id> ...` | 런타임의 구성 업데이트 |
## 기타
| 명령어 | 용도 |
|---|---|
| `multica repo checkout <url>` | 에이전트가 사용할 수 있도록 리포지토리를 로컬에 클론 |
| `multica config` | 로컬 CLI 구성 보기 또는 편집 |
| `multica version` | CLI 버전 출력 |
| `multica update` | CLI를 최신 릴리스로 업그레이드 |
| `multica attachment download <id>` | 이슈나 댓글에서 첨부 파일 다운로드 |
## 전체 플래그 확인하기
모든 명령은 `--help`를 지원합니다:
```bash
multica issue create --help
multica agent update --help
```
v2에서는 각 명령마다 전용 상세 레퍼런스 페이지를 제공할 예정입니다.
## 다음 단계
- [인증과 토큰](/auth-tokens) — PAT vs. JWT vs. 데몬 토큰
- [데몬과 런타임](/daemon-runtimes) — `daemon` 명령이 내부적으로 동작하는 방식
- [에이전트 생성과 구성](/agents-create) — `multica agent create`의 모든 옵션

View File

@@ -88,7 +88,7 @@ For the difference between token types, see [Authentication and tokens](/auth-to
| `multica squad create --name "..." --leader <agent>` | Create a squad (owner / admin) |
| `multica squad update <id> ...` | Update name, description, instructions, leader, or avatar |
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
| `multica squad member list/add/remove <squad-id>` | Manage squad members |
| `multica squad member list/add/remove/set-role <squad-id>` | Manage squad members and update roles in place |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Used by squad leader agents to record an evaluation per turn |
See [Squads](/squads) for the full model.

View File

@@ -88,7 +88,7 @@ Token 类型的详细区分见 [认证与令牌](/auth-tokens)。
| `multica squad create --name "..." --leader <agent>` | 创建小队owner / admin|
| `multica squad update <id> ...` | 修改名字、描述、instructions、队长、头像 |
| `multica squad delete <id>` | 归档(软删除)—— 同时把分配给小队的 issue 转给队长 |
| `multica squad member list/add/remove <squad-id>` | 管理小队成员 |
| `multica squad member list/add/remove/set-role <squad-id>` | 管理小队成员并原地更新 role |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 队长智能体每轮结束时调用,记录 evaluation |
完整模型见 [小队](/squads)。

View File

@@ -0,0 +1,119 @@
---
title: Cloud クイックスタート
description: サインアップからエージェントへの最初のタスク割り当てまで 5 分で。
---
import { Callout } from "fumadocs-ui/components/callout";
このページは Multica Cloud を最初から最後まで案内します — **サインアップ → [CLI](/cli) のインストール → [デーモン](/daemon-runtimes)の起動 → [エージェント](/agents)の作成 → 最初の[タスク](/tasks)の割り当て**。約 5 分かかります。
前提条件は 1 つだけです: ローカルに [AI コーディングツール](/providers)[Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi) のいずれか)を少なくとも 1 つ、すでにインストールしておくこと。デーモンは起動時にこれらを自動検出し、1 つもなければ起動を拒否します。
## 1. アカウントを作成する
[multica.ai](https://multica.ai) でサインアップしてください。メール6 桁の確認コード)または Google でログインできます。
サインアップ後は(アカウント名から生成された)デフォルトのワークスペースに自動的に配置されます。後で名前を変更したり、新しいワークスペースを作成したりできます。
## 2. Multica CLI をインストールする
**macOS / LinuxHomebrew 推奨)**:
```bash
brew install multica-ai/tap/multica
```
**macOS / LinuxHomebrew なし)**:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
**WindowsPowerShell**:
```powershell
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
```
インストールを確認します。
```bash
multica version
```
## 3. ログイン + デーモンの起動
コマンド 1 つでログインとデーモンの起動を処理します。
```bash
multica setup
```
`multica setup` は次を実行します。
1. CLI が Multica Cloud に接続するよう構成します
2. ログインのためにブラウザを開きますWeb と同じメール確認コード / Google OAuth
3. 生成された PAT を `~/.multica/config.json` に保存します
4. **デーモンを自動的に起動します** — 3 秒ごとにタスクをポーリングし、15 秒ごとにハートビートを送信し始めます
<Callout type="info">
**デスクトップアプリを使用していますか?** デスクトップアプリは起動時に**デーモンを自動的に起動します** — `multica setup` を手動で実行する必要はありません。[デスクトップアプリ](/desktop-app)を参照してください。
</Callout>
デーモンが実行中かどうかを確認します。
```bash
multica daemon status
```
`online` はサーバーに登録されたことを意味します。
## 4. ランタイムがオンラインか確認する
Web UI で **Settings → Runtimes** に移動します。先ほど起動したデーモンが、1 つ以上のアクティブなランタイムとして表示されるはずです — ローカルにインストールされた AI コーディングツールごとに 1 つです。
オフラインと表示されても慌てないでください — [トラブルシューティング → デーモンがサーバーに接続できない](/troubleshooting#daemon-cant-connect-to-the-server)を参照してください。
## 5. エージェントを作成する
Web UI で **Settings → Agents** に移動し、**New Agent** をクリックします。
- **Name** — ボードやコメントでこのエージェントに表示される名前です。好きな名前を選んでください
- **Provider** — ローカルにインストールした AI コーディングツールを選択します(ドロップダウンにはランタイムで検出されたツールのみが表示されます)
- **Model**(任意) — そのツール内部のモデル選択(プロバイダーによって静的な一覧または動的探索)
- **Instructions**(任意) — このエージェントのためのシステムプロンプト
作成されると、エージェントはワークスペースのメンバー一覧に表示され、人間のメンバーと同じように作業を割り当てられます。
## 6. 最初のタスクを割り当てる
Web UI でイシューを作成するか、CLI から作成します。
```bash
multica issue create --title "Add an ASCII architecture diagram to the README"
```
先ほど作成したエージェントにイシューを割り当てます — Web UI でアバターをクリックするか、CLI を使用します。
```bash
multica issue assign MUL-1 --to my-agent-name
```
`--to` はエージェントまたはメンバーの**名前**を受け取ります。部分文字列の一致も機能します — エージェント名が `my-code-reviewer` なら、`reviewer` でそれに解決されます。ワークスペースに名前が重複している場合は、代わりに `--to-id <uuid>``--to` と相互排他を渡してください。UUID は `multica agent list --output json` または `multica workspace member list --output json` で調べられます。
**次にデーモンで起きること**:
1. 3 秒以内にタスクを取得します(ステータスが `queued` から `dispatched` に変わります)
2. 一致する AI コーディングツールを呼び出して作業を開始します(ステータスが `running` になります)
3. AI がローカルで作業します — コードディレクトリを読んだり、コマンドを実行したり、ファイルを編集したりできます
4. 完了すると結果を Multica に報告します(自動リトライが作動するかどうかに応じて、ステータスが `completed` または `failed` になります)
Web UI は**リアルタイムで**WebSocket を通じて)更新されます — 再読み込みは不要です。
## 次のステップ
- [デーモンとランタイム](/daemon-runtimes) — デーモンがどう動作するかとランタイムの意味
- [タスク](/tasks) — タスクのライフサイクルとリトライルール
- [AI コーディングツール比較](/providers) — 12 個のツール間の機能差
- [デスクトップアプリ](/desktop-app) — デーモンを自分で実行したくない場合
- [セルフホストクイックスタート](/self-host-quickstart) — 自前のバックエンドを実行する

View File

@@ -0,0 +1,119 @@
---
title: Cloud 빠른 시작
description: 가입부터 에이전트에게 첫 작업을 할당하기까지 5분 만에.
---
import { Callout } from "fumadocs-ui/components/callout";
이 페이지는 Multica Cloud를 처음부터 끝까지 안내합니다 — **가입 → [CLI](/cli) 설치 → [데몬](/daemon-runtimes) 시작 → [에이전트](/agents) 생성 → 첫 [작업](/tasks) 할당**. 약 5분이 걸립니다.
전제 조건은 하나뿐입니다: 로컬에 [AI 코딩 도구](/providers)([Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi) 중 하나)를 이미 최소 하나는 설치해 두어야 합니다. 데몬은 시작할 때 이들을 자동으로 감지하며, 하나도 없으면 시작을 거부합니다.
## 1. 계정 만들기
[multica.ai](https://multica.ai)에서 가입하세요. 이메일(6자리 인증 코드) 또는 Google로 로그인할 수 있습니다.
가입 후에는 (계정 이름으로 생성된) 기본 워크스페이스에 자동으로 배치됩니다. 나중에 이름을 변경하거나 새 워크스페이스를 만들 수 있습니다.
## 2. Multica CLI 설치하기
**macOS / Linux (Homebrew 권장)**:
```bash
brew install multica-ai/tap/multica
```
**macOS / Linux (Homebrew 없이)**:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
**Windows (PowerShell)**:
```powershell
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
```
설치를 확인하세요:
```bash
multica version
```
## 3. 로그인 + 데몬 시작하기
명령어 하나로 로그인과 데몬 시작을 처리합니다:
```bash
multica setup
```
`multica setup`은 다음을 수행합니다:
1. CLI가 Multica Cloud에 연결하도록 구성합니다
2. 로그인을 위해 브라우저를 엽니다(웹과 동일한 이메일 인증 코드 / Google OAuth)
3. 생성된 PAT를 `~/.multica/config.json`에 저장합니다
4. **데몬을 자동으로 시작합니다** — 3초마다 작업을 폴링하고 15초마다 하트비트를 전송하기 시작합니다
<Callout type="info">
**데스크톱 앱을 사용 중이신가요?** 데스크톱 앱은 실행 시 **데몬을 자동으로 시작합니다** — `multica setup`을 직접 실행할 필요가 없습니다. [데스크톱 앱](/desktop-app)을 참고하세요.
</Callout>
데몬이 실행 중인지 확인하세요:
```bash
multica daemon status
```
`online`은 서버에 등록되었음을 의미합니다.
## 4. 런타임이 온라인인지 확인하기
웹 UI에서 **설정 → 런타임**으로 이동하세요. 방금 시작한 데몬이 하나 이상의 활성 런타임으로 표시되어야 합니다 — 로컬에 설치된 AI 코딩 도구당 하나씩입니다.
오프라인으로 표시되더라도 당황하지 마세요 — [문제 해결 → 데몬이 서버에 연결할 수 없음](/troubleshooting#daemon-cant-connect-to-the-server)을 참고하세요.
## 5. 에이전트 생성하기
웹 UI에서 **설정 → 에이전트**로 이동하여 **새 에이전트**를 클릭하세요:
- **이름** — 보드와 댓글에서 이 에이전트에 표시되는 이름입니다. 원하는 이름을 고르세요
- **제공자** — 로컬에 설치한 AI 코딩 도구를 선택하세요(드롭다운에는 런타임에서 감지된 도구만 나열됩니다)
- **모델**(선택) — 해당 도구 내부의 모델 선택(제공자에 따라 정적 목록 또는 동적 발견)
- **지침**(선택) — 이 에이전트를 위한 시스템 프롬프트
생성되면 에이전트는 워크스페이스 멤버 목록에 나타나며, 사람 멤버처럼 작업을 할당할 수 있습니다.
## 6. 첫 작업 할당하기
웹 UI에서 이슈를 만들거나, CLI에서 만드세요:
```bash
multica issue create --title "Add an ASCII architecture diagram to the README"
```
방금 만든 에이전트에게 이슈를 할당하세요 — 웹 UI에서 아바타를 클릭하거나, CLI를 사용하세요:
```bash
multica issue assign MUL-1 --to my-agent-name
```
`--to`는 에이전트 또는 멤버의 **이름**을 받습니다. 부분 문자열 일치도 동작합니다 — 에이전트 이름이 `my-code-reviewer`라면 `reviewer`로 해석됩니다. 워크스페이스에 이름이 겹치는 경우, 대신 `--to-id <uuid>`(`--to`와 상호 배타적)를 전달하세요. UUID는 `multica agent list --output json` 또는 `multica workspace member list --output json`으로 조회하세요.
**다음으로 데몬에서 일어나는 일**:
1. 3초 이내에 작업을 가져갑니다(상태가 `queued`에서 `dispatched`로 바뀝니다)
2. 일치하는 AI 코딩 도구를 호출하여 작업을 시작합니다(상태가 `running`이 됩니다)
3. AI가 로컬에서 작업합니다 — 코드 디렉터리를 읽고, 명령을 실행하고, 파일을 편집할 수 있습니다
4. 완료되면 결과를 Multica로 다시 보고합니다(자동 재시도 동작 여부에 따라 상태가 `completed` 또는 `failed`가 됩니다)
웹 UI는 **실시간으로**(WebSocket을 통해) 업데이트됩니다 — 새로 고침이 필요 없습니다.
## 다음 단계
- [데몬과 런타임](/daemon-runtimes) — 데몬이 어떻게 동작하는지와 런타임의 의미
- [작업](/tasks) — 작업 생명주기와 재시도 규칙
- [AI 코딩 도구 비교](/providers) — 12개 도구 간의 기능 차이
- [데스크톱 앱](/desktop-app) — 데몬을 직접 실행하고 싶지 않은 경우
- [자체 호스팅 빠른 시작](/self-host-quickstart) — 자체 백엔드 실행하기

View File

@@ -0,0 +1,81 @@
---
title: コメントとメンション
description: イシューの下での共同作業 — コメント、返信、`@` メンション、リアクション、そしてコメントからエージェントをトリガーする方法。
---
import { Callout } from "fumadocs-ui/components/callout";
すべての[イシュー](/issues)にはコメントスレッドがあります。コメントを投稿し、誰かに返信し、[メンバー](/members-roles)や[エージェント](/agents)を `@` でメンションし、リアクションを追加する — これまで使ってきたどのタスク管理ツールでも行ってきたのと同じ操作です。唯一の違いは、**`@` でエージェントをメンションすると、そのエージェントが作業を開始するようトリガーされる**ことです。
## コメントを投稿する
イシュー詳細ページ下部の入力欄に内容を入力し、**送信**を押してください。コメントはすぐにスレッドに表示されます。コメントは Markdown に対応しています — 見出し、リスト、コードブロック、リンクがすべて使えます。
## コメントに返信する
任意のコメントの右上にある**返信**をクリックすると、その下にネストされた入力欄が開きます。返信はそのコメントの子要素として表示され、会話スレッドを形成します。返信にもさらに返信を付けられ、必要なだけ深くネストできます。
イシュー一覧にはトップレベルのコメント数だけが表示され、イシューを開くと会話ツリー全体が見えます。
## リアクション
各コメントの右上には、素早く意思を伝えるためのリアクションボタンがあります(👍、👀、🎉)— 同意を示すために「+1」コメントをわざわざ投稿する必要はありません。
## `@` メンション
コメントに `@` を入力するとピッカーが開きます。メンバーまたはエージェントを選ぶと、`@` と対象のスラッグが挿入されます(`@alice` や `@reviewer-bot`)。メンションされた相手は自分の[インボックス](/inbox)に通知を受け取ります。
**エージェントをメンションすると自動的にトリガーされます** — [コメントでエージェントをメンションする](/mentioning-agents)を参照してください。
1 つのコメントで同じ人を複数回メンションしても、通知は**1 つだけ**発生します。
### `@all` はワークスペース全体に通知する
`@all` は特別な対象です。ワークスペースのすべてのメンバーに通知を送ります。人もエージェントも `@all` を使えます — つまり進捗を報告するエージェントも `@all` できるので、エージェントの指示には控えめに使うよう伝えておきましょう。
<Callout type="warning">
**`@all` は慎重に使ってください。** 規模の大きいワークスペースでは、たった 1 回の `@all` がその人数分のインボックス通知を瞬時に生成します。全員が本当に知る必要があることだけに使い、日常的な更新には使わないでください。
</Callout>
## イシューを参照する
別のイシューをリンクするには、`MUL-123` のようにそのイシューキーを入力してください。Multica はコメント内で実在するイシューキーを解決し、内部的に `mention://issue/<uuid>` リンクとして保存します。イシューリンクは単なる相互参照にすぎません。人に通知を送ることはなく、エージェントをトリガーすることもありません。
通常は `[MUL-123](mention://issue/<uuid>)` を手で書く必要はありません。その形式は、Multica がキーを解決した後に使う標準的な内部表現です。
<Callout type="info">
Markdown の強調は CommonMark のルールに従います。太字テキストが句読点や閉じ引用符で終わり、その直後に韓国語の助詞が続く場合、閉じの `**` が認識されないことがあります。
引用符を太字の範囲の外に出すことをおすすめします。
```markdown
"**무엇을 먼저 정해두고 시작할지**"가
```
次の代わりに:
```markdown
**"무엇을 먼저 정해두고 시작할지"**가
```
</Callout>
## コメントの編集と削除
コメントは作成者のみが編集または削除できます。
コメントを削除すると、その下の**すべての返信も一緒に削除されます**(返信への返信も含む)。内容だけを変えたい場合は、削除ではなく編集を使ってください。
<Callout type="warning">
**コメントを編集して `@` を追加しても、エージェントはトリガーされません。** トリガーはコメントが**作成された**その瞬間に発生します — 後から編集して新しい `@` を追加したり、対象を変えたりしても、新しい通知は送られず、エージェントも起きません。見逃したエージェントを呼び出すには、そのエージェントを `@` する**新しいコメントを投稿**してください。
</Callout>
---
ここまで扱ってきた内容はすべて「人の世界」です — ワークスペース、メンバー、イシュー、プロジェクト、コメント。Linear や Jira を使ったことがあれば、これまでの内容はまったく目新しくないはずです。
しかし Multica の決定的な特徴はまだ登場していません。**エージェントをワークスペースの一級メンバーとして扱うこと**です。次はまさにこの話に移ります。
## 次へ
- [エージェント](/agents) — 何であり、人とどう違うのか
- [コメントでエージェントをメンションする](/mentioning-agents) — コメントで `@` を使ってエージェントを起動する

View File

@@ -0,0 +1,81 @@
---
title: 댓글과 멘션
description: 이슈 아래에서 협업하기 — 댓글, 답글, `@` 멘션, 반응, 그리고 댓글에서 에이전트를 트리거하기.
---
import { Callout } from "fumadocs-ui/components/callout";
모든 [이슈](/issues)에는 댓글 스레드가 있습니다. 댓글을 작성하고, 다른 사람에게 답글을 달고, [멤버](/members-roles)나 [에이전트](/agents)를 `@`로 멘션하고, 반응을 추가하세요 — 지금까지 써온 어떤 작업 관리 도구에서나 하던 것과 똑같은 동작입니다. 단 한 가지 다른 점은: **`@`로 에이전트를 멘션하면 에이전트가 작업을 시작하도록 트리거된다**는 것입니다.
## 댓글 작성하기
이슈 상세 페이지 하단의 입력란에 내용을 입력하고 **보내기**를 누르세요. 댓글이 스레드에 즉시 나타납니다. 댓글은 Markdown을 지원합니다 — 제목, 목록, 코드 블록, 링크를 모두 사용할 수 있습니다.
## 댓글에 답글 달기
어떤 댓글이든 오른쪽 상단의 **답글**을 클릭하면 그 아래에 중첩된 입력란이 열립니다. 작성한 답글은 해당 댓글의 하위 항목으로 표시되어 대화 스레드를 이룹니다. 답글에도 다시 답글을 달 수 있으며, 필요한 만큼 깊이 중첩됩니다.
이슈 목록에는 최상위 댓글 수만 표시되며, 이슈를 열어야 전체 대화 트리를 볼 수 있습니다.
## 반응
각 댓글의 오른쪽 상단에는 빠른 신호를 보내기 위한 반응 버튼이 있습니다(👍, 👀, 🎉) — 동의를 표시하려고 "+1" 댓글을 따로 달 필요가 없습니다.
## `@` 멘션
댓글에 `@`를 입력하면 선택 창이 열립니다. 멤버나 에이전트를 고르면 `@`와 대상의 slug가 함께 삽입됩니다(`@alice` 또는 `@reviewer-bot`). 멘션된 대상은 자신의 [인박스](/inbox)에서 알림을 받습니다.
**에이전트를 멘션하면 자동으로 트리거됩니다** — [댓글에서 에이전트 멘션하기](/mentioning-agents)를 참고하세요.
하나의 댓글에서 같은 사람을 여러 번 멘션해도 알림은 **하나만** 발생합니다.
### `@all`은 워크스페이스 전체에 알립니다
`@all`은 특별한 대상입니다. 워크스페이스의 모든 멤버에게 알림을 보냅니다. 사람과 에이전트 모두 `@all`을 사용할 수 있습니다 — 즉 진행 상황을 보고하는 에이전트도 `@all`을 할 수 있으므로, 에이전트의 지침에 이를 아껴서 사용하라고 일러두세요.
<Callout type="warning">
**`@all`은 신중하게 사용하세요.** 규모가 큰 워크스페이스에서는 단 한 번의 `@all`이 그만큼의 인박스 알림을 순식간에 생성합니다. 모두가 정말로 알아야 하는 일에만 사용하고 — 일상적인 업데이트에는 쓰지 마세요.
</Callout>
## 이슈 참조하기
다른 이슈를 링크하려면 `MUL-123`처럼 이슈 키를 입력하세요. Multica는 댓글에서 실제 존재하는 이슈 키를 해석하여 내부적으로 `mention://issue/<uuid>` 링크로 저장합니다. 이슈 링크는 단순한 상호 참조일 뿐입니다. 사람에게 알림을 보내지 않으며 에이전트를 트리거하지도 않습니다.
보통은 `[MUL-123](mention://issue/<uuid>)`을 직접 손으로 작성할 필요가 없습니다. 그 형식은 Multica가 키를 해석한 뒤에 사용하는 표준 내부 표현입니다.
<Callout type="info">
Markdown 강조는 CommonMark 규칙을 따릅니다. 굵은 텍스트가 문장 부호나 닫는 따옴표로 끝나고 그 뒤에 한국어 조사가 바로 이어지면, 닫는 `**`가 인식되지 않을 수 있습니다.
따옴표를 굵게 표시 범위 밖으로 옮기는 것을 권장합니다:
```markdown
"**무엇을 먼저 정해두고 시작할지**"가
```
다음 대신:
```markdown
**"무엇을 먼저 정해두고 시작할지"**가
```
</Callout>
## 댓글 편집과 삭제
댓글은 작성자만 편집하거나 삭제할 수 있습니다.
댓글을 삭제하면 그 아래의 **모든 답글도 함께 삭제됩니다**(답글의 답글 포함). 내용만 바꾸려면 삭제 대신 편집을 사용하세요.
<Callout type="warning">
**댓글을 편집하면서 `@`를 추가해도 에이전트는 트리거되지 않습니다.** 트리거는 댓글이 **생성되는** 그 순간에 발생합니다 — 나중에 편집해서 새로운 `@`를 추가하거나 대상을 바꿔도 새 알림이 발송되지 않고 에이전트가 깨어나지도 않습니다. 놓친 에이전트를 불러오려면 그 에이전트를 `@`하는 **새 댓글을 작성**하세요.
</Callout>
---
여기까지 다룬 내용은 모두 "사람의 세계"입니다 — 워크스페이스, 멤버, 이슈, 프로젝트, 댓글. Linear나 Jira를 써본 적이 있다면 지금까지의 내용은 전혀 낯설지 않을 것입니다.
하지만 Multica의 결정적인 특징은 아직 등장하지 않았습니다: **에이전트를 워크스페이스의 일급 멤버로 대우하는 것**입니다. 다음 장에서 바로 이 이야기를 다룹니다.
## 다음
- [에이전트](/agents) — 무엇이며, 사람과 어떻게 다른지
- [댓글에서 에이전트 멘션하기](/mentioning-agents) — 댓글에서 `@`로 에이전트를 시작시키기

View File

@@ -37,6 +37,28 @@ Mentioning the same person multiple times in one comment still produces **only o
**Use `@all` carefully.** In a larger workspace, a single `@all` generates that many inbox notifications instantly. Reserve it for things everyone genuinely needs to know — not day-to-day updates.
</Callout>
## Referencing issues
To link another issue, type its issue key, such as `MUL-123`. Multica resolves real issue keys in comments and stores them as an internal `mention://issue/<uuid>` link. Issue links are cross-references only: they do not notify people and they do not trigger agents.
You normally do not need to write `[MUL-123](mention://issue/<uuid>)` by hand. That format is the canonical internal representation after Multica has resolved the key.
<Callout type="info">
Markdown emphasis follows CommonMark rules. When bold text ends with punctuation or a closing quote and is immediately followed by a Korean particle, the closing `**` may not be recognized.
Prefer moving the quote outside the bold span:
```markdown
"**무엇을 먼저 정해두고 시작할지**"가
```
instead of:
```markdown
**"무엇을 먼저 정해두고 시작할지"**가
```
</Callout>
## Editing and deleting a comment
Only the author of a comment can edit or delete it.

View File

@@ -37,6 +37,28 @@ import { Callout } from "fumadocs-ui/components/callout";
**谨慎使用 `@all`**。工作区人数较多时,一条 `@all` 的评论会瞬间生成同等数量的收件箱通知。只在确实需要全员知晓的重大事项上使用——不是日常琐事。
</Callout>
## 引用 issue
要链接另一个 issue直接输入它的 issue key例如 `MUL-123`。Multica 会在评论中解析真实存在的 issue key并把它存成内部的 `mention://issue/<uuid>` 链接。Issue 链接只是交叉引用:不会通知成员,也不会触发智能体。
通常不需要手写 `[MUL-123](mention://issue/<uuid>)`。这是 Multica 解析 key 之后使用的内部规范格式。
<Callout type="info">
Markdown 加粗遵循 CommonMark 规则。当加粗文本以标点或闭引号结尾,并且后面紧跟韩语助词时,结尾的 `**` 可能不会被识别。
推荐把引号放到加粗范围外:
```markdown
"**무엇을 먼저 정해두고 시작할지**"가
```
而不是:
```markdown
**"무엇을 먼저 정해두고 시작할지"**가
```
</Callout>
## 编辑和删除评论
只有评论的作者能编辑或删除自己的评论。

View File

@@ -0,0 +1,111 @@
---
title: デーモンとランタイム
description: エージェントは Multica のサーバーでは実行されません — あなた自身のマシンで実行されます。
---
import { Callout } from "fumadocs-ui/components/callout";
import { Mermaid } from "@/components/mermaid";
Multica では、[エージェント](/agents)は私たちのサーバーでは実行され**ません** — ローカルにインストールされた [AI コーディングツール](/providers)を呼び出す**デーモン**という小さなプログラムが駆動し、あなた自身のマシンで実行されます。Multica サーバーは調整役に徹します。[イシュー](/issues)を保存し、[タスク](/tasks)をキューに入れ、適切な**ランタイム**へ分配します(ランタイム = デーモン × AI コーディングツール 1 つ)。
この構造が Multica と Linear / Jira の最大の違いです。**あなたの API キー、ツールチェーン、コードディレクトリはすべてあなたのマシンに残り**、Multica サーバーはそのどれも見ることはありません。つまり「自分のエージェントが動かない」はほとんど常にローカルの問題です。デーモンが実行されていない、AI ツールがインストールされていない、キーが期限切れになっている、といったことです。まずローカルを確認してください。案内は[トラブルシューティング](/troubleshooting)を参照してください。
## デーモンを起動する
デーモンは Multica CLI の一部です。[Multica CLI](/cli) をインストールしたら、あなた自身のマシンで実行してください。
```bash
multica daemon start
```
起動時にデーモンは 4 つのことを行います。
1. ログイン時に保存された認証情報を読み込みます
2. `PATH` にインストールされた AI コーディングツールを検出します(内蔵 12 種: [Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)
3. 検出した各ツールに対するランタイムとともに、自身をサーバーに登録します
4. **3 秒ごと**に取得すべきタスクがないかポーリングし、**15 秒ごとにハートビートを送信**し続けます
よく使うコマンド:
| コマンド | 用途 |
|---|---|
| `multica daemon start` | 起動(デフォルトはバックグラウンド。フォアグラウンドで実行するには `--foreground` を追加) |
| `multica daemon stop` | 停止 |
| `multica daemon restart` | 再起動 |
| `multica daemon status` | ステータス表示 |
| `multica daemon logs` | ログ表示(追従するには `-f` を追加) |
完全な CLI リファレンスは [CLI コマンド](/cli)を確認してください。
**デスクトップアプリにはデーモンが同梱されています。** [デスクトップアプリ](/desktop-app)を使う場合、`multica daemon start` を手動で実行する必要はありません。起動時にデーモンを自動的に立ち上げます。あなたのワークフローにどの方式が合うかは、[デスクトップアプリ](/desktop-app)ページを参照してください。
## 1 つのマシンに複数のランタイムができる理由
ランタイムはサーバーでもコンテナでもありません。「**デーモン × AI コーディングツール 1 つ**」の組み合わせです。たとえば、Claude Code と Codex の両方がインストールされた MacBook でデーモンを起動し、あなたが 2 つのワークスペースのメンバーだとします。すると Multica は 4 つのランタイムを登録します。
<Mermaid chart={`
graph TD
D["あなたのデーモン<br/>MacBook"]
D --> R1["ランタイム<br/>ワークスペース A × Claude Code"]
D --> R2["ランタイム<br/>ワークスペース A × Codex"]
D --> R3["ランタイム<br/>ワークスペース B × Claude Code"]
D --> R4["ランタイム<br/>ワークスペース B × Codex"]
`} />
要点:
- **1 つのデーモンは複数のランタイムにマッピングされ得ます** — インストールされたツールと、あなたが所属するワークスペースの組み合わせごとに 1 つできます
- **同じデーモン、ワークスペース、ツールは、ちょうど 1 つのランタイムを作ります** — デーモンを再起動しても重複レコードは生まれません
- Multica UI の**ランタイム**ページがこれらの行を一覧表示します
<Callout type="info">
**クラウドランタイムが近日提供されます。** 現在は順番待ちリストの段階です。提供が始まれば、ローカルのデーモンを実行せずに Multica Cloud 上で直接エージェントタスクを実行できるようになります。[ダウンロードページ](https://multica.ai/download)でメールアドレスを登録すると通知を受け取れます。
</Callout>
## ランタイムがオフラインと表示される時点
Multica はハートビートでランタイムがオンラインかどうかを判断します。3 つの重要な数値があります。
| イベント | しきい値 |
|---|---|
| デーモンのハートビート頻度 | **15 秒**ごと |
| 欠落として表示 | **45 秒**間ハートビートなし3 回欠落) |
| 自動削除 | 関連するエージェントがない状態で **7 日**以上欠落 |
欠落は永続的ではありません。デーモンが再びハートビートを送った瞬間にオンラインに戻り、ランタイムレコードも保持されます。デーモンを再起動してもランタイムは失われません。
<Callout type="warning">
**欠落したランタイムで実行中だったタスクは失敗として表示されます**(失敗理由 `runtime_offline`。リトライ可能なソースイシュー、チャットについては、Multica が自動的に再度キューに入れます。オートパイロットがトリガーしたタスクは自動的にはリトライされません。[タスク → どの失敗が自動リトライされるか](/tasks#which-failures-retry-automatically-which-dont)を参照してください。
</Callout>
## いくつのタスクを並列に実行できるか
Multica は 2 つの層で同時実行数の制限を適用します。
- **デーモン層**: デフォルトで**同時タスク 20 個**(環境変数 `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` で調整可能)
- **エージェント層**: デフォルトで**エージェントあたり同時タスク 6 個**(エージェントごとに設定)
2 つのうち厳しい方が適用されます。デーモンがすでにタスク 20 個を実行中なら、あるエージェントに余裕が残っていても新しいタスクは待機します。
タスクが `dispatched` に進めず `queued` で止まっている場合、通常はこの 2 つの制限のいずれかが飽和しています。
## デーモンのクラッシュ後、進行中だったタスクはどうなるか
デーモンがクラッシュしたり強制終了されたりすると、デーモンが取得していたタスクは `dispatched` または `running` 状態に残ります。次回の起動時、デーモンはサーバーに「これらのタスクはもう私のものではないので、失敗として表示してください」と伝えます。サーバーはそれを理由 `runtime_recovery` とともに `failed` に切り替えます。リトライ可能なソースについては、タスクが自動的に再度キューに入ります。
この手順がネットワークの問題で失敗しても、バックアップとして**30 秒ごと**にサーバー側のスキャンが回ります。45 秒以上ハートビートのないランタイムは欠落として表示され、その上のタスクも一緒に回収されます。
## 動かないエージェントのトラブルシューティング
「自分のエージェントが動かない」という問題に遭遇したら、まずこの 3 ステップのチェックリストを進めてください。
1. `multica daemon status` を実行し、デーモンが実行中でオンラインかを確認します
2. `multica daemon logs -f` を実行し、エラーがないかを確認します
3. Multica UI の**ランタイム**ページを開き、ランタイムが「オンライン」と表示されているかを確認します
より多くのシナリオは[トラブルシューティング](/troubleshooting)を参照してください。
## 次へ
- [タスク](/tasks) — デーモンがタスクを取得した後の全ライフサイクル
- [プロバイダー対照表](/providers) — 12 種の AI コーディングツールの機能の違い

View File

@@ -0,0 +1,111 @@
---
title: 데몬과 런타임
description: 에이전트는 Multica 서버에서 실행되지 않습니다. 여러분의 기기에서 실행됩니다.
---
import { Callout } from "fumadocs-ui/components/callout";
import { Mermaid } from "@/components/mermaid";
Multica에서 [에이전트](/agents)는 우리 서버에서 실행되지 **않습니다**. 로컬에 설치된 [AI 코딩 도구](/providers)를 호출하는 **데몬**이라는 작은 프로그램이 구동하여, 여러분 자신의 기기에서 실행됩니다. Multica 서버는 조율만 담당합니다. [이슈](/issues)를 저장하고, [작업](/tasks)을 대기열에 넣고, 알맞은 **런타임**으로 분배합니다(런타임 = 데몬 × AI 코딩 도구 하나).
이 구조가 Multica와 Linear / Jira의 가장 큰 차이점입니다. **여러분의 API 키, 툴체인, 코드 디렉터리는 모두 여러분의 기기에 남아 있으며**, Multica 서버는 그중 어느 것도 보지 못합니다. 따라서 "내 에이전트가 동작하지 않는다"는 거의 항상 로컬 문제입니다. 데몬이 실행 중이 아니거나, AI 도구가 설치되어 있지 않거나, 키가 만료되었을 수 있습니다. 먼저 로컬을 확인하세요. 안내는 [문제 해결](/troubleshooting)을 참고하세요.
## 데몬 시작하기
데몬은 Multica CLI의 일부입니다. [Multica CLI](/cli)를 설치한 뒤, 여러분의 기기에서 실행하세요.
```bash
multica daemon start
```
시작 시 데몬은 네 가지 일을 합니다.
1. 로그인할 때 저장된 인증 정보를 읽습니다
2. `PATH`에 설치된 AI 코딩 도구를 감지합니다(내장 12종: [Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi))
3. 감지된 각 도구에 대한 런타임과 함께 자신을 서버에 등록합니다
4. **3초마다** 가져올 작업이 있는지 폴링하고, **15초마다 하트비트를 전송**합니다
자주 쓰는 명령:
| 명령 | 용도 |
|---|---|
| `multica daemon start` | 시작(기본은 백그라운드. 포그라운드로 실행하려면 `--foreground` 추가) |
| `multica daemon stop` | 중지 |
| `multica daemon restart` | 재시작 |
| `multica daemon status` | 상태 표시 |
| `multica daemon logs` | 로그 표시(따라가려면 `-f` 추가) |
전체 CLI 참고는 [CLI 명령](/cli)을 확인하세요.
**데스크톱 앱에는 데몬이 포함되어 있습니다.** [데스크톱 앱](/desktop-app)을 사용한다면 `multica daemon start`를 수동으로 실행할 필요가 없습니다. 시작할 때 데몬을 자동으로 띄웁니다. 여러분의 워크플로에 어떤 방식이 맞는지는 [데스크톱 앱](/desktop-app) 페이지를 참고하세요.
## 한 기기에 여러 런타임이 생기는 이유
런타임은 서버도 아니고 컨테이너도 아닙니다. "**데몬 × AI 코딩 도구 하나**"의 조합입니다. 예를 들어, Claude Code와 Codex가 모두 설치된 MacBook에서 데몬을 시작하고, 여러분이 두 개의 워크스페이스 멤버라고 합시다. 그러면 Multica는 4개의 런타임을 등록합니다.
<Mermaid chart={`
graph TD
D["여러분의 데몬<br/>MacBook"]
D --> R1["런타임<br/>워크스페이스 A × Claude Code"]
D --> R2["런타임<br/>워크스페이스 A × Codex"]
D --> R3["런타임<br/>워크스페이스 B × Claude Code"]
D --> R4["런타임<br/>워크스페이스 B × Codex"]
`} />
핵심 포인트:
- **하나의 데몬은 여러 런타임에 매핑될 수 있습니다.** 설치된 도구와 가입한 워크스페이스의 조합마다 하나씩 생깁니다
- **같은 데몬, 워크스페이스, 도구는 정확히 하나의 런타임을 만듭니다.** 데몬을 재시작해도 중복 레코드가 생기지 않습니다
- Multica UI의 **런타임** 페이지가 이 행들을 나열합니다
<Callout type="info">
**클라우드 런타임이 곧 제공됩니다.** 현재는 대기자 명단 단계입니다. 제공이 시작되면 로컬 데몬을 실행하지 않고도 Multica Cloud에서 직접 에이전트 작업을 실행할 수 있습니다. [다운로드 페이지](https://multica.ai/download)에서 이메일로 등록하면 알림을 받을 수 있습니다.
</Callout>
## 런타임이 오프라인으로 표시되는 시점
Multica는 하트비트로 런타임이 온라인인지 판단합니다. 세 가지 핵심 수치:
| 이벤트 | 임곗값 |
|---|---|
| 데몬 하트비트 빈도 | **15초**마다 |
| 누락으로 표시 | **45초** 동안 하트비트 없음(3회 누락) |
| 자동 삭제 | 연결된 에이전트 없이 **7일** 넘게 누락 상태 |
누락은 영구적이지 않습니다. 데몬이 다시 하트비트를 보내는 즉시 온라인으로 돌아오며, 런타임 레코드도 보존됩니다. 데몬을 재시작해도 런타임은 사라지지 않습니다.
<Callout type="warning">
**누락된 런타임에서 실행 중이던 작업은 실패로 표시됩니다**(실패 사유 `runtime_offline`). 재시도 가능한 출처(이슈, 채팅)에 대해서는 Multica가 자동으로 다시 대기열에 넣습니다. 오토파일럿이 트리거한 작업은 자동으로 재시도되지 않습니다. [작업 → 어떤 실패가 자동 재시도되는지](/tasks#which-failures-retry-automatically-which-dont)를 참고하세요.
</Callout>
## 동시에 실행할 수 있는 작업 수
Multica는 두 계층에서 동시성 제한을 적용합니다.
- **데몬 계층**: 기본 **동시 작업 20개**(환경 변수 `MULTICA_DAEMON_MAX_CONCURRENT_TASKS`로 조정 가능)
- **에이전트 계층**: 기본 **에이전트당 동시 작업 6개**(에이전트별로 설정)
둘 중 더 엄격한 쪽이 적용됩니다. 데몬이 이미 작업 20개를 실행 중이라면, 어떤 에이전트에 여유가 남아 있어도 새 작업은 대기합니다.
작업이 `dispatched`로 넘어가지 못하고 `queued`에 멈춰 있다면, 보통 이 두 제한 중 하나가 포화 상태인 것입니다.
## 데몬 충돌 후 진행 중이던 작업은 어떻게 되나
데몬이 충돌하거나 강제 종료되면, 데몬이 가져갔던 작업은 `dispatched` 또는 `running` 상태에 남습니다. 다음 시작 시 데몬은 서버에 "이 작업들은 더 이상 제 것이 아니니, 실패로 표시해 주세요"라고 알립니다. 서버는 이를 사유 `runtime_recovery`와 함께 `failed`로 전환합니다. 재시도 가능한 출처에 대해서는 작업이 자동으로 다시 대기열에 들어갑니다.
이 단계가 네트워크 문제로 실패하더라도, 백업으로 **30초마다** 서버 측 스캔이 돕니다. 45초 넘게 하트비트가 없는 런타임은 누락으로 표시되며, 그 위의 작업도 함께 회수됩니다.
## 동작하지 않는 에이전트 문제 해결
"내 에이전트가 동작하지 않는다"는 문제를 만나면, 먼저 이 세 단계 체크리스트를 진행하세요.
1. `multica daemon status`를 실행해 데몬이 실행 중이고 온라인인지 확인하세요
2. `multica daemon logs -f`를 실행해 오류가 있는지 확인하세요
3. Multica UI의 **런타임** 페이지를 열어 런타임이 "온라인"으로 표시되는지 확인하세요
더 많은 시나리오는 [문제 해결](/troubleshooting)을 참고하세요.
## 다음
- [작업](/tasks) — 데몬이 작업을 가져온 뒤의 전체 생애 주기
- [제공자 대조표](/providers) — 12종 AI 코딩 도구의 기능 차이

View File

@@ -0,0 +1,99 @@
---
title: デスクトップアプリ
description: Multica Desktop とは何か、Web アプリとどう違うのか、そしてどんなときに使う価値があるのかを解説します。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica Desktop は macOS、Windows、Linux 向けのネイティブデスクトップアプリです。設定された環境に対して、Web アプリと同じバックエンドに接続し、同じデータを表示します。Desktop はデフォルトで Multica Cloud を使用しますが、セルフホスト環境はローカルのランタイム設定ファイルで構成できます。Desktop はブラウザにはできないいくつかの機能も追加で提供します。**[ワークスペース](/workspaces)ごとの独立したタブグループ**、**[デーモン](/daemon-runtimes)の自動起動**、**ワンクリックアップグレード**です。
## Desktop か Web か — どちらを選ぶか
| | Web | Desktop |
|---|---|---|
| アクセス方法 | ブラウザで URL を開く | ネイティブアプリをインストール |
| 複数タブ | ブラウザ自体のタブ(ワークスペースの区別なし) | **ワークスペースごとに独立したタブグループ 1 つ** |
| デーモン | `multica daemon start` を自分で実行 | 起動時に**自動的に開始** |
| アップグレード | 更新すると最新版になる | アプリがバックグラウンドで確認し、次回起動時にインストール |
| ログイン後のデータ | 同一 | 同一 |
**Web を選ぶ**: 一度きりの利用、他人のマシンでの作業、何もインストールしたくないとき。
**Desktop を選ぶ**: 毎日の利用、複数のワークスペースを同時に扱うとき、デーモンを手動で管理したくないとき。
## 複数タブ: ワークスペースを切り替えるとどうなるか
Desktop は**参加しているすべてのワークスペース**ごとに独立したタブグループを保持します。ワークスペースを切り替えると、現在のワークスペースのタブが 1 つの単位として非表示になり、以前のワークスペースのタブは離れたときのまま復元されます — VSCode のマルチワークスペースの挙動や、Slack でワークスペースを切り替えるのに似ています。
例: ワークスペース A でイシューのタブを 3 つ開いた状態でワークスペース B に切り替えます。A のタブ 3 つは消え、B には B で最後に開いていたものが表示されます。再び A に切り替えると、その 3 つのタブが以前の状態そのままに戻ってきます。**タブはワークスペース間で決して漏れ出しません。**
ログアウトすると**すべてのワークスペースのタブ状態が消去される**ため、複数のユーザーでマシンを共有していてもデータが漏れることはありません。
## Desktop の自動更新の仕組み
起動時に Desktop は GitHub Releases でより新しいバージョンがないかを確認します。新しいバージョンが見つかると、
1. バックグラウンドで新しいバージョンを静かにダウンロードします。
2. 「準備完了 — 次回起動時にインストールされます」と通知します。
3. 終了時(または次回の再起動時)に、アプリが閉じる前に更新をインストールします。
4. 次回起動時に新しいバージョンが実行されます。
このプロセス全体は**作業中の内容を妨げません**。
<Callout type="warning">
**Windows では ARM64 と x64 は別々の更新チャンネルです** — 間違ったアーキテクチャをインストールすると更新が検出されません。ダウンロードする際は、マシンに合った `.exe` を選んでくださいARM ビルドには `arm64` のサフィックスが付いています)。
</Callout>
macOS ビルドは署名・公証されているため、初回起動時に「未確認の開発者」の警告は表示されません。Linux ビルドは `.AppImage` です — 自動更新は electron-updater に依存しており、一部のディストリビューションでは不安定になることがあります。**自動更新が動作しない場合は、新しいバージョンを手動でダウンロードして古いファイルを置き換えてください。**
## 単体の CLI とデーモンはまだ必要ですか?
**いいえ。** Desktop には同じ `multica` CLI バイナリが内蔵されており、起動時に独自のデーモンプロファイルを起動します(ターミナルから手動で実行しているデーモンとは隔離されます)。
すでに CLI をインストールして `multica daemon start` を手動で実行していても、Desktop はそのデーモンを乗っ取りません — 別のプロファイルで独自のデーモンを開始します。両者は**異なるランタイム**として登録され、UI では 2 つの独立したランタイムが表示されます。
ターミナルで CLI コマンドを実行したい場合、Desktop は特別な経路を提供しません — 別途インストールした CLI を使うか、アプリのリソースディレクトリ内 `resources/bin/multica` にあるバンドル済みのコピーを実行してください。
## ダウンロードとインストール
[Multica ダウンロードページ](https://multica.ai/download)から、使用するプラットフォームのインストーラーを入手してください。
| プラットフォーム | ファイル |
|---|---|
| macOS (Intel または Apple Silicon) | `.dmg` |
| Windows x64 | `.exe`(標準) |
| Windows ARM64 | `.exe``arm64` サフィックス付き) |
| Linux | `.AppImage` |
初回起動時にはログインが必要です — Web アプリと同じメール + 認証コードのフローです。ログインすると、Desktop はワークスペース一覧を自動的に同期します。
<Callout type="info">
**Desktop はデフォルトで Multica Cloud を使用しますが、ローカルの設定ファイルでセルフホスト環境を指すように設定できます。** アプリ内には依然として「セルフホストに接続」を選ぶピッカーはありません。Desktop はレンダラーが起動する前に `~/.multica/desktop.json` を読み込みます。ファイルがない場合は Cloud のデフォルト値を使用します。
最小構成のセルフホスト設定:
```json
{
"schemaVersion": 1,
"apiUrl": "https://api.your-domain"
}
```
`apiUrl` は必須で、`http` または `https` を使用する必要があります。Desktop は同一オリジン上で `wsUrl` を `/ws` として導出し(`https` なら `wss`、`http` なら `ws`、API オリジンから `appUrl` を導出します。デプロイ環境が異なるオリジンを使用する場合は明示的に設定してください。
```json
{
"schemaVersion": 1,
"apiUrl": "https://api.your-domain",
"wsUrl": "wss://api.your-domain/ws",
"appUrl": "https://your-domain"
}
```
`desktop.json` は存在するが無効な場合、Desktop は安全側に倒して動作を停止し、Cloud に静かにフォールバックする代わりにブロック型の設定エラーを表示します。開発ビルドの場合、`electron-vite dev` 中は依然として `VITE_API_URL` / `VITE_WS_URL` / `VITE_APP_URL` が優先されます。ランタイムでの Desktop セルフホスト構成は [issue #1371](https://github.com/multica-ai/multica/issues/1371) で実装されました。
</Callout>
## 次のステップ
- [Cloud Quickstart](/cloud-quickstart) — Desktop 向けの Cloud オンボーディングフロー
- [Self-Host Quickstart](/self-host-quickstart) — 自前のバックエンドを実行し、CLI または Desktop のランタイム設定で接続する
- [デーモンとランタイム](/daemon-runtimes) — デーモンの仕組みDesktop が代わりに起動してくれますが、動作は同じです)

View File

@@ -0,0 +1,99 @@
---
title: 데스크톱 앱
description: Multica Desktop이 무엇인지, 웹 앱과 어떻게 다른지, 언제 쓸 만한지 알아봅니다.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica Desktop은 macOS, Windows, Linux용 네이티브 데스크톱 앱입니다. 구성된 환경에 대해 웹 앱과 동일한 백엔드에 연결되며 동일한 데이터를 보여줍니다. 기본적으로 Desktop은 Multica Cloud를 사용하며, 자체 호스팅 인스턴스는 로컬 런타임 설정 파일로 구성할 수 있습니다. Desktop은 브라우저가 할 수 없는 몇 가지 기능도 추가로 제공합니다: **[워크스페이스](/workspaces)별 독립적인 탭 그룹**, **[데몬](/daemon-runtimes) 자동 시작**, **원클릭 업그레이드**입니다.
## Desktop 또는 웹 — 무엇을 선택할까
| | 웹 | Desktop |
|---|---|---|
| 접근 방식 | 브라우저에서 URL 열기 | 네이티브 앱 설치 |
| 다중 탭 | 브라우저 자체 탭 (워크스페이스 구분 없음) | **워크스페이스별 독립 탭 그룹 한 개** |
| 데몬 | `multica daemon start`를 직접 실행 | 실행 시 **자동으로 시작** |
| 업그레이드 | 새로고침하면 최신 버전 | 앱이 백그라운드에서 확인하고 다음 실행 시 설치 |
| 로그인 후 데이터 | 동일 | 동일 |
**웹을 선택**: 일회성으로 사용하거나, 다른 사람의 기기에서 작업하거나, 아무것도 설치하고 싶지 않을 때.
**Desktop을 선택**: 매일 사용하거나, 여러 워크스페이스를 동시에 다루거나, 데몬을 수동으로 관리하고 싶지 않을 때.
## 다중 탭: 워크스페이스를 전환하면 어떻게 되나
Desktop은 **참여한 모든 워크스페이스**마다 독립적인 탭 그룹을 유지합니다. 워크스페이스를 전환하면 현재 워크스페이스의 탭이 하나의 단위로 숨겨지고, 이전 워크스페이스의 탭은 떠났던 그대로 복원됩니다 — VSCode의 멀티 워크스페이스 동작이나 Slack에서 워크스페이스를 전환하는 것과 비슷합니다.
예시: 워크스페이스 A에서 이슈 탭 3개를 열어 둔 상태로 워크스페이스 B로 전환합니다. A의 탭 3개는 사라지고, B에는 B에서 마지막으로 열어 두었던 것이 표시됩니다. 다시 A로 전환하면 그 3개의 탭이 정확히 이전 상태 그대로 돌아옵니다. **탭은 워크스페이스 간에 절대 새어 나가지 않습니다.**
로그아웃하면 **모든 워크스페이스의 탭 상태가 지워지므로**, 여러 사용자가 기기를 공유하더라도 데이터가 새어 나가지 않습니다.
## Desktop의 자동 업데이트 방식
실행 시 Desktop은 GitHub Releases에서 더 최신 버전이 있는지 확인합니다. 새 버전이 발견되면:
1. 백그라운드에서 새 버전을 조용히 다운로드합니다.
2. "준비 완료 — 다음 실행 시 설치됩니다"라고 알려 줍니다.
3. 종료(또는 다음 재시작) 시, 앱이 닫히기 전에 업데이트를 설치합니다.
4. 다음 실행 시 새 버전이 실행됩니다.
이 전체 과정은 **작업 중인 내용을 방해하지 않습니다**.
<Callout type="warning">
**Windows에서는 ARM64와 x64가 별도의 업데이트 채널입니다** — 잘못된 아키텍처를 설치하면 업데이트가 감지되지 않습니다. 다운로드할 때 기기에 맞는 `.exe`를 선택하세요 (ARM 빌드에는 `arm64` 접미사가 붙어 있습니다).
</Callout>
macOS 빌드는 서명 및 공증되어 있으므로, 첫 실행 시 "확인되지 않은 개발자" 경고가 표시되지 않습니다. Linux 빌드는 `.AppImage`입니다 — 자동 업데이트는 electron-updater에 의존하는데, 일부 배포판에서는 불안정할 수 있습니다. **자동 업데이트가 작동하지 않으면, 새 버전을 수동으로 다운로드하여 기존 파일을 교체하세요.**
## 별도의 CLI와 데몬이 여전히 필요한가요?
**아닙니다.** Desktop에는 동일한 `multica` CLI 바이너리가 내장되어 있으며, 시작 시 자체 데몬 프로필을 실행합니다 (터미널에서 수동으로 실행 중인 데몬과는 격리됩니다).
이미 CLI를 설치하고 `multica daemon start`를 직접 실행했더라도, Desktop은 그 데몬을 가로채지 않습니다 — 별도의 프로필로 자체 데몬을 시작합니다. 둘은 **서로 다른 런타임**으로 등록되며, UI에서 두 개의 독립된 런타임을 볼 수 있습니다.
터미널에서 CLI 명령을 실행하고 싶다면, Desktop이 특별한 경로를 제공하지는 않습니다 — 별도로 설치한 CLI를 사용하거나, 앱의 리소스 디렉터리 안 `resources/bin/multica`에 있는 번들 사본을 실행하세요.
## 다운로드 및 설치
[Multica 다운로드 페이지](https://multica.ai/download)에서 사용하는 플랫폼의 설치 프로그램을 받으세요:
| 플랫폼 | 파일 |
|---|---|
| macOS (Intel 또는 Apple Silicon) | `.dmg` |
| Windows x64 | `.exe` (표준) |
| Windows ARM64 | `.exe` (`arm64` 접미사 포함) |
| Linux | `.AppImage` |
첫 실행 시 로그인이 필요합니다 — 웹 앱과 동일한 이메일 + 인증 코드 흐름입니다. 로그인하면 Desktop이 워크스페이스 목록을 자동으로 동기화합니다.
<Callout type="info">
**Desktop은 기본적으로 Multica Cloud를 사용하지만, 로컬 설정 파일로 자체 호스팅 인스턴스를 가리키도록 할 수 있습니다.** 앱 안에는 여전히 "자체 호스팅에 연결" 선택기가 없습니다. Desktop은 렌더러가 시작되기 전에 `~/.multica/desktop.json`을 읽습니다. 파일이 없으면 Cloud 기본값을 사용합니다.
최소 자체 호스팅 설정:
```json
{
"schemaVersion": 1,
"apiUrl": "https://api.your-domain"
}
```
`apiUrl`은 필수이며 `http` 또는 `https`를 사용해야 합니다. Desktop은 동일 오리진에서 `wsUrl`을 `/ws`로 유도하고(`https`이면 `wss`, `http`이면 `ws`), API 오리진에서 `appUrl`을 유도합니다. 배포 환경이 서로 다른 오리진을 사용한다면 명시적으로 설정하세요:
```json
{
"schemaVersion": 1,
"apiUrl": "https://api.your-domain",
"wsUrl": "wss://api.your-domain/ws",
"appUrl": "https://your-domain"
}
```
`desktop.json`이 존재하지만 유효하지 않으면, Desktop은 안전하게 동작을 차단하여 Cloud로 조용히 되돌아가는 대신 차단형 설정 오류를 표시합니다. 개발 빌드의 경우, `electron-vite dev` 중에는 여전히 `VITE_API_URL` / `VITE_WS_URL` / `VITE_APP_URL`이 우선합니다. 런타임 Desktop 자체 호스팅 구성은 [issue #1371](https://github.com/multica-ai/multica/issues/1371)에서 구현되었습니다.
</Callout>
## 다음 단계
- [Cloud Quickstart](/cloud-quickstart) — Desktop을 위한 Cloud 온보딩 흐름
- [Self-Host Quickstart](/self-host-quickstart) — 자체 백엔드를 실행하고 CLI 또는 Desktop 런타임 설정으로 연결하기
- [데몬 및 런타임](/daemon-runtimes) — 데몬의 작동 방식 (Desktop이 대신 시작해 주지만 동작은 동일합니다)

View File

@@ -0,0 +1,302 @@
---
title: 規約
description: コードのネーミング、i18n 翻訳用語集、中国語ボイスガイドの単一の信頼できる情報源。
---
このページは、コードのネーミング、i18n 翻訳用語集、中国語ボイスガイドの単一の信頼できる情報源です。かつて `packages/views/locales/glossary.md` やあちこちに散らばったコメントにあった内容は、現在すべてここに集約されています。
Multica のコードを書く、翻訳を変更する、あるいは中国語の製品コピーを書く場合は、このページを参照してください。
---
## 1. コードのネーミング
### ルート
ワークスペース進入前のルート(ユーザーがワークスペースに入る前から存在するルート)は、必ず単一の単語または `/{noun}/{verb}` パターンを使用しなければなりません。
- ✅ `/login`, `/inbox`, `/workspaces/new`
- ❌ `/new-workspace`, `/create-team`, `/accept-invite`
ルート直下のハイフンで連結した単語のまとまりは、ユーザーが自分で選んだワークスペースの slug と衝突し、際限のない予約 slug の監査を強いることになります。名詞(`workspaces`)を予約しておけば、`/workspaces/*` のサブツリー全体が自動的に保護されます。
### ワークスペーススコープのルート
常に `/{slug}/{section}` の下に置きます — `/{slug}/issues`、`/{slug}/agents`、`/{slug}/settings`。ワークスペースのルーティングロジックを絶対に重複させず、共有コードではフレームワーク固有の link API ではなく `useNavigation().push()` を使用してください。
### パッケージとモジュール
モノレポは厳格なパッケージ境界を強制します。
| パッケージ | 依存可能 | 依存禁止 |
| --- | --- | --- |
| `packages/core` | アプリ固有でないもののみ | `react-dom`, `localStorage`, `process.env`, `next/*`, UI ライブラリ |
| `packages/ui` | なし | `@multica/core`, ビジネスロジック |
| `packages/views` | `core/`, `ui/` | `next/*`, `react-router-dom`, stores |
| `apps/web/platform/` | `next/*` | 他のアプリ |
| `apps/desktop/.../platform/` | `react-router-dom`, electron | 他のアプリ |
両方のアプリに同じロジックが現れる場合は、必ず共有パッケージに抽出しなければなりません。「ささいな」重複という例外はありません。
### ファイルとコンポーネント
- ファイル: `kebab-case.tsx` / `kebab-case.ts`(例: `agent-row-actions.tsx`
- コンポーネント: `PascalCase`(例: `AgentRowActions`
- フック: `useCamelCase`(例: `useWorkspaceId`
- テスト: `<file>.test.ts(x)` として同じ場所に配置
- ストアZustand: `<feature>-store.ts`、`use<Feature>Store` として export
### データベースGo + sqlc
- テーブル: `snake_case` の単数形(`user`, `workspace`, `agent_runtime`
- カラム: `snake_case``workspace_id`, `created_at`, `last_seen_at`
- 外部キー: `<table>_id`
- ブール値: `is_<state>` または `<state>_at`(状態変更にはタイムスタンプ形式を推奨)
- マイグレーションファイル: `NNN_descriptive_name.up.sql` + `.down.sql` — 常に双方向を提供する
### Go
- 標準の `gofmt` + `go vet`。例外なし。
- Handler ファイルはドメインを反映する: `agent.go`, `auth.go`, `runtime.go`
- テスト: `<file>_test.go` を同じ場所に配置
- handler での UUID パースは、ルートの `CLAUDE.md` のルールに従ってください — 境界の入力には `parseUUIDOrBadRequest`、信頼できる往復には `parseUUID`panic 版を使い、error を確認せずに `util.ParseUUID` を直接使用しないでください。
### TypeScript
- ネットワーク上の API レスポンスは `snake_case` で、api client が境界で `camelCase` に変換します。TS コード内部では**常に camelCase**。
- 型: `PascalCase``Issue`, `AgentRuntime``IPrefix` は禁止、`_t` サフィックスも禁止。
- 列挙: string literal union を推奨し、ランタイムで反復処理が必要な場合にのみ `enum` を使用。
- TanStack Query のキー: `<feature>/queries.ts` 内のファクトリ関数、例: `issueKeys.detail(id)`。
### イシューキー
すべてのイシューには `MUL-123` のような人が読めるキーがあります。ワークスペースの `issue_prefix`(大文字と数字、通常 3 文字、最大 10 文字)+ 連番です。ワークスペースの admin は Settings → General で接頭辞を変更できますが、変更すると既存のすべてのイシューが番号を振り直されるため、古い接頭辞が埋め込まれた外部参照PR タイトル、ブランチ名、ドキュメントやチャット内のリンク)は解決されなくなります。
### コード内のコメント
英語のみです。リポジトリは Go と TypeScript の両方でこれを強制します。コード内に中国語のコメントを見つけたら、それはバグなので置き換えてください。
### コミットメッセージ
Conventional 形式: `feat(scope)`, `fix(scope)`, `refactor(scope)`, `docs`, `test(scope)`, `chore(scope)`。意図ごとにまとめられたアトミックなコミット。
---
## 2. i18n 翻訳用語集
これは、すべての翻訳 PR が**必ず**守らなければならない用語集です。かつては `packages/views/locales/glossary.md` にありましたが、そのファイルは現在ここを指す stub です。
### 中核となる区別: エンティティ vs 概念
Multica の製品名詞は 2 つのカテゴリに分かれます。
- **エンティティEntity** — URL、データベースの row、API の型を持ちます。中国語のテキストでは**小文字の英語**で表記し、視覚的に型名のように読めて「これは Multica のシステムエンティティだ」というシグナルを与えます。
- **概念Concept** — 一般名詞であり、データベースのエンティティではありません。**完全に翻訳**し、中国語ユーザーが流れるテキストの中にギザギザの英語を見ないようにします。
このルールは `apps/docs/content/docs/*.zh.mdx` と整合しています — これらのドキュメントは事実上の中国語ボイス標準であり、20 ページ以上で実戦検証されています。
### エンティティ — 混合ルール(`issue` / `skill` / `task`
`issue` / `skill` / `task` は Multica の中核エンティティです。スキーマのカラム、API のフィールド、製品 UI のラベルがすべて英語です。中国語のテキストでは**混合ルール**に従い、何を使うかは単語がどこに現れるかによって変わります。
| 文脈 | 表記 | 例 |
| --- | --- | --- |
| **UI 文字列、状態名、コード参照** | 小文字の英語 | "排队中的 task"、"创建子 issue"、"为智能体注入 skill" |
| **ドキュメントのタイトル / セクション見出し** | Title-case の英語 **または** 中国語の用語 | "Issue 与 project"、"Skills"、"执行任务" |
| **長文ドキュメントの本文で、エンティティが文の主語になっている場合** | 中国語の用語、初出時に括弧内に英語 | "**执行任务**task是智能体每一次工作的单位" |
| **API / DB フィールド** | 常に `task` / `issue` / `skill` | `task_id`, `issue_status`, `skill_uuid` |
中国語の用語の参考:
- `task` ↔ `执行任务`(文脈が明確になれば `任务` に短縮)
- `issue` には定着した中国語訳がありません — 英語のまま;タイトルでは `Issue` のように大文字にできます
- `skill` には定着した中国語訳がありません — 英語のまま;タイトルでは `Skills` のように大文字にできます
**`issue` / `skill` / `task` が `project` / `autopilot` のように中国語へ強制的に翻訳されない理由**:
- **`issue` / `task`**: 開発チームは英語で会話します。中国語の候補("任务" — あいまいすぎて "工作" とほぼ同義;"工单" — IT チケットのニュアンス;"议题" — GitHub 風だが製品の感覚に合わない)はいずれも `issue` よりも読みづらくなります。**ただし**、長文ドキュメントの本文で小文字の `task` を 50 回繰り返すとリズムが崩れるため、本文では `执行任务` を許容しつつ、UI 文字列と状態名は小文字の英語のままにします。
- **`skill`**: 定着した中国語の用語がない Multica 固有の概念です。
- **`project` → "项目"**: 定着した主流の中国語の単語です。Feishu / Tower / Teambition / PingCode / GitHub Projects — すべての中国語製品がこれを翻訳します。中国語の文脈で `project` をそのまま残す製品はありません。
- **`autopilot` → "自动化"**: 中国語で "autopilot" は Tesla の "自动驾驶" を連想させ、この機能が行うことスケジュールに従ってタスクを実行すると合いません。Notion も Feishu も "自动化" を使っており、それが業界の合意です。
### 翻訳しない — ブランドと頭字語
| カテゴリ | 用語 |
| --- | --- |
| ブランド | **Multica**, GitHub, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
| 頭字語 | API, CLI, URL, SDK, OAuth, JWT, SSO, WebSocket, HTTP, JSON, YAML, SQL |
### 完全に翻訳する — 概念
| English | Chinese |
| --- | --- |
| Workspace | **工作区** |
| Agent | **智能体** |
| Project | **项目** |
| Autopilot | **自动化** |
| Daemon | **守护进程** |
| Runtime | **运行时** |
| Inbox | **收件箱** |
| Comment | **评论** |
| Reply | **回复** |
| Notifications | **通知** |
| Member | **成员** |
| Label | **标签** |
| Settings | **设置** |
| Onboarding | **上手引导** |
### 完全に翻訳する — 一般的な UI 用語
| English | Chinese |
| --- | --- |
| Invite / Invitation | 邀请 |
| Search | 搜索 |
| Email | 邮箱 (label) / 邮件 (action) |
| Password | 密码 |
| Sign in / Log in | 登录 |
| Sign up | 注册 |
| Sign out / Log out | 退出登录 |
| Save / Cancel / Delete | 保存 / 取消 / 删除 |
| Confirm / Continue / Back | 确认 / 继续 / 返回 |
| Edit / New / Create / Add | 编辑 / 新建 / 创建 / 添加 |
| Remove / Send / Open / Close | 移除 / 发送 / 打开 / 关闭 |
| Preview / Download / Upload | 预览 / 下载 / 上传 |
| Done / Loading... | 完成 / 加载中... |
| Profile / Account / Appearance | 个人资料 / 账号 / 外观 |
| Theme / Language | 主题 / 语言 |
| Light / Dark / System | 浅色 / 深色 / 跟随系统 |
| Active / Archived | 活跃 (or 启用) / 已归档 |
| Status / Priority | 状态 / 优先级 |
| Assignee / Reporter | 负责人 / 报告人 |
| Description / Title | 描述 / 标题 |
| Date / Time | 日期 / 时间 |
| Today / Yesterday / Tomorrow | 今天 / 昨天 / 明天 |
| Empty / Failed / Success | 空 / 失败 / 成功 |
| Error / Warning | 错误 / 警告 |
### ロールと状態の列挙型(小文字の英語、翻訳しない)
これらはスキーマレベルの識別子です。中国語の文脈でも小文字の英語で表記します。
- ロール: `owner` / `admin` / `member`
- イシューの状態: `backlog` / `todo` / `in_progress` / `in_review` / `done` / `blocked` / `cancelled`
UI ではこれらの値を英語で表示します(必要に応じて `code-style` で囲む)。
- "你需要 owner 权限"
- "已切换到 in_progress"
### 単語の組み合わせルール
英語の単語(エンティティ / ブランド / 頭字語)と周囲の中国語の間には、常に**単一の空白**を入れます。
- "Create new issue" → "新建 issue"
- "Assign to agent" → "分配给智能体"
- "Configure runtime" → "配置运行时"
- "Stop daemon" → "停止守护进程"
### 複数形と数
i18next は `_one` / `_other` を使います。中国語には文法的な数がないため、`_other` のみを埋めます。
```json
// en/issues.json
{
"issue_count_one": "{{count}} issue",
"issue_count_other": "{{count}} issues"
}
// zh-Hans/issues.json
{
"issue_count_other": "{{count}} 个 issue"
}
```
一般的な数の形式:
- `{{count}} issues` → `{{count}} 个 issue`
- `{{count}} agents` → `{{count}} 个智能体`
- `{{count}} workspaces` → `{{count}} 个工作区`
- `{{count}} comments` → `{{count}} 条评论`
- `{{count}} members` → `{{count}} 位成员`
- `{{count}} skills` → `{{count}} 个 skill`
### 補間
`{{var}}` を使います。中国語の翻訳では、自然な文章の流れのために順序を並べ替えてもかまいません。
```json
// en
{ "welcome_message": "Welcome back, {{name}}!" }
// zh-Hans
{ "welcome_message": "欢迎回来,{{name}}" }
```
### 翻訳キーのネーミング
3 階層のネスト: `feature.component.action`。
```json
{
"feature_or_component": {
"subcomponent_or_section": {
"action_or_label": "..."
}
}
}
```
例:
- `issues.toolbar.batch_update_success`
- `issues.detail.comment_form.placeholder`
- `inbox.empty.title`
- `settings.preferences.language.title`
### Web 専用 / Desktop 専用のコピー
- 共有コピー: namespace JSON の最上位
- Web 専用: `web` セクション
- Desktop 専用: `desktop` セクション
正式な例は `auth.json` を参照してください(`web` セクションに `prefer_desktop` / `desktop_handoff.*` が含まれます)。
---
## 3. 中国語のボイスとスタイル
### 句読点
- 中国語では全角の句読点を使用: `,。:;!?`
- 引用符: 英語の原文に合わせて、まっすぐな二重引用符 `"..."` を使用。`「」` や丸い引用符は使わないでください。
- 省略記号: 単一文字の `…` ではなく、3 つの点 `...`。英語の原文に合わせてください。
- 中国語と英語の混在: 英語の単語の両側にそれぞれ単一の空白(単語の組み合わせルールを参照)。
### スタイルの原則
- **簡潔かつ直接的に。** 翻訳調を避ける: "对于 X 来说"、"作为 X"、"我们的"。
- **エラーメッセージ**: 穏やかだが明確に。"无法保存修改" は "保存修改失败了!" よりも優れています。
- **ボタン**: 動詞を先頭に、2〜4 文字。"取消"、"保存修改"、"立即同步"。
- **ツールチップ**: 完結した短い文。"复制链接到剪贴板"。
- **プレースホルダー**: 例の形式。"输入 issue 标题..."。
### 迷ったときに参照する場所
用語集が特定の用語を扱っていない場合は、次を参照してください。
1. `apps/docs/content/docs/*.zh.mdx` — 事実上の中国語ボイス標準、一貫した翻訳が 20 ページ以上
2. `packages/views/locales/zh-Hans/auth.json` と `editor.json` — JSON 構造 + selector API パターン
3. `packages/views/auth/login-page.tsx` — コンポーネントレベルの selector API 呼び出し箇所
4. `packages/views/settings/components/preferences-tab.tsx` — 言語切り替えの参考
---
## このページを更新するとき
ここのルールを変更した場合は、次も併せて行ってください。
1. 関連する locale JSON / CLAUDE.md / ドキュメントページに適用する
2. PR の説明に変更点を記録し、レビュアーが下流の一括対応を確認できるようにする
このページが契約です。他の何ものもこれを上書きできません。

View File

@@ -0,0 +1,302 @@
---
title: 규약
description: 코드 네이밍, i18n 번역 용어집, 중국어 보이스 가이드의 단일 진실 공급원.
---
이 페이지는 코드 네이밍, i18n 번역 용어집, 중국어 보이스 가이드의 단일 진실 공급원입니다. 예전에 `packages/views/locales/glossary.md`나 여기저기 흩어진 주석에 있던 내용은 이제 모두 이곳에 모여 있습니다.
Multica 코드를 작성하거나, 번역을 변경하거나, 중국어 제품 카피를 작성한다면 이 페이지를 참고하세요.
---
## 1. 코드 네이밍
### 라우트
워크스페이스 진입 전 라우트(사용자가 워크스페이스에 들어가기 전에 존재하는 라우트)는 반드시 단일 단어 또는 `/{noun}/{verb}` 패턴을 사용해야 합니다.
- ✅ `/login`, `/inbox`, `/workspaces/new`
- ❌ `/new-workspace`, `/create-team`, `/accept-invite`
루트에 하이픈으로 연결된 단어 묶음은 사용자가 직접 고른 워크스페이스 slug와 충돌하며, 끝없는 예약어 slug 점검을 강요합니다. 명사(`workspaces`)를 예약해 두면 `/workspaces/*` 하위 트리 전체가 자동으로 보호됩니다.
### 워크스페이스 범위 라우트
항상 `/{slug}/{section}` 아래에 둡니다 — `/{slug}/issues`, `/{slug}/agents`, `/{slug}/settings`. 워크스페이스 라우팅 로직을 절대 중복하지 말고, 공유 코드에서는 프레임워크별 link API 대신 `useNavigation().push()`를 사용하세요.
### 패키지와 모듈
모노레포는 엄격한 패키지 경계를 강제합니다:
| 패키지 | 의존 가능 | 의존 금지 |
| --- | --- | --- |
| `packages/core` | 앱 종속적이지 않은 것만 | `react-dom`, `localStorage`, `process.env`, `next/*`, UI 라이브러리 |
| `packages/ui` | 없음 | `@multica/core`, 비즈니스 로직 |
| `packages/views` | `core/`, `ui/` | `next/*`, `react-router-dom`, stores |
| `apps/web/platform/` | `next/*` | 다른 앱 |
| `apps/desktop/.../platform/` | `react-router-dom`, electron | 다른 앱 |
두 앱 모두에 동일한 로직이 나타난다면 반드시 공유 패키지로 추출해야 합니다. "사소한" 중복이라는 예외는 없습니다.
### 파일과 컴포넌트
- 파일: `kebab-case.tsx` / `kebab-case.ts` (예: `agent-row-actions.tsx`)
- 컴포넌트: `PascalCase` (예: `AgentRowActions`)
- Hook: `useCamelCase` (예: `useWorkspaceId`)
- 테스트: `<file>.test.ts(x)`로 같은 위치에 배치
- Store (Zustand): `<feature>-store.ts`, `use<Feature>Store`로 export
### 데이터베이스 (Go + sqlc)
- 테이블: `snake_case` 단수 (`user`, `workspace`, `agent_runtime`)
- 컬럼: `snake_case` (`workspace_id`, `created_at`, `last_seen_at`)
- 외래 키: `<table>_id`
- 불리언: `is_<state>` 또는 `<state>_at` (상태 변경에는 타임스탬프 형태 선호)
- 마이그레이션 파일: `NNN_descriptive_name.up.sql` + `.down.sql` — 항상 양방향을 모두 제공
### Go
- 표준 `gofmt` + `go vet`. 예외 없음.
- Handler 파일은 도메인을 반영: `agent.go`, `auth.go`, `runtime.go`
- 테스트: `<file>_test.go`로 같은 위치에 배치
- handler에서의 UUID 파싱은 루트 `CLAUDE.md`의 규칙을 따릅니다 — 경계 입력에는 `parseUUIDOrBadRequest`, 신뢰할 수 있는 왕복에는 `parseUUID`(panic 버전), 절대 error를 확인하지 않고 `util.ParseUUID`를 직접 사용하지 마세요.
### TypeScript
- 네트워크상의 API 응답은 `snake_case`이며, api client가 경계에서 `camelCase`로 변환합니다. TS 코드 내부에서는 **항상 camelCase**.
- 타입: `PascalCase` (`Issue`, `AgentRuntime`); `IPrefix` 금지, `_t` 접미사 금지.
- 열거형: string literal union을 선호하고, 런타임 순회가 필요한 경우에만 `enum`을 사용.
- TanStack Query 키: `<feature>/queries.ts` 안의 팩토리 함수, 예: `issueKeys.detail(id)`.
### 이슈 키
모든 이슈에는 `MUL-123` 같은 사람이 읽을 수 있는 키가 있습니다: 워크스페이스 `issue_prefix`(대문자와 숫자, 보통 3자, 최대 10자) + 일련번호. 워크스페이스 admin은 Settings → General에서 접두사를 변경할 수 있는데, 변경하면 기존의 모든 이슈가 다시 번호 매겨지므로 옛 접두사가 박혀 있는 외부 참조(PR 제목, 브랜치 이름, 문서와 채팅 속 링크)는 더 이상 연결되지 않습니다.
### 코드 내 주석
영어만 사용합니다. 레포는 Go와 TypeScript 모두에 이를 강제합니다. 코드에서 중국어 주석을 발견하면 그것은 버그이니 교체하세요.
### 커밋 메시지
Conventional 형식: `feat(scope)`, `fix(scope)`, `refactor(scope)`, `docs`, `test(scope)`, `chore(scope)`. 의도별로 묶인 원자적 커밋.
---
## 2. i18n 번역 용어집
이것은 모든 번역 PR이 **반드시** 지켜야 하는 용어집입니다. 예전에는 `packages/views/locales/glossary.md`에 있었는데, 그 파일은 이제 이곳을 가리키는 stub입니다.
### 핵심 구분: 엔티티 vs 개념
Multica의 제품 명사는 두 가지 범주로 나뉩니다:
- **엔티티(Entity)** — URL, 데이터베이스 row, API 타입을 가집니다. 중국어 텍스트에서는 **소문자 영문**으로 표기하여 시각적으로 타입 이름처럼 읽히게 하고 "이것은 Multica 시스템 엔티티다"라는 신호를 줍니다.
- **개념(Concept)** — 일반 명사이며, 데이터베이스 엔티티가 아닙니다. **완전히 번역**하여 중국어 사용자가 흐르는 텍스트 속에 들쭉날쭉한 영문을 보지 않도록 합니다.
이 규칙은 `apps/docs/content/docs/*.zh.mdx`와 정렬되어 있습니다 — 이 문서들은 사실상의 중국어 보이스 표준이며 20개 이상의 페이지에서 실전 검증되었습니다.
### 엔티티 — 혼합 규칙 (`issue` / `skill` / `task`)
`issue` / `skill` / `task`는 Multica의 핵심 엔티티입니다. 스키마 컬럼, API 필드, 제품 UI 레이블이 모두 영어입니다. 중국어 텍스트에서는 **혼합 규칙**을 따르며 — 무엇을 사용할지는 단어가 어디에 나타나는지에 따라 달라집니다:
| 맥락 | 표기 | 예시 |
| --- | --- | --- |
| **UI 문자열, 상태 이름, 코드 참조** | 소문자 영문 | "排队中的 task"、"创建子 issue"、"为智能体注入 skill" |
| **문서 제목 / 섹션 헤딩** | Title-case 영문 **또는** 중국어 용어 | "Issue 与 project"、"Skills"、"执行任务" |
| **장문 문서 산문에서 엔티티가 진행 중인 주어일 때** | 중국어 용어, 첫 언급 시 괄호 안에 영문 | "**执行任务**task是智能体每一次工作的单位" |
| **API / DB 필드** | 항상 `task` / `issue` / `skill` | `task_id`, `issue_status`, `skill_uuid` |
중국어 용어 참고:
- `task` ↔ `执行任务` (맥락이 분명해지면 `任务`로 줄여서 사용)
- `issue`에는 정착된 중국어 번역이 없습니다 — 영문 유지; 제목에서는 `Issue`처럼 대문자로 표기 가능
- `skill`에는 정착된 중국어 번역이 없습니다 — 영문 유지; 제목에서는 `Skills`처럼 대문자로 표기 가능
**`issue` / `skill` / `task`가 `project` / `autopilot`처럼 중국어로 강제 번역되지 않는 이유**:
- **`issue` / `task`**: 개발 팀은 영어로 대화합니다. 중국어 후보들("任务" — 너무 모호하고 "工作"과 거의 동의어; "工单" — IT 티켓 어감; "议题" — GitHub 스타일이지만 제품 느낌과 맞지 않음)은 모두 `issue`보다 못하게 읽힙니다. **하지만** 장문 문서 산문에서 소문자 `task`를 50번 반복하면 리듬이 깨지므로 — 산문에서는 `执行任务`를 허용하되, UI 문자열과 상태 이름은 소문자 영문으로 유지합니다.
- **`skill`**: 정착된 중국어 용어가 없는 Multica 고유 개념입니다.
- **`project` → "项目"**: 정착된 주류 중국어 단어. Feishu / Tower / Teambition / PingCode / GitHub Projects — 모든 중국어 제품이 이를 번역합니다. 중국어 맥락에서 `project`를 그대로 두는 제품은 없습니다.
- **`autopilot` → "自动化"**: 중국어에서 "autopilot"은 Tesla의 "自动驾驶"를 연상시키며, 이 기능이 하는 일(일정에 따라 task 실행)과 맞지 않습니다. Notion과 Feishu 모두 "自动化"를 사용하며, 그것이 업계 합의입니다.
### 번역하지 않음 — 브랜드와 약어
| 범주 | 용어 |
| --- | --- |
| 브랜드 | **Multica**, GitHub, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
| 약어 | API, CLI, URL, SDK, OAuth, JWT, SSO, WebSocket, HTTP, JSON, YAML, SQL |
### 완전히 번역 — 개념
| English | Chinese |
| --- | --- |
| Workspace | **工作区** |
| Agent | **智能体** |
| Project | **项目** |
| Autopilot | **自动化** |
| Daemon | **守护进程** |
| Runtime | **运行时** |
| Inbox | **收件箱** |
| Comment | **评论** |
| Reply | **回复** |
| Notifications | **通知** |
| Member | **成员** |
| Label | **标签** |
| Settings | **设置** |
| Onboarding | **上手引导** |
### 완전히 번역 — 일반 UI 단어
| English | Chinese |
| --- | --- |
| Invite / Invitation | 邀请 |
| Search | 搜索 |
| Email | 邮箱 (label) / 邮件 (action) |
| Password | 密码 |
| Sign in / Log in | 登录 |
| Sign up | 注册 |
| Sign out / Log out | 退出登录 |
| Save / Cancel / Delete | 保存 / 取消 / 删除 |
| Confirm / Continue / Back | 确认 / 继续 / 返回 |
| Edit / New / Create / Add | 编辑 / 新建 / 创建 / 添加 |
| Remove / Send / Open / Close | 移除 / 发送 / 打开 / 关闭 |
| Preview / Download / Upload | 预览 / 下载 / 上传 |
| Done / Loading... | 完成 / 加载中... |
| Profile / Account / Appearance | 个人资料 / 账号 / 外观 |
| Theme / Language | 主题 / 语言 |
| Light / Dark / System | 浅色 / 深色 / 跟随系统 |
| Active / Archived | 活跃 (or 启用) / 已归档 |
| Status / Priority | 状态 / 优先级 |
| Assignee / Reporter | 负责人 / 报告人 |
| Description / Title | 描述 / 标题 |
| Date / Time | 日期 / 时间 |
| Today / Yesterday / Tomorrow | 今天 / 昨天 / 明天 |
| Empty / Failed / Success | 空 / 失败 / 成功 |
| Error / Warning | 错误 / 警告 |
### 역할과 상태 열거형 (소문자 영문, 번역하지 않음)
이것들은 스키마 수준의 식별자입니다; 중국어 맥락에서도 소문자 영문으로 표기합니다.
- 역할: `owner` / `admin` / `member`
- 이슈 상태: `backlog` / `todo` / `in_progress` / `in_review` / `done` / `blocked` / `cancelled`
UI에서는 이 값들을 영어로 표시합니다(필요 시 `code-style`로 감쌈):
- "你需要 owner 权限"
- "已切换到 in_progress"
### 단어 조합 규칙
영문 단어(엔티티 / 브랜드 / 약어)와 주변 중국어 사이에는 항상 **단일 공백**을 둡니다:
- "Create new issue" → "新建 issue"
- "Assign to agent" → "分配给智能体"
- "Configure runtime" → "配置运行时"
- "Stop daemon" → "停止守护进程"
### 복수형과 개수
i18next는 `_one` / `_other`를 사용합니다; 중국어에는 문법적 수가 없으므로 `_other`만 채웁니다.
```json
// en/issues.json
{
"issue_count_one": "{{count}} issue",
"issue_count_other": "{{count}} issues"
}
// zh-Hans/issues.json
{
"issue_count_other": "{{count}} 个 issue"
}
```
일반적인 개수 형식:
- `{{count}} issues` → `{{count}} 个 issue`
- `{{count}} agents` → `{{count}} 个智能体`
- `{{count}} workspaces` → `{{count}} 个工作区`
- `{{count}} comments` → `{{count}} 条评论`
- `{{count}} members` → `{{count}} 位成员`
- `{{count}} skills` → `{{count}} 个 skill`
### 보간
`{{var}}`를 사용합니다. 중국어 번역은 자연스러운 문장 흐름을 위해 순서를 재배치할 수 있습니다.
```json
// en
{ "welcome_message": "Welcome back, {{name}}!" }
// zh-Hans
{ "welcome_message": "欢迎回来,{{name}}" }
```
### 번역 키 네이밍
3단계 중첩: `feature.component.action`.
```json
{
"feature_or_component": {
"subcomponent_or_section": {
"action_or_label": "..."
}
}
}
```
예시:
- `issues.toolbar.batch_update_success`
- `issues.detail.comment_form.placeholder`
- `inbox.empty.title`
- `settings.preferences.language.title`
### Web 전용 / Desktop 전용 카피
- 공유 카피: namespace JSON의 최상위
- Web 전용: `web` 섹션
- Desktop 전용: `desktop` 섹션
정식 예시는 `auth.json`을 참고하세요(`web` 섹션에 `prefer_desktop` / `desktop_handoff.*`가 포함됨).
---
## 3. 중국어 보이스와 스타일
### 구두점
- 중국어에서는 전각 구두점 사용: `,。:;!?`
- 따옴표: 영어 원문과 맞추기 위해 곧은 큰따옴표 `"..."`를 사용. `「」`나 둥근 따옴표는 사용하지 마세요.
- 줄임표: 단일 문자 `…`가 아닌 세 개의 점 `...`. 영어 원문과 일치시키세요.
- 중국어-영어 혼용: 영문 단어 양옆에 각각 단일 공백(단어 조합 규칙 참고).
### 스타일 원칙
- **간결하고 직접적으로.** 번역투 회피: "对于 X 来说"、"作为 X"、"我们的".
- **오류 메시지**: 부드럽지만 명확하게. "无法保存修改"가 "保存修改失败了!"보다 낫습니다.
- **버튼**: 동사를 먼저, 2~4자. "取消"、"保存修改"、"立即同步".
- **툴팁**: 완결된 짧은 문장. "复制链接到剪贴板".
- **플레이스홀더**: 예시 형태. "输入 issue 标题...".
### 막힐 때 참고할 곳
용어집이 특정 용어를 다루지 않을 때는 다음을 참고하세요:
1. `apps/docs/content/docs/*.zh.mdx` — 사실상의 중국어 보이스 표준, 일관된 번역 20개 이상 페이지
2. `packages/views/locales/zh-Hans/auth.json`과 `editor.json` — JSON 구조 + selector API 패턴
3. `packages/views/auth/login-page.tsx` — 컴포넌트 수준 selector API 호출 지점
4. `packages/views/settings/components/preferences-tab.tsx` — 언어 전환기 참고
---
## 이 페이지를 업데이트할 때
이곳의 규칙을 변경하면 다음도 함께 수행하세요:
1. 관련 locale JSON / CLAUDE.md / 문서 페이지에 적용
2. PR 설명에 변경 사항을 기록하여 리뷰어가 다운스트림 정리를 살펴보도록 알리기
이 페이지가 계약입니다; 다른 어떤 것도 이를 무시할 수 없습니다.

View File

@@ -0,0 +1,4 @@
{
"title": "開発者",
"pages": ["conventions"]
}

View File

@@ -0,0 +1,4 @@
{
"title": "Developers",
"pages": ["conventions"]
}

View File

@@ -0,0 +1,224 @@
---
title: 環境変数
description: セルフホストの Multica サーバーを実行するための環境変数の完全な一覧です。
---
import { Callout } from "fumadocs-ui/components/callout";
セルフホストの Multica [サーバー](/self-host-quickstart)は、起動時に環境変数から設定を読み込みます — データベース、サインイン、メール、ストレージ、サインアップ許可リストはすべてここにあります。このページでは、すべての変数を用途別にグループ化しています。各セクションでは、**設定しないと何が起きるか**、そして**プロダクションで必ず設定すべきものはどれか**を明確に説明します。auth 関連の変数を実際にどう設定するかについては、[サインインとサインアップの設定](/auth-setup)を参照してください。
## コアサーバー変数
デプロイ前に必ず検討すべきコア変数です — 一部はサーバーを起動できるようにするデフォルト値を持っていますが、プロダクションでは必須項目を明示的に設定すべきです。
| 変数 | デフォルト | プロダクションで必須? |
|---|---|---|
| `DATABASE_URL` | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` | **はい** |
| `PORT` | `8080` | いいえ(ポートを変更する場合を除く) |
| `JWT_SECRET` | `multica-dev-secret-change-in-production` | **はい**(デフォルトは安全ではありません) |
| `APP_ENV` | 空 | **はい**`production` である必要があります) |
| `FRONTEND_ORIGIN` | 空 | **はい**(セルフホストは自身のドメインを設定する必要があります) |
| `MULTICA_DEV_VERIFICATION_CODE` | 空 | いいえ(プロダクションでは必ず空のままにしてください) |
<Callout type="warning">
**プロダクションでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにしてください。** 固定のローカルテストコードはデフォルトで無効になっていますが、`MULTICA_DEV_VERIFICATION_CODE=888888` で有効にすると、`APP_ENV` が production 以外の間は、コードを要求できる誰もがその固定値でサインインできてしまいます。このショートカットは `APP_ENV=production` のときには無視されます。
</Callout>
### データベース接続プール
| 変数 | デフォルト | 説明 |
|---|---|---|
| `DATABASE_MAX_CONNS` | `25` | pgxpool の最大接続数。デーモンは頻繁に3 秒ごとに)ポーリングして接続を使用するため、規模の大きいデプロイではより高い値が必要になる場合があります |
| `DATABASE_MIN_CONNS` | `5` | 最小アイドル接続数 |
**設定しない場合**、上記の値が使われます — 以前プロダクションでプール枯渇を引き起こした pgx 組み込みの 4/NumCPU デフォルトでは**ありません**。
## メール設定
Multica は 2 つの配信バックエンドをサポートします — クラウドデプロイ向けの [Resend](https://resend.com/) と、内部 / オンプレミスネットワーク向けの SMTP relay です。両方が設定されている場合は `SMTP_HOST` が `RESEND_API_KEY` より優先されます。
### Resend
| 変数 | デフォルト | 説明 |
|---|---|---|
| `RESEND_API_KEY` | 空 | Resend API key |
| `RESEND_FROM_EMAIL` | `noreply@multica.ai` | 送信元アドレスResend アカウントで検証済みのドメインである必要があり、SMTP を使用する場合も `From:` ヘッダーとして再利用されます) |
### SMTP relay
| 変数 | デフォルト | 説明 |
|---|---|---|
| `SMTP_HOST` | 空 | SMTP relay のホスト名。これを設定すると SMTP モードが有効になり、Resend を上書きします |
| `SMTP_PORT` | `25` | SMTP ポート。STARTTLS サブミッションには `587` を使用してください。**ポート 465SMTPS / 暗黙的 TLSはサポートされていません** |
| `SMTP_USERNAME` | 空 | SMTP ユーザー名。認証なしの relay の場合は空のままにしてください |
| `SMTP_PASSWORD` | 空 | SMTP パスワード |
| `SMTP_TLS_INSECURE` | `false` | TLS 証明書の検証をスキップするには `true` に設定(プライベート CA / 自己署名証明書のみ) |
サーバーが STARTTLS を通知すると自動的にアップグレードされます。dial タイムアウトは 10 秒で、SMTP セッション全体には 30 秒のデッドラインがあるため、ブラックホール化した relay が auth ハンドラーをハングさせることはできません。
**どちらも設定していない場合の動作**: サーバーはエラーを出しませんが、送信されるはずだったすべてのメール(検証コード、招待リンク)は**サーバーの stdout にのみ記録されます**。ローカル開発には便利です — サーバーログからコードをコピーして使ってください。**プロダクションでこれを設定し忘れると、静かなブラックホールが生まれ**、ユーザーはメールをまったく受け取れず、エラーも一切表面化しません。
## Google OAuth 設定
任意です。メール + 検証コードのみを使用する場合は設定しないままにし、サインインページに「Sign in with Google」を追加する場合は設定してください。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `GOOGLE_CLIENT_ID` | 空 | Google Cloud OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | 空 | Google Cloud OAuth secret |
| `GOOGLE_REDIRECT_URI` | `http://localhost:3000/auth/callback` | OAuth コールバック URLセルフホスト: 自身のフロントエンドドメインに置き換えてください) |
**ランタイムで適用されます**: フロントエンドはこれらの設定をランタイムに `/api/config` 経由で読み込むため、**変更してもフロントエンドのリビルドや再デプロイは不要です** — サーバーを再起動すれば適用されます。
完全なセットアップGoogle Cloud Console の手順を含む)は[サインインとサインアップの設定](/auth-setup#google-oauth-configuration)にあります。
## ファイルストレージ設定
Multica はユーザーがアップロードした添付ファイル(コメント内の画像やファイル)を保存します。**S3 が推奨されます**。S3 が設定されていない場合はローカルディスクにフォールバックします。
### S3 / S3 互換ストレージ
| 変数 | デフォルト | 説明 |
|---|---|---|
| `S3_BUCKET` | 空 | **バケット名のみ**(例: `my-bucket`)。`.s3.<region>.amazonaws.com` サフィックスは含め**ないでください** — サーバーが `S3_BUCKET` + `S3_REGION` から公開ホストを構築します。これを設定すると S3 ストレージが有効になります |
| `S3_REGION` | `us-west-2` | AWS リージョン。バケットの実際のリージョンと一致する必要があります — SDK 署名と公開 URL の構築の両方に使われます |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | 空 | 静的な認証情報。両方を設定しない場合は AWS SDK のデフォルト認証情報チェーンIAM role / 環境認証情報)が使われます |
| `AWS_ENDPOINT_URL` | 空 | カスタムの S3 互換エンドポイント(例: [MinIO](https://min.io/))。これを設定すると path-style URL に切り替わります |
**`S3_BUCKET` を設定しない場合**: サーバーは起動時に `"S3_BUCKET not set, cloud upload disabled"` をログに記録し、すべてのアップロードはローカルディスクにフォールバックします。
**公開 URL** は次の優先順位で構築されます。
1. `CLOUDFRONT_DOMAIN` が設定されている場合は `https://<CLOUDFRONT_DOMAIN>/<key>`。
2. `AWS_ENDPOINT_URL` が設定されている場合は `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>`path-style
3. `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>`virtual-hosted-style。`S3_BUCKET` にドットが含まれる場合、AWS が発行するワイルドカード TLS 証明書がドットを含むバケットホストを検証できないため、サーバーは `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>`path-styleにフォールバックします。
### ローカルディスクS3 が設定されていない場合)
| 変数 | デフォルト | 説明 |
|---|---|---|
| `LOCAL_UPLOAD_DIR` | `./data/uploads` | ローカルストレージのディレクトリ |
| `LOCAL_UPLOAD_BASE_URL` | 空(相対パスを返します) | 公開 base URL — 設定しないとフロントエンドが添付ファイルの完全な URL を解決できません |
### CloudFront任意
S3 の前段に CloudFront を置く場合、3 つの変数が適用されます: `CLOUDFRONT_DOMAIN`、`CLOUDFRONT_KEY_PAIR_ID`、`CLOUDFRONT_PRIVATE_KEY`(または Secrets Manager から読み込むには `CLOUDFRONT_PRIVATE_KEY_SECRET`。CloudFront を使わない場合はスキップしてください — S3 設定とは競合しません。
### Cookie ドメイン
| 変数 | デフォルト | 説明 |
|---|---|---|
| `COOKIE_DOMAIN` | 空 | セッション cookie のスコープ |
- **空**: cookie は訪問した正確なホストでのみ有効です(単一ホストのデプロイに適切)
- **`.example.com` に設定**: cookie がサブドメイン間で共有されます(そのため `app.example.com` と `api.example.com` がサインインセッションを共有します)
- 警告: IP アドレスにはできません(ブラウザは無視します)
## 誰がサインアップできるかを制限する
3 つの許可リストの層が優先順位に従って組み合わされます。**いずれか 1 つの層でも空でない値に設定されると、一致しないメールは拒否されます** — `ALLOW_SIGNUP=true` でさえこれを上書きできません。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `ALLOWED_EMAILS` | 空 | 明示的なメール許可リスト(カンマ区切り)。空でない場合、リストにあるメールのみがサインアップできます |
| `ALLOWED_EMAIL_DOMAINS` | 空 | ドメイン許可リスト(カンマ区切り)。空でない場合、リストにあるドメインのみがサインアップできます |
| `ALLOW_SIGNUP` | `true` | サインアップのマスタースイッチ。サインアップを完全に無効にするには `false` に設定 |
**直感に反する部分**: `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true` は「company.io または全員を許可」という意味では**なく**、**company.io のみを許可**という意味です。許可リストの層は AND セマンティクスです — 完全な決定木は[サインインとサインアップの設定 → サインアップ許可リスト](/auth-setup#restricting-who-can-sign-up)にあります。
**招待フロー自体はサインアップ許可リストをチェックしません** — ただし、招待された人は招待を承諾する前に依然として**サインイン**できる必要があります。すでに Multica アカウントを持っている場合(例: 別のワークスペースから)、許可リストの影響を受けずに直接承諾できます。**一度もサインアップしたことがない場合**、サインインの最初のステップ(検証コードの要求)は依然として許可リストのチェックを通過し、`ALLOW_SIGNUP=false` や `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` によって拒否されたメールは**サインアップを完了できず、したがって招待を承諾できません**。
## ワークスペース作成をロックダウンする
`ALLOW_SIGNUP=false` は新しいアカウントをブロックしますが、すでにサインイン済みのユーザーが `POST /api/workspaces` 経由で別のワークスペースを作成することは**ブロックしません**。すべてのイシュー、リポジトリ、エージェントがプラットフォーム管理者に見えなければならないセルフホストインスタンスでは、そのギャップを塞ぐために `DISABLE_WORKSPACE_CREATION=true` を設定してください。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `DISABLE_WORKSPACE_CREATION` | `false` | `true` の場合、`POST /api/workspaces` へのすべての呼び出しが `403 workspace creation is disabled for this instance` を返します。Web UI は `/api/config` 経由ですべての「ワークスペース作成」要素を非表示にします。役割 / owner の例外はありません — このゲートはインスタンス単位で全体に適用されます |
推奨されるブートストラップ手順:
1. `DISABLE_WORKSPACE_CREATION` を設定しないまま(デフォルト)インスタンスを起動します。
2. 管理者としてサインインし、共有ワークスペースを作成します。
3. `DISABLE_WORKSPACE_CREATION=true` を設定してバックエンドを再起動します。この時点から、ユーザーは招待によってのみ参加できます。
招待されたユーザーが最初の検証コードでサインアップを完了できるよう `ALLOW_SIGNUP=true` を維持したい場合は、`DISABLE_WORKSPACE_CREATION=true` を `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS` と組み合わせて、どのアドレスがサインアップできるかの範囲を指定してください。`ALLOW_SIGNUP=false` を設定すると、保留中の招待対象者がアカウントを作成すること自体も追加でブロックされます — すべてのメンバーがすでに Multica アカウントを持っているインスタンスでのみ有用です。
## レート制限(任意の Redis
公開 auth エンドポイント — `/auth/send-code`、`/auth/verify-code`、`/auth/google` — の前段には、IP ごとの固定ウィンドウのレート制限があります。リミッターは Redis によって支えられています。`REDIS_URL` を設定しない場合、ミドルウェアは **no-op**fail-openになり、バックエンドは起動時に `rate limiting disabled: REDIS_URL not configured` をログに記録します。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `REDIS_URL` | 空 | Redis 接続 URL例: `redis://localhost:6379/0`)。設定しないと auth エンドポイントのレート制限が無効になります。同じ Redis はリアルタイムハブの fan-out、PAT キャッシュ、デーモントークンキャッシュでも使われます — 設定しない場合はすべてインメモリ / 直接 DB モードにフォールバックします |
| `RATE_LIMIT_AUTH` | `5` | `/auth/send-code` および `/auth/google` に対する IP あたり毎分の最大リクエスト数 |
| `RATE_LIMIT_AUTH_VERIFY` | `20` | `/auth/verify-code` に対する IP あたり毎分の最大リクエスト数 |
| `RATE_LIMIT_TRUSTED_PROXIES` | 空 | リミッターがその `X-Forwarded-For` ヘッダーを信頼することを許可する、カンマ区切りの CIDR。空デフォルトは **XFF を決して信頼しない**ことを意味します — リミッターは直接接続の `RemoteAddr` のみを使用します |
リクエストが制限を超えると、サーバーは `429 Too Many Requests`、`Retry-After: 60`、そして本文 `{"error":"too many requests"}` で応答します。
<Callout type="warning">
**リバースプロキシの背後では `RATE_LIMIT_TRUSTED_PROXIES` を必ず設定する必要があります。** そうしないと、バックエンドの観点ではすべての実際のユーザーがプロキシの IP を共有することになり、デプロイ全体が 1 つのバケットに入り、`/auth/send-code` がサイト全体で毎分 5 リクエストになってしまいます。一般的な値: 同一ホストの Caddy / Nginx には `127.0.0.1/32,::1/128`、Cloudflare / ALB / CloudFront には該当 CDN が公開している IP 範囲。`RemoteAddr` がこれらの CIDR のいずれかに含まれる IP のみが、`X-Forwarded-For` を使ってクライアントを識別できます。
</Callout>
この独立した `RATE_LIMIT_TRUSTED_PROXIES` は、オートパイロット webhook リミッター(`/api/webhooks/autopilots/{token}`)を制御する `MULTICA_TRUSTED_PROXIES` とは**異なります**。各リミッターは自身のリストをパースするため、プロキシの背後にあるデプロイは両方を設定すべきです。
## デーモンのチューニングパラメータ
デーモンはユーザーのローカルマシン上で実行され、その設定もローカル環境変数から読み込まれます。一般的なものは次のとおりです。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | サーバーアドレス(セルフホスト: 自身のドメインに置き換えてください) |
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | ハートビート間隔 |
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | タスクのポーリング間隔 |
| `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` | `20` | 最大同時タスク数 |
| `MULTICA_<PROVIDER>_PATH` | CLI 名に一致 | 各 AI コーディングツールの実行ファイルへのパス(例: `MULTICA_CLAUDE_PATH` |
| `MULTICA_<PROVIDER>_MODEL` | 空 | 各 AI コーディングツールのデフォルトモデル |
各パラメータがデーモンの動作にどう影響するかの完全な説明は、[デーモンとランタイム](/daemon-runtimes)を参照してください。
## フロントエンドのアクセス制御
| 変数 | デフォルト | 説明 |
|---|---|---|
| `FRONTEND_ORIGIN` | 空 | フロントエンドアドレス。招待メールのリンク、CORS 許可リスト、cookie ドメインはすべてこの値から派生します。設定しない場合、招待メールのリンクはホスト型ドメイン `https://app.multica.ai` にフォールバックします — セルフホストはこれを明示的に設定する必要があります |
| `CORS_ALLOWED_ORIGINS` | 空 | 追加で許可する CORS originカンマ区切り |
| `ALLOWED_ORIGINS` | 空 | WebSocket 専用の origin 許可リスト(カンマ区切り)。設定しない場合、フォールバック順序は `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174` です |
<Callout type="warning">
**`FRONTEND_ORIGIN` を設定しないと 2 つの静かな失敗が発生します**: (1) 招待メールのリンクが `https://app.multica.ai`(ホスト型ドメイン)を指し、クリックしてもユーザーがセルフホストインスタンスに戻ってこない。(2) WebSocket の Origin チェックが `localhost:3000 / 5173 / 5174` にフォールバックするため、プロダクションデプロイのすべての WebSocket 接続が拒否され、フロントエンドが「リアルタイム更新を受け取れない」ように見える。
</Callout>
## GitHub 連携
[GitHub PR ↔ イシュー連携](/github-integration)には 2 つの変数が必要です。設定で Connect GitHub を有効にし、受信 webhook を受け付けるには両方を設定してください。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `GITHUB_APP_SLUG` | 空 | GitHub App の slug`https://github.com/apps/<slug>` の末尾部分)。設定 → GitHub のインストールボタン URL を構成します |
| `GITHUB_WEBHOOK_SECRET` | 空 | GitHub App に設定した Webhook secret。すべての `pull_request` / `installation` delivery の HMAC-SHA256 検証に使われ、setup コールバックの state token の HMAC キーとしても使われます |
**どちらかが設定されていない場合の動作:**
- 設定 → GitHub の `Connect GitHub` が**無効**になり、admin に「not configured」というヒントを表示します。
- `/api/webhooks/github` エンドポイントは **`503 github webhooks not configured`** を返します — Multica はすべての署名を有効として扱うのではなく、secret なしではイベント処理を拒否します。
**注:** `GITHUB_WEBHOOK_SECRET` はインストールフローの state token の署名キーとして再利用されるため、運用者は secret を 1 つだけ管理すればよいです。これは GitHub App の *Client* secret では**ありません** — Client secret は OAuth 関連であり、この連携では使われません。完全な手順は [GitHub 連携 → セルフホストのセットアップ](/github-integration#self-host-setup)を参照してください。
## 使用量分析
デフォルトでは、サーバーは Multica の公式 PostHog インスタンスにレポートします。オプトアウトするには `ANALYTICS_DISABLED=true` を設定してください。
| 変数 | デフォルト | 説明 |
|---|---|---|
| `ANALYTICS_DISABLED` | `false` | バックエンド分析を完全に無効にするには `true` に設定 |
| `POSTHOG_API_KEY` | 組み込みのデフォルトキー | 自身の PostHog インスタンスを指す場合に設定 |
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog をセルフホストする場合は自身のホストに変更 |
## 次へ
- [サインインとサインアップの設定](/auth-setup) — 上記の auth 関連変数を実際にどう設定するか、そして落とし穴がどこにあるか
- [GitHub 連携](/github-integration) — `GITHUB_APP_SLUG` / `GITHUB_WEBHOOK_SECRET` を支える GitHub App をどうセットアップするか
- [トラブルシューティング](/troubleshooting) — よくある設定ミスの症状と対処法
- [デーモンとランタイム](/daemon-runtimes) — `MULTICA_DAEMON_*` パラメータが実際に何をするか

View File

@@ -0,0 +1,224 @@
---
title: 환경 변수
description: 자체 호스팅 Multica 서버를 실행하기 위한 환경 변수 전체 목록입니다.
---
import { Callout } from "fumadocs-ui/components/callout";
자체 호스팅 Multica [서버](/self-host-quickstart)는 시작 시 환경 변수에서 설정을 읽습니다 — 데이터베이스, 로그인, 이메일, 스토리지, 가입 허용 목록이 모두 여기에 있습니다. 이 페이지는 모든 변수를 용도별로 그룹화합니다: 각 섹션은 **설정하지 않으면 어떻게 되는지**, 그리고 **프로덕션에서 반드시 설정해야 하는 변수가 무엇인지**를 명확히 설명합니다. auth 관련 변수를 실제로 어떻게 설정하는지는 [로그인 및 가입 설정](/auth-setup)을 참고하세요.
## 핵심 서버 변수
배포 전에 반드시 고려해야 하는 핵심 변수입니다 — 일부는 서버를 시작할 수 있게 해주는 기본값을 가지고 있지만, 프로덕션에서는 필수 항목을 명시적으로 설정해야 합니다.
| 변수 | 기본값 | 프로덕션 필수? |
|---|---|---|
| `DATABASE_URL` | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` | **예** |
| `PORT` | `8080` | 아니오 (포트를 변경하는 경우 제외) |
| `JWT_SECRET` | `multica-dev-secret-change-in-production` | **예** (기본값은 안전하지 않음) |
| `APP_ENV` | 비어 있음 | **예** (`production`이어야 함) |
| `FRONTEND_ORIGIN` | 비어 있음 | **예** (자체 호스팅은 자체 도메인을 설정해야 함) |
| `MULTICA_DEV_VERIFICATION_CODE` | 비어 있음 | 아니오 (프로덕션에서는 반드시 비워 두어야 함) |
<Callout type="warning">
**프로덕션에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두세요.** 고정된 로컬 테스트 코드는 기본적으로 비활성화되어 있지만, `MULTICA_DEV_VERIFICATION_CODE=888888`로 활성화하면 `APP_ENV`가 production이 아닌 동안에는 코드를 요청할 수 있는 누구나 그 고정 값으로 로그인할 수 있습니다. 이 단축 코드는 `APP_ENV=production`일 때 무시됩니다.
</Callout>
### 데이터베이스 커넥션 풀
| 변수 | 기본값 | 설명 |
|---|---|---|
| `DATABASE_MAX_CONNS` | `25` | pgxpool 최대 연결 수. 데몬은 자주(3초마다) 폴링하며 연결을 사용하므로, 규모가 큰 배포에서는 더 높은 값이 필요할 수 있습니다 |
| `DATABASE_MIN_CONNS` | `5` | 최소 유휴 연결 수 |
**설정하지 않으면** 위 값이 사용됩니다 — 이전에 프로덕션에서 풀 고갈을 일으켰던 pgx 내장 기본값 4/NumCPU가 **아닙니다**.
## 이메일 설정
Multica는 두 가지 전송 백엔드를 지원합니다 — 클라우드 배포용 [Resend](https://resend.com/), 또는 내부 / 온프레미스 네트워크용 SMTP relay. 둘 다 설정된 경우 `SMTP_HOST`가 `RESEND_API_KEY`보다 우선합니다.
### Resend
| 변수 | 기본값 | 설명 |
|---|---|---|
| `RESEND_API_KEY` | 비어 있음 | Resend API key |
| `RESEND_FROM_EMAIL` | `noreply@multica.ai` | 발신 주소 (Resend 계정에서 검증된 도메인이어야 하며, SMTP를 사용할 때도 `From:` 헤더로 재사용됨) |
### SMTP relay
| 변수 | 기본값 | 설명 |
|---|---|---|
| `SMTP_HOST` | 비어 있음 | SMTP relay 호스트명. 이를 설정하면 SMTP 모드가 활성화되고 Resend를 덮어씁니다 |
| `SMTP_PORT` | `25` | SMTP 포트. STARTTLS 제출에는 `587`을 사용하세요; **포트 465(SMTPS / 암묵적 TLS)는 지원되지 않습니다** |
| `SMTP_USERNAME` | 비어 있음 | SMTP 사용자명. 인증 없는 relay의 경우 비워 두세요 |
| `SMTP_PASSWORD` | 비어 있음 | SMTP 비밀번호 |
| `SMTP_TLS_INSECURE` | `false` | TLS 인증서 검증을 건너뛰려면 `true`로 설정 (사설 CA / 자체 서명 인증서만 해당) |
서버가 STARTTLS를 알리면 자동으로 업그레이드됩니다. dial 타임아웃은 10초이고 전체 SMTP 세션에는 30초 데드라인이 있어, 블랙홀이 된 relay가 auth 핸들러를 멈추게 할 수 없습니다.
**둘 다 설정하지 않았을 때의 동작**: 서버는 오류를 내지 않지만, 전송되어야 했던 모든 이메일(인증 코드, 초대 링크)은 **서버의 stdout에만 기록됩니다**. 로컬 개발에는 편리합니다 — 서버 로그에서 코드를 복사해 사용하세요; **프로덕션에서는 이를 설정하는 것을 잊으면 조용한 블랙홀이 되어**, 사용자는 이메일을 전혀 받지 못하고 아무런 오류도 드러나지 않습니다.
## Google OAuth 설정
선택 사항입니다. 이메일 + 인증 코드만 사용하려면 설정하지 않은 채로 두고, 로그인 페이지에 "Sign in with Google"을 추가하려면 설정하세요.
| 변수 | 기본값 | 설명 |
|---|---|---|
| `GOOGLE_CLIENT_ID` | 비어 있음 | Google Cloud OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | 비어 있음 | Google Cloud OAuth secret |
| `GOOGLE_REDIRECT_URI` | `http://localhost:3000/auth/callback` | OAuth 콜백 URL (자체 호스팅: 자신의 프론트엔드 도메인으로 교체) |
**런타임에 적용됨**: 프론트엔드는 런타임에 `/api/config`를 통해 이 설정을 읽으므로, **변경해도 프론트엔드 리빌드나 재배포가 필요 없습니다** — 서버를 재시작하면 적용됩니다.
전체 설정(Google Cloud Console 단계 포함)은 [로그인 및 가입 설정](/auth-setup#google-oauth-configuration)에 있습니다.
## 파일 스토리지 설정
Multica는 사용자가 업로드한 첨부 파일(댓글의 이미지와 파일)을 저장합니다. **S3가 권장됩니다**; S3가 설정되어 있지 않으면 로컬 디스크로 폴백합니다.
### S3 / S3 호환 스토리지
| 변수 | 기본값 | 설명 |
|---|---|---|
| `S3_BUCKET` | 비어 있음 | **버킷 이름만** (예: `my-bucket`). `.s3.<region>.amazonaws.com` 접미사를 포함하지 **마세요** — 서버가 `S3_BUCKET` + `S3_REGION`으로 공개 호스트를 구성합니다. 이를 설정하면 S3 스토리지가 활성화됩니다 |
| `S3_REGION` | `us-west-2` | AWS 리전. 버킷의 실제 리전과 일치해야 합니다 — SDK 서명과 공개 URL 구성 모두에 사용됩니다 |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | 비어 있음 | 정적 자격 증명. 둘 다 설정하지 않으면 AWS SDK 기본 자격 증명 체인(IAM role / 환경 자격 증명)이 사용됩니다 |
| `AWS_ENDPOINT_URL` | 비어 있음 | 사용자 정의 S3 호환 엔드포인트 (예: [MinIO](https://min.io/)). 이를 설정하면 path-style URL로 전환됩니다 |
**`S3_BUCKET`을 설정하지 않으면**: 서버는 시작 시 `"S3_BUCKET not set, cloud upload disabled"`를 로깅하고, 모든 업로드는 로컬 디스크로 폴백합니다.
**공개 URL**은 다음 우선순위 순서로 구성됩니다:
1. `CLOUDFRONT_DOMAIN`이 설정된 경우 `https://<CLOUDFRONT_DOMAIN>/<key>`.
2. `AWS_ENDPOINT_URL`이 설정된 경우 `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>` (path-style).
3. `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>` (virtual-hosted-style). `S3_BUCKET`에 점이 포함된 경우, AWS가 발급한 와일드카드 TLS 인증서가 점이 포함된 버킷 호스트를 검증하지 못하므로 서버는 `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>` (path-style)로 폴백합니다.
### 로컬 디스크 (S3가 설정되지 않은 경우)
| 변수 | 기본값 | 설명 |
|---|---|---|
| `LOCAL_UPLOAD_DIR` | `./data/uploads` | 로컬 스토리지 디렉터리 |
| `LOCAL_UPLOAD_BASE_URL` | 비어 있음 (상대 경로 반환) | 공개 base URL — 설정하지 않으면 프론트엔드가 첨부 파일의 전체 URL을 확인할 수 없습니다 |
### CloudFront (선택)
S3 앞에 CloudFront를 두는 경우 세 가지 변수가 적용됩니다: `CLOUDFRONT_DOMAIN`, `CLOUDFRONT_KEY_PAIR_ID`, `CLOUDFRONT_PRIVATE_KEY` (또는 Secrets Manager에서 읽으려면 `CLOUDFRONT_PRIVATE_KEY_SECRET`). CloudFront를 사용하지 않으면 건너뛰세요 — S3 설정과 충돌하지 않습니다.
### Cookie 도메인
| 변수 | 기본값 | 설명 |
|---|---|---|
| `COOKIE_DOMAIN` | 비어 있음 | 세션 cookie의 범위 |
- **비어 있음**: cookie는 방문한 정확한 호스트에서만 유효합니다 (단일 호스트 배포에 적합)
- **`.example.com`으로 설정**: cookie가 하위 도메인 간에 공유됩니다 (그래서 `app.example.com`과 `api.example.com`이 로그인 세션을 공유함)
- 경고: IP 주소일 수 없습니다 (브라우저가 무시함)
## 누가 가입할 수 있는지 제한하기
세 개의 허용 목록 계층이 우선순위에 따라 결합됩니다. **어느 한 계층이라도 비어 있지 않은 값으로 설정되면, 일치하지 않는 이메일은 거부됩니다** — `ALLOW_SIGNUP=true`조차 이를 무시할 수 없습니다.
| 변수 | 기본값 | 설명 |
|---|---|---|
| `ALLOWED_EMAILS` | 비어 있음 | 명시적 이메일 허용 목록 (쉼표로 구분). 비어 있지 않으면 목록에 있는 이메일만 가입할 수 있습니다 |
| `ALLOWED_EMAIL_DOMAINS` | 비어 있음 | 도메인 허용 목록 (쉼표로 구분). 비어 있지 않으면 목록에 있는 도메인만 가입할 수 있습니다 |
| `ALLOW_SIGNUP` | `true` | 가입 마스터 스위치. 가입을 완전히 비활성화하려면 `false`로 설정 |
**직관에 반하는 부분**: `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true`는 "company.io 또는 모두를 허용"한다는 뜻이 **아니라** **company.io만 허용**한다는 뜻입니다. 허용 목록 계층은 AND 시맨틱입니다 — 전체 결정 트리는 [로그인 및 가입 설정 → 가입 허용 목록](/auth-setup#restricting-who-can-sign-up)에 있습니다.
**초대 흐름 자체는 가입 허용 목록을 확인하지 않습니다** — 하지만 초대받은 사람은 초대를 수락하기 전에 여전히 **로그인**할 수 있어야 합니다. 이미 Multica 계정이 있다면(예: 다른 워크스페이스에서) 허용 목록의 영향을 받지 않고 바로 수락할 수 있습니다; **한 번도 가입한 적이 없다면**, 로그인의 첫 단계(인증 코드 요청)는 여전히 허용 목록 확인을 거치며, `ALLOW_SIGNUP=false`나 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS`에 의해 거부된 이메일은 **가입을 완료할 수 없고, 따라서 초대를 수락할 수 없습니다**.
## 워크스페이스 생성 잠그기
`ALLOW_SIGNUP=false`는 새 계정을 차단하지만, 이미 로그인한 사용자가 `POST /api/workspaces`를 통해 또 다른 워크스페이스를 생성하는 것은 **차단하지 않습니다**. 모든 이슈, 저장소, 에이전트가 플랫폼 관리자에게 보여야 하는 자체 호스팅 인스턴스에서는, 그 틈을 막기 위해 `DISABLE_WORKSPACE_CREATION=true`로 설정하세요.
| 변수 | 기본값 | 설명 |
|---|---|---|
| `DISABLE_WORKSPACE_CREATION` | `false` | `true`이면 `POST /api/workspaces`에 대한 모든 호출이 `403 workspace creation is disabled for this instance`를 반환합니다. 웹 UI는 `/api/config`를 통해 모든 "워크스페이스 생성" 요소를 숨깁니다. 역할/owner 예외는 없습니다 — 이 게이트는 인스턴스 단위로 전역 적용됩니다 |
권장 부트스트랩 순서:
1. `DISABLE_WORKSPACE_CREATION`을 설정하지 않은 채로(기본값) 인스턴스를 시작합니다.
2. 관리자로 로그인하여 공유 워크스페이스를 생성합니다.
3. `DISABLE_WORKSPACE_CREATION=true`로 설정하고 백엔드를 재시작합니다. 이 시점부터 사용자는 초대를 통해서만 참여할 수 있습니다.
초대받은 사용자가 첫 인증 코드로 가입을 완료할 수 있도록 `ALLOW_SIGNUP=true`를 유지하고 싶다면, `DISABLE_WORKSPACE_CREATION=true`를 `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS`와 결합하여 어떤 주소가 가입할 수 있는지 범위를 지정하세요. `ALLOW_SIGNUP=false`로 설정하면 대기 중인 초대 대상자가 계정을 만드는 것조차 추가로 차단됩니다 — 모든 멤버가 이미 Multica 계정을 가지고 있는 인스턴스에서만 유용합니다.
## 속도 제한 (선택적 Redis)
공개 auth 엔드포인트 — `/auth/send-code`, `/auth/verify-code`, `/auth/google` — 앞에는 IP별 고정 윈도우 속도 제한이 있습니다. 리미터는 Redis로 뒷받침됩니다. `REDIS_URL`을 설정하지 않으면 미들웨어는 **no-op**(fail-open)이 되고, 백엔드는 시작 시 `rate limiting disabled: REDIS_URL not configured`를 로깅합니다.
| 변수 | 기본값 | 설명 |
|---|---|---|
| `REDIS_URL` | 비어 있음 | Redis 연결 URL (예: `redis://localhost:6379/0`). 설정하지 않으면 auth 엔드포인트의 속도 제한이 비활성화됩니다. 동일한 Redis는 실시간 허브 fan-out, PAT 캐시, 데몬 토큰 캐시에서도 사용됩니다 — 설정하지 않으면 모두 인메모리 / 직접 DB 모드로 폴백합니다 |
| `RATE_LIMIT_AUTH` | `5` | `/auth/send-code` 및 `/auth/google`에 대한 IP당 분당 최대 요청 수 |
| `RATE_LIMIT_AUTH_VERIFY` | `20` | `/auth/verify-code`에 대한 IP당 분당 최대 요청 수 |
| `RATE_LIMIT_TRUSTED_PROXIES` | 비어 있음 | 리미터가 그 `X-Forwarded-For` 헤더를 신뢰하도록 허용하는, 쉼표로 구분된 CIDR. 비어 있음(기본값)은 **XFF를 절대 신뢰하지 않음**을 의미합니다 — 리미터는 직접 연결의 `RemoteAddr`만 사용합니다 |
요청이 제한을 초과하면 서버는 `429 Too Many Requests`, `Retry-After: 60`, 그리고 본문 `{"error":"too many requests"}`로 응답합니다.
<Callout type="warning">
**리버스 프록시 뒤에서는 `RATE_LIMIT_TRUSTED_PROXIES`를 반드시 설정해야 합니다.** 그렇지 않으면 백엔드 관점에서 모든 실제 사용자가 프록시의 IP를 공유하게 되어, 배포 전체가 하나의 버킷에 들어가고, `/auth/send-code`가 사이트 전체에 대해 분당 5회가 됩니다. 일반적인 값: 같은 호스트의 Caddy / Nginx에는 `127.0.0.1/32,::1/128`; Cloudflare / ALB / CloudFront에는 해당 CDN의 공개된 IP 범위. `RemoteAddr`이 이 CIDR 중 하나에 속하는 IP만 `X-Forwarded-For`를 사용해 클라이언트를 식별할 수 있습니다.
</Callout>
이 별도의 `RATE_LIMIT_TRUSTED_PROXIES`는 오토파일럿 webhook 리미터(`/api/webhooks/autopilots/{token}`)를 제어하는 `MULTICA_TRUSTED_PROXIES`와는 **다릅니다**. 각 리미터는 자체 목록을 파싱하므로, 프록시 뒤에 있는 배포는 둘 다 설정해야 합니다.
## 데몬 튜닝 파라미터
데몬은 사용자의 로컬 기기에서 실행되며, 그 설정도 로컬 환경 변수에서 읽습니다. 일반적으로 사용되는 것들:
| 변수 | 기본값 | 설명 |
|---|---|---|
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | 서버 주소 (자체 호스팅: 자신의 도메인으로 교체) |
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | 하트비트 간격 |
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | 작업 폴링 간격 |
| `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` | `20` | 최대 동시 작업 수 |
| `MULTICA_<PROVIDER>_PATH` | CLI 이름과 일치 | 각 AI 코딩 도구 실행 파일의 경로 (예: `MULTICA_CLAUDE_PATH`) |
| `MULTICA_<PROVIDER>_MODEL` | 비어 있음 | 각 AI 코딩 도구의 기본 모델 |
각 파라미터가 데몬 동작에 어떻게 영향을 미치는지에 대한 전체 설명은 [데몬과 런타임](/daemon-runtimes)을 참고하세요.
## 프론트엔드 액세스 제어
| 변수 | 기본값 | 설명 |
|---|---|---|
| `FRONTEND_ORIGIN` | 비어 있음 | 프론트엔드 주소. 초대 이메일 링크, CORS 허용 목록, cookie 도메인이 모두 이 값에서 파생됩니다. 설정하지 않으면 초대 이메일 링크는 호스팅 도메인 `https://app.multica.ai`로 폴백합니다 — 자체 호스팅은 이를 명시적으로 설정해야 합니다 |
| `CORS_ALLOWED_ORIGINS` | 비어 있음 | 추가로 허용할 CORS origin (쉼표로 구분) |
| `ALLOWED_ORIGINS` | 비어 있음 | WebSocket 전용 origin 허용 목록 (쉼표로 구분); 설정하지 않으면 폴백 순서는 `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174`입니다 |
<Callout type="warning">
**`FRONTEND_ORIGIN`을 설정하지 않으면 두 가지 조용한 실패가 발생합니다**: (1) 초대 이메일 링크가 `https://app.multica.ai`(호스팅 도메인)를 가리켜, 클릭해도 사용자가 자체 호스팅 인스턴스로 돌아오지 않습니다; (2) WebSocket Origin 검사가 `localhost:3000 / 5173 / 5174`로 폴백하여, 프로덕션 배포의 모든 WebSocket 연결이 거부되고 프론트엔드가 "실시간 업데이트를 받지 못하는" 것처럼 보입니다.
</Callout>
## GitHub 연동
[GitHub PR ↔ 이슈 연동](/github-integration)에는 두 개의 변수가 필요합니다. 설정에서 Connect GitHub를 활성화하고 들어오는 webhook을 수락하려면 둘 다 설정하세요.
| 변수 | 기본값 | 설명 |
|---|---|---|
| `GITHUB_APP_SLUG` | 비어 있음 | GitHub App의 slug (`https://github.com/apps/<slug>`의 끝부분). 설정 → GitHub 설치 버튼 URL을 구성합니다 |
| `GITHUB_WEBHOOK_SECRET` | 비어 있음 | GitHub App에 설정한 Webhook secret. 모든 `pull_request` / `installation` delivery의 HMAC-SHA256 검증에 사용되며, setup 콜백 state token의 HMAC 키로도 사용됩니다 |
**둘 중 하나라도 설정하지 않았을 때의 동작:**
- 설정 → GitHub의 `Connect GitHub`가 **비활성화**되고 admin에게 "not configured" 힌트를 표시합니다.
- `/api/webhooks/github` 엔드포인트는 **`503 github webhooks not configured`**를 반환합니다 — Multica는 모든 서명을 유효한 것으로 취급하기보다, secret 없이는 이벤트 처리를 거부합니다.
**참고:** `GITHUB_WEBHOOK_SECRET`은 설치 흐름 state token의 서명 키로 재사용되므로, 운영자는 secret 하나만 관리하면 됩니다. 이것은 GitHub App의 *Client* secret이 **아닙니다** — Client secret은 OAuth 관련이며 이 연동에서는 사용되지 않습니다. 전체 안내는 [GitHub 연동 → 자체 호스팅 설정](/github-integration#self-host-setup)을 참고하세요.
## 사용량 분석
기본적으로 서버는 Multica의 공식 PostHog 인스턴스로 보고합니다. 옵트아웃하려면 `ANALYTICS_DISABLED=true`로 설정하세요.
| 변수 | 기본값 | 설명 |
|---|---|---|
| `ANALYTICS_DISABLED` | `false` | 백엔드 분석을 완전히 비활성화하려면 `true`로 설정 |
| `POSTHOG_API_KEY` | 내장 기본 키 | 자신의 PostHog 인스턴스를 가리킬 때 설정 |
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog를 자체 호스팅하는 경우 자신의 호스트로 변경 |
## 다음
- [로그인 및 가입 설정](/auth-setup) — 위의 auth 관련 변수를 실제로 어떻게 설정하는지, 그리고 함정이 어디에 있는지
- [GitHub 연동](/github-integration) — `GITHUB_APP_SLUG` / `GITHUB_WEBHOOK_SECRET`을 뒷받침하는 GitHub App을 어떻게 설정하는지
- [문제 해결](/troubleshooting) — 흔한 설정 오류의 증상과 해결책
- [데몬과 런타임](/daemon-runtimes) — `MULTICA_DAEMON_*` 파라미터가 실제로 하는 일

View File

@@ -49,9 +49,10 @@ Multica supports two delivery backends — [Resend](https://resend.com/) for clo
| Variable | Default | Description |
|---|---|---|
| `SMTP_HOST` | empty | SMTP relay hostname. Setting this activates SMTP mode and overrides Resend |
| `SMTP_PORT` | `25` | SMTP port. Use `587` for STARTTLS submission; **port 465 (SMTPS / implicit TLS) is not supported** |
| `SMTP_PORT` | `25` | SMTP port. Use `587` for STARTTLS submission, or `465` for SMTPS (implicit TLS, auto-enabled) |
| `SMTP_USERNAME` | empty | SMTP username. Leave empty for unauthenticated relay |
| `SMTP_PASSWORD` | empty | SMTP password |
| `SMTP_TLS` | `starttls` | TLS mode. `implicit` (aliases `smtps`, `ssl`) forces an immediate TLS handshake on connect (SMTPS); port `465` auto-enables it. Unset / `starttls` upgrades via STARTTLS after connect |
| `SMTP_TLS_INSECURE` | `false` | Set `true` to skip TLS certificate verification (private CA / self-signed only) |
STARTTLS is upgraded automatically when the server advertises it. The dial timeout is 10s and the whole SMTP session has a 30s deadline, so a black-holed relay can't hang the auth handler.
@@ -128,6 +129,22 @@ Three allowlist layers combine by priority. **If any layer is set to a non-empty
**Invite flows themselves do not check the signup allowlist** — but the invitee must still be able to **sign in** before accepting the invite. If they already have a Multica account (for example from another workspace), they can accept directly, unaffected by the allowlist; **if they have never signed up**, the first step of sign-in (requesting a verification code) still passes through the allowlist check, and an email rejected by `ALLOW_SIGNUP=false` or by `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` **cannot finish signup, and therefore cannot accept the invite**.
## Locking down workspace creation
`ALLOW_SIGNUP=false` blocks new accounts, but it does **not** block an already-signed-in user from creating another workspace via `POST /api/workspaces`. On a self-hosted instance where every issue, repo, and agent must be visible to the platform admin, set `DISABLE_WORKSPACE_CREATION=true` to close that gap.
| Variable | Default | Description |
|---|---|---|
| `DISABLE_WORKSPACE_CREATION` | `false` | When `true`, every call to `POST /api/workspaces` returns `403 workspace creation is disabled for this instance`. The web UI hides every "Create workspace" affordance via `/api/config`. There is no role/owner exception — the gate is global per instance |
Recommended bootstrap sequence:
1. Start the instance with `DISABLE_WORKSPACE_CREATION` unset (the default).
2. Sign in as the admin and create the shared workspace.
3. Set `DISABLE_WORKSPACE_CREATION=true` and restart the backend. From this point on, users join via invitation only.
If you also want to keep `ALLOW_SIGNUP=true` so invited users can finish signup with their first verification code, combine `DISABLE_WORKSPACE_CREATION=true` with `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS` to scope which addresses can sign up. Setting `ALLOW_SIGNUP=false` will additionally block pending invitees from creating their account at all — useful only on instances where every member already has a Multica account.
## Rate limiting (optional Redis)
Public auth endpoints — `/auth/send-code`, `/auth/verify-code`, `/auth/google` — have per-IP fixed-window rate limiting in front of them. The limiter is backed by Redis. When `REDIS_URL` is unset the middleware is a **no-op** (fail-open) and the backend logs `rate limiting disabled: REDIS_URL not configured` at startup.
@@ -167,6 +184,8 @@ For a full explanation of how each parameter affects daemon behavior, see [Daemo
| Variable | Default | Description |
|---|---|---|
| `FRONTEND_ORIGIN` | empty | Frontend address. Invite email links, the CORS allowlist, and the cookie domain are all derived from this. When unset, invite email links fall back to the hosted domain `https://app.multica.ai` — self-host must set this explicitly |
| `MULTICA_APP_URL` | empty | Frontend URL for CLI login flow. Also used by the web UI to show self-host daemon setup commands with your app domain; for same-origin deployments this is also used as daemon `server_url` when `MULTICA_PUBLIC_URL` is unset |
| `MULTICA_PUBLIC_URL` | empty | Public API URL, without trailing slash. Used for autopilot webhook URLs and by the web UI as the daemon `server_url` |
| `CORS_ALLOWED_ORIGINS` | empty | Additional allowed CORS origins (comma-separated) |
| `ALLOWED_ORIGINS` | empty | WebSocket-specific origin allowlist (comma-separated); when unset, fallback order is `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174` |

View File

@@ -49,9 +49,10 @@ Multica 支持两种邮件发送通道——[Resend](https://resend.com/) 适合
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `SMTP_HOST` | 空 | SMTP relay 主机名。设置后即启用 SMTP 模式并覆盖 Resend |
| `SMTP_PORT` | `25` | SMTP 端口。STARTTLS 提交端口用 `587`**暂不支持 465SMTPS / 隐式 TLS** |
| `SMTP_PORT` | `25` | SMTP 端口。STARTTLS 提交端口用 `587`SMTPS隐式 TLS,自动启用)用 `465` |
| `SMTP_USERNAME` | 空 | SMTP 用户名。留空表示未认证 relay |
| `SMTP_PASSWORD` | 空 | SMTP 密码 |
| `SMTP_TLS` | `starttls` | TLS 模式。`implicit`(别名 `smtps`、`ssl`)在连接时立即进行 TLS 握手SMTPS`465` 端口会自动启用。未设置 / `starttls` 则在连接后通过 STARTTLS 升级 |
| `SMTP_TLS_INSECURE` | `false` | 设为 `true` 跳过 TLS 证书校验(仅限私有 CA / 自签证书)|
服务端 advertise STARTTLS 时会自动升级。dial 超时 10s整个 SMTP 会话有 30s deadline避免 relay 黑洞把 auth handler 挂死。
@@ -128,6 +129,22 @@ Multica 存储用户上传的附件(评论里的图片、文件等)。**优
**邀请流程本身不检查 signup 白名单**——但被邀请人必须先能**登录**才能接受邀请。如果对方已经有 Multica 账号(比如在其他工作区注册过),可以直接接受,不受白名单影响;**如果对方还没注册过**,他们登录的第一步(发送验证码)仍然会过白名单检查,被 `ALLOW_SIGNUP=false` 或 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` 拒绝的邮箱**无法完成注册,也就没法接受邀请**。
## 锁死工作区创建
`ALLOW_SIGNUP=false` 能挡住新注册,但**挡不住**已经登录的用户继续 `POST /api/workspaces` 自助开新工作区。在希望平台管理员能看到全部 issue / 仓库 / 智能体的自部署实例里,把 `DISABLE_WORKSPACE_CREATION=true` 打开以堵上这个口子。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `DISABLE_WORKSPACE_CREATION` | `false` | 设为 `true` 时,对 `POST /api/workspaces` 的任何调用都返回 `403 workspace creation is disabled for this instance`。Web UI 通过 `/api/config` 隐藏所有「创建工作区」入口。该开关是实例级的,没有 owner / admin 例外 |
推荐的开服流程:
1. 启动实例时保持 `DISABLE_WORKSPACE_CREATION` 未设置(默认)。
2. 管理员登录并创建共享工作区。
3. 把 `DISABLE_WORKSPACE_CREATION=true` 设上并重启 backend。之后新用户只能通过邀请加入。
如果还想让被邀请的新用户能完成首次注册,保留 `ALLOW_SIGNUP=true`(必要时用 `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS` 限定可注册邮箱),只把 `DISABLE_WORKSPACE_CREATION=true` 打开即可;如果同时设 `ALLOW_SIGNUP=false`,连有 pending invite 的邮箱都无法完成首次注册,仅适合所有成员都已有 Multica 账号的实例。
## 速率限制(可选 Redis
公开认证端点——`/auth/send-code`、`/auth/verify-code`、`/auth/google`——前面挂了按 IP 的固定窗口限流。限流器后端是 Redis。`REDIS_URL` 不设时中间件**直通**fail-open后端启动会打日志 `rate limiting disabled: REDIS_URL not configured`。
@@ -167,6 +184,8 @@ Multica 存储用户上传的附件(评论里的图片、文件等)。**优
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `FRONTEND_ORIGIN` | 空 | 前端地址。邀请邮件里的链接、CORS 白名单、cookie domain 都从这里推导。邮件链接在不设时会 fallback 到托管版域名 `https://app.multica.ai`——self-host 必须显式填 |
| `MULTICA_APP_URL` | 空 | CLI 登录流程使用的前端 URL。Web UI 也会用它显示带你自己 app domain 的 self-host 守护进程 setup 命令;同源部署中,如果 `MULTICA_PUBLIC_URL` 未设置,它也会作为守护进程的 `server_url` |
| `MULTICA_PUBLIC_URL` | 空 | 公开 API URL不带结尾 slash。用于 autopilot webhook URL也会被 Web UI 用作守护进程的 `server_url` |
| `CORS_ALLOWED_ORIGINS` | 空 | 额外的 CORS 允许来源(逗号分隔)|
| `ALLOWED_ORIGINS` | 空 | WebSocket 专用的 origin 白名单(逗号分隔);不设就按 `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174` 顺序回落 |

View File

@@ -0,0 +1,183 @@
---
title: GitHub 連携
description: GitHub App を一度連携すれば、ブランチ・タイトル・本文にイシュー識別子を含む PR が該当イシューに自動で紐づきます。そして PR をマージするとイシューが Done に移動します。
---
import { Callout } from "fumadocs-ui/components/callout";
**設定 → GitHub** で GitHub アカウントまたは組織を一度だけ連携してください。その後は、ブランチ名・タイトル・本文にイシュー識別子(例: `MUL-123`)を含むあらゆる pull request が該当する[イシュー](/issues)に**自動で紐づき**、イシューサイドバーの **Pull requests** に表示され、PR がマージされるとイシューが **Done** に移動します。
イシューごとの設定はありません。フロー全体が識別子で駆動されます。
## 連携が行うこと
| 場所 | 動作 |
|---|---|
| **設定 → GitHub** | ワークスペースの admin には、マスタートグル、**Connect GitHub** ボタン、機能スイッチPR サイドバー、Co-authored-by、自動紐づけを備えた GitHub タブが表示されます。インストール後は GitHub タブに戻ります。 |
| **イシューサイドバー → Pull requests** | このイシューに自動で紐づいたすべての PR が、タイトル、リポジトリ、状態(`Open` / `Draft` / `Merged` / `Closed`)、作成者とともに表示されます。行をクリックすると GitHub の該当 PR に移動します。 |
| **Webhookバックグラウンド** | すべての `pull_request` イベントで、Multica は PR 行を upsert し、PR からイシュー識別子をスキャンして、紐づけ行を(再)構築します。冪等性があり、同じ delivery を再送しても変化はありません。 |
| **マージ時のステータス自動変更** | PR が `merged` に遷移すると、まだ `Done` でも `Cancelled` でもない、紐づいたすべてのイシューが `Done` に移動します。ステータス変更は source `github_pr_merged` でタイムラインに記録されます。 |
ミラーリングされるのは PR 自体のみです。コミット、オープンな PR のないブランチ ref、CI チェックの状態はモデル化され**ません**。この連携は意図的に狭く設計されています。
## 識別子のマッチング方法
Webhook は次の順序で 3 つのフィールドから識別子を抽出します: **PR head ブランチ**、**PR タイトル**、**PR 本文**。マッチャーは次のとおりです。
- 大文字小文字を区別しません — `mul-123`、`MUL-123`、`Mul-123` はすべてマッチします。
- 境界があります — 左側の `\b` と右側の数字アンカーにより、`v1.2-3` のようなバージョン番号やメール形式の文字列を誤って拾わないようにしています。
- ワークスペーススコープに限定されます — そのワークスペース固有の[イシュー prefix](/workspaces)にのみマッチします。prefix が `MUL` のワークスペースでは、整数が別のイシューと一致しても `FOO-1` は無視されます。
- 重複が除去されます — 本文に `MUL-1, MUL-1` と並べても、イシューは一度だけ紐づきます。
1 つの PR で**複数のイシュー**を参照できます。`Closes MUL-1, MUL-2` は PR を両方のイシューに紐づけ、マージすると両方が `Done` に進みます。
## マージ時の Done 自動変更ルール
PR の `merged` フィールドが `true` に切り替わると、紐づいたすべてのイシューが評価されます。
| イシューの現在のステータス | 結果 |
|---|---|
| `done` | 変化なし(すでに終了状態)。 |
| `cancelled` | **変化なし** — cancelled はユーザーが作業を明示的に放棄したことを意味するため、連携はこのシグナルを上書きしません。 |
| それ以外すべて(`todo`、`in_progress`、`in_review`、`blocked`、`backlog` | `done` に移動。 |
PR をマージ**せずに**クローズした場合は、PR カードの状態が `Closed` に更新されるだけです。紐づいたイシューはそのまま維持されます — マージせずにクローズすることが何を意味するかはユーザーが決めるからです。
<Callout type="info">
この動作はタイムライン上で `system` アクターに帰属します。イシューの購読者は、人がステータスを移動したときと同じように、ステータス変更に関するインボックス通知を受け取ります。
</Callout>
## 自動で紐づかないもの
- **コミットメッセージ内の識別子** — ブランチ / タイトル / 本文のみがスキャンされます。`MUL-123: fix login` というタイトルのコミットは、同じ文字列が PR タイトルや本文にも現れない限り自動では紐づきません。
- **PR コメント内の識別子** — PR 自体のメタデータのみがスキャンされ、後から付いた GitHub コメントは無視されます。
- **App がインストールされていないリポジトリの PR** — App がなければ、Multica は webhook をまったく受け取りません。
- **PR をイシューに手動で紐づける** — まだこのための UI はありません。チームの慣習で識別子を Multica が読まない場所に置いている場合は、PR タイトルや本文に追加してください。
## 連携解除
**設定 → GitHub** にはインストール一覧はありません — 既存のインストールは GitHub から直接管理します。
- **GitHub から** — `https://github.com/settings/installations`(個人)または `https://github.com/organizations/<org>/settings/installations`(組織)で Multica GitHub App をアンインストールします。Multica は `installation.deleted` webhook を受け取ってリアルタイムで行を削除し、開いている Settings タブはリロードなしで更新されます。
- **Multica 内部からの連携解除は admin 専用です** — GitHub タブの連携解除コントロールは、admin 以外のユーザーには非表示です。マスター GitHub スイッチがオフでも利用可能なままなので、admin はワンクリックで機能を無効化した後でも、古いインストールを取り消せます。
連携解除後も、ミラーリングされた PR 行はデータベースに残り、過去のイシューサイドバーで何が紐づいていたかを引き続き表示しますが、そのインストールから新たに入ってくる webhook イベントは受理されなくなります。
## 権限と可視性
- **連携 / 連携解除**にはワークスペースの **owner または admin** が必要です。member にはカードの説明は見えますが、Connect ボタンは見えません。
- イシューの **Pull requests** サイドバーは、そのイシューを閲覧できる誰にでも表示されます — イシュー詳細の他の部分と同じ権限です。
- GitHub App は pull request とメタデータへの**読み取り専用**アクセスを要求します。Multica はコミット、コメント、ステータスチェックを GitHub に書き戻すことはありません。
## セルフホストのセットアップ
Multica Cloud で Multica を実行している場合、連携はすでに構成済みです — このセクションは飛ばしてください。
セルフホストの場合は、GitHub App を 1 つ作成し、サーバーを指すように設定し、環境変数を 2 つ設定します。フロー全体は以下のとおりです。
### 1. GitHub App を作成する
次のいずれかにアクセスしてください。
- 個人アカウント → `https://github.com/settings/apps/new`
- 組織 → `https://github.com/organizations/<org>/settings/apps/new`
次を入力します。
| フィールド | 値 |
|---|---|
| **GitHub App name** | 見分けやすい名前、例: `Multica` または `Multica (staging)`。 |
| **Homepage URL** | Multica フロントエンド、例: `https://multica.example.com`。 |
| **Callback URL** | 空欄のままにしてください — Multica は OAuth ユーザー ID を使用しません。 |
| **Setup URL** | `https://<api-host>/api/github/setup`。**「Redirect on update」をチェックしてください。** |
| **Webhook → Active** | 有効。 |
| **Webhook URL** | `https://<api-host>/api/webhooks/github`。 |
| **Webhook secret** | 長いランダム文字列を生成してください(例: `openssl rand -hex 32`)。手順 2 で同じ値を Multica の env に貼り付けます。 |
| **Permissions → Repository → Pull requests** | **Read-only**。 |
| **Permissions → Repository → Metadata** | Read-only必須。 |
| **Subscribe to events** | **Pull request** をチェックしてください。 |
| **Where can this GitHub App be installed?** | お好みで。単一組織のセットアップなら `Only on this account` で十分です。 |
**Create GitHub App** の後、App の詳細ページから 2 つのことを控えておいてください。
- 上部の **public link** — その末尾が slug です。`https://github.com/apps/multica-acme` → slug = `multica-acme`。
- 先ほど生成した **webhook secret**(後で GitHub から読み戻すことはできません — 今すぐ保存してください)。
<Callout type="warning">
**Webhook secret ≠ Client secret。** App 設定ページには両方のフィールドが並んで配置されています。**Webhook secret** は `pull_request` の payload に署名する値で、Multica が必要とするものです。**Client secret** は OAuth 用で、この連携では使用しません。この 2 つを混同すると、すべての webhook delivery で紛らわしい `401 invalid signature` が発生します。
</Callout>
### 2. 環境変数を設定する
API サーバーで:
```dotenv
GITHUB_APP_SLUG=multica-acme
GITHUB_WEBHOOK_SECRET=<the webhook secret you generated>
```
両方の変数が必須です。どちらかが欠けていると:
- Settings の `Connect GitHub` が**無効**になり、「not configured」のヒントが表示されます。
- `/api/webhooks/github` エンドポイントが **`503 github webhooks not configured`** を返します — Multica は secret なしでイベントを処理することを拒否し、すべての署名を黙って有効として扱うことはありません。
`FRONTEND_ORIGIN` も設定されている必要がありますどのプロダクションのセルフホストでもすでに設定されています。インストール後、setup コールバックがユーザーを `<FRONTEND_ORIGIN>/settings?tab=github` に戻します。
env 変数を設定した後は API を再起動してください。
### 3. マイグレーションを実行する
この連携はテーブルをマイグレーション `079_github_integration` で提供します。古いデプロイをアップグレードする場合:
```bash
make migrate-up
```
3 つのテーブルが作成されます: `github_installation`、`github_pull_request`、`issue_pull_request`。これらはワークスペースとともに cascade-delete されるため、ワークスペースを削除すると自動的にクリーンアップされます。
### 4. UI から連携する
Multica で:
1. owner または admin 権限で **設定 → GitHub** を開きます。
2. **Connect GitHub** をクリックします。GitHub が新しいタブで開きます。
3. アクセスを付与するリポジトリを選び、**Install** します。
4. GitHub が `<api-host>/api/github/setup` にリダイレクトしてインストールを記録し、`<FRONTEND_ORIGIN>/settings?tab=github&github_connected=1` に戻します。
その後、ブランチ / タイトル / 本文にイシュー識別子を含む PR を開いてみてください — 数秒以内に、そのイシューの詳細ページに Pull requests ブロックが表示されます。
### 5. curl プローブで検証する
インストール後に GitHub の **Recent Deliveries** ページで `401 invalid signature` が報告される場合、両側の secret が異なっています。どちらが間違っているかを最も速く突き止める方法は、GitHub を迂回することです。
```bash
SECRET="<the value you put in GITHUB_WEBHOOK_SECRET>"
BODY='{"zen":"test"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -i -X POST https://<api-host>/api/webhooks/github \
-H "X-Hub-Signature-256: sha256=$SIG" \
-H "X-GitHub-Event: ping" \
-H "Content-Type: application/json" \
-d "$BODY"
```
| HTTP ステータス | 意味 | 解決方法 |
|---|---|---|
| `200` `{"ok":"pong"}` | サーバーがロードした secret が `$SECRET` と一致します。不一致は GitHub 側にあります。 | App → Webhook secret を編集 → **同じ値を貼り付け** → **Save changes**(保存せずにフィールドの外をクリックすると古い secret が維持されます)。再送してください。 |
| `401 invalid signature` | サーバーがロードした secret が思っている値で**ありません**。 | env 変数が実行中のプロセスに反映されたか確認してください(例: `kubectl exec` → `echo -n "$GITHUB_WEBHOOK_SECRET" \| wc -c`)。再デプロイしてください。 |
| `503 github webhooks not configured` | プロセスで `GITHUB_WEBHOOK_SECRET` が空です。 | env 変数を設定し、API を再起動してください。 |
## 制限事項
現時点で知っておくべき、いくつかの粗い部分があります。
- **まだ手動の紐づけ UI はありません** — PR を紐づける唯一の方法は、ブランチ、タイトル、本文に識別子を置くことです。
- **CI / チェック状態はありません** — PR 自体のみがミラーリングされます。ビルド状態、レビューコメント、レビュアーは Multica には表示されません。
- マージ → Done ルールに対する**ワークスペースレベルの設定はありません** — 固定のデフォルトです(`cancelled` でない限り `merged → done`)。ワークスペースでカスタマイズできるマッピングは将来の追加予定です。
- **1 つのイシューに複数の PR が紐づく場合、マージは保守的です** — 2 つの PR がどちらも `MUL-123` を参照していて最初の 1 つがマージされると、イシューはただちに `Done` に移動します。進める前に紐づいたすべての PR が解決されるのを待つ後続の変更が進行中です。
## 次に
- [イシュー](/issues) — PR から参照されるイシュー識別子(`MUL-123`
- [ワークスペース](/workspaces) — ワークスペース固有のイシュー prefix を設定する場所
- [環境変数](/environment-variables) — 上記の GitHub 変数を含む env の完全なリファレンス

View File

@@ -0,0 +1,183 @@
---
title: GitHub 연동
description: GitHub App을 한 번만 연결하면, 브랜치·제목·본문에 이슈 식별자가 들어간 PR이 해당 이슈에 자동으로 연결됩니다. 그리고 PR을 머지하면 이슈가 완료로 이동합니다.
---
import { Callout } from "fumadocs-ui/components/callout";
**설정 → GitHub**에서 GitHub 계정 또는 조직을 한 번만 연결하세요. 그 후에는 브랜치 이름, 제목, 본문에 이슈 식별자(예: `MUL-123`)가 들어 있는 모든 pull request가 해당 [이슈](/issues)에 **자동으로 연결**되고, 이슈 사이드바의 **Pull requests** 아래에 표시되며, PR이 머지되면 이슈가 **완료**로 이동합니다.
이슈별 설정은 없습니다. 전체 흐름은 식별자로 동작합니다.
## 연동이 하는 일
| 위치 | 동작 |
|---|---|
| **설정 → GitHub** | 워크스페이스 admin에게는 마스터 토글, **Connect GitHub** 버튼, 기능 스위치(PR 사이드바, Co-authored-by, 자동 연결)가 있는 GitHub 탭이 보입니다. 설치 후에는 GitHub 탭으로 다시 돌아옵니다. |
| **이슈 사이드바 → Pull requests** | 이 이슈에 자동 연결된 모든 PR이 제목, 저장소, 상태(`Open` / `Draft` / `Merged` / `Closed`), 작성자와 함께 표시됩니다. 행을 클릭하면 GitHub의 해당 PR로 이동합니다. |
| **Webhook(백그라운드)** | 모든 `pull_request` 이벤트에서 Multica는 PR 행을 upsert하고, PR에서 이슈 식별자를 스캔한 뒤, 연결 행을 (다시) 구성합니다. 멱등성이 있어 동일 delivery를 재전송해도 변화가 없습니다. |
| **머지 시 상태 자동 변경** | PR이 `merged`로 전환되면, 아직 `Done`이나 `Cancelled`가 아닌 모든 연결된 이슈가 `Done`으로 이동합니다. 상태 변경은 source `github_pr_merged`로 타임라인에 기록됩니다. |
미러링되는 것은 PR 자체뿐입니다. 커밋, 열린 PR이 없는 브랜치 ref, CI 체크 상태는 모델링되지 **않습니다**. 이 연동은 의도적으로 좁게 설계되었습니다.
## 식별자 매칭 방식
Webhook은 다음 순서로 세 필드에서 식별자를 추출합니다: **PR head 브랜치**, **PR 제목**, **PR 본문**. 매처는 다음과 같습니다.
- 대소문자를 구분하지 않습니다 — `mul-123`, `MUL-123`, `Mul-123`이 모두 매칭됩니다.
- 경계가 있습니다 — 왼쪽의 `\b`와 오른쪽의 숫자 앵커 덕분에 `v1.2-3` 같은 버전 번호나 이메일 형식 문자열을 잘못 잡지 않습니다.
- 워크스페이스 범위로 제한됩니다 — 해당 워크스페이스 고유의 [이슈 prefix](/workspaces)에만 매칭됩니다. prefix가 `MUL`인 워크스페이스에서는 정수가 다른 이슈와 일치하더라도 `FOO-1`이 무시됩니다.
- 중복이 제거됩니다 — 본문에 `MUL-1, MUL-1`을 나열해도 이슈는 한 번만 연결됩니다.
하나의 PR에서 **여러 이슈**를 참조할 수 있습니다. `Closes MUL-1, MUL-2`는 PR을 두 이슈에 모두 연결하고, 머지하면 두 이슈 모두 `Done`으로 진행됩니다.
## 머지 시 완료 자동 변경 규칙
PR의 `merged` 필드가 `true`로 바뀌면, 연결된 모든 이슈가 평가됩니다.
| 이슈 현재 상태 | 결과 |
|---|---|
| `done` | 변화 없음(이미 종료 상태). |
| `cancelled` | **변화 없음** — 취소됨은 사용자가 작업을 명시적으로 포기했다는 의미이므로, 연동이 이 신호를 덮어쓰지 않습니다. |
| 그 외 모두(`todo`, `in_progress`, `in_review`, `blocked`, `backlog`) | `done`으로 이동. |
PR을 머지하지 **않고** 닫으면 PR 카드의 상태만 `Closed`로 업데이트됩니다. 연결된 이슈는 그대로 유지됩니다 — 머지 없이 닫는 것이 무엇을 의미하는지는 사용자가 결정합니다.
<Callout type="info">
이 동작은 타임라인에서 `system` 액터에게 귀속됩니다. 이슈 구독자는 사람이 상태를 옮겼을 때와 동일하게 상태 변경에 대한 인박스 알림을 받습니다.
</Callout>
## 자동 연결되지 않는 것
- **커밋 메시지의 식별자** — 브랜치 / 제목 / 본문만 스캔됩니다. `MUL-123: fix login`이라는 제목의 커밋은 동일한 문자열이 PR 제목이나 본문에도 나타나지 않는 한 자동 연결되지 않습니다.
- **PR 댓글의 식별자** — PR 자체의 메타데이터만 스캔되며, 이후의 GitHub 댓글은 무시됩니다.
- **App이 설치되지 않은 저장소의 PR** — App이 없으면 Multica는 webhook을 전혀 받지 못합니다.
- **PR을 이슈에 수동으로 연결하기** — 아직 이를 위한 UI는 없습니다. 팀의 규칙상 식별자를 Multica가 읽지 않는 위치에 둔다면, PR 제목이나 본문에 추가하세요.
## 연결 해제
**설정 → GitHub**에는 설치 목록이 없습니다 — 기존 설치는 GitHub에서 직접 관리합니다.
- **GitHub에서** — `https://github.com/settings/installations`(개인) 또는 `https://github.com/organizations/<org>/settings/installations`(조직)에서 Multica GitHub App을 제거합니다. Multica는 `installation.deleted` webhook을 받아 실시간으로 행을 삭제하며, 열려 있는 Settings 탭은 새로고침 없이 업데이트됩니다.
- **Multica 내부에서의 연결 해제는 admin 전용입니다** — GitHub 탭의 연결 해제 컨트롤은 admin이 아닌 사용자에게는 숨겨집니다. 마스터 GitHub 스위치가 꺼져 있어도 계속 사용할 수 있어, admin이 원클릭으로 기능을 비활성화한 후에도 오래된 설치를 해제할 수 있습니다.
연결 해제 후에도 미러링된 PR 행은 데이터베이스에 남아 과거 이슈 사이드바에서 무엇이 연결되어 있었는지 계속 보여주지만, 해당 설치에서 새로 들어오는 webhook 이벤트는 더 이상 수락되지 않습니다.
## 권한 및 가시성
- **연결 / 연결 해제**에는 워크스페이스 **owner 또는 admin**이 필요합니다. member에게는 카드 설명은 보이지만 Connect 버튼은 보이지 않습니다.
- 이슈의 **Pull requests** 사이드바는 해당 이슈를 읽을 수 있는 모든 사람에게 보입니다 — 이슈 상세의 나머지 부분과 동일한 권한입니다.
- GitHub App은 pull request와 메타데이터에 대한 **읽기 전용** 액세스를 요청합니다. Multica는 커밋, 댓글, 상태 체크를 GitHub로 다시 푸시하지 않습니다.
## 자체 호스팅 설정
Multica Cloud에서 Multica를 실행 중이라면 연동이 이미 구성되어 있습니다 — 이 섹션은 건너뛰세요.
자체 호스팅의 경우, GitHub App을 하나 만들고, 서버를 가리키게 한 뒤, 환경 변수 두 개를 설정합니다. 전체 흐름은 아래와 같습니다.
### 1. GitHub App 만들기
다음 중 하나로 이동하세요.
- 개인 계정 → `https://github.com/settings/apps/new`
- 조직 → `https://github.com/organizations/<org>/settings/apps/new`
다음을 입력하세요.
| 필드 | 값 |
|---|---|
| **GitHub App name** | 알아보기 쉬운 이름, 예: `Multica` 또는 `Multica (staging)`. |
| **Homepage URL** | Multica 프론트엔드, 예: `https://multica.example.com`. |
| **Callback URL** | 비워 두세요 — Multica는 OAuth 사용자 신원을 사용하지 않습니다. |
| **Setup URL** | `https://<api-host>/api/github/setup`. **"Redirect on update"를 체크하세요.** |
| **Webhook → Active** | 활성화. |
| **Webhook URL** | `https://<api-host>/api/webhooks/github`. |
| **Webhook secret** | 긴 무작위 문자열을 생성하세요(예: `openssl rand -hex 32`). 2단계에서 동일한 값을 Multica의 env에 붙여넣게 됩니다. |
| **Permissions → Repository → Pull requests** | **Read-only**. |
| **Permissions → Repository → Metadata** | Read-only(필수). |
| **Subscribe to events** | **Pull request**를 체크하세요. |
| **Where can this GitHub App be installed?** | 선택 사항. 단일 조직 설정에는 `Only on this account`로 충분합니다. |
**Create GitHub App** 후, App 상세 페이지에서 두 가지를 기록해 두세요.
- 상단의 **public link** — 그 꼬리가 slug입니다. `https://github.com/apps/multica-acme` → slug = `multica-acme`.
- 방금 생성한 **webhook secret**(나중에 GitHub에서 다시 읽을 수 없으니 — 지금 저장하세요).
<Callout type="warning">
**Webhook secret ≠ Client secret.** App 설정 페이지에는 두 필드가 함께 쌓여 있습니다. **Webhook secret**은 `pull_request` payload에 서명하는 값으로, Multica가 필요로 하는 것입니다. **Client secret**은 OAuth용이며 이 연동에서는 사용되지 않습니다. 이 둘을 혼동하면 모든 webhook delivery에서 혼란스러운 `401 invalid signature`가 발생합니다.
</Callout>
### 2. 환경 변수 설정
API 서버에서:
```dotenv
GITHUB_APP_SLUG=multica-acme
GITHUB_WEBHOOK_SECRET=<the webhook secret you generated>
```
두 변수 모두 필수입니다. 둘 중 하나라도 누락되면:
- Settings의 `Connect GitHub`이 **비활성화**되고 "not configured" 힌트가 표시됩니다.
- `/api/webhooks/github` 엔드포인트가 **`503 github webhooks not configured`**를 반환합니다 — Multica는 secret 없이 이벤트를 처리하기를 거부하며, 모든 서명을 조용히 유효한 것으로 취급하지 않습니다.
`FRONTEND_ORIGIN`도 설정되어 있어야 합니다(어떤 프로덕션 자체 호스팅이든 이미 설정되어 있습니다). 설치 후 setup 콜백이 사용자를 `<FRONTEND_ORIGIN>/settings?tab=github`으로 다시 돌려보냅니다.
env 변수를 설정한 후 API를 재시작하세요.
### 3. 마이그레이션 실행
이 연동은 테이블을 마이그레이션 `079_github_integration`으로 제공합니다. 기존 배포를 업그레이드하는 경우:
```bash
make migrate-up
```
세 개의 테이블이 생성됩니다: `github_installation`, `github_pull_request`, `issue_pull_request`. 이들은 워크스페이스와 함께 cascade-delete되므로, 워크스페이스를 제거하면 자동으로 정리됩니다.
### 4. UI에서 연결
Multica에서:
1. owner 또는 admin 권한으로 **설정 → GitHub**를 엽니다.
2. **Connect GitHub**를 클릭합니다. GitHub가 새 탭에서 열립니다.
3. 액세스를 부여할 저장소를 선택하고 **Install**합니다.
4. GitHub가 `<api-host>/api/github/setup`으로 리디렉션하여 설치를 기록한 뒤, `<FRONTEND_ORIGIN>/settings?tab=github&github_connected=1`로 돌려보냅니다.
그 후, 브랜치 / 제목 / 본문에 이슈 식별자가 들어 있는 PR을 열어 보세요 — 몇 초 내에 해당 이슈의 상세 페이지에 Pull requests 블록이 나타납니다.
### 5. curl 프로브로 검증
설치 후 GitHub의 **Recent Deliveries** 페이지에서 `401 invalid signature`가 보고된다면, 양쪽의 secret이 다른 것입니다. 어느 쪽이 잘못되었는지 가장 빠르게 찾는 방법은 GitHub를 우회하는 것입니다.
```bash
SECRET="<the value you put in GITHUB_WEBHOOK_SECRET>"
BODY='{"zen":"test"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -i -X POST https://<api-host>/api/webhooks/github \
-H "X-Hub-Signature-256: sha256=$SIG" \
-H "X-GitHub-Event: ping" \
-H "Content-Type: application/json" \
-d "$BODY"
```
| HTTP 상태 | 의미 | 해결 방법 |
|---|---|---|
| `200` `{"ok":"pong"}` | 서버가 로드한 secret이 `$SECRET`과 일치합니다. 불일치는 GitHub 쪽에 있습니다. | App → Webhook secret 편집 → **동일한 값을 붙여넣기** → **Save changes**(저장하지 않고 필드 밖을 클릭하면 이전 secret이 유지됩니다). 재전송하세요. |
| `401 invalid signature` | 서버가 로드한 secret이 생각하는 값이 **아닙니다**. | env 변수가 실행 중인 프로세스에 적용되었는지 확인하세요(예: `kubectl exec` → `echo -n "$GITHUB_WEBHOOK_SECRET" \| wc -c`). 재배포하세요. |
| `503 github webhooks not configured` | 프로세스에서 `GITHUB_WEBHOOK_SECRET`이 비어 있습니다. | env 변수를 설정하고 API를 재시작하세요. |
## 제한 사항
현재 알아 둬야 할 몇 가지 거친 부분이 있습니다.
- **아직 수동 연결 UI가 없습니다** — PR을 연결하는 유일한 방법은 브랜치, 제목, 본문에 식별자를 두는 것입니다.
- **CI / 체크 상태가 없습니다** — PR 자체만 미러링됩니다. 빌드 상태, 리뷰 댓글, 리뷰어는 Multica에 표시되지 않습니다.
- 머지 → 완료 규칙에 대한 **워크스페이스 수준 설정이 없습니다** — 고정된 기본값입니다(`cancelled`가 아닌 한 `merged → done`). 워크스페이스에서 커스터마이즈할 수 있는 매핑은 향후 추가될 예정입니다.
- **하나의 이슈에 여러 PR이 연결된 경우 머지가 보수적입니다** — 두 PR이 모두 `MUL-123`을 참조하고 첫 번째가 머지되면, 이슈는 즉시 `Done`으로 이동합니다. 진행하기 전에 연결된 모든 PR이 해결되기를 기다리는 후속 변경이 진행 중입니다.
## 다음
- [이슈](/issues) — PR에서 참조하는 이슈 식별자(`MUL-123`)
- [워크스페이스](/workspaces) — 워크스페이스별 이슈 prefix를 설정하는 곳
- [환경 변수](/environment-variables) — 위의 GitHub 변수를 포함한 전체 env 참조

View File

@@ -0,0 +1,54 @@
---
title: Multica の仕組み
description: 3 つのコア構成要素(サーバー / デーモン / AI コーディングツール)がどのように連携してエージェントの作業を実行するかを説明します。
---
import { ArchitectureDiagram } from "@/components/architecture-diagram";
Multica は**分散型**プラットフォームです。あなたが目にする Web インターフェースは表に見えている部分にすぎず、実際の作業は 3 つの構成要素が処理します。**Multica サーバー**はデータを保持します([ワークスペース](/workspaces)、[イシュー](/issues)、[メンバー](/members-roles)、[タスク](/tasks)キューなど)。**[デーモン](/daemon-runtimes)**はあなた自身のマシンで実行され、タスクを取得して AI コーディングツールを駆動します。そして **[AI コーディングツール](/providers)**Claude Code、Codex、その他のローカル CLIが、実際にコードを書く構成要素です。これが Multica と Linear や Jira との最大の違いです。**[エージェント](/agents)は当社のサーバーではなく、あなたのマシンで実行されます。**
## 3 つのコア構成要素
<ArchitectureDiagram />
- **Multica サーバー** — あなたが目にするワークスペース、イシュー一覧、コメントスレッドは、すべてここのデータベースに保存されます。また、あなたと同僚の間でリアルタイム更新をプッシュする WebSocket ハブでもあります。エージェントのタスクは**実行しません**。
- **デーモン** — Multica CLI の一部であり、あなた自身のマシンで実行されます。起動時にローカルにインストールされた AI コーディングツールを検出し、サーバーに登録したうえで、3 秒ごとにタスクをポーリングし、15 秒ごとにハートビートを送信し始めます。
- **AI コーディングツール** — 次の 12 種類のうちの 1 つ(または複数を並列で): [Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)。デーモンがタスクを取得した後は、これらのツールを使って実際の作業を行います。
ツールチェーンがローカルに留まるため、**あなたの API キー、コードディレクトリ、認可されたツール**は、あなたのマシン上でのみ使用されます。Multica サーバーはそのいずれも目にすることはありません。これはセルフホストでも Cloud でも同じように適用されます。
## タスクのライフサイクル
最も一般的なシナリオである、イシューをエージェントに割り当てる場合を見てみましょう。
1. あなたが Web UI で割り当てをクリックします。ブラウザが Multica サーバーへ HTTP リクエストを送ります。
2. サーバーがそのイシューの担当者をエージェントに設定し、同時にタスクキューに状態 `queued` の実行タスクを作成します。
3. あなたのマシンにあるデーモンが、次のポーリング3 秒以内)でタスクを取得します。タスクの状態が `dispatched` に変わります。
4. デーモンがローカルに隔離された作業ディレクトリを作成し、該当する AI コーディングツールを呼び出します。タスクの状態が `running` に変わります。
5. AI がローカルでコードを書き、テストを実行し、コメントをサーバーへ投稿します。
6. 実行が終了します。デーモンが結果(成功 / 失敗)をサーバーに報告し、タスクの状態が `completed` または `failed` に変わります。あなたは Web UI で進捗の更新をリアルタイムにWebSocket を通じて)確認します。
詳しい動作の仕組みは、[デーモンとランタイム](/daemon-runtimes)および[タスク](/tasks)を参照してください。
## エージェントを動かす 4 つの方法
「イシューの割り当て」だけではありません。Multica にはコラボレーションのスタイルごとに 1 つずつ、4 つのトリガーがあります。
| 方法 | 一般的なシナリオ | ドキュメント |
|---|---|---|
| **イシューの割り当て** | 最も一般的な方法。イシューをエージェントに割り当てると、自分で作業を始めます | [イシューの割り当て](/assigning-issues) |
| **コメントでエージェントを @メンション** | 「これちょっと見てくれる?」— 担当者や状態を変えず、コメント 1 つで実行を開始します | [エージェントのメンション](/mentioning-agents) |
| **ダイレクトチャット** | イシューに紐づかない独立した会話 — 質問したり、イシューの下書きを作らせたりします | [チャット](/chat) |
| **オートパイロット(スケジュール)** | 常時の指示 — 「毎週月曜の朝にスタンドアップのまとめをして」のようなもの | [オートパイロット](/autopilots) |
## ランタイム: どこで実行され、ツールは何個か
**ランタイム**とは「デーモン × 1 つの AI コーディングツール」の組み合わせです。あるマシンのデーモンに Claude Code と Codex の両方がインストールされており、2 つのワークスペースに参加している場合、Multica は 4 つの独立したランタイム(ワークスペース 2 個 × ツール 2 個)を登録します。
現在は**ローカルデーモン**のランタイムモデルのみがサポートされています。クラウドランタイム(自分のマシンを起動しておく必要がない方式)は**近日提供予定**で、現在はウェイトリストの登録のみを受け付けています。[ダウンロード](https://multica.ai/download)ページでお申し込みください。
## 次のステップ
- [Cloud Quickstart](/cloud-quickstart) — 5 分で Multica Cloud に接続する
- [Self-Host Quickstart](/self-host-quickstart) — 自前のバックエンドを実行する
- [デーモンとランタイム](/daemon-runtimes) — アーキテクチャが依存する構成要素を深掘りする

View File

@@ -0,0 +1,54 @@
---
title: Multica의 작동 방식
description: 세 가지 핵심 구성 요소(서버 / 데몬 / AI 코딩 도구)가 어떻게 협력하여 에이전트의 작업을 실행하는지 설명합니다.
---
import { ArchitectureDiagram } from "@/components/architecture-diagram";
Multica는 **분산형** 플랫폼입니다. 여러분이 보는 웹 인터페이스는 겉으로 드러난 부분일 뿐이고, 실제 작업은 세 가지 구성 요소가 처리합니다. **Multica 서버**는 데이터를 소유합니다([워크스페이스](/workspaces), [이슈](/issues), [멤버](/members-roles), [작업](/tasks) 대기열 등). **[데몬](/daemon-runtimes)**은 여러분 자신의 기기에서 실행되며 작업을 가져와 AI 코딩 도구를 구동합니다. 그리고 **[AI 코딩 도구](/providers)**(Claude Code, Codex, 그 밖의 로컬 CLI)는 실제로 코드를 작성하는 구성 요소입니다. 이것이 Multica와 Linear 또는 Jira의 가장 큰 차이입니다. **[에이전트](/agents)는 우리 서버가 아니라 여러분의 기기에서 실행됩니다.**
## 세 가지 핵심 구성 요소
<ArchitectureDiagram />
- **Multica 서버** — 여러분이 보는 워크스페이스, 이슈 목록, 댓글 스레드는 모두 이곳의 데이터베이스에 저장됩니다. 또한 여러분과 동료 사이의 실시간 업데이트를 푸시하는 WebSocket 허브이기도 합니다. 에이전트 작업은 **실행하지 않습니다.**
- **데몬** — Multica CLI의 일부로, 여러분 자신의 기기에서 실행됩니다. 시작 시 로컬에 설치된 AI 코딩 도구를 감지하고, 서버에 등록한 다음, 3초마다 작업을 폴링하고 15초마다 하트비트를 전송하기 시작합니다.
- **AI 코딩 도구** — 다음 열두 가지 중 하나(또는 여러 개를 병렬로): [Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). 데몬이 작업을 가져온 뒤에는 이러한 도구를 사용해 실제 작업을 수행합니다.
도구 체인이 로컬에 유지되므로 **여러분의 API 키, 코드 디렉터리, 인증된 도구**는 오직 여러분의 기기에서만 사용됩니다. Multica 서버는 그중 어떤 것도 보지 못합니다. 이는 자체 호스팅을 하든 Cloud를 사용하든 동일하게 적용됩니다.
## 작업의 수명 주기
가장 흔한 시나리오인, 이슈를 에이전트에게 할당하는 경우를 살펴보겠습니다.
1. 여러분이 웹 UI에서 할당을 클릭합니다. 브라우저가 Multica 서버로 HTTP 요청을 보냅니다.
2. 서버가 해당 이슈의 담당자를 에이전트로 설정하고, 동시에 작업 대기열에 상태 `queued`인 실행 작업을 생성합니다.
3. 여러분의 기기에 있는 데몬이 다음 폴링(3초 이내) 때 작업을 가져옵니다. 작업 상태가 `dispatched`로 바뀝니다.
4. 데몬이 로컬에 격리된 작업 디렉터리를 생성하고 해당 AI 코딩 도구를 호출합니다. 작업 상태가 `running`으로 바뀝니다.
5. AI가 로컬에서 코드를 작성하고, 테스트를 실행하고, 댓글을 서버로 다시 게시합니다.
6. 실행이 종료됩니다. 데몬이 결과(성공 / 실패)를 서버에 보고하고, 작업 상태가 `completed` 또는 `failed`로 바뀝니다. 여러분은 웹 UI에서 진행 상황 업데이트를 실시간으로(WebSocket을 통해) 확인합니다.
자세한 동작 원리는 [데몬과 런타임](/daemon-runtimes) 및 [작업](/tasks)을 참조하세요.
## 에이전트를 작동시키는 네 가지 방법
"이슈 할당"만 있는 것은 아닙니다. Multica에는 협업 스타일별로 하나씩, 4가지 트리거가 있습니다.
| 방법 | 일반적인 시나리오 | 문서 |
|---|---|---|
| **이슈 할당** | 가장 흔한 방법. 이슈를 에이전트에게 할당하면 스스로 작업을 시작합니다 | [이슈 할당하기](/assigning-issues) |
| **댓글에서 에이전트 @멘션** | "이거 한번 봐 줘" — 담당자나 상태를 바꾸지 않고 댓글 하나로 실행을 시작합니다 | [에이전트 멘션하기](/mentioning-agents) |
| **다이렉트 채팅** | 이슈에 묶이지 않은 독립적인 대화 — 질문하거나, 이슈 초안을 작성하게 합니다 | [채팅](/chat) |
| **오토파일럿(예약)** | 상시 지시 — "매주 월요일 아침에 스탠드업 요약을 해 줘" 같은 것 | [오토파일럿](/autopilots) |
## 런타임: 어디서 실행되고, 도구는 몇 개인가
**런타임**은 "데몬 × 하나의 AI 코딩 도구"의 조합입니다. 한 기기의 데몬에 Claude Code와 Codex가 모두 설치되어 있고 두 개의 워크스페이스에 참여해 있다면, Multica는 4개의 독립적인 런타임(워크스페이스 2개 × 도구 2개)을 등록합니다.
현재는 **로컬 데몬** 런타임 모델만 지원됩니다. 클라우드 런타임(여러분 자신의 기기를 켜 둘 필요가 없는 방식)은 **곧 제공될 예정**이며, 현재는 대기자 명단 등록만 받고 있습니다. [다운로드](https://multica.ai/download) 페이지에서 신청하세요.
## 다음 단계
- [Cloud Quickstart](/cloud-quickstart) — 5분 만에 Multica Cloud에 연결하기
- [Self-Host Quickstart](/self-host-quickstart) — 자체 백엔드 실행하기
- [데몬과 런타임](/daemon-runtimes) — 아키텍처가 의존하는 구성 요소에 대한 심층 분석

View File

@@ -0,0 +1,65 @@
---
title: インボックスと購読
description: Multica がいつ通知を送るか、そして関心のないイシューをミュートする方法。
---
import { Callout } from "fumadocs-ui/components/callout";
インボックスは Multica があなたを**割り込む**場所です。あなたに割り当てられた[イシュー](/issues)、[`@` メンション](/comments)、そしてあなたが購読しているイシューのアクティビティがすべてここに届きます。
あなたは**購読**と**購読解除**を通じて、どのイシューのアクティビティが自分に届くかを制御します。
## インボックスに表示されるもの
次のイベントがあなたのインボックスに通知を届けます。
- **イシューの割り当て / 割り当て解除 / 再割り当て** — あなたが新しい担当者(または以前の担当者)になると通知を受け取ります
- **あなたが購読しているイシューのステータス、優先度、期限の変更**
- **あなたが購読しているイシューの新しいコメント**
- **あなたがコメントで `@` メンションされた** — 購読しているかどうかに関係なく届きます
- **誰かがあなたのイシューやコメントにリアクションした**
- **あなたが割り当てたエージェントの[タスク](/tasks)が失敗した**
## `@all` はワークスペース全体に通知します
`@all` は特殊な対象です。ワークスペースの**すべてのメンバー**に通知をプッシュします。
<Callout type="warning">
**`@all` は控えめに使ってください。** 50 人規模のワークスペースでは、`@all` コメント 1 つで瞬時に 50 件のインボックス通知が生成されます。日常的な議論ではなく、重大な事案(プロダクション障害、マイルストーンの告知)にのみ使ってください。
</Callout>
## エージェントは通知を受け取りません
エージェントは**決して**インボックス通知を受け取りません。担当者や作成者であるときも、コメントで `@` メンションされたときも受け取りません。
これはバグではありません。エージェントはインボックスを読みません。エージェントは[**即時トリガー**](/assigning-issues)方式で動作します。イシューを割り当てたり、コメントでエージェントを `@` メンションしたりすると、ただちにそのエージェント向けのタスクが始まります。インボックスは人間のためのリマインダーの仕組みであり、エージェントにとっては何の意味も持ちません。
## 購読のルール
次の 4 つの状況で、あなたはイシューに**自動購読**されます。
- あなたがそのイシューを**作成**した場合
- あなたがそのイシューに**割り当て**られた場合
- あなたがそのイシューに**コメント**した場合
- あなたがそのイシューまたはそのコメントで **`@` メンション**された場合
自動購読は一度だけ起こります。作成者であり同時にメンション対象でもあっても、2 回購読されることはありません。
<Callout type="warning">
**再割り当ては自動で購読を解除しません。** あなたが以前は担当者だったのに交代させられた場合でも、**そのイシューの更新を引き続き受け取ります** — 自動購読がデータベースにそのまま残っているためです。
通知を受け取らないようにするには、イシューを開いて手動で購読を解除してください。
</Callout>
また、どのイシュー(無関係なイシューでも)でも**手動で購読**したり、どの自動購読でも**手動で購読解除**したりできます。UI ではイシューページの右パネルを使い、CLI では `multica issue subscriber add/remove` を使ってください。
## 子イシューのステータス変更は親イシューに伝播します
子イシューの**ステータス**が変更されると、親イシューの購読者も通知を受け取ります。たとえ子イシューを購読していなくても同様です。
これは**ステータスにのみ**適用されます。子イシューのコメント、優先度、期限の変更は親イシューに伝播**しません**。
## 次へ
- [コメントとメンション](/comments) — `@` メンションの仕組みと注意点
- [エージェントにイシューを割り当てる](/assigning-issues) — エージェントがトリガーされる仕組み(そしてエージェントがインボックスを読まない理由)

View File

@@ -0,0 +1,65 @@
---
title: 인박스와 구독
description: Multica가 언제 알림을 보내는지, 그리고 관심 없는 이슈를 음소거하는 방법.
---
import { Callout } from "fumadocs-ui/components/callout";
인박스는 Multica가 여러분을 **방해**하는 곳입니다. 여러분에게 할당된 [이슈](/issues), [`@` 멘션](/comments), 그리고 여러분이 구독한 이슈의 활동이 모두 여기에 도착합니다.
여러분은 **구독**과 **구독 취소**를 통해 어떤 이슈 활동이 여러분에게 도달할지 제어합니다.
## 인박스에 표시되는 것
다음 이벤트가 여러분의 인박스로 알림을 전달합니다.
- **이슈 할당 / 할당 해제 / 재할당** — 여러분이 새 담당자(또는 이전 담당자)가 되면 알림을 받습니다
- **여러분이 구독한 이슈의 상태, 우선순위, 마감일 변경**
- **여러분이 구독한 이슈의 새 댓글**
- **여러분이 댓글에서 `@`로 멘션됨** — 구독 여부와 관계없이 전달됩니다
- **누군가 여러분의 이슈나 댓글에 반응함**
- **여러분이 할당한 에이전트 [작업](/tasks)이 실패함**
## `@all`은 워크스페이스 전체에 알립니다
`@all`은 특수한 대상입니다. 워크스페이스의 **모든 멤버**에게 알림을 푸시합니다.
<Callout type="warning">
**`@all`은 아껴서 사용하세요.** 50명 규모의 워크스페이스에서 `@all` 댓글 하나는 즉시 50개의 인박스 알림을 생성합니다. 일상적인 논의가 아니라 중대한 사안(프로덕션 장애, 마일스톤 공지)에만 사용하세요.
</Callout>
## 에이전트는 알림을 받지 않습니다
에이전트는 **절대** 인박스 알림을 받지 않습니다. 담당자나 생성자일 때도, 댓글에서 `@`로 멘션될 때도 받지 않습니다.
이것은 버그가 아닙니다. 에이전트는 인박스를 읽지 않습니다. 에이전트는 [**즉시 트리거**](/assigning-issues) 방식으로 동작합니다. 이슈를 할당하거나 댓글에서 에이전트를 `@`로 멘션하면 곧바로 해당 에이전트를 위한 작업이 시작됩니다. 인박스는 사람을 위한 알림 메커니즘이며, 에이전트에게는 아무런 의미가 없습니다.
## 구독 규칙
다음 네 가지 상황에서 여러분은 이슈에 **자동 구독**됩니다.
- 여러분이 이슈를 **생성**한 경우
- 여러분이 이슈에 **할당**된 경우
- 여러분이 이슈에 **댓글**을 단 경우
- 여러분이 이슈 또는 그 댓글에서 **`@`로 멘션**된 경우
자동 구독은 한 번만 일어납니다. 생성자이면서 동시에 멘션 대상이더라도 두 번 구독되지는 않습니다.
<Callout type="warning">
**재할당은 자동으로 구독을 취소하지 않습니다.** 여러분이 예전에 담당자였다가 교체되었더라도, **그 이슈의 업데이트를 계속 받게 됩니다.** 자동 구독이 데이터베이스에 그대로 남아 있기 때문입니다.
더 이상 알림을 받지 않으려면 이슈를 열어 직접 구독을 취소하세요.
</Callout>
또한 어떤 이슈든(관련 없는 이슈라도) **직접 구독**하거나, 어떤 자동 구독이든 **직접 구독을 취소**할 수 있습니다. UI에서는 이슈 페이지의 오른쪽 패널을 사용하고, CLI에서는 `multica issue subscriber add/remove`를 사용하세요.
## 하위 이슈의 상태 변경은 상위 이슈로 전파됩니다
하위 이슈의 **상태**가 변경되면, 상위 이슈의 구독자도 알림을 받습니다. 그들이 하위 이슈를 구독하지 않았더라도 마찬가지입니다.
이것은 **상태에만** 적용됩니다. 하위 이슈의 댓글, 우선순위, 마감일 변경은 상위 이슈로 전파되지 **않습니다**.
## 다음
- [댓글과 멘션](/comments) — `@` 멘션의 작동 방식과 주의할 점
- [에이전트에게 이슈 할당하기](/assigning-issues) — 에이전트가 트리거되는 방식(그리고 에이전트가 인박스를 읽지 않는 이유)

View File

@@ -0,0 +1,50 @@
---
title: ようこそ
description: 人間と AI エージェントが同じワークスペースで一緒に働く、タスクコラボレーションプラットフォーム。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica は、人間と AI [エージェント](/agents)が同じ[ワークスペース](/workspaces)で一緒に働くタスクコラボレーションプラットフォームです。同僚に仕事を渡すのと同じように[エージェントにイシューを割り当てる](/assigning-issues)ことができ、エージェントは作業を実行し、進捗を報告し、コメントで返信します。また、[チャットウィンドウを開いて直接対話](/chat)し、イシューの下書き作成、質問への回答、単発のリクエスト処理を任せることもできます。
このページでは、エージェントがどこで実行されるか、そして Multica を使い始めるさまざまな方法を説明します。
## エージェントが実行される場所
エージェントは Multica のサーバー上でタスクを実行**しません**。現在 Multica は 1 つのランタイムモデルをサポートしています。
- **ローカル[デーモン](/daemon-runtimes)** — 自分のマシンで `multica daemon` を実行すると、デーモンがローカルにインストールされた [AI コーディングツール](/providers)を駆動します。現在 12 種類が標準で組み込まれています: [Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)。API キー、ツールチェーン、コードディレクトリはすべて自分のマシンに留まります。
<Callout type="info">
**クラウドランタイムが近日提供予定です。** 現在はウェイトリストのみで運用されています。提供が開始されればローカルデーモンは不要になり、エージェントのタスクは Multica Cloud 上で直接実行されます。[ダウンロード](https://multica.ai/download)ページで登録すると通知を受け取れます。
</Callout>
## Multica を使う 3 つの方法
最初の 2 つのカードは**バックエンドの選択肢**で、Multica サーバーがどこで実行されるかを決めます。3 つ目は**クライアントの選択肢**で、どのインターフェースを使うかを決めます。デスクトップアプリはどちらのバックエンドとも組み合わせて使えます。
<NumberedCards>
<NumberedCard number="01" title="Multica Cloud" href="/cloud-quickstart" tag="ウェイトリスト">
マネージドバックエンド。CLI をインストールし、ローカルでデーモンを実行してから、Multica がホスティングするサーバーに接続します。約 5 分で完了します。
</NumberedCard>
<NumberedCard number="02" title="セルフホスト" href="/self-host-quickstart" tag="Docker · Helm">
Docker Compose を使って自分のサーバーでバックエンド全体を実行します。データベース、サーバー、ストレージがすべて自分のインフラ上に配置されます。
</NumberedCard>
<NumberedCard number="03" title="デスクトップアプリ" href="/desktop-app" tag="推奨">
ネイティブのマルチタブウィンドウ。CLI が内蔵されており、起動時にデーモンを自動的に開始します。インストール後に実行するコマンドは一切ありません。Multica Cloud またはセルフホストのバックエンドに接続します。
</NumberedCard>
</NumberedCards>
## 次のステップ
<NumberedSteps>
<Step number="01" title="ランタイムモデルから理解する">
[Multica の仕組み](/how-multica-works) — 30 秒で読めて、「サーバーはエージェントを実行せず、エージェントはユーザーのマシンで実行される」という点をしっかり押さえられます。
</Step>
<Step number="02" title="始める方法を選ぶ">
上記の 3 つから 1 つを選びましょう。ほとんどの方は[デスクトップアプリ](/desktop-app)から始めます。CLI のセットアップが不要で、5 分で動き出します。
</Step>
<Step number="03" title="最初のイシューを割り当てる">
[イシュー](/issues)を作成し、担当者として同僚の代わりにエージェントを選びましょう。エージェントが結果を届けるのを待つだけです。
</Step>
</NumberedSteps>

View File

@@ -0,0 +1,50 @@
---
title: 환영합니다
description: 인간과 AI 에이전트가 같은 워크스페이스에서 함께 일하는 작업 협업 플랫폼.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica는 인간과 AI [에이전트](/agents)가 같은 [워크스페이스](/workspaces)에서 함께 일하는 작업 협업 플랫폼입니다. 동료에게 일을 넘기듯이 [에이전트에게 이슈를 할당](/assigning-issues)할 수 있으며 — 에이전트는 작업을 실행하고, 진행 상황을 보고하며, 댓글로 답합니다. 또한 [채팅 창을 열어 직접 대화](/chat)하면서 이슈 초안 작성, 질문 답변, 일회성 요청 처리를 맡길 수도 있습니다.
이 페이지에서는 에이전트가 어디에서 실행되는지, 그리고 Multica를 사용하기 시작하는 여러 방법을 설명합니다.
## 에이전트가 실행되는 곳
에이전트는 Multica 서버에서 작업을 실행하지 **않습니다**. 현재 Multica는 하나의 런타임 모델을 지원합니다:
- **로컬 [데몬](/daemon-runtimes)** — 자신의 기기에서 `multica daemon`을 실행하면, 데몬이 로컬에 설치된 [AI 코딩 도구](/providers)를 구동합니다. 현재 열두 가지가 기본 내장되어 있습니다: [Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). API 키, 툴체인, 코드 디렉터리는 모두 자신의 기기에 머뭅니다.
<Callout type="info">
**클라우드 런타임이 곧 제공됩니다.** 현재는 대기 명단으로만 운영됩니다. 출시되면 로컬 데몬이 필요 없어지며 — 에이전트 작업이 Multica Cloud에서 직접 실행됩니다. [다운로드](https://multica.ai/download) 페이지에서 등록하면 알림을 받을 수 있습니다.
</Callout>
## Multica를 사용하는 세 가지 방법
처음 두 카드는 **백엔드 선택지** — Multica 서버가 어디에서 실행되는지를 정합니다. 세 번째는 **클라이언트 선택지** — 어떤 인터페이스를 사용할지를 정합니다. 데스크톱 앱은 두 백엔드 중 어느 쪽과도 함께 사용할 수 있습니다.
<NumberedCards>
<NumberedCard number="01" title="Multica Cloud" href="/cloud-quickstart" tag="대기 명단">
관리형 백엔드. CLI를 설치하고 로컬에서 데몬을 실행한 뒤, Multica가 호스팅하는 서버에 연결합니다. 약 5분이면 됩니다.
</NumberedCard>
<NumberedCard number="02" title="자체 호스팅" href="/self-host-quickstart" tag="Docker · Helm">
Docker Compose로 자신의 서버에서 전체 백엔드를 실행합니다. 데이터베이스, 서버, 스토리지가 모두 자신의 인프라에 위치합니다.
</NumberedCard>
<NumberedCard number="03" title="데스크톱 앱" href="/desktop-app" tag="추천">
네이티브 멀티탭 창. CLI가 내장되어 있고 실행 시 데몬을 자동으로 시작합니다 — 설치 후 실행할 명령이 전혀 없습니다. Multica Cloud 또는 자체 호스팅 백엔드에 연결합니다.
</NumberedCard>
</NumberedCards>
## 다음 단계
<NumberedSteps>
<Step number="01" title="런타임 모델부터 이해하기">
[Multica는 어떻게 작동하나요](/how-multica-works) — 30초면 읽을 수 있으며, "서버는 에이전트를 실행하지 않고 에이전트는 사용자의 기기에서 실행된다"는 점을 확실히 짚어줍니다.
</Step>
<Step number="02" title="시작할 방법 고르기">
위의 세 가지 중 하나를 선택하세요 — 대부분은 [데스크톱 앱](/desktop-app)으로 시작합니다. CLI 설정이 필요 없고 5분이면 실행됩니다.
</Step>
<Step number="03" title="첫 이슈 할당하기">
[이슈](/issues)를 만들고 담당자로 동료 대신 에이전트를 선택하세요. 에이전트가 결과를 가져올 때까지 기다리면 됩니다.
</Step>
</NumberedSteps>

View File

@@ -0,0 +1,180 @@
---
title: エージェントランタイムをインストールする
description: Multica はあなたのマシンにインストールされている AI コーディングツールを駆動します。このページでは、デーモンがそれらを検出できるように、サポートされている 12 種のツールをそれぞれインストールする方法を説明します。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica における**ランタイム**とは、あなたのマシンのデーモンと、デーモンが `PATH` で見つけた AI コーディングツール 1 つが組になったものです。オンボーディングの「ランタイムを接続」ステップで **No supported tools detected** と表示される場合、それはデーモンが `PATH` をスキャンしたものの、駆動方法を知っている 12 種のツールのいずれも見つけられなかったことを意味します。以下のツールのいずれか(または複数)をインストールしてから、そのステップに戻って再スキャンしてください — 数秒以内にランタイムが表示されます。
このページは次のドキュメントのインストール側の補完ドキュメントです。
- [デーモンとランタイム](/daemon-runtimes) — 検出の仕組み
- [AI コーディングツールマトリクス](/providers) — 各ツールができることとできないことセッション再開、MCP、モデル選択
<Callout type="info">
Multica サーバーがあなたの API キーやツール自体を見ることは決してありません。以下のすべて — インストール、認証、モデルアクセス — はあなたのローカルマシン上に存在します。何かが失敗する場合、それはほぼ常にローカルの問題です。
</Callout>
## 始める前に
以下の**すべての**ツールに 2 つの前提条件が適用されます。
1. **Multica デーモンが実行中である必要があります。** [Multica CLI](/cli) をインストールした後に `multica daemon start` を実行するか、デーモンを自動的に起動する [Multica デスクトップアプリ](/desktop-app)を使用してください。デーモンが実行されていなければ、ツールを検出する主体がありません。
2. **ツールのバイナリが `PATH` で到達可能である必要があります。** デーモンは各ツールを名前で呼び出して実行します(各セクションの**デーモンが探す名前**の列を参照)。ターミナルで `which <name>` で見つからなければ、デーモンも見つけられません。インストール後は、新しいターミナルを開く(またはデーモンを再起動する)ことで、新しい `PATH` エントリが反映されるようにしてください。
ツールをインストールした後は、デーモンを再起動してください。
```bash
multica daemon restart
```
または、デスクトップアプリではアプリを再起動するだけで構いません。デーモンは起動するたびに `PATH` を再スキャンします。
## サポートされている 12 種のツール
おおよそ利用者の多い順に並べています。すでに認証情報を持っているものを選んで使ってください — 12 種すべてをインストールする必要はありません。
### Claude Code (Anthropic)
最も完全な連携です。セッション再開が動作し、MCP が動作し、**11 種のうちエージェントの `mcp_config` フィールドを実際に読み込む唯一のツール**です(詳しくは[マトリクス](/providers#mcp-configuration-only-claude-code-actually-reads-it)を参照)。
| | |
|---|---|
| デーモンが探す名前 | `claude` |
| インストール | [claude.com/claude-code](https://www.claude.com/claude-code) の公式ガイドに従ってください。標準的な方法は npm パッケージ `@anthropic-ai/claude-code` ですNode.js 18+ が必要)。 |
| 認証 | `claude` を一度実行して CLI 内のログイン手順に従うか、`ANTHROPIC_API_KEY` を設定してください。 |
| 備考 | 新しいユーザーに最初に推奨する選択肢です。 |
### Codex (OpenAI)
よりきめ細かい承認ゲートを備えた JSON-RPC 2.0 のトランスポートです。**セッション再開のコードは存在しますが、現在は到達できません** — 再開が必要な場合は Claude Code か ACP 系列のいずれかを選んでください。
| | |
|---|---|
| デーモンが探す名前 | `codex` |
| インストール | [github.com/openai/codex](https://github.com/openai/codex) の公式ガイドに従ってください。標準的な方法は npm パッケージ `@openai/codex` です。 |
| 認証 | `codex login`(ブラウザベース)または `OPENAI_API_KEY`。 |
### Cursor (Anysphere)
Cursor エディタに対応する CLI です。**セッション再開は動作しません** — Cursor の CLI がセッション id を返さないため、再開時に渡す値は常に無効です。
| | |
|---|---|
| デーモンが探す名前 | `cursor-agent` |
| インストール | [Cursor エディタ](https://cursor.com/)をインストールしてから、[docs.cursor.com](https://docs.cursor.com/) のドキュメントに従って CLI をインストールしてください。バイナリ名は `cursor` ではなく `cursor-agent` です。 |
| 認証 | Cursor エディタを通じてログインすると、CLI がそのセッションを再利用します。 |
### GitHub Copilot
モデルのルーティングはあなたの GitHub アカウントのエンタイトルメントentitlementを通じて行われます — ツールが自分でモデルを選ぶのではなく、どのモデルを受け取るかは GitHub が決めます。
| | |
|---|---|
| デーモンが探す名前 | `copilot` |
| インストール | GitHub の CLI ドキュメント [github.com/github/copilot-cli](https://github.com/github/copilot-cli) を参照してください。 |
| 認証 | CLI を通じたブラウザベースの GitHub ログイン。 |
| 備考 | ログインしているアカウントに有効な GitHub Copilot サブスクリプションが必要です。 |
### Gemini (Google)
Gemini 2.5 および 3 シリーズをサポートします。セッション再開と MCP はありません — 単発のタスクに適しています。
| | |
|---|---|
| デーモンが探す名前 | `gemini` |
| インストール | [github.com/google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) の公式ガイドに従ってください。標準的な方法は npm パッケージ `@google/gemini-cli` です。 |
| 認証 | `gemini` を実行すると Google アカウントのログインを求められるか、`GEMINI_API_KEY` を設定してください。 |
### OpenCode (SST)
オープンソースの CLI エージェントです。独自の設定ファイルから利用可能なモデルを動的に発見します — 自分のモデルカタログを持ち込みたいユーザーによく合います。
| | |
|---|---|
| デーモンが探す名前 | `opencode` |
| インストール | [opencode.ai](https://opencode.ai/) の公式ガイド、または GitHub リポジトリ [github.com/sst/opencode](https://github.com/sst/opencode) に従ってください。一般的な方法はインストールスクリプトまたは npm パッケージです。 |
| 認証 | OpenCode のドキュメントに従ってモデルプロバイダーAnthropic、OpenAI など)を構成してください。 |
### Kiro CLI (Amazon)
ACP-over-stdio のトランスポートです。セッション再開は ACP `session/load` を通じて動作し、スキルは `.kiro/skills/` にコピーされます。
| | |
|---|---|
| デーモンが探す名前 | `kiro-cli` |
| インストール | [kiro.dev](https://kiro.dev/) の Kiro ドキュメントを参照してください。バイナリ名は `kiro` ではなく `kiro-cli` です。 |
| 認証 | AWS アカウントベースで、Kiro 独自のオンボーディングに従ってください。 |
### Kimi (Moonshot)
ACP プロトコルのエージェントで、主に中国市場を対象としています。スキルは `.kimi/skills/` 配下に置かれます(ネイティブ発見)。
| | |
|---|---|
| デーモンが探す名前 | `kimi` |
| インストール | [github.com/MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) の公式ガイドに従ってください。 |
| 認証 | Moonshot API キーで、ベンダーのドキュメントに従って構成します。 |
### Hermes (Nous Research)
ACP プロトコルのエージェントですKimi とトランスポートを共有)。セッション再開が動作します。スキル注入のパスは汎用の `.agent_context/skills/` にフォールバックします — 依存する前に、スキルが正しくロードされているか確認してください。
| | |
|---|---|
| デーモンが探す名前 | `hermes` |
| インストール | 最新の CLI ディストリビューションは Nous Research のリポジトリ [github.com/NousResearch](https://github.com/NousResearch) を参照してください。 |
| 認証 | ベンダーのドキュメントに従います。 |
### OpenClaw
オープンソースの CLI エージェントオーケストレーターです。**モデルはエージェント層にバインドされます**`openclaw agents add --model` — タスクごとに上書きすることはできず、Multica から `--model` や `--system-prompt` を渡すこともできません。
| | |
|---|---|
| デーモンが探す名前 | `openclaw` |
| インストール | プロジェクト [github.com/openclaw-org/openclaw](https://github.com/openclaw-org/openclaw) を参照してください(コミュニティによる保守)。 |
| 認証 | OpenClaw のドキュメントに従って、基盤となるモデルプロバイダーを構成してください。 |
### Pi (Inflection AI)
ミニマルです。**セッション再開の方式が特殊です** — 再開 id が文字列 id ではなく、ディスク上のセッションファイルへのパスです。
| | |
|---|---|
| デーモンが探す名前 | `pi` |
| インストール | Inflection の CLI ドキュメント [pi.ai](https://pi.ai/) を参照してください。 |
| 認証 | ベンダーのドキュメントに従います。 |
### Antigravity (Google)
Google の Antigravity CLI`agy`です。Google の Antigravity サービスと組になり、Gemini ベースのモデルを実行します。セッション再開は `--conversation <id>` を通じて動作し、デーモンが CLI のログファイルからこれをキャプチャします。モデル選択は Antigravity CLI 自体の内部で管理されます — Multica はこのプロバイダーに対してエージェントごとのモデルピッカーを無効にします。スキルは `.agents/skills/` に書き込まれますCLI が Gemini CLI のワークスペーススキルレイアウトを継承します — [Antigravity ドキュメント](https://antigravity.google/docs/gcli-migration)を参照)。
| | |
|---|---|
| デーモンが探す名前 | `agy` |
| インストール | [antigravity.google/docs/cli-overview](https://antigravity.google/docs/cli-overview) の公式ガイドに従ってください。CLI はあらかじめビルドされて提供されます — `agy install` を一度実行して PATH とシェルエイリアスを設定してください。 |
| 認証 | `agy` を対話的に一度実行して Google アカウントのログインを完了するか、Antigravity デスクトップアプリを通じてログインしてください — CLI は GUI が書き込んだ keyring エントリを再利用します。 |
| 備考 | CLI は構造化されたイベントストリームではなく、stdout に通常のアシスタントテキストを出力します。途中の「I will run X」の行と最終的な応答の両方がテキストとして Multica に中継されます。 |
## インストールした後
1. **バイナリが `PATH` にあるか確認してください。** 新しいターミナルを開いて `which <name>`(例: `which claude`、`which cursor-agent`、`which kiro-cli`、`which agy`)を実行してください。パスが出力されれば、デーモンが見つけられます。何も出力されない場合は、まずシェルの `PATH` を修正してください(典型的な原因は、リロードされていないシェルごとの rc ファイルです)。
2. **デーモンを再起動してください。** `multica daemon restart` を実行するか、デスクトップアプリを再起動してください。デーモンは起動時にのみ `PATH` をスキャンします。
3. **ランタイムページを確認してください。** Multica UI の**ランタイム**ページに、`(ワークスペース × ツール)` の組み合わせごとに 1 行ずつ表示されるはずです。行に「offline」と表示される場合は、[デーモンとランタイム → ランタイムがオフラインと表示されるとき](/daemon-runtimes#when-a-runtime-is-marked-offline)を参照してください。
4. **オンボーディングに戻ってください。** 「ランタイムを接続」ステップはポーリングを行い、数秒以内に新しいランタイムを認識します — リロードは不要です。
## トラブルシューティング
- **`which` はバイナリを見つけるのにデーモンは見つけません。** デーモンが古い `PATH` で起動されています。再起動してください。
- **バイナリは存在するのに起動に失敗します。** ターミナルからツール自体の `--version` や `--help` を一度実行してください — ここで発生する失敗のほとんどは、認証の欠落、期限切れのトークン、または Node.js / ランタイムの不一致です。
- **ランタイムページに行は表示されるのに、タスクがすぐに失敗します。** タスクをトリガーしながら `multica daemon logs -f` を確認してください。デーモンはツール自体のエラー出力をそのまま表示します。
より広範な症状については、[トラブルシューティングガイド](/troubleshooting)を参照してください。
## 次に
- [デーモンとランタイム](/daemon-runtimes) — 検出、ハートビート、オフライン処理の仕組み
- [AI コーディングツールマトリクス](/providers) — ツールが接続された後の機能差
- [エージェントの作成と構成](/agents-create) — エージェントに使うツールを選び、タスクの実行を開始する

View File

@@ -0,0 +1,180 @@
---
title: 에이전트 런타임 설치하기
description: Multica는 사용자 기기에 설치된 AI 코딩 도구를 구동합니다. 이 페이지에서는 데몬이 도구를 감지할 수 있도록 지원되는 12종의 도구를 각각 설치하는 방법을 설명합니다.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica에서 **런타임**이란 사용자 기기의 데몬과, 데몬이 `PATH`에서 찾아낸 AI 코딩 도구 하나가 짝을 이룬 것입니다. 온보딩의 "런타임 연결" 단계에서 **지원되는 도구를 감지하지 못했습니다**라고 표시된다면, 데몬이 `PATH`를 스캔했지만 구동 방법을 아는 12종의 도구 중 어느 것도 찾지 못했다는 뜻입니다. 아래 도구 중 하나(또는 여러 개)를 설치한 다음 해당 단계로 돌아와 다시 스캔하세요. 몇 초 안에 런타임이 나타납니다.
이 페이지는 다음 문서의 설치 측면 동반 문서입니다.
- [데몬과 런타임](/daemon-runtimes) — 감지가 작동하는 방식
- [AI 코딩 도구 매트릭스](/providers) — 각 도구가 할 수 있는 것과 할 수 없는 것(세션 재개, MCP, 모델 선택)
<Callout type="info">
Multica 서버는 사용자의 API 키나 도구 자체를 결코 보지 못합니다. 아래의 모든 것 — 설치, 인증, 모델 접근 — 은 사용자의 로컬 기기에 존재합니다. 무언가 실패한다면 거의 항상 로컬 문제입니다.
</Callout>
## 시작하기 전에
아래 **모든** 도구에 두 가지 사전 조건이 적용됩니다.
1. **Multica 데몬이 실행 중이어야 합니다.** [Multica CLI](/cli)를 설치한 후 `multica daemon start`를 실행하거나, 데몬을 자동으로 시작하는 [Multica 데스크톱 앱](/desktop-app)을 사용하세요. 데몬이 실행 중이지 않으면 도구를 감지할 주체가 없습니다.
2. **도구의 바이너리가 `PATH`에서 접근 가능해야 합니다.** 데몬은 각 도구를 이름으로 호출하여 실행합니다(각 섹션의 **데몬이 찾는 이름** 열 참고). 터미널에서 `which <name>`으로 찾을 수 없다면 데몬도 찾지 못합니다. 설치한 후에는 새 터미널을 열거나(또는 데몬을 재시작하여) 새 `PATH` 항목이 반영되도록 하세요.
도구를 설치한 후에는 데몬을 재시작하세요.
```bash
multica daemon restart
```
또는 데스크톱 앱에서는 앱을 다시 실행하기만 하면 됩니다. 데몬은 시작될 때마다 `PATH`를 다시 스캔합니다.
## 지원되는 12종의 도구
대략 많이 쓰이는 순서대로 나열했습니다. 이미 자격 증명을 갖고 있는 것을 골라 사용하세요. 12종을 모두 설치할 필요는 없습니다.
### Claude Code (Anthropic)
가장 완전한 연동입니다. 세션 재개가 작동하고, MCP가 작동하며, 에이전트의 `mcp_config` 필드를 소비합니다(자세한 내용은 [매트릭스](/providers) 참고).
| | |
|---|---|
| 데몬이 찾는 이름 | `claude` |
| 설치 | [claude.com/claude-code](https://www.claude.com/claude-code)의 공식 가이드를 따르세요. 일반적인 방법은 npm 패키지 `@anthropic-ai/claude-code`입니다(Node.js 18+ 필요). |
| 인증 | `claude`를 한 번 실행하고 CLI 내 로그인 절차를 따르거나, `ANTHROPIC_API_KEY`를 설정하세요. |
| 비고 | 새 사용자에게 가장 먼저 권장하는 선택지입니다. |
### Codex (OpenAI)
더 세분화된 승인 게이트를 갖춘 JSON-RPC 2.0 전송 방식입니다. MCP 구성은 작업별 `$CODEX_HOME/config.toml`에 기록됩니다. **세션 재개 코드는 존재하지만 현재 도달할 수 없습니다** — 재개가 필요하다면 Claude Code 또는 ACP 계열 중 하나를 선택하세요.
| | |
|---|---|
| 데몬이 찾는 이름 | `codex` |
| 설치 | [github.com/openai/codex](https://github.com/openai/codex)의 공식 가이드를 따르세요. 일반적인 방법은 npm 패키지 `@openai/codex`입니다. |
| 인증 | `codex login`(브라우저 기반) 또는 `OPENAI_API_KEY`. |
### Cursor (Anysphere)
Cursor 에디터에 대응하는 CLI입니다. **세션 재개가 작동하지 않습니다** — Cursor의 CLI가 세션 id를 반환하지 않으므로 재개 시 전달하는 값은 항상 유효하지 않습니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `cursor-agent` |
| 설치 | [Cursor 에디터](https://cursor.com/)를 설치한 다음 [docs.cursor.com](https://docs.cursor.com/)의 문서에 따라 CLI를 설치하세요. 바이너리 이름은 `cursor`가 아니라 `cursor-agent`입니다. |
| 인증 | Cursor 에디터를 통해 로그인하면 CLI가 해당 세션을 재사용합니다. |
### GitHub Copilot
모델 라우팅은 사용자의 GitHub 계정 권한(entitlement)을 통해 이루어집니다 — 도구가 직접 모델을 고르지 않고, 어떤 모델을 받을지는 GitHub가 결정합니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `copilot` |
| 설치 | GitHub의 CLI 문서 [github.com/github/copilot-cli](https://github.com/github/copilot-cli)를 참고하세요. |
| 인증 | CLI를 통한 브라우저 기반 GitHub 로그인. |
| 비고 | 로그인한 계정에 활성화된 GitHub Copilot 구독이 필요합니다. |
### Gemini (Google)
Gemini 2.5 및 3 시리즈를 지원합니다. 세션 재개와 MCP는 없습니다 — 단발성 작업에 적합합니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `gemini` |
| 설치 | [github.com/google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli)의 공식 가이드를 따르세요. 일반적인 방법은 npm 패키지 `@google/gemini-cli`입니다. |
| 인증 | `gemini`를 실행하면 Google 계정 로그인을 요청하거나, `GEMINI_API_KEY`를 설정하세요. |
### OpenCode (SST)
오픈 소스 CLI 에이전트입니다. 자체 설정 파일에서 사용 가능한 모델을 동적으로 발견합니다 — 자신만의 모델 카탈로그를 직접 가져오려는 사용자에게 잘 맞습니다. `OPENCODE_CONFIG_CONTENT`를 통해 에이전트의 `mcp_config` 필드도 소비합니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `opencode` |
| 설치 | [opencode.ai](https://opencode.ai/)의 공식 가이드 또는 GitHub 저장소 [github.com/sst/opencode](https://github.com/sst/opencode)를 따르세요. 일반적인 방법은 설치 스크립트 또는 npm 패키지입니다. |
| 인증 | OpenCode의 문서에 따라 모델 제공자(Anthropic, OpenAI 등)를 구성하세요. |
### Kiro CLI (Amazon)
ACP-over-stdio 전송 방식입니다. 세션 재개는 ACP `session/load`를 통해 작동하며, MCP 구성은 ACP `mcpServers`로 전달되고, 스킬은 `.kiro/skills/`로 복사됩니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `kiro-cli` |
| 설치 | [kiro.dev](https://kiro.dev/)의 Kiro 문서를 참고하세요. 바이너리 이름은 `kiro`가 아니라 `kiro-cli`입니다. |
| 인증 | AWS 계정 기반이며, Kiro 자체 온보딩을 따르세요. |
### Kimi (Moonshot)
ACP 프로토콜 에이전트로, 주로 중국 시장을 겨냥합니다. MCP 구성은 ACP `mcpServers`로 전달되며, 스킬은 `.kimi/skills/` 아래에 위치합니다(네이티브 발견).
| | |
|---|---|
| 데몬이 찾는 이름 | `kimi` |
| 설치 | [github.com/MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli)의 공식 가이드를 따르세요. |
| 인증 | Moonshot API 키이며, 공급사 문서에 따라 구성합니다. |
### Hermes (Nous Research)
ACP 프로토콜 에이전트입니다(Kimi와 전송 방식을 공유). 세션 재개가 작동하고, MCP 구성은 ACP `mcpServers`로 전달됩니다. 스킬 주입 경로는 일반적인 `.agent_context/skills/`로 폴백됩니다 — 의존하기 전에 스킬이 제대로 로드되는지 확인하세요.
| | |
|---|---|
| 데몬이 찾는 이름 | `hermes` |
| 설치 | 최신 CLI 배포본은 Nous Research의 저장소 [github.com/NousResearch](https://github.com/NousResearch)를 참고하세요. |
| 인증 | 공급사 문서에 따릅니다. |
### OpenClaw
오픈 소스 CLI 에이전트 오케스트레이터입니다. MCP 구성은 Multica의 작업별 config wrapper를 통해 기록됩니다. **모델은 에이전트 계층에 바인딩됩니다**(`openclaw agents add --model`) — 작업별로 재정의할 수 없으며, Multica에서 `--model`이나 `--system-prompt`를 전달할 수 없습니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `openclaw` |
| 설치 | 프로젝트 [github.com/openclaw-org/openclaw](https://github.com/openclaw-org/openclaw)를 참고하세요(커뮤니티 유지 관리). |
| 인증 | OpenClaw의 문서에 따라 기반 모델 제공자를 구성하세요. |
### Pi (Inflection AI)
미니멀합니다. **세션 재개 방식이 특이합니다** — 재개 id가 문자열 id가 아니라 디스크에 있는 세션 파일의 경로입니다.
| | |
|---|---|
| 데몬이 찾는 이름 | `pi` |
| 설치 | Inflection의 CLI 문서 [pi.ai](https://pi.ai/)를 참고하세요. |
| 인증 | 공급사 문서에 따릅니다. |
### Antigravity (Google)
Google의 Antigravity CLI(`agy`)입니다. Google의 Antigravity 서비스와 짝을 이루며 Gemini 기반 모델을 실행합니다. 세션 재개는 `--conversation <id>`를 통해 작동하며, 데몬이 CLI 로그 파일에서 이를 캡처합니다. 모델 선택은 Antigravity CLI 자체 내부에서 관리됩니다 — Multica는 이 제공자에 대해 에이전트별 모델 선택기를 비활성화합니다. 스킬은 `.agents/skills/`에 기록됩니다(CLI가 Gemini CLI의 워크스페이스 스킬 레이아웃을 상속함 — [Antigravity 문서](https://antigravity.google/docs/gcli-migration) 참고).
| | |
|---|---|
| 데몬이 찾는 이름 | `agy` |
| 설치 | [antigravity.google/docs/cli-overview](https://antigravity.google/docs/cli-overview)의 공식 가이드를 따르세요. CLI는 미리 빌드되어 제공됩니다 — `agy install`을 한 번 실행하여 PATH와 셸 별칭을 설정하세요. |
| 인증 | `agy`를 대화형으로 한 번 실행하여 Google 계정 로그인을 완료하거나, Antigravity 데스크톱 앱을 통해 로그인하세요 — CLI는 GUI가 기록한 keyring 항목을 재사용합니다. |
| 비고 | CLI는 구조화된 이벤트 스트림이 아니라 stdout에 일반 어시스턴트 텍스트를 출력합니다. 중간의 "I will run X" 줄과 최종 응답 모두 텍스트로 Multica에 전달됩니다. |
## 설치한 후에
1. **바이너리가 `PATH`에 있는지 확인하세요.** 새 터미널을 열고 `which <name>`(예: `which claude`, `which cursor-agent`, `which kiro-cli`, `which agy`)을 실행하세요. 경로가 출력되면 데몬이 찾을 수 있습니다. 아무것도 출력되지 않으면 먼저 셸의 `PATH`를 수정하세요(전형적인 원인은 다시 로드되지 않은 셸별 rc 파일입니다).
2. **데몬을 재시작하세요.** `multica daemon restart`를 실행하거나 데스크톱 앱을 다시 실행하세요. 데몬은 시작 시에만 `PATH`를 스캔합니다.
3. **런타임 페이지를 확인하세요.** Multica UI의 **런타임** 페이지에 이제 `(워크스페이스 × 도구)` 조합별로 한 행씩 나열되어야 합니다. 행에 "offline"이라고 표시되면 [데몬과 런타임 → 런타임이 오프라인으로 표시될 때](/daemon-runtimes#when-a-runtime-is-marked-offline)를 참고하세요.
4. **온보딩으로 돌아가세요.** "런타임 연결" 단계는 폴링을 수행하며 몇 초 안에 새 런타임을 인식합니다 — 새로 고칠 필요가 없습니다.
## 문제 해결
- **`which`는 바이너리를 찾는데 데몬은 찾지 못합니다.** 데몬이 예전 `PATH`로 시작되었습니다. 재시작하세요.
- **바이너리는 존재하지만 실행에 실패합니다.** 터미널에서 도구 자체의 `--version`이나 `--help`를 한 번 실행하세요 — 여기서 발생하는 대부분의 실패는 인증 누락, 만료된 토큰, 또는 Node.js / 런타임 불일치입니다.
- **런타임 페이지에 행은 표시되지만 작업이 즉시 실패합니다.** 작업을 트리거하면서 `multica daemon logs -f`를 확인하세요. 데몬은 도구 자체의 오류 출력을 그대로 보여줍니다.
더 광범위한 증상은 [문제 해결 가이드](/troubleshooting)를 참고하세요.
## 다음
- [데몬과 런타임](/daemon-runtimes) — 감지, 하트비트, 오프라인 처리가 작동하는 방식
- [AI 코딩 도구 매트릭스](/providers) — 도구가 연결된 후의 기능 차이
- [에이전트 생성 및 구성](/agents-create) — 에이전트에 사용할 도구를 선택하고 작업 실행 시작하기

View File

@@ -37,7 +37,7 @@ Listed roughly from most to least common. Pick whichever ones you already have c
### Claude Code (Anthropic)
The most complete integration. Session resumption works, MCP works, and it's the **only one of the 11 that actually consumes the `mcp_config` field** on agents (see the [matrix](/providers#mcp-configuration-only-claude-code-actually-reads-it)).
The most complete integration. Session resumption works, MCP works, and it consumes the `mcp_config` field on agents (see the [matrix](/providers#mcp-configuration-provider-specific-support)).
| | |
|---|---|
@@ -48,7 +48,7 @@ The most complete integration. Session resumption works, MCP works, and it's the
### Codex (OpenAI)
JSON-RPC 2.0 transport with finer-grained approval gates. **Session resumption code exists but is currently unreachable** — pick Claude Code or one of the ACP family if you need resume.
JSON-RPC 2.0 transport with finer-grained approval gates. MCP config is written into the per-task `$CODEX_HOME/config.toml`. **Session resumption code exists but is currently unreachable** — pick Claude Code or one of the ACP family if you need resume.
| | |
|---|---|
@@ -89,7 +89,7 @@ Supports the Gemini 2.5 and 3 series. No session resumption, no MCP — suitable
### OpenCode (SST)
Open-source CLI agent. Dynamically discovers available models from its own configuration file — good fit for users who want to bring their own model catalog.
Open-source CLI agent. Dynamically discovers available models from its own configuration file — good fit for users who want to bring their own model catalog. Consumes the agent's `mcp_config` field through `OPENCODE_CONFIG_CONTENT`.
| | |
|---|---|
@@ -99,7 +99,7 @@ Open-source CLI agent. Dynamically discovers available models from its own confi
### Kiro CLI (Amazon)
ACP-over-stdio transport. Session resumption works through ACP `session/load`; skills are copied into `.kiro/skills/`.
ACP-over-stdio transport. Session resumption works through ACP `session/load`; MCP config is passed through ACP `mcpServers`; skills are copied into `.kiro/skills/`.
| | |
|---|---|
@@ -109,7 +109,7 @@ ACP-over-stdio transport. Session resumption works through ACP `session/load`; s
### Kimi (Moonshot)
ACP-protocol agent, primarily aimed at the Chinese market. Skills live under `.kimi/skills/` (native discovery).
ACP-protocol agent, primarily aimed at the Chinese market. MCP config is passed through ACP `mcpServers`; skills live under `.kimi/skills/` (native discovery).
| | |
|---|---|
@@ -119,7 +119,7 @@ ACP-protocol agent, primarily aimed at the Chinese market. Skills live under `.k
### Hermes (Nous Research)
ACP-protocol agent (shares the transport with Kimi). Session resumption works. The skill injection path falls back to the generic `.agent_context/skills/` — verify your skills are loading before relying on them.
ACP-protocol agent (shares the transport with Kimi). Session resumption works, and MCP config is passed through ACP `mcpServers`. The skill injection path falls back to the generic `.agent_context/skills/` — verify your skills are loading before relying on them.
| | |
|---|---|
@@ -129,7 +129,7 @@ ACP-protocol agent (shares the transport with Kimi). Session resumption works. T
### OpenClaw
Open-source CLI agent orchestrator. **Model is bound at the agent layer** (`openclaw agents add --model`) — it can't be overridden per task, and you can't pass `--model` or `--system-prompt` from Multica.
Open-source CLI agent orchestrator. MCP config is materialized through Multica's per-task config wrapper. **Model is bound at the agent layer** (`openclaw agents add --model`) — it can't be overridden per task, and you can't pass `--model` or `--system-prompt` from Multica.
| | |
|---|---|

View File

@@ -37,7 +37,7 @@ multica daemon restart
### Claude CodeAnthropic
集成最完整的一款。会话续接好用MCP 好用,而且 **12 款里只有它真正会读 agent 配置里的 `mcp_config` 字段**(见[矩阵](/zh/providers))。
集成最完整的一款。会话续接好用MCP 好用,会消费 agent 配置里的 `mcp_config` 字段(见[矩阵](/zh/providers))。
| | |
|---|---|
@@ -48,7 +48,7 @@ multica daemon restart
### CodexOpenAI
JSON-RPC 2.0 传输,审批粒度更细。**会话续接的代码在,但调不到** —— 要续接的话选 Claude Code 或 ACP 系列。
JSON-RPC 2.0 传输,审批粒度更细。MCP 配置会写入单次任务的 `$CODEX_HOME/config.toml`。**会话续接的代码在,但调不到** —— 要续接的话选 Claude Code 或 ACP 系列。
| | |
|---|---|
@@ -89,7 +89,7 @@ Cursor 编辑器的 CLI 对应物。**会话续接是坏的** —— Cursor CLI
### OpenCodeSST
开源 CLI agent。会从自己的配置文件里动态发现可用模型 —— 适合想自己掌控模型清单的用户。
开源 CLI agent。会从自己的配置文件里动态发现可用模型 —— 适合想自己掌控模型清单的用户。会通过 `OPENCODE_CONFIG_CONTENT` 消费 agent 配置里的 `mcp_config` 字段。
| | |
|---|---|
@@ -99,7 +99,7 @@ Cursor 编辑器的 CLI 对应物。**会话续接是坏的** —— Cursor CLI
### Kiro CLIAmazon
ACP-over-stdio 传输。会话续接通过 ACP `session/load` 工作skills 拷到 `.kiro/skills/`。
ACP-over-stdio 传输。会话续接通过 ACP `session/load` 工作;MCP 配置通过 ACP `mcpServers` 传入;skills 拷到 `.kiro/skills/`。
| | |
|---|---|
@@ -109,7 +109,7 @@ ACP-over-stdio 传输。会话续接通过 ACP `session/load` 工作skills
### KimiMoonshot
ACP 协议 agent主要面向中国市场。Skills 放在 `.kimi/skills/`(原生发现路径)。
ACP 协议 agent主要面向中国市场。MCP 配置通过 ACP `mcpServers` 传入skills 放在 `.kimi/skills/`(原生发现路径)。
| | |
|---|---|
@@ -119,7 +119,7 @@ ACP 协议 agent主要面向中国市场。Skills 放在 `.kimi/skills/`
### HermesNous Research
ACP 协议 agent和 Kimi 共享传输层。会话续接可用。Skill 注入用的是通用回退路径 `.agent_context/skills/` —— 用之前先验证 skills 真的被加载了。
ACP 协议 agent和 Kimi 共享传输层)。会话续接可用MCP 配置通过 ACP `mcpServers` 传入。Skill 注入用的是通用回退路径 `.agent_context/skills/` —— 用之前先验证 skills 真的被加载了。
| | |
|---|---|
@@ -129,7 +129,7 @@ ACP 协议 agent和 Kimi 共享传输层。会话续接可用。Skill 注
### OpenClaw
开源 CLI agent 编排器。**模型绑在 agent 层**`openclaw agents add --model`)—— 不能按任务覆盖,从 Multica 也传不了 `--model` / `--system-prompt`。
开源 CLI agent 编排器。MCP 配置通过 Multica 的单次任务配置 wrapper 写入。**模型绑在 agent 层**`openclaw agents add --model`)—— 不能按任务覆盖,从 Multica 也传不了 `--model` / `--system-prompt`。
| | |
|---|---|

View File

@@ -0,0 +1,79 @@
---
title: イシューとプロジェクト
description: 人またはエージェントに割り当てられる、Multica の中心的な作業単位。
---
import { Callout } from "fumadocs-ui/components/callout";
イシューは Multica における独立した作業単位です — バグ、新機能、対応が必要なことなら何でも構いません。すべてのイシューには **タイトル**、**説明**(Markdown 対応)、**ステータス**、**優先度**、**担当者** があり、任意で **プロジェクト** に属することもできます。Linear や Jira を使ったことがあれば、同じ形だと分かるはずです。
**Multica の最大の特徴は、イシューの担当者が人でもエージェントでもよいという点です** — [エージェント](/agents) — ここから始めましょう。
## エージェントにイシューを割り当てる
イシューをエージェントに[割り当てる](/assigning-issues)と、その作業をエージェントに引き渡すことになります。エージェントは **自動的に開始します** — 数秒以内に実行を始め、コメントで進捗を報告し、完了するとステータスを done に切り替えます。同僚に仕事を渡すのとの唯一の違いは、エージェントはオフラインにならず、リマインドも要らず、24時間365日いつでも対応できることです。
<Callout type="info">
エージェントのアイデンティティ、設定、実行場所については [エージェント](/agents) を参照してください。
</Callout>
非公開エージェントをイシューに割り当てられるのは、ワークスペースの owner と admin だけです。ロールの権限については [メンバーとロール](/members-roles) を参照してください。
## ステータス
Multica には7つのステータスがあります。**どのステータスからでも、ほかのどのステータスへも直接移動できます** — Multica はワークフローを強制せず、`backlog` から `done` へ一気に飛んでも止めません。
| ステータス | 意味 |
|---|---|
| `backlog` | まだ予定に入っていない |
| `todo` | 予定が決まり、着手できる |
| `in_progress` | 作業中 |
| `in_review` | レビュー待ち |
| `done` | 完了 |
| `blocked` | 外部要因で止まっている |
| `cancelled` | キャンセル済み |
イシューがエージェントに割り当てられると、エージェントは自動的にステータスを `backlog` / `todo` から `in_progress` に移し、完了すると `done` にします。いつでも手動で変更することもできます。
## 優先度
優先度には5段階があり、デフォルトのイシュー一覧の並び替えに使われます:
| 優先度 | 用途 |
|---|---|
| `No priority` | まだ決めていない(デフォルト) |
| `Urgent` | 緊急 |
| `High` | 高 |
| `Medium` | 中 |
| `Low` | 低 |
## イシュー番号
すべてのイシューには、ワークスペース内で一意の番号が `<prefix>-<digits>` 形式で付きます — 例えば `MUL-123` のように。番号は作成時にシステムが付与し、**決して変わりません**。[ワークスペース → イシュー番号](/workspaces#issue-numbers) を参照してください。
## コメント
イシューの下のコメントスレッドは、協業が行われる場所です — コメントに返信し、人やエージェントを `@` でメンションし、リアクションを追加できます。
コメントでエージェントを `@` でメンションすると **自動的にトリガーされます** — これは「割り当て」と並ぶ、エージェントを起動する2つ目の方法です。[コメントとメンション](/comments) と [コメントでエージェントをメンションする](/mentioning-agents) を参照してください。
## イシューを削除する
<Callout type="warning">
イシューを削除すると、その下のすべてのコメント、リアクション、添付ファイルと、キューに入っているエージェントのタスクが **即座に** 消えます(実行中のタスクはキャンセルされます)。**元に戻せません。**
単にイシューを見えないようにしたいだけなら、**ステータスを `cancelled` に変更するほうが削除より安全です** — データは残り、後から戻すことができます。
</Callout>
## プロジェクト
プロジェクトは、複数のイシューをまとめるコンテナです。イシューは最大1つのプロジェクトに属するか、どのプロジェクトにも属さないかのいずれかです。
プロジェクトには独自の **リード** がいます — **イシューの担当者と同じように、リードも人でもエージェントでもかまいません**。
プロジェクトを削除しても **その中のイシューは削除されません**: それらのイシューはプロジェクトから切り離されるだけで、ワークスペースにそのまま残ります。
## 次に読む
- [コメントとメンション](/comments) — イシューの下で協業する
- [エージェント](/agents) — 「エージェントに割り当てる」が実際にどう動くのかを理解する

View File

@@ -0,0 +1,79 @@
---
title: 이슈와 프로젝트
description: 사람 또는 에이전트에게 할당할 수 있는 Multica의 핵심 작업 단위.
---
import { Callout } from "fumadocs-ui/components/callout";
이슈는 Multica에서 독립적인 작업 단위입니다 — 버그, 새 기능, 처리해야 할 어떤 일이든 될 수 있습니다. 모든 이슈에는 **제목**, **설명**(Markdown 지원), **상태**, **우선순위**, **담당자**가 있으며, 선택적으로 **프로젝트**에 속할 수 있습니다. Linear나 Jira를 사용해 보셨다면 동일한 형태입니다.
**Multica의 가장 큰 특징은 이슈의 담당자가 사람일 수도, [에이전트](/agents)일 수도 있다는 점입니다** — 여기서부터 시작하겠습니다.
## 에이전트에게 이슈 할당하기
이슈를 에이전트에게 [할당](/assigning-issues)하면 그 작업을 에이전트에게 넘기는 것입니다. 에이전트는 **자동으로 시작합니다** — 몇 초 안에 실행을 시작하고, 댓글로 진행 상황을 보고하며, 완료되면 상태를 done으로 전환합니다. 동료에게 일을 넘기는 것과의 유일한 차이는 에이전트는 오프라인이 되지 않고, 알림이 필요 없으며, 연중무휴 24시간 사용할 수 있다는 점입니다.
<Callout type="info">
에이전트의 정체성, 구성, 실행 위치에 대해서는 [에이전트](/agents)를 참고하세요.
</Callout>
비공개 에이전트는 워크스페이스 owner와 admin만 이슈에 할당할 수 있습니다. 역할 권한에 대해서는 [멤버와 역할](/members-roles)을 참고하세요.
## 상태
Multica에는 일곱 가지 상태가 있습니다. **어떤 상태든 다른 어떤 상태로도 바로 이동할 수 있습니다** — Multica는 워크플로를 강제하지 않으며, `backlog`에서 곧바로 `done`으로 건너뛰어도 막지 않습니다.
| 상태 | 의미 |
|---|---|
| `backlog` | 아직 일정에 없음 |
| `todo` | 일정이 잡혔고, 시작할 준비됨 |
| `in_progress` | 작업 중 |
| `in_review` | 리뷰 대기 중 |
| `done` | 완료됨 |
| `blocked` | 외부 요인으로 막힘 |
| `cancelled` | 취소됨 |
이슈가 에이전트에게 할당되면, 에이전트는 자동으로 상태를 `backlog` / `todo`에서 `in_progress`로 옮기고, 완료 시 `done`으로 옮깁니다. 언제든지 직접 변경할 수도 있습니다.
## 우선순위
우선순위에는 다섯 단계가 있으며, 기본 이슈 목록의 정렬에 사용됩니다:
| 우선순위 | 용도 |
|---|---|
| `No priority` | 아직 결정되지 않음 (기본값) |
| `Urgent` | 긴급 |
| `High` | 높음 |
| `Medium` | 중간 |
| `Low` | 낮음 |
## 이슈 번호
모든 이슈에는 워크스페이스 내에서 고유한 번호가 `<prefix>-<digits>` 형식으로 부여됩니다 — 예를 들어 `MUL-123`처럼요. 번호는 생성 시점에 시스템이 부여하며 **절대 변하지 않습니다**. [워크스페이스 → 이슈 번호](/workspaces#issue-numbers)를 참고하세요.
## 댓글
이슈 아래의 댓글 스레드는 협업이 일어나는 곳입니다 — 댓글에 답글을 달고, 사람이나 에이전트를 `@`로 멘션하고, 반응을 추가할 수 있습니다.
댓글에서 에이전트를 `@`로 멘션하면 **자동으로 트리거됩니다** — 이것은 "할당"과 더불어 에이전트를 시작하는 두 번째 방법입니다. [댓글과 멘션](/comments)과 [댓글에서 에이전트 멘션하기](/mentioning-agents)를 참고하세요.
## 이슈 삭제하기
<Callout type="warning">
이슈를 삭제하면 그 아래의 모든 댓글, 반응, 첨부 파일과 대기 중인 에이전트 작업이 **즉시** 삭제됩니다(실행 중인 작업은 취소됩니다). **되돌릴 수 없습니다.**
이슈를 단지 보이지 않게 하고 싶을 뿐이라면, **상태를 `cancelled`로 변경하는 것이 삭제보다 안전합니다** — 데이터가 남아 있어 나중에 다시 가져올 수 있습니다.
</Callout>
## 프로젝트
프로젝트는 여러 이슈를 하나로 묶는 컨테이너입니다. 이슈는 최대 하나의 프로젝트에 속하거나, 아예 어떤 프로젝트에도 속하지 않을 수 있습니다.
프로젝트에는 자체 **리더**가 있습니다 — **이슈의 담당자와 마찬가지로, 리더도 사람일 수도, 에이전트일 수도 있습니다**.
프로젝트를 삭제해도 **그 안의 이슈는 삭제되지 않습니다**: 해당 이슈들은 단지 프로젝트에서 분리되어 워크스페이스에 그대로 남습니다.
## 다음 단계
- [댓글과 멘션](/comments) — 이슈 아래에서 협업하기
- [에이전트](/agents) — "에이전트에게 할당"이 실제로 어떻게 작동하는지 이해하기

View File

@@ -0,0 +1,60 @@
---
title: メンバーと役割
description: ワークスペースの3つの役割owner、admin、memberがそれぞれ何をできるのか、そして人をどのように招待するのかを説明します。
---
import { Callout } from "fumadocs-ui/components/callout";
[ワークスペース](/workspaces)に属するすべての人は役割を持ち、その役割によって何ができるかが決まります。Multica には3つの役割があります。**owner**(ワークスペースのオーナー)、**admin**、**member** です。[イシュー](/issues)の作成、[コメント](/comments)の作成、[エージェント](/agents)の利用といった日常的な作業のほとんどは、3つの役割すべてで利用できます。**違いはチーム管理の領域に集中しています。**
## 権限の一覧
以下の表は、チーム管理アクションにおける最も重要な違いをまとめたものです。
| アクション | owner | admin | member |
|---|---|---|---|
| 新しい admin または member を招待 | ✓ | ✓ | ✗ |
| **新しい owner を招待** | ✓ | ✗ | ✗ |
| admin または member を降格 / 削除 | ✓ | ✓ | ✗ |
| **別の owner を降格 / 削除** | ✓ | ✗ | ✗ |
| ワークスペースの削除 | ✓ | ✗ | ✗ |
**member は誰も招待できません** — 招待は admin 層の権限です。**owner だけが他の人を owner に昇格できます** — admin は member や他の admin を昇格・降格できますが、新しい owner を作成することはできません。同様に、admin は member や他の admin を削除できますが、**既存の owner には手を出せません**。要点は、最上位の層をすでに保有している人だけがその層を付与できるようにすることです — 権限は上方向に漏れません。
<Callout type="info">
エージェントの可視性には「workspace」と「private」の2種類があります。private エージェントは owner と admin だけがイシューに割り当てられます — これは特定の人だけが利用するように作られた構成を保護するためです。[エージェント](/agents)を参照してください。
</Callout>
## 新しいメンバーを招待する
Multica はメールで新しいメンバーを招待します。
1. ワークスペース設定ページで **メンバーを招待** をクリックし、メールアドレスを入力して役割を選択します。
2. Multica が一意のリンクを含む招待メールを送信します。
3. 受信者がリンクをクリックしてログイン(または登録)し、**招待を承諾** するとワークスペースに参加します。
招待されるメールアドレスは **あらかじめ Multica に登録されている必要はありません** — アカウントがなければ、招待を承諾した時点で自動的に作成されます。
招待メールの配信に失敗しても(誤ったアドレス、メールサービスの不具合など)、招待レコードはそのまま保持されます。ワークスペース設定からメールを再送するか、招待リンクを別の経路で共有できます。
招待は **7日間有効** です。それ以降にリンクをクリックすると「期限切れ」のメッセージが表示され、招待した人が新しく送り直す必要があります。
## 常に最低1人の owner を維持する
すべてのワークスペースには **常に最低1人の owner が存在しなければなりません**。この制約により、2つの操作が自動的にブロックされます。
- 最後の owner は自分自身を降格できません。
- 他の owner や admin は、最後の owner を削除できません。
<Callout type="warning">
あなたが最後の owner でチームを離れようとしている場合は、**まず owner の役割を別のメンバーに譲渡してから**、ワークスペースを離れるか引き継いでください。そうしないと操作が拒否されます。
</Callout>
## メンバーを削除する
owner と admin は、ワークスペースから他のメンバーを削除できます。削除されたメンバーは即座にアクセス権を失います。そのメンバーが作成したイシュー、コメントなどのコンテンツは、ワークスペースにそのまま保持されます。
## 次へ
- [イシューとプロジェクト](/issues) — メンバーが取り組む対象
- [コメントとメンション](/comments) — イシューの下で協業する

View File

@@ -0,0 +1,60 @@
---
title: 멤버와 역할
description: 워크스페이스의 세 가지 역할 — owner, admin, member — 가 각각 무엇을 할 수 있는지, 그리고 사람을 어떻게 초대하는지 설명합니다.
---
import { Callout } from "fumadocs-ui/components/callout";
[워크스페이스](/workspaces)에 속한 모든 사람은 역할을 가지며, 역할에 따라 무엇을 할 수 있는지가 결정됩니다. Multica에는 세 가지 역할이 있습니다. **owner**(워크스페이스의 소유자), **admin**, **member**입니다. [이슈](/issues) 생성, [댓글](/comments) 작성, [에이전트](/agents) 사용 같은 대부분의 일상 작업은 세 역할 모두 사용할 수 있습니다. **차이는 팀 관리 영역에 집중되어 있습니다.**
## 권한 한눈에 보기
아래 표는 팀 관리 작업에서 나타나는 가장 중요한 차이를 정리한 것입니다.
| 작업 | owner | admin | member |
|---|---|---|---|
| 새 admin 또는 member 초대 | ✓ | ✓ | ✗ |
| **새 owner 초대** | ✓ | ✗ | ✗ |
| admin 또는 member 강등 / 제거 | ✓ | ✓ | ✗ |
| **다른 owner 강등 / 제거** | ✓ | ✗ | ✗ |
| 워크스페이스 삭제 | ✓ | ✗ | ✗ |
**member는 누구도 초대할 수 없습니다** — 초대는 admin 등급의 권한입니다. **owner만 다른 사람을 owner로 승격할 수 있습니다** — admin은 member나 다른 admin을 승격하거나 강등할 수 있지만, 새 owner를 만들 수는 없습니다. 마찬가지로 admin은 member나 다른 admin을 제거할 수 있지만 **기존 owner는 건드릴 수 없습니다**. 핵심은 최고 등급을 이미 보유한 사람만이 그 등급을 부여할 수 있도록 하는 것입니다 — 권한은 위쪽으로 새어 나가지 않습니다.
<Callout type="info">
에이전트 가시성에는 "workspace"와 "private" 두 가지가 있습니다. private 에이전트는 owner와 admin만 이슈에 할당할 수 있습니다 — 이는 특정 사람들만 사용하도록 만든 구성을 보호하기 위함입니다. [에이전트](/agents)를 참고하세요.
</Callout>
## 새 멤버 초대하기
Multica는 이메일로 새 멤버를 초대합니다.
1. 워크스페이스 설정 페이지에서 **멤버 초대**를 클릭하고, 이메일을 입력한 뒤 역할을 선택합니다.
2. Multica가 고유 링크가 담긴 초대 이메일을 보냅니다.
3. 수신자가 링크를 클릭하고 로그인(또는 가입)한 뒤 **초대를 수락**하면 워크스페이스에 합류합니다.
초대받는 이메일은 **미리 Multica에 등록되어 있을 필요가 없습니다** — 계정이 없으면 초대를 수락할 때 자동으로 생성됩니다.
초대 이메일 전송이 실패하더라도(잘못된 주소, 메일 서비스 장애 등) 초대 기록은 그대로 유지됩니다. 워크스페이스 설정에서 이메일을 다시 보내거나, 초대 링크를 다른 경로로 공유할 수 있습니다.
초대는 **7일간 유효합니다**. 그 이후에 링크를 클릭하면 "만료됨" 메시지가 표시되며, 초대한 사람이 새로 보내야 합니다.
## 항상 최소 한 명의 owner 유지
모든 워크스페이스에는 **항상 최소 한 명의 owner가 있어야 합니다**. 이 제약은 두 가지 작업을 자동으로 차단합니다.
- 마지막 owner는 자신을 강등할 수 없습니다.
- 다른 owner나 admin은 마지막 owner를 제거할 수 없습니다.
<Callout type="warning">
당신이 마지막 owner이고 팀을 떠나려 한다면, **먼저 owner 역할을 다른 멤버에게 양도한 뒤** 워크스페이스를 떠나거나 넘기세요. 그렇지 않으면 작업이 거부됩니다.
</Callout>
## 멤버 제거하기
owner와 admin은 워크스페이스에서 다른 멤버를 제거할 수 있습니다. 제거된 멤버는 즉시 접근 권한을 잃습니다. 그 멤버가 만든 이슈, 댓글 등의 콘텐츠는 워크스페이스에 그대로 유지됩니다.
## 다음
- [이슈와 프로젝트](/issues) — 멤버가 작업하는 대상
- [댓글과 멘션](/comments) — 이슈 아래에서 협업하기

View File

@@ -0,0 +1,63 @@
---
title: "コメントでエージェントを @メンションする"
description: コメントで @ を使ってエージェントをメンションし、ちょっと見てもらいましょう — 担当者の変更も、ステータスの変更もなく、割り当てより軽い操作です。
---
import { Callout } from "fumadocs-ui/components/callout";
[コメント](/comments)で[エージェント](/agents)を `@`メンションするのは、より軽いトリガーです — **担当者の変更も、ステータスの変更もなく**、ただエージェントに現在の[イシュー](/issues)を見てもらうよう軽く促すだけです。[**割り当て**](/assigning-issues)(エージェントを担当者にしてイシューを渡すこと)と比べると、@メンションは「この部分をちょっと見て」「別の角度から分析して」「ちょっと呼び込んで一緒に議論しよう」といった場面に向いています。
## コメントでエージェントをメンションする
メンバーをメンションするのと同じです — `@` を入力してピッカーを開き、エージェントを選んでください。コメントが投稿されると、Multica はメンションされた各エージェントに対して**そのコメント**をトリガーコンテキストとして、すぐに `task` をキューに入れます。エージェントがタスクを受け取ると、次のものを読めます。
- イシュー全体(説明 + すべての過去のコメント)
- トリガーコメント自体 — 今回の実行の起点として
`@mention` の Markdown 構文、ピッカー、そして `@all` のセマンティクスは [**コメント**](/comments)で扱います。
<Callout type="info">
**コメントで[スクワッド](/squads)を `@`メンションすることもできます。** 同じピッカーがメンバーやエージェントと並べてスクワッドも表示します。スクワッドを選ぶと `[@SquadName](mention://squad/<uuid>)` が挿入され、スクワッドの**リーダーエージェント**が応答を調整するようトリガーされます — 担当者とステータスはそのまま維持されます。
</Callout>
## 割り当てとどう違うか
どちらもエージェントを働かせますが、仕組みはまったく異なります。
| 観点 | 割り当て | @メンション |
|---|---|---|
| `assignee` の変更 | ✓ | ✗ |
| `status` の変更 | ✗ | ✗ |
| `task` のキュー追加 | すぐにBacklog 以外) | すぐに |
| トリガーコメント ID | 任意 | 常に現在のコメントを含む |
| 1 回の操作あたりの対象エージェント | 1担当者 1 名) | 複数1 つのコメントで複数を @ 可能) |
| 優先度 | イシューから継承 | イシューから継承 |
判断の目安は単純です。**エージェントに「これからこのイシューを所有してほしい」なら割り当てを、「現在のコンテキストをちょっと見てほしい」なら @メンションを使ってください。**
## 複数のエージェントを @ するとどうなるか
1 つのコメントで複数のエージェントを @メンションすると、各エージェントは自分のランタイムで独立した `task` をキューに受け取ります — **互いをブロックすることなく並列に実行されます**。
同じイシューで、あるエージェントがすでに `queued` または `dispatched` 状態の `task` を持っている場合(たとえば、たった今メンションされてまだ開始していない場合)、今回のメンションは**重複排除**され、重複した `task` はキューに追加されません。重複排除は**単一のコメント単位で**適用されます — 数秒間隔で投稿された別々の 2 つのコメントが両方とも同じエージェントを @ すると、どちらも `task` をキューに入れます。
<Callout type="warning">
**コメントを編集して @ を追加しても、再トリガーされません。** 投稿した後で `@agent` を追加しなければと気づいた場合、編集で入れた `@` は表示される内容を変えるだけで、そのエージェントに新しい `task` を**届けません**。トリガーするには、新しいコメントを投稿するか、イシューをそのエージェントに割り当ててください。
</Callout>
## `@all` はどのエージェントもトリガーしない
`@all` で全員を呼ぶとき、**ワークスペースのメンバーだけがインボックスに入り、エージェントは `@all` の展開に含まれません。** これは意図された設計です。エージェントはインボックス通知を受け取らないため、`@all` はエージェントにとって意味を持ちません。エージェントを働かせるには、名前で直接メンションしてください。
## エージェントが自分自身を @メンションしてもループしない
エージェントは実行中にコメントを投稿でき、そのコメントには `@mention` が含まれることがあります。Multica にはハードコードされたガードがあります。**コメントの作成者が `@` メンションの対象エージェントと同じ場合、そのメンションはスキップされます** — 「エージェント A がエージェント A を @ → 新しい task → 再びエージェント A を @」のような無限ループは発生しません。
このガードは**直接的な自己参照だけをブロックします。** エージェント A がエージェント B を @メンションするのは正常に動作し、その後 B が応答で A を @メンションすると A が再びトリガーされます — つまり**間接的な再帰はブロックされません**。エージェントの指示を書くときは、複数のエージェントが互いを @メンションして循環を作らないよう注意してください。
## 次へ
- [**スクワッド**](/squads) — スクワッドを `@`メンションすると、リーダーが質問を適切なメンバーにルーティングします
- [**チャット**](/chat) — イシューと無関係な 1 対 1 の会話
- [**オートパイロット**](/autopilots) — エージェントがスケジュールに沿って自動的に作業を開始するようにする
- [**コメント**](/comments) — `@mention` の構文、ピッカー、そして `@all` のセマンティクス

View File

@@ -0,0 +1,63 @@
---
title: "댓글에서 에이전트 @-멘션하기"
description: 댓글에서 @로 에이전트를 멘션해 살펴보게 하세요 — 담당자 변경도, 상태 변경도 없이, 할당보다 가볍습니다.
---
import { Callout } from "fumadocs-ui/components/callout";
[댓글](/comments)에서 [에이전트](/agents)를 `@`-멘션하는 것은 더 가벼운 트리거입니다 — **담당자 변경도, 상태 변경도 없이**, 그저 에이전트가 현재 [이슈](/issues)를 살펴보도록 살짝 알리는 것입니다. [**할당**](/assigning-issues)(에이전트를 담당자로 만들고 이슈를 넘기는 것)과 비교하면, @-멘션은 "이 부분 좀 봐줘", "다른 관점으로 분석해줘", "잠깐 끌어들여서 같이 논의하자" 같은 상황에 잘 맞습니다.
## 댓글에서 에이전트 멘션하기
멤버를 멘션하는 것과 동일합니다 — `@`를 입력해 피커를 열고 에이전트를 선택하세요. 댓글이 게시되면 Multica는 멘션된 각 에이전트에게 **그 댓글**을 트리거 컨텍스트로 삼아 즉시 `task`를 대기열에 넣습니다. 에이전트가 작업을 받으면 다음을 읽을 수 있습니다.
- 이슈 전체(설명 + 모든 과거 댓글)
- 트리거 댓글 자체 — 이번 실행의 시작점으로
`@mention` Markdown 문법, 피커, 그리고 `@all` 의미는 [**댓글**](/comments)에서 다룹니다.
<Callout type="info">
**댓글에서 [스쿼드](/squads)도 `@`-멘션할 수 있습니다.** 동일한 피커가 멤버 및 에이전트와 함께 스쿼드도 보여줍니다. 스쿼드를 선택하면 `[@SquadName](mention://squad/<uuid>)`이 삽입되고, 스쿼드의 **리더 에이전트**가 응답을 조율하도록 트리거됩니다 — 담당자와 상태는 그대로 유지됩니다.
</Callout>
## 할당과 어떻게 다른가
둘 다 에이전트를 일하게 만들지만, 동작 방식은 완전히 다릅니다.
| 항목 | 할당 | @-멘션 |
|---|---|---|
| `assignee` 변경 | ✓ | ✗ |
| `status` 변경 | ✗ | ✗ |
| `task` 대기열 추가 | 즉시(백로그가 아닌 경우) | 즉시 |
| 트리거 댓글 ID | 선택 사항 | 항상 현재 댓글을 포함 |
| 한 번의 동작당 대상 에이전트 | 1명(담당자 한 명) | 다수(댓글 하나에서 여러 명을 @ 가능) |
| 우선순위 | 이슈에서 상속 | 이슈에서 상속 |
판단 기준은 간단합니다. **에이전트가 "지금부터 이 이슈를 책임지길" 원하면 할당을, "현재 컨텍스트를 한번 살펴보길" 원하면 @-멘션을 사용하세요.**
## 여러 에이전트를 @ 하면 어떻게 되는가
댓글 하나에서 여러 에이전트를 @-멘션하면, 각 에이전트는 자신의 런타임에서 독립적인 `task`를 대기열에 받습니다 — **서로를 막지 않고 병렬로 실행됩니다**.
같은 이슈에서 어떤 에이전트가 이미 `queued` 또는 `dispatched` 상태의 `task`를 가지고 있다면(예: 방금 멘션되었고 아직 시작하지 않은 경우), 이번 멘션은 **중복 제거**되어 중복 `task`가 대기열에 추가되지 않습니다. 중복 제거는 **단일 댓글 단위로** 적용됩니다 — 몇 초 간격으로 게시된 서로 다른 두 댓글이 모두 같은 에이전트를 @ 하면, 둘 다 `task`를 대기열에 넣습니다.
<Callout type="warning">
**댓글을 편집해 @를 추가해도 다시 트리거되지 않습니다.** 게시한 뒤에야 `@agent`를 추가해야겠다고 떠올렸다면, 편집으로 넣은 `@`는 표시되는 내용만 바꿀 뿐 — 그 에이전트에게 새 `task`를 **전달하지 않습니다**. 트리거하려면 새 댓글을 게시하거나 이슈를 그 에이전트에게 할당하세요.
</Callout>
## `@all`은 어떤 에이전트도 트리거하지 않는다
`@all`로 전체를 호출할 때, **워크스페이스 멤버만 인박스에 들어가며 — 에이전트는 `@all` 확장에 포함되지 않습니다.** 이는 의도된 설계입니다. 에이전트는 인박스 알림을 받지 않으므로 `@all`은 에이전트에게 아무 의미가 없습니다. 에이전트를 일하게 하려면 이름으로 직접 멘션하세요.
## 에이전트가 자기 자신을 @-멘션해도 루프에 빠지지 않는다
에이전트는 실행 중에 댓글을 게시할 수 있으며, 그 댓글에는 `@mention`이 포함될 수 있습니다. Multica에는 하드코딩된 가드가 있습니다. **댓글 작성자가 `@` 멘션의 대상 에이전트와 동일하다면, 그 멘션은 건너뜁니다** — "에이전트 A가 에이전트 A를 @ → 새 task → 다시 에이전트 A를 @" 같은 무한 루프는 발생하지 않습니다.
이 가드는 **직접적인 자기 참조만 차단합니다.** 에이전트 A가 에이전트 B를 @-멘션하는 것은 정상적으로 동작하며, 그 후 B가 응답에서 A를 @-멘션하면 A가 다시 트리거됩니다 — 다시 말해 **간접 재귀는 차단되지 않습니다**. 에이전트 지침을 작성할 때 여러 에이전트가 서로를 @-멘션해 순환을 이루지 않도록 주의하세요.
## 다음
- [**스쿼드**](/squads) — 스쿼드를 `@`-멘션하면 리더가 질문을 적절한 멤버에게 라우팅합니다
- [**채팅**](/chat) — 이슈 밖에서의 일대일 대화
- [**오토파일럿**](/autopilots) — 에이전트가 일정에 따라 자동으로 작업을 시작하게 하세요
- [**댓글**](/comments) — `@mention` 문법, 피커, 그리고 `@all` 의미

View File

@@ -0,0 +1,46 @@
{
"title": "ドキュメント",
"pages": [
"index",
"how-multica-works",
"cloud-quickstart",
"self-host-quickstart",
"---ワークスペース & チーム---",
"workspaces",
"members-roles",
"issues",
"projects",
"comments",
"project-resources",
"---エージェント---",
"agents",
"agents-create",
"skills",
"squads",
"---エージェントの実行方法---",
"daemon-runtimes",
"install-agent-runtime",
"tasks",
"providers",
"---エージェントとの協業---",
"assigning-issues",
"mentioning-agents",
"chat",
"autopilots",
"---インボックス---",
"inbox",
"---連携---",
"github-integration",
"---セルフホスト & 運用---",
"environment-variables",
"auth-setup",
"troubleshooting",
"---リファレンス---",
"cli",
"auth-tokens",
"desktop-app",
"mobile-app",
"---開発者---",
"developers"
]
}

View File

@@ -0,0 +1,46 @@
{
"title": "Documentation",
"pages": [
"index",
"how-multica-works",
"cloud-quickstart",
"self-host-quickstart",
"---워크스페이스 & 팀---",
"workspaces",
"members-roles",
"issues",
"projects",
"comments",
"project-resources",
"---에이전트---",
"agents",
"agents-create",
"skills",
"squads",
"---에이전트 실행 방식---",
"daemon-runtimes",
"install-agent-runtime",
"tasks",
"providers",
"---에이전트와 협업---",
"assigning-issues",
"mentioning-agents",
"chat",
"autopilots",
"---인박스---",
"inbox",
"---연동---",
"github-integration",
"---자체 호스팅 & 운영---",
"environment-variables",
"auth-setup",
"troubleshooting",
"---참고---",
"cli",
"auth-tokens",
"desktop-app",
"mobile-app",
"---개발자---",
"developers"
]
}

View File

@@ -0,0 +1,82 @@
---
title: モバイルアプリ (iOS)
description: まだ App Store にないオープンソースの Multica iOS アプリを、自分の iPhone に自分でビルドする方法。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica の iOS クライアントはオープンソースで、web、desktop、バックエンドとともに[メインリポジトリ](https://github.com/multica-ai/multica)に含まれています。まだ App Store にはなく、その状況が変わるまでは、iPhone で使いたい人がソースから自分でビルドします。ビルドは初回は約 10〜20 分、それ以降は約 2 分かかり、[multica.ai](https://multica.ai) と同じバックエンドと通信するため、既存のアカウントがそのまま使えます。
<Callout type="info">
このページは**個人利用**向けです。アプリ開発者はリポジトリの [`apps/mobile/README.md`](https://github.com/multica-ai/multica/blob/main/apps/mobile/README.md) を読んでください — dev / staging のバリアントと全スクリプト一覧を扱っています。
</Callout>
## 必要なもの
- Xcode がインストールされた **Mac**App Store から無料で入手できます)。
- Xcode → Settings → Accounts に追加した無料の **Apple ID**。有料の Apple Developer Program アカウントは任意で、7 日間の署名期間を 1 年に延長するだけです — 下記の [7 日制限](#7-day-signing-limit)を参照してください。
- USB ケーブルで接続され、[Developer Mode が有効になった](https://docs.expo.dev/guides/ios-developer-mode/) **iPhone**(設定 → プライバシーとセキュリティ → デベロッパモード)。
- チェックアウトした Multica のソースコード:
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
pnpm install
```
この一覧に欠けているものがあれば、Expo の [Set up your environment](https://docs.expo.dev/get-started/set-up-your-environment/) に沿って進めてください(**Development build → iOS Device** を選択)。リポジトリのチェックアウトを除くすべてに関する公式のセットアップガイドです。
## ビルドする
コマンドは 1 つ:
```bash
pnpm ios:mobile:device:prod:release
```
Xcode は Apple ID が自動的に所有する "Personal Team" でビルドに署名します — この team はどの Apple ID であれ初めて Xcode にサインインしたときに静かに作成されるため、何かを設定した記憶がなくてもすでに存在しています。これは **Release ビルド**です。Metro への依存がなく、スプラッシュ画面 → アプリへとつながり、App Store からインストールしたものとまったく同じです。
初回ビルドは CocoaPods をダウンロードし、React Native をソースからコンパイルします — 10〜20 分ほど見込んでください。以降のビルドは Xcode のキャッシュを再利用します。
一般的な手順はこれで終わりです。署名が失敗したら [トラブルシューティング](#troubleshooting)に進んでください。
## 7 日間の署名制限
無料の Apple ID はビルドを **7 日間**署名します。それを過ぎると、アプリは iPhone での起動を拒否し、「untrusted developer」エラーを表示します。Mac に再び接続して同じコマンドを再実行し、再署名してください — データはアプリではなくバックエンドにあるため、そのまま保持されます。
これを延長する唯一の方法は **Apple Developer Program アカウント**です([developer.apple.com](https://developer.apple.com) で年間 $99。すると署名は更新の間 1 年間有効になり、TestFlight を通じて他のデバイスに配布することもできます。
## 更新
まだ自動更新はありません。Multica のコードベースが進んだら、pull して再ビルドしてください。
```bash
git pull
pnpm install
pnpm ios:mobile:device:prod:release
```
Xcode がネイティブコンパイルをキャッシュするため、以降のビルドは高速です。
## なぜまだ App Store にないのか
iOS アプリはまだ速いペースで動いています — チームは今のところ App Store の審査サイクルよりも、リリースして反復改善することを好んでいます。正式な App Store リリースの前に TestFlight ベータが最も可能性の高い次のステップです。それまでは、上記の自前ビルドが iOS で Multica を使う唯一の方法です。
TestFlight が公開されたときに通知を受け取りたい場合は、[GitHub リポジトリ](https://github.com/multica-ai/multica)を watch してください。
## トラブルシューティング
**「No matching provisioning profiles found」** — Xcode がデフォルトのバンドル id `ai.multica.mobile` をあなたの Apple ID で署名することを拒否しています。まれですが、誰かが Apple の開発者ポータルでそのプレフィックスを登録していると発生します。自分が管理する任意の逆引きドメイン(`com.yourname.multica` で十分です)を選んで export し、再実行してください。
```bash
export EXPO_BUNDLE_IDENTIFIER_PROD=com.yourname.multica
pnpm ios:mobile:device:prod:release
```
id 自体に意味がある必要はありません — Apple は単に他の team に占有されていないことだけを求めています。
**「Could not launch &lt;app&gt;」/「Untrusted Developer」** — 7 日制限に達したかビルドを再実行してください、iPhone で開発者プロファイルを手動で信頼する必要があります。設定 → 一般 → VPN とデバイス管理 → あなたの Apple ID をタップ → 信頼。
**`Pod install` でビルドが止まる、または延々とコンパイルが続く** — 初回ビルドは CocoaPods が依存関係をダウンロードし、Xcode が React Native をソースからコンパイルするため、実際に 10〜20 分かかります。以降のビルドははるかに高速です。
**アプリがバックエンドに接続できない** — `apps/mobile/.env.production` が変更されていないか確認してください(デフォルトでは `EXPO_PUBLIC_API_URL=https://api.multica.ai` が同梱されています)。変更した場合は `git checkout apps/mobile/.env.production` で復元してください。

View File

@@ -0,0 +1,82 @@
---
title: 모바일 앱 (iOS)
description: 아직 App Store에 없는 오픈소스 Multica iOS 앱을 직접 본인 iPhone에 빌드하는 방법.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica의 iOS 클라이언트는 오픈소스이며, web, desktop, 백엔드와 함께 [메인 저장소](https://github.com/multica-ai/multica)에 들어 있습니다. 아직 App Store에는 없으며, 그 상황이 바뀌기 전까지는 iPhone에서 사용하려는 사람이 직접 소스에서 빌드합니다. 빌드는 처음에는 약 10~20분, 그 이후에는 약 2분이 걸리며, [multica.ai](https://multica.ai)와 동일한 백엔드와 통신하므로 기존 계정이 그대로 동작합니다.
<Callout type="info">
이 페이지는 **개인 사용자**를 위한 것입니다. 앱 개발자는 저장소의 [`apps/mobile/README.md`](https://github.com/multica-ai/multica/blob/main/apps/mobile/README.md)를 읽어야 합니다 — dev / staging 변형과 전체 스크립트 목록을 다룹니다.
</Callout>
## 필요한 것
- Xcode가 설치된 **Mac** (App Store에서 무료로 받을 수 있습니다).
- Xcode → Settings → Accounts에 추가한 무료 **Apple ID**. 유료 Apple Developer Program 계정은 선택 사항이며, 7일 서명 기간을 1년으로 늘려줄 뿐입니다 — 아래 [7일 제한](#7-day-signing-limit)을 참고하세요.
- USB 케이블로 연결되어 있고 [Developer Mode가 활성화된](https://docs.expo.dev/guides/ios-developer-mode/) **iPhone** (설정 → 개인 정보 보호 및 보안 → 개발자 모드).
- 체크아웃한 Multica 소스 코드:
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
pnpm install
```
이 목록에서 빠진 것이 있다면, Expo의 [Set up your environment](https://docs.expo.dev/get-started/set-up-your-environment/)를 따라 진행하세요 (**Development build → iOS Device**를 선택). 저장소 체크아웃을 제외한 모든 것에 대한 공식 설정 가이드입니다.
## 빌드하기
명령어 하나:
```bash
pnpm ios:mobile:device:prod:release
```
Xcode는 Apple ID가 자동으로 소유하는 "Personal Team"으로 빌드에 서명합니다 — 이 team은 어떤 Apple ID로든 처음 Xcode에 로그인할 때 조용히 생성되므로, 무언가를 설정한 기억이 없더라도 이미 존재합니다. 이것은 **Release 빌드**입니다: Metro 의존성이 없고, 스플래시 화면 → 앱으로 이어지며, App Store에서 설치한 것과 똑같습니다.
첫 빌드는 CocoaPods를 다운로드하고 React Native를 소스에서 컴파일합니다 — 10~20분 정도 예상하세요. 이후 빌드는 Xcode의 캐시를 재사용합니다.
일반적인 경로는 이것으로 끝입니다. 서명이 실패하면 [문제 해결](#troubleshooting)로 이동하세요.
## 7일 서명 제한
무료 Apple ID는 빌드를 **7일** 동안 서명합니다. 그 이후에는 앱이 iPhone에서 실행을 거부하고 "untrusted developer" 오류를 표시합니다. Mac에 다시 연결하고 동일한 명령어를 다시 실행하여 재서명하세요 — 데이터는 앱이 아니라 백엔드에 있으므로 그대로 유지됩니다.
이를 연장하는 유일한 방법은 **Apple Developer Program 계정**입니다 ([developer.apple.com](https://developer.apple.com)에서 연 $99). 그러면 서명이 갱신 사이 1년 동안 유효하며, TestFlight를 통해 다른 기기에 배포할 수도 있습니다.
## 업데이트
아직 자동 업데이트는 없습니다. Multica 코드베이스가 앞으로 나아가면, pull한 후 다시 빌드하세요:
```bash
git pull
pnpm install
pnpm ios:mobile:device:prod:release
```
Xcode가 네이티브 컴파일을 캐시하므로 이후 빌드는 빠릅니다.
## 아직 App Store에 없는 이유
iOS 앱은 여전히 빠르게 움직이고 있습니다 — 팀은 지금 App Store 심사 주기보다 출시 후 반복 개선을 선호합니다. 정식 App Store 출시 전에 TestFlight 베타가 가장 유력한 다음 단계입니다. 그때까지는 위의 직접 빌드 방식이 iOS에서 Multica를 사용하는 유일한 방법입니다.
TestFlight가 열릴 때 알림을 받고 싶다면 [GitHub 저장소](https://github.com/multica-ai/multica)를 watch하세요.
## 문제 해결
**"No matching provisioning profiles found"** — Xcode가 기본 번들 id `ai.multica.mobile`를 Apple ID로 서명하기를 거부합니다. 드물지만, 누군가 Apple 개발자 포털에서 해당 접두사를 등록했다면 발생합니다. 본인이 제어하는 아무 역방향 도메인(`com.yourname.multica`이면 충분합니다)을 골라 export한 후 다시 실행하세요:
```bash
export EXPO_BUNDLE_IDENTIFIER_PROD=com.yourname.multica
pnpm ios:mobile:device:prod:release
```
id 자체는 어떤 의미가 있을 필요가 없습니다 — Apple은 단지 다른 team이 점유하지 않았다는 것만 요구합니다.
**"Could not launch &lt;app&gt;" / "Untrusted Developer"** — 7일 제한에 도달했거나(빌드를 다시 실행하세요), iPhone에서 개발자 프로필을 수동으로 신뢰해야 합니다: 설정 → 일반 → VPN 및 기기 관리 → Apple ID를 탭 → 신뢰.
**`Pod install`에서 빌드가 멈추거나 끝없이 컴파일됨** — 첫 빌드는 CocoaPods가 의존성을 다운로드하고 Xcode가 React Native를 소스에서 컴파일하기 때문에 실제로 10~20분이 걸립니다. 이후 빌드는 훨씬 빠릅니다.
**앱이 백엔드에 연결할 수 없음** — `apps/mobile/.env.production`이 수정되지 않았는지 확인하세요(기본값은 `EXPO_PUBLIC_API_URL=https://api.multica.ai`입니다). 변경했다면 `git checkout apps/mobile/.env.production`으로 복원하세요.

View File

@@ -0,0 +1,262 @@
---
title: プロジェクトリソース
description: 型付きのポインターGit リポジトリ、ローカルディレクトリ、今後さらに増える種類)をプロジェクトに添付し、エージェントが範囲を限定したコンテキストとして取り込めるようにします。
---
**プロジェクトリソースProject Resource** は型付きのポインターです — Git リポジトリの URL、自分のマシン上のパス、いずれは Notion ページまで — これを[プロジェクト](/workspaces)に添付します。[エージェント](/agents)がそのプロジェクト内のイシューに対して実行されると、デーモンはプロジェクトのリソース一覧をエージェントの作業ディレクトリと[メタスキル](/skills)プロンプトに自動的に書き込みます。
その結果、エージェントは、どのリポジトリをチェックアウトすべきか(またはどのローカルディレクトリで作業すべきか)、そしてこのプロジェクトの「主要な参照資料」が何かを、誰もコンテキストをイシュー本文にコピー&ペーストしなくても把握できます。
## メンタルモデル
プロジェクトはもはや単なるラベルではありません。小さな **リソースコンテナ** です。
- プロジェクトは 0..N 個の **リソース** を持ちます。
- リソースは `resource_type`(例: `github_repo`、`local_directory`)と `resource_ref``resource_type` によって型が定まる JSON ペイロード)を持ちます。
- 新しいリソースタイプを追加するには、文字列1つ + ハンドラー1つを追加するだけです。**スキーマのマイグレーションも、フロントエンドの書き直しも不要です。**
この形は意図的なものです — Multica がエージェントプロバイダーですでに使っているのと同じパターンです: `type` の判別子1つと型付きのペイロード。スキーマを安定させるため、後で「Notion ページ」「Google Doc」「アップロードされたファイル」「外部 URL」を追加するのは、小さく追加的な変更で済みます。
現在、2つのリソースタイプが提供されています: [`github_repo`](#resource-type-github_repo)(タスクごとに隔離されたワークツリーへクローン)と [`local_directory`](#resource-type-local_directory)(特定のデーモンのマシン上のフォルダ内で直接実行)です。
## リソースタイプ: `github_repo`
デフォルトのリソースタイプです — タスクごとに隔離されたワークツリーへチェックアウトされます。
```json
{
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/owner/repo",
"default_branch_hint": "main"
}
}
```
`default_branch_hint` は任意です — 指定すると、デーモンがこれをメタスキルに出すため、エージェントはどのブランチを基準に作業すべきかを把握できます。
## リソースタイプ: `local_directory`
タスクごとに再クローンするのが現実的でないリポジトリ — 数ギガバイトのゲームのチェックアウト、大規模な monorepo、またはタスクごとのワークツリーモデルが煩わしいあらゆるプロジェクト — の場合、プロジェクトは代わりに **特定の[デーモン](/daemon-runtimes)のマシン上にある既存のディレクトリ** を指すことができます。エージェントは、クローンもコピーもワークツリーもなしに **そのフォルダ内で直接** 実行されます。
```json
{
"resource_type": "local_directory",
"resource_ref": {
"local_path": "/Users/me/code/big-game",
"daemon_id": "0001234e-…",
"label": "main checkout"
}
}
```
`github_repo` と比べたトレードオフは意図的です。バインドされたデーモンだけがそのディレクトリに対するタスクを取得でき、同じディレクトリに対するタスクは並列ではなく **直列で** 実行されます。その代わり、既存のチェックアウト、既存のブランチ、既存のダーティな状態をそのまま保てます — Multica は決して再クローンしません。
### `github_repo` より `local_directory` を選ぶとき
| 検討事項 | `github_repo`(ワークツリー) | `local_directory` |
| --- | --- | --- |
| タスクごとのチェックアウトコスト | 新規クローン + ワークツリー | なし — エージェントがその場で実行 |
| 同じリポジトリでの並行性 | 複数タスクを並列に | ディレクトリごとに一度に1つ |
| ブランチ / ダーティな状態 | タスクごとにデフォルトから新しいブランチを取得 | ディレクトリが現在持っているそのまま |
| 実行できる場所 | 任意のデーモン | ちょうど1つのデーモンバインドされたもの |
| ディスク使用量 | タスクごとにワークツリー1つ | オーバーヘッド0 — 既存のフォルダ |
次のいずれか **1つでも** 当てはまる場合は `local_directory` を選んでください。
1. **再クローンのコストが法外に大きい場合** — 数ギガバイトのゲームのチェックアウト、重い LFS アセットを持つ monorepo、またはタスクごとの `git clone` が実際の作業を圧倒するあらゆる場合。並行性をクローン不要の実行と引き換えにすることになります。
2. **変更がきめ細かく、変更が起きるそばからローカルでレビューしたい場合** — 単一のコンポーネントを繰り返し磨いていて、数分ごとにエージェントの編集と自分のエディターを行き来したく、`~/multica_workspaces/` から掘り出さなければならないタスクごとのワークツリーよりも、既存のチェックアウトを信頼できる情報源にしたい場合です。
どちらの場合でも受け入れるトレードオフは同じです: **このバージョンはファイル単位の書き込みロックを提供しません。** ディレクトリごとの直列ゲート同じフォルダで一度に1つのタスクが、別々の2つのイシューのエージェントが同時に同じファイルを触るのを防ぐ唯一の保護手段です。2つのイシューのエージェントを同じ `local_directory` に向けると、それらのタスクは並列化されずにキューに入ります — これは意図された動作です。同じコードベースで本物の並列性が必要なら、`github_repo` を使い続けてください。
### ローカルディレクトリを添付する
フォルダピッカーは **Desktop アプリ** にのみあります — Web アプリは OS のパスを読み取る方法がないため、「ローカルディレクトリを追加」ボタンはそこでは非表示になっています。Desktop では:
1. プロジェクトを開く → **Resources** パネル。
2. **ローカルディレクトリを追加** をクリックします。ネイティブのフォルダピッカーが開きます。
3. フォルダを選択します。そのパスは **この Desktop インストールが現在登録しているデーモン** にバインドされます — リソースレコードにはパスとそのデーモンの ID が一緒に保存されます。
Desktop では、このマシンのデーモンがオフラインのとき、またはプロジェクトにすでにこのデーモンへバインドされた `local_directory` があるとき、ボタンは表示されたまま **ヒントとともに無効化** されます — そのため *なぜ* 使えないのかが分かります。Web アプリではそもそもフォルダピッカーが一切ないため、ボタンは完全に非表示になります。)別のマシンのディレクトリをバインドするには、そのマシンに Desktop をインストールし、そこからリソースを追加してください。
CLI からも可能です(デーモン ID を自分で指定すれば、Web 専用環境でも動作します)。
```bash
multica project resource add <project-id> \
--type local_directory \
--local-path /Users/me/code/big-game \
--daemon-id <daemon-uuid> \
--ref-label "main checkout" # optional
multica project resource update <project-id> <resource-id> \
--local-path /Users/me/code/big-game-new
```
`--daemon-id` は `multica daemon list` から取得できます。CLI は、ペイロードを直接渡したい場合のための汎用的な `--ref '<json>'` の脱出口も受け付けます。
### パスのルール
添付するパスは、添付時の検証とタスクごとの検証の両方を通過しなければなりません。どちらもリソースを所有するデーモンが強制します — サーバーは JSON を保存するだけです。いずれかのルールに違反するパスは、型付きのエラーとともにタスクを失敗させ、あなたのディレクトリには手を付けずに残します。
- 必ず **絶対パス** でなければなりません。
- 必ず **存在** し、**ディレクトリ** でなければなりません(ファイル、ファイルへの symlink、デバイスードではなく
- デーモンプロセスが **読み書き可能** でなければなりません。
- システムルートやユーザープロファイル全体であってはいけません — `/`、`/Users`、`/home`、`/root`、`/etc`、`/tmp`、`/var`、`/usr`、`/opt`、`/Users/Shared`、自分の `$HOME`、任意の Windows ドライブルート(`C:\`、`D:\`、…)、または `C:\Users` / `C:\ProgramData` / `C:\Program Files` / `C:\Program Files (x86)` / `C:\Windows`。
- 上記のいずれかに解決される symlink は拒否され、OS がエイリアス処理するパスの正規形canonical formも同様です例: macOS で `/private/tmp` を入力するのは `/tmp` と同じように拒否されます)。
このブラックリストは意図的に攻撃的です — ホームディレクトリを選ぶと Multica のランタイムファイルがアカウントのルートに置かれることになりますが、これは決して望ましい結果ではありません。代わりにサブフォルダ(通常は実際のプロジェクトのチェックアウト)を選んでください。
### プロジェクト、デーモンごとに1つ
プロジェクトは **デーモンごとに最大1つの `local_directory`** しか持てません。同じデーモンに2つ目を追加しようとすると、API は `409` を返します。Desktop のボタンは上限にすでに達すると自動的に非表示になり、理由を説明するツールチップを表示します。
異なるデーモンは独立しています — 共有プロジェクトはチームメイトのマシンごとに1つずつ `local_directory` を持つことができ、それぞれが同じプロジェクトを別々のホストの別々のフォルダにバインドします。デーモンがタスクを取得するときは、自分の ID に一致する行を選び、残りは無視します。
### リソースタイプの混在、および複数の `local_directory` リソース
実際に登場する2つの横断的なリソース構成があります。
- **同じプロジェクトに `github_repo` + `local_directory`。** 一致する `local_directory` バインディングを持つデーモンでは、ローカルディレクトリが **優先** されます。エージェントはあなたのフォルダで実行され、デーモンはそのタスクのために `github_repo` ワークツリーを作成も使用もしません。(ワークスペースごとのリポジトリキャッシュは平常どおり同期される場合がありますが、これはこのタスクの作業ツリーとは無関係なバックグラウンドの動作です。)`github_repo` の URL は参照用として `.multica/project/resources.json` とエージェントの `## Repositories` セクションに依然として表示されますが、エージェントが編集する作業ツリーはワークツリーではなく、あなたのローカルなものです。このプロジェクトに対する `local_directory` 行を **持たない** デーモン別のマシン、またはそのチームメイトが1つを添付する前では、タスクは通常の `github_repo` ワークツリーのフローにフォールバックします。実質的に、ローカルディレクトリはワークツリーパスに対するデーモンごとのオーバーライドです。
- **同じプロジェクトに2つの `local_directory` リソース。** 各 `local_directory` はちょうど1つのデーモンにバインドされるため、これは別々の2つのマシンの間でのみ発生しますAPI は添付時に同じデーモンへの2つを拒否します。上記参照。タスクは、どのデーモンがローカルディレクトリを持っているかではなく、エージェントのランタイム割り当てによってルーティングされます。タスクは受信するエージェントのランタイムを所有するデーモンに届き、そのデーモンは自分の ID に一致する `local_directory` 行を選び、残りは無視します。ロードバランシングはありません — 特定のマシンにタスクを実行させたい場合は、そのマシンのランタイムにバインドされたエージェントをディスパッチしてください。
別の場所に1つがバインドされているプロジェクトに対して `local_directory` 行を持たないデーモンは **ブロックされません** — そのタスクは単に、プロジェクトの他のリソース(通常は `github_repo` のフォールバック)を通じて進みます。`local_directory` は、それがバインドされたデーモンに対してのみ意味があります。
### ローカルディレクトリに対してタスクを実行する
プロジェクトが受信デーモンにバインドされた `local_directory` を持つイシューでタスクがディスパッチされると、デーモンは次のことを行います。
1. パスを再検証します(上記のルール)。
2. symlink を解決した実際のパスをキーとして、ディレクトリごとのロックを取得します — そのため、同じフォルダに向かう2つの経路1つは symlink 経由、1つは直接も依然として直列化されます。
3. エージェントの `CLAUDE.md` / `AGENTS.md`(および `.multica/project/resources.json`)を **ユーザーのディレクトリ内に** 書き込みます。エージェントは、あなたが自分でそのフォルダを開いたのとまったく同じように、そこで作業します。
4. Multica のランタイム成果物(`output/`、`logs/`、`.gc_meta.json`)は、ユーザーのディレクトリの **外側** の別の envRoot に置きます。
同じディレクトリに対する2つ目のタスクが、1つ目のタスクの実行中に届くと、ステータス **ローカルディレクトリ待ちWaiting for local directory** で待機します。このステータスは、タスクがあるあらゆる場所で見えます — チャットのタスクピル、エージェントのバナー、実行ログ、アクティビティインジケーター — そして待機中のタスクはエージェントの「キュー済み」のプレゼンスにカウントされます。待機中のタスクをキャンセルすると、そのスロットが即座に解放されます。実行中のタスクをキャンセルすると、次のタスクが昇格します。
この待機はタイムアウトではありません — 待機中のタスクは、ロックが解放されるか、ユーザー / エージェントがキャンセルするまで待機し続けます。
### Multica があなたのディレクトリで触れるものと触れないもの
- **書き込みます**: `CLAUDE.md` / `AGENTS.md`(またはエージェントのプロバイダーに対応する同等物)と `.multica/project/resources.json` をディレクトリのルートに。そのためエージェントはメタスキルとリソース一覧を持ちます。コミットされたくない場合は、これらを `.gitignore` に追加してください。
- **書き込みます**: エージェントが行うと判断したあらゆるコード編集を — あなたが自分でローカルでエージェントを実行したのとまったく同じ方法で。
- **物理的に削除することは決してありません**: ディレクトリやその中の何も。ガベージコレクションはパスを認識します: `local_directory` の envRoot の場合、`workspacesRoot` の下にある自身の `output/` と `logs/` だけをクリーンアップし、ユーザーのディレクトリは立ち入り禁止として扱います。
### v1 の制限事項(後続作業で狭まる予定)
最初のリリースは意図的に `github_repo` より鋭い角を持って出荷されます。この一覧は時間とともに縮小していくと考えてください — ここに記載されているのは今日時点で事実の内容です。
- **自動ブランチ切り替えなし。** エージェントは、あなたがチェックアウトしているブランチで実行されます。重要なら、ディスパッチ前にブランチを切り替えてください。
- **ダーティツリーの保護や自動コミットなし。** コミットされていない変更はエージェントから見え、その場で変更される可能性があり、stash されません。ディレクトリを実際の作業ツリーとして扱い、危険な実行の前にコミットしてください。
- **自動 PR なし。** タスクが終わると、変更は作られたブランチのまま残ります — 何も push されず、PR も開かれません。準備ができたら、自分で push して PR を開いてください。
- **`waiting_local_directory` はステータスを示しますが、保有者は示しません。** バッジはタスクが待機していることを伝えます。どのタスクやどのファイルパスが現在ディレクトリを保有しているかは表示しません。
これらはローカルディレクトリ作業のエージェントタスクライフサイクルの後続項目として追跡されています。それが出荷されるまでは、`local_directory` を「エージェントがあなたのフォルダで、あなたがするのと同じ方法で実行する」ものとして扱ってください。
## プロジェクト作成時にリポジトリを添付する
**Web** または **Desktop** アプリで *新規プロジェクト* を開くと、ステータス / 優先度 / リードの横に **Repos** ピルが表示されるようになりました。ワークスペースにバインドされたリポジトリを選択する(またはアドホックな URL を貼り付ける)と、プロジェクトが作成される瞬間にそれらが `github_repo` リソースとして添付されます。
**CLI** から:
```bash
# Create + attach in one shot. The server attaches resources in the same
# transaction as the project create — invalid resources roll back the whole
# operation, so you never end up with a project that has half its resources.
multica project create \
--title "Agent UX 2026" \
--repo https://github.com/multica-ai/multica
# Manage resources later
multica project resource list <project-id>
multica project resource add <project-id> --type github_repo --url <url>
multica project resource remove <project-id> <resource-id>
# Generic escape hatch for any resource_type the server understands —
# no CLI change needed when a new type ships:
multica project resource add <project-id> \
--type notion_page \
--ref '{"page_id":"…","title":"…"}'
```
`--repo` は繰り返し指定できます。各値は別々の `github_repo` リソースとして添付されます。
## ランタイムにエージェントが見るもの
デーモンがプロジェクト内のイシューのためにエージェントを生成すると、2つのことが起こります。
### 1. `.multica/project/resources.json`
API レスポンスの構造化されたパススルーpass-throughで、エージェントの作業ディレクトリに書き込まれます。
```json
{
"project_id": "…",
"project_title": "Agent UX 2026",
"resources": [
{
"id": "…",
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/multica-ai/multica",
"default_branch_hint": "main"
}
}
]
}
```
スキル、ヘルパースクリプト、またはエージェント自身が、この実行のための *正確な* リソースの集合が必要なときに、このファイルをパースできます。
### 2. メタスキルプロンプトの「Project Context」セクション
エージェントの `CLAUDE.md` / `AGENTS.md`(プロバイダーによって異なる)には、人間が読める要約が含まれるようになりました。
```
## Project Context
This issue belongs to **Agent UX 2026**.
Project resources (also written to `.multica/project/resources.json`):
- **GitHub repo**: https://github.com/multica-ai/multica (default branch: `main`)
Resources are pointers — open them only when relevant to the task. For
`github_repo` resources, use `multica repo checkout <url>` to fetch the code.
```
このテキストは意図的に最小限です。完全なペイロードはディスク上にあり、プロンプトはエージェントがプロジェクトが存在することと何が添付されているかを把握できるように方向付けるだけです。
### 失敗モード
リソースの取得は **best-effort** です。API 呼び出しが失敗すると、プロンプトからプロジェクトセクションが省略され、ファイルも書き込まれませんが、タスクは依然として開始されます。エージェントは、欠落したプロジェクトコンテキストのために止まることは決してありません。
## 新しいリソースタイプを追加する
この抽象化の要点は、新しいタイプが安価だということです。完全な経路は:
1. **サーバーの検証器**`server/internal/handler/project_resource.go`)— `validateAndNormalizeResourceRef` に、新しいペイロードをパースして正規化する case を追加します。
2. **デーモンのメタスキルフォーマッター**`server/internal/daemon/execenv/runtime_config.go`)— `formatProjectResource` に case を追加し、エージェントのプロンプトが新しいタイプを読みやすい箇条書きとしてレンダリングするようにします。
3. **TypeScript の型**`packages/core/types/project.ts`)— `ProjectResourceType` を拡張し、ペイロードのインターフェースを追加します。
4. **UI レンダラー**`packages/views/projects/components/project-resources-section.tsx`)— `ResourceRow` に新しいタイプのための case を追加します。
**スキーマのマイグレーションも、新しい sqlc クエリも、新しいエンドポイントも、そして CLI の変更もありません** — CLI の汎用的な `--ref '<json>'` フラグが、検証器が理解するあらゆるペイロードを受け付けるため、新しいタイプの初日サポートは純粋に上記の4ステップだけです。後でタイプごとの CLI ショートカットを *任意で* 追加できますが、必須ではありません。)
同じ `project_resource` テーブルと同じ3つの CRUD 呼び出しが、すべてのタイプを処理します。
## ワークスペースのリポジトリ vs. プロジェクトのリポジトリ
エージェントに表示されるリポジトリ一覧(`CLAUDE.md` / `AGENTS.md` の `## Repositories` ブロック)は、デーモンのクレームハンドラーが次の優先順位で選びます。
- **プロジェクトが最低1つの `github_repo` リソースを持つ** → そのリポジトリだけがエージェントに出されます。ワークスペースにバインドされたリポジトリは、エージェントがこのイシューにどれが属するかを推測しなくて済むように、意図的に隠されます。
- **プロジェクトが `github_repo` リソースを持たない(またはイシューがプロジェクトに属さない)** → 従来どおりワークスペースのリポジトリ一覧にフォールバックします。
これによりエージェントの作業セットが引き締まります: プロジェクトがリポジトリについて明示的であれば、それが権威ある答えです。`.multica/project/resources.json` の構造化されたリソース一覧は常に完全な集合を運ぶため、すべてを検査したいスキルは依然としてそうできます。
デーモンはチェックアウト側でもこれを反映します: プロジェクト範囲の `github_repo` URL を持つタスクが届くと、それらの URL はエージェントが生成される前に、ワークスペースごとの許可リストにマージされ *同時に* ローカルのリポジトリキャッシュに同期されます。そのため、ワークスペースレベルでバインドされていないプロジェクトのリポジトリ URL も、依然として `multica repo checkout` の有効な引数になります — デーモンはそれを「構成されていない」として拒否しません。許可リストの分割は内部的なものです: ワークスペースにバインドされた URL とタスク範囲の URL は別々に追跡されるため、ワークスペースリポジトリの再読み込みが実行の途中でプロジェクト URL を誤って取り消すことはありません。
## ここで意図的に範囲に **含めなかった** もの
- **プロジェクト間の共有。** 今日時点で、各リソースはちょうど1つのプロジェクトにのみ存在します。
- **スキルごとのリソース範囲指定。** すべてのリソースは、エージェント実行のすべてのスキルから見えます。タイプを認識したフィルタリングは後続作業です。
- **キャッシュ / 同期。** `github_repo` は単なるメタデータです — チェックアウトは依然として必要に応じて `multica repo checkout` を通じて行われます。Notion / Google Docs のキャッシュされた文書テキストは、それらのタイプとともに提供される予定です。
これらは意図的な省略です — 最初のカットの目標は、可動部分を最小限にしてこの抽象化を検証することです。

View File

@@ -0,0 +1,262 @@
---
title: 프로젝트 리소스
description: 타입이 지정된 포인터(Git 저장소, 로컬 디렉터리, 그리고 추후 더 많은 종류)를 프로젝트에 연결해, 에이전트가 범위가 한정된 컨텍스트로 가져갈 수 있게 합니다.
---
**프로젝트 리소스(Project Resource)** 는 타입이 지정된 포인터입니다 — Git 저장소 URL, 본인 기기의 경로, 내일이면 Notion 페이지까지 — 이것을 [프로젝트](/workspaces)에 연결합니다. [에이전트](/agents)가 해당 프로젝트 안의 이슈에 대해 실행될 때, 데몬은 프로젝트의 리소스 목록을 에이전트의 작업 디렉터리와 [메타 스킬](/skills) 프롬프트에 자동으로 기록합니다.
그 결과: 에이전트는 어떤 저장소를 체크아웃해야 하는지(또는 어떤 로컬 디렉터리에서 작업해야 하는지), 그리고 이 프로젝트의 "주요 참고 자료"가 무엇인지를, 아무도 컨텍스트를 이슈 본문에 복사해 붙여 넣지 않아도 알게 됩니다.
## 멘탈 모델
프로젝트는 더 이상 단순한 라벨이 아닙니다. 작은 **리소스 컨테이너**입니다:
- 프로젝트는 0..N개의 **리소스**를 가집니다.
- 리소스는 `resource_type`(예: `github_repo`, `local_directory`)과 `resource_ref`(`resource_type`에 따라 타입이 정해지는 JSON 페이로드)를 가집니다.
- 새 리소스 타입을 추가하려면 문자열 하나 + 핸들러 하나만 추가하면 됩니다. **스키마 마이그레이션도, 프런트엔드 재작성도 필요 없습니다.**
이 형태는 의도적인 것입니다 — Multica가 에이전트 제공자에 이미 사용하는 것과 동일한 패턴입니다: `type` 판별자 하나와 타입이 지정된 페이로드. 스키마를 안정적으로 유지하므로, 추후 "Notion 페이지", "Google Doc", "업로드된 파일", "외부 URL"을 추가하는 것은 작고 점진적인 변경이 됩니다.
오늘날 두 가지 리소스 타입이 제공됩니다: [`github_repo`](#resource-type-github_repo)(작업마다 격리된 워크트리로 클론)와 [`local_directory`](#resource-type-local_directory)(특정 데몬 기기의 폴더 안에서 직접 실행).
## 리소스 타입: `github_repo`
기본 리소스 타입입니다 — 작업마다 격리된 워크트리로 체크아웃됩니다:
```json
{
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/owner/repo",
"default_branch_hint": "main"
}
}
```
`default_branch_hint`는 선택 사항입니다 — 지정하면 데몬이 이를 메타 스킬에 노출하므로, 에이전트가 어떤 브랜치를 기준으로 작업할지 알게 됩니다.
## 리소스 타입: `local_directory`
작업마다 다시 클론하기 곤란한 저장소 — 수 기가바이트짜리 게임 체크아웃, 대형 monorepo, 또는 작업마다 워크트리를 만드는 모델이 번거로운 모든 프로젝트 — 의 경우, 프로젝트는 대신 **특정 [데몬](/daemon-runtimes) 기기에 있는 기존 디렉터리**를 가리킬 수 있습니다. 에이전트는 클론도, 복사도, 워크트리도 없이 **그 폴더 안에서 직접** 실행됩니다.
```json
{
"resource_type": "local_directory",
"resource_ref": {
"local_path": "/Users/me/code/big-game",
"daemon_id": "0001234e-…",
"label": "main checkout"
}
}
```
`github_repo`와 비교한 트레이드오프는 의도적입니다: 바인딩된 데몬만 해당 디렉터리에 대한 작업을 가져갈 수 있고, 같은 디렉터리에 대한 작업은 병렬이 아니라 **직렬로** 실행됩니다. 그 대가로 기존 체크아웃, 기존 브랜치, 기존 더티 상태를 그대로 유지합니다 — Multica는 절대 다시 클론하지 않습니다.
### `github_repo` 대신 `local_directory`를 선택할 때
| 고려 사항 | `github_repo`(워크트리) | `local_directory` |
| --- | --- | --- |
| 작업당 체크아웃 비용 | 새 클론 + 워크트리 | 없음 — 에이전트가 제자리에서 실행 |
| 같은 저장소의 동시성 | 여러 작업 병렬 | 디렉터리당 한 번에 하나 |
| 브랜치 / 더티 상태 | 작업마다 기본 브랜치에서 새 브랜치를 받음 | 디렉터리가 현재 가진 그대로 |
| 실행 가능한 위치 | 모든 데몬 | 정확히 하나의 데몬(바인딩된 것) |
| 디스크 사용량 | 작업당 워크트리 하나 | 오버헤드 0 — 기존 폴더 사용 |
다음 중 **하나라도** 해당하면 `local_directory`를 선택하세요:
1. **다시 클론하는 비용이 지나치게 큰 경우** — 수 기가바이트짜리 게임 체크아웃, 무거운 LFS 자산을 가진 monorepo, 또는 작업당 `git clone`이 실제 작업을 압도하는 모든 경우. 동시성을 클론 없는 실행과 맞바꾸는 것입니다.
2. **변경이 세밀하고, 변경되는 즉시 로컬에서 리뷰하고 싶은 경우** — 단일 컴포넌트를 반복적으로 다듬고 있고, 몇 분마다 에이전트의 편집과 본인의 에디터를 오가고 싶으며, `~/multica_workspaces/`에서 파헤쳐야 하는 작업당 워크트리보다 기존 체크아웃을 진실의 원천으로 삼고 싶은 경우입니다.
두 경우 모두에서 받아들이는 트레이드오프는 동일합니다: **이 버전은 파일 단위 쓰기 잠금을 제공하지 않습니다.** 디렉터리당 직렬 게이트(같은 폴더에서 한 번에 하나의 작업)가 서로 다른 두 이슈의 에이전트가 동시에 같은 파일을 건드리는 것을 막는 유일한 보호 장치입니다. 두 이슈의 에이전트를 같은 `local_directory`로 향하게 하면, 그 작업들은 병렬화되지 않고 대기열에 들어갑니다 — 이는 의도된 동작입니다. 같은 코드베이스에서 진짜 병렬성이 필요하다면 `github_repo`를 계속 사용하세요.
### 로컬 디렉터리 연결하기
폴더 선택기는 **Desktop 앱**에만 있습니다 — 웹 앱은 OS 경로를 읽을 방법이 없으므로 "로컬 디렉터리 추가" 버튼이 거기서는 숨겨져 있습니다. Desktop에서는:
1. 프로젝트를 엽니다 → **Resources** 패널.
2. **로컬 디렉터리 추가**를 클릭합니다. 네이티브 폴더 선택기가 열립니다.
3. 폴더를 선택합니다. 그 경로는 **이 Desktop 설치가 현재 등록한 데몬**에 바인딩됩니다 — 리소스 레코드에는 경로와 해당 데몬의 ID가 함께 저장됩니다.
Desktop에서는 이 기기의 데몬이 오프라인이거나, 프로젝트에 이미 이 데몬에 바인딩된 `local_directory`가 있을 때, 버튼은 계속 보이되 **힌트와 함께 비활성화**됩니다 — 그래서 *왜* 사용할 수 없는지 알 수 있습니다. (웹 앱에서는 애초에 폴더 선택기가 전혀 없으므로 버튼이 완전히 숨겨집니다.) 다른 기기의 디렉터리를 바인딩하려면, 그 기기에 Desktop을 설치하고 거기서 리소스를 추가하세요.
CLI에서도 가능합니다(데몬 ID를 직접 제공하기만 하면 웹 전용 환경에서도 동작합니다):
```bash
multica project resource add <project-id> \
--type local_directory \
--local-path /Users/me/code/big-game \
--daemon-id <daemon-uuid> \
--ref-label "main checkout" # optional
multica project resource update <project-id> <resource-id> \
--local-path /Users/me/code/big-game-new
```
`--daemon-id`는 `multica daemon list`에서 얻을 수 있습니다. CLI는 페이로드를 직접 전달하고 싶을 때를 위한 범용 `--ref '<json>'` 탈출구도 받습니다.
### 경로 규칙
연결하는 경로는 연결 시점 검증과 작업별 검증을 모두 통과해야 합니다. 둘 다 리소스를 소유한 데몬이 강제합니다 — 서버는 JSON만 저장합니다. 어떤 규칙이라도 위반하는 경로는 타입이 지정된 오류와 함께 작업을 실패시키고, 디렉터리는 손대지 않은 채로 남겨 둡니다:
- 반드시 **절대 경로**여야 합니다.
- 반드시 **존재**해야 하고 **디렉터리**여야 합니다(파일, 파일을 가리키는 symlink, 또는 디바이스 노드가 아니어야 합니다).
- 데몬 프로세스가 **읽기와 쓰기**가 가능해야 합니다.
- 시스템 루트나 사용자 프로필 전체일 수 없습니다 — `/`, `/Users`, `/home`, `/root`, `/etc`, `/tmp`, `/var`, `/usr`, `/opt`, `/Users/Shared`, 본인의 `$HOME`, 임의의 Windows 드라이브 루트(`C:\`, `D:\`, …), 또는 `C:\Users` / `C:\ProgramData` / `C:\Program Files` / `C:\Program Files (x86)` / `C:\Windows`.
- 위 중 어느 것으로든 해석되는 symlink는 거부되며, OS가 별칭 처리하는 경로의 정규형(canonical form)도 마찬가지입니다(예: macOS에서 `/private/tmp`를 입력하는 것은 `/tmp`와 동일하게 거부됩니다).
블랙리스트는 의도적으로 공격적입니다 — 홈 디렉터리를 선택하면 Multica의 런타임 파일이 계정의 루트에 놓이게 되는데, 이는 절대 원하는 결과가 아닙니다. 대신 하위 폴더(보통은 실제 프로젝트 체크아웃)를 선택하세요.
### (프로젝트, 데몬)당 하나
프로젝트는 **데몬당 최대 하나의 `local_directory`**만 가질 수 있습니다. 같은 데몬에 두 번째를 추가하려 하면 API가 `409`를 반환합니다. Desktop 버튼은 한도에 이미 도달하면 스스로 숨겨지고, 이유를 설명하는 툴팁을 표시합니다.
서로 다른 데몬은 독립적입니다 — 공유 프로젝트는 팀원의 기기마다 하나씩 `local_directory`를 가질 수 있으며, 각각은 같은 프로젝트를 서로 다른 호스트의 서로 다른 폴더에 바인딩합니다. 데몬이 작업을 가져갈 때는 자신의 ID와 일치하는 행을 고르고 나머지는 무시합니다.
### 리소스 타입 혼용, 그리고 여러 개의 `local_directory` 리소스
실제로 등장하는 두 가지 교차 리소스 구성이 있습니다:
- **같은 프로젝트에 `github_repo` + `local_directory`.** 일치하는 `local_directory` 바인딩을 가진 데몬에서는 로컬 디렉터리가 **우선**합니다: 에이전트는 당신의 폴더에서 실행되며, 데몬은 그 작업을 위해 `github_repo` 워크트리를 생성하거나 사용하지 않습니다. (워크스페이스별 저장소 캐시는 평소처럼 동기화될 수 있는데, 이는 이 작업의 작업 트리와는 무관한 백그라운드 동작입니다.) `github_repo` URL은 참고용으로 `.multica/project/resources.json`과 에이전트의 `## Repositories` 섹션에 여전히 나타나지만, 에이전트가 편집하는 작업 트리는 워크트리가 아니라 당신의 로컬 디렉터리입니다. 이 프로젝트에 대한 `local_directory` 행이 **없는** 데몬(다른 기기이거나, 그 팀원이 하나를 연결하기 전)에서는, 작업이 평소의 `github_repo` 워크트리 흐름으로 폴백합니다. 사실상 로컬 디렉터리는 워크트리 경로에 대한 데몬별 재정의입니다.
- **같은 프로젝트에 두 개의 `local_directory` 리소스.** 각 `local_directory`는 정확히 하나의 데몬에 바인딩되므로, 이는 서로 다른 두 기기 사이에서만 발생합니다(API는 연결 시점에 같은 데몬에 두 개를 거부합니다, 위 참조). 작업은 어느 데몬이 로컬 디렉터리를 가졌는지가 아니라 에이전트의 런타임 할당에 따라 라우팅됩니다: 작업은 수신 에이전트의 런타임을 소유한 데몬에 도착하고, 그 데몬은 자신의 ID와 일치하는 `local_directory` 행을 고르고 나머지는 무시합니다. 로드 밸런싱은 없습니다 — 특정 기기가 작업을 실행하게 하려면, 그 기기의 런타임에 바인딩된 에이전트를 디스패치하세요.
다른 곳에 하나가 바인딩된 프로젝트에 대해 `local_directory` 행이 없는 데몬은 **차단되지 않습니다** — 그 작업들은 단순히 프로젝트의 다른 리소스(보통 `github_repo` 폴백)를 통해 진행됩니다. `local_directory`는 바인딩된 데몬에 대해서만 의미가 있습니다.
### 로컬 디렉터리에 대해 작업 실행하기
프로젝트가 수신 데몬에 바인딩된 `local_directory`를 가진 이슈에서 작업이 디스패치되면, 데몬은:
1. 경로를 다시 검증합니다(위의 규칙).
2. symlink로 해석된 실제 경로를 키로 하여 디렉터리당 잠금을 획득합니다 — 그래서 같은 폴더로 향하는 두 경로(하나는 symlink 경유, 하나는 직접)도 여전히 직렬화됩니다.
3. 에이전트의 `CLAUDE.md` / `AGENTS.md`(그리고 `.multica/project/resources.json`)를 **사용자의 디렉터리 안에** 기록합니다. 에이전트는 당신이 직접 그 폴더를 연 것과 똑같이 그곳에서 작업합니다.
4. Multica의 런타임 산출물(`output/`, `logs/`, `.gc_meta.json`)은 사용자의 디렉터리 **바깥**의 별도 envRoot에 둡니다.
같은 디렉터리에 대한 두 번째 작업이 첫 번째 작업이 실행 중일 때 도착하면, **로컬 디렉터리 대기 중(Waiting for local directory)** 상태로 멈춥니다. 이 상태는 작업이 있는 모든 곳에서 보입니다 — 채팅 작업 알약(pill), 에이전트 배너, 실행 로그, 활동 표시기 — 그리고 멈춘 작업은 에이전트의 "대기 중" 존재로 집계됩니다. 멈춘 작업을 취소하면 그 슬롯이 즉시 해제됩니다. 실행 중인 작업을 취소하면 다음 작업이 승격됩니다.
이 대기는 타임아웃이 아닙니다 — 멈춘 작업은 잠금이 해제되거나 사용자 / 에이전트가 취소할 때까지 멈춰 있습니다.
### Multica가 당신의 디렉터리에서 건드리는 것과 건드리지 않는 것
- **기록합니다**: `CLAUDE.md` / `AGENTS.md`(또는 에이전트 제공자에 해당하는 등가물)와 `.multica/project/resources.json`을 디렉터리 루트에, 그래서 에이전트가 메타 스킬과 리소스 목록을 갖게 합니다. 커밋되기를 원하지 않으면 이것들을 `.gitignore`에 추가하세요.
- **기록합니다**: 에이전트가 결정하는 모든 코드 편집을 — 당신이 직접 로컬에서 에이전트를 실행한 것과 정확히 같은 방식으로.
- **절대 물리적으로 삭제하지 않습니다**: 디렉터리나 그 안의 어떤 것도. 가비지 컬렉션은 경로를 인식합니다: `local_directory` envRoot의 경우 `workspacesRoot` 아래의 자체 `output/`과 `logs/`만 정리하며, 사용자의 디렉터리는 출입 금지로 취급합니다.
### v1 제한 사항(후속 작업에서 좁혀질 예정)
첫 릴리스는 의도적으로 `github_repo`보다 더 날카로운 모서리를 가지고 출시됩니다. 이 목록은 시간이 지나며 줄어들 것으로 예상하세요 — 여기 문서화된 것은 오늘 사실인 내용입니다:
- **자동 브랜치 전환 없음.** 에이전트는 당신이 체크아웃해 둔 브랜치에서 실행됩니다. 중요하다면 디스패치 전에 브랜치를 전환하세요.
- **더티 트리 보호나 자동 커밋 없음.** 커밋되지 않은 변경은 에이전트에게 보이고, 제자리에서 수정될 수 있으며, stash되지 않습니다. 디렉터리를 실제 작업 트리로 취급하고 위험한 실행 전에 커밋하세요.
- **자동 PR 없음.** 작업이 끝나면 변경은 만들어진 브랜치 그대로 남아 있습니다 — 아무것도 push되지 않고 PR도 열리지 않습니다. 준비되면 직접 push하고 PR을 여세요.
- **`waiting_local_directory`는 상태를 보여 주지만 보유자는 보여 주지 않습니다.** 배지는 작업이 멈췄다고 알려 줍니다. 어떤 작업이나 어떤 파일 경로가 현재 디렉터리를 보유하고 있는지는 드러내지 않습니다.
이것들은 로컬 디렉터리 작업의 에이전트 작업 수명 주기 후속 항목으로 추적됩니다. 그것이 출시되기 전까지는 `local_directory`를 "에이전트가 당신의 폴더에서, 당신이 하는 것과 같은 방식으로 실행된다"로 취급하세요.
## 프로젝트 생성 시 저장소 연결하기
**Web** 또는 **Desktop** 앱에서 *새 프로젝트*를 열면, 이제 상태 / 우선순위 / 리드 옆에 **Repos** 알약(pill)이 표시됩니다. 워크스페이스에 바인딩된 저장소를 선택하면(또는 임시 URL을 붙여 넣으면), 프로젝트가 생성되는 순간 그것들이 `github_repo` 리소스로 연결됩니다.
**CLI**에서는:
```bash
# Create + attach in one shot. The server attaches resources in the same
# transaction as the project create — invalid resources roll back the whole
# operation, so you never end up with a project that has half its resources.
multica project create \
--title "Agent UX 2026" \
--repo https://github.com/multica-ai/multica
# Manage resources later
multica project resource list <project-id>
multica project resource add <project-id> --type github_repo --url <url>
multica project resource remove <project-id> <resource-id>
# Generic escape hatch for any resource_type the server understands —
# no CLI change needed when a new type ships:
multica project resource add <project-id> \
--type notion_page \
--ref '{"page_id":"…","title":"…"}'
```
`--repo`는 반복할 수 있습니다. 각 값은 별도의 `github_repo` 리소스로 연결됩니다.
## 런타임에 에이전트가 보는 것
데몬이 프로젝트 안의 이슈를 위해 에이전트를 생성하면, 두 가지 일이 일어납니다:
### 1. `.multica/project/resources.json`
API 응답의 구조화된 패스스루(pass-through)로, 에이전트의 작업 디렉터리에 기록됩니다:
```json
{
"project_id": "…",
"project_title": "Agent UX 2026",
"resources": [
{
"id": "…",
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/multica-ai/multica",
"default_branch_hint": "main"
}
}
]
}
```
스킬, 헬퍼 스크립트, 또는 에이전트 자신이 이번 실행의 *정확한* 리소스 집합이 필요할 때 이 파일을 파싱할 수 있습니다.
### 2. 메타 스킬 프롬프트의 "Project Context" 섹션
에이전트의 `CLAUDE.md` / `AGENTS.md`(제공자에 따라 다름)에는 이제 사람이 읽을 수 있는 요약이 포함됩니다:
```
## Project Context
This issue belongs to **Agent UX 2026**.
Project resources (also written to `.multica/project/resources.json`):
- **GitHub repo**: https://github.com/multica-ai/multica (default branch: `main`)
Resources are pointers — open them only when relevant to the task. For
`github_repo` resources, use `multica repo checkout <url>` to fetch the code.
```
이 텍스트는 의도적으로 최소한입니다. 전체 페이로드는 디스크에 있고, 프롬프트는 에이전트가 프로젝트가 존재한다는 것과 무엇이 연결되었는지를 알도록 방향만 잡아 줍니다.
### 실패 모드
리소스 가져오기는 **best-effort**입니다. API 호출이 실패하면 프롬프트에서 프로젝트 섹션이 생략되고 파일도 기록되지 않지만, 작업은 여전히 시작됩니다. 에이전트는 누락된 프로젝트 컨텍스트 때문에 멈추는 일이 절대 없습니다.
## 새 리소스 타입 추가하기
이 추상화의 핵심은 새 타입이 저렴하다는 것입니다. 전체 경로는:
1. **서버 검증기**(`server/internal/handler/project_resource.go`) — `validateAndNormalizeResourceRef`에 새 페이로드를 파싱하고 정규화하는 case를 추가합니다.
2. **데몬 메타 스킬 포매터**(`server/internal/daemon/execenv/runtime_config.go`) — `formatProjectResource`에 case를 추가해, 에이전트 프롬프트가 새 타입을 읽기 좋은 글머리 기호로 렌더링하게 합니다.
3. **TypeScript 타입**(`packages/core/types/project.ts`) — `ProjectResourceType`를 확장하고 페이로드 인터페이스를 추가합니다.
4. **UI 렌더러**(`packages/views/projects/components/project-resources-section.tsx`) — `ResourceRow`에 새 타입에 대한 case를 추가합니다.
**스키마 마이그레이션도, 새 sqlc 쿼리도, 새 엔드포인트도, 그리고 CLI 변경도 없습니다** — CLI의 범용 `--ref '<json>'` 플래그가 검증기가 이해하는 모든 페이로드를 받으므로, 새 타입에 대한 첫날 지원은 순전히 위 네 단계입니다. (나중에 타입별 CLI 단축 명령을 *선택적으로* 추가할 수 있지만, 필수는 아닙니다.)
같은 `project_resource` 테이블과 같은 세 개의 CRUD 호출이 모든 타입을 처리합니다.
## 워크스페이스 저장소 vs. 프로젝트 저장소
에이전트에게 보이는 저장소 목록(`CLAUDE.md` / `AGENTS.md`의 `## Repositories` 블록)은 데몬의 클레임 핸들러가 다음 우선순위로 선택합니다:
- **프로젝트가 최소한 하나의 `github_repo` 리소스를 가짐** → 그 저장소만 에이전트에게 노출됩니다. 워크스페이스에 바인딩된 저장소는 의도적으로 숨겨, 에이전트가 이 이슈에 어느 것이 속하는지 추측하지 않아도 되게 합니다.
- **프로젝트가 `github_repo` 리소스를 갖지 않음(또는 이슈가 프로젝트에 속하지 않음)** → 이전처럼 워크스페이스의 저장소 목록으로 폴백합니다.
이는 에이전트의 작업 집합을 좁게 유지합니다: 프로젝트가 저장소를 명확히 밝히면 그것이 권위 있는 답입니다. `.multica/project/resources.json`의 구조화된 리소스 목록은 항상 전체 집합을 담으므로, 모든 것을 검사하려는 스킬은 여전히 그렇게 할 수 있습니다.
데몬은 체크아웃 측에서도 이를 반영합니다: 프로젝트 범위의 `github_repo` URL을 가진 작업이 도착하면, 그 URL들은 에이전트가 생성되기 전에 워크스페이스별 허용 목록에 병합되고 *동시에* 로컬 저장소 캐시에 동기화됩니다. 그래서 워크스페이스 수준에서 바인딩되지 않은 프로젝트 저장소 URL도 여전히 `multica repo checkout`의 유효한 인자가 됩니다 — 데몬은 그것을 "구성되지 않음"으로 거부하지 않습니다. 허용 목록 분리는 내부적입니다: 워크스페이스에 바인딩된 URL과 작업 범위의 URL은 별도로 추적되므로, 워크스페이스 저장소 새로 고침이 실행 도중에 프로젝트 URL을 실수로 취소하는 일이 없습니다.
## 여기서 의도적으로 범위에 **포함하지 않은** 것
- **프로젝트 간 공유.** 오늘날 각 리소스는 정확히 하나의 프로젝트에만 존재합니다.
- **스킬별 리소스 범위 지정.** 모든 리소스는 에이전트 실행의 모든 스킬에 보입니다. 타입 인식 필터링은 후속 작업입니다.
- **캐싱 / 동기화.** `github_repo`는 그냥 메타데이터입니다 — 체크아웃은 여전히 필요할 때 `multica repo checkout`을 통해 일어납니다. Notion / Google Docs의 캐시된 문서 텍스트는 해당 타입과 함께 제공될 것입니다.
이것들은 의도적인 누락입니다 — 첫 번째 컷의 목표는 가장 적은 수의 움직이는 부품으로 이 추상화를 검증하는 것입니다.

View File

@@ -0,0 +1,49 @@
---
title: プロジェクト
description: 関連するイシューをまとめて1つの単位として追跡します — 優先度、ステータス、進捗、担当者とともに。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica の **プロジェクト** は、関連する[イシュー](/issues)をまとめるコンテナです。作業の分量がイシュー1つよりは大きいが、ワークスペース全体よりは小さいときに使ってください — リリース、マイグレーション、複数の部分に分かれる機能、いくつもの枝に分岐する調査などです。
各プロジェクトには、名前、アイコン、説明、**リード**(メンバーまたは[エージェント](/agents))、**ステータス**`planned` / `in_progress` / `paused` / `completed` / `cancelled`)、**優先度**`urgent` / `high` / `medium` / `low` / `none`)、そして紐づくイシューのステータスから自動的に算出される **進捗** の百分率があります。
## プロジェクトとイシューの関係
プロジェクトとイシューは独立したオブジェクトで、多対一の関係です。1つのイシューは **最大1つの** プロジェクトに属することができ、1つのプロジェクトは **任意の数の** イシューを保持できます。紐づけと紐づけ解除はいつでも元に戻せます — ボードビューでドラッグするか、イシュー右側のプロパティパネルにあるプロジェクトピッカーを使ってください。
プロジェクトの進捗バーは、紐づくイシューから計算されます — `done` に到達したイシューが多いほど、バーがより満たされます。`cancelled` のイシューは集計から除外されます。`backlog` のイシューは分母にはカウントされますが、分子にはカウントされません。
## サイドバーにピン留めする
プロジェクト右上のピンアイコンをクリックすると、サイドバーのピン留めリストに追加されます。ピン留めされたプロジェクトは、ワークスペースのどこにいてもワンクリックでアクセスできます。チームの全員がそれぞれ独立してピン留めできます — ピン留めは個人ごとの設定です。
サイドバーの **ワークスペース → プロジェクト** リンクは、常にワークスペースのすべてのプロジェクトを表示します。ピン留めはその上に重ねる個人用のショートカットにすぎません。
## リソースを添付する
各プロジェクトには、GitHub リポジトリを添付する **Resources** セクションがあります。添付すると、このプロジェクトのイシューに割り当てられた[エージェント](/agents)は、タスクを実行する際にそれらのリポジトリを読み書きできます — Multica がリポジトリ URL をコンテキストとして[デーモン](/daemon-runtimes)に渡します。
リソースはプロジェクト単位です。複数のプロジェクトが同じリポジトリを共有する場合は、それぞれに添付してください。
## プロジェクトを削除する
プロジェクトを削除しても **イシューは削除されません**。紐づくイシューは単に紐づけが解除され、ワークスペースのフラットなイシュー一覧に戻ります。これは意図された動作です — プロジェクトの枠組みが変わっても、プロジェクトの範囲として定められた作業が使い捨てになることはまれだからです。
<Callout type="info">
作業も一緒に削除したい場合は、まずイシューをアーカイブまたは削除してから、プロジェクトを削除してください。
</Callout>
## プロジェクトリード
リードは、プロジェクトに対して責任を負う人 — またはエージェント — です。これはアクセス制御ではなく、弱いシグナルです。誰がリードであるかに関わらず、ワークスペースのすべてのメンバーがプロジェクトを編集できます。プロジェクトのリードには次のものを設定できます。
- ワークスペースのメンバー(人間のチームメイト)
- [エージェント](/agents) — プロジェクトの作業の大半をエージェントに委任する場合に便利です(例: 「週次のバグトリアージ」をトリアージ用エージェントがリードする)
## 次へ
- [イシュー](/issues) — プロジェクトの中に存在する作業の単位
- [プロジェクトリードとしてのエージェント](/agents) — エージェントが適切な担当者となる場合
- [Multica の仕組み](/how-multica-works) — より広い全体像

View File

@@ -0,0 +1,49 @@
---
title: 프로젝트
description: 관련 이슈를 하나로 묶어 하나의 단위로 추적하세요 — 우선순위, 상태, 진행률, 담당자와 함께.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica의 **프로젝트**는 관련 [이슈](/issues)를 담는 컨테이너입니다. 작업의 분량이 이슈 하나보다는 크지만 워크스페이스 전체보다는 작을 때 사용하세요 — 출시, 마이그레이션, 여러 부분으로 나뉘는 기능, 여러 갈래로 분기되는 조사 같은 경우입니다.
각 프로젝트에는 이름, 아이콘, 설명, **리더**(멤버 또는 [에이전트](/agents)), **상태**(`planned` / `in_progress` / `paused` / `completed` / `cancelled`), **우선순위**(`urgent` / `high` / `medium` / `low` / `none`), 그리고 연결된 이슈의 상태로부터 자동으로 산출되는 **진행률** 백분율이 있습니다.
## 프로젝트와 이슈의 관계
프로젝트와 이슈는 독립적인 객체이며 다대일 관계입니다. 하나의 이슈는 **최대 하나의** 프로젝트에 속할 수 있고, 하나의 프로젝트는 **임의 개수의** 이슈를 담을 수 있습니다. 연결과 연결 해제는 언제든지 되돌릴 수 있습니다 — 보드 뷰에서 드래그하거나, 이슈 우측 속성 패널의 프로젝트 선택기를 사용하세요.
프로젝트의 진행률 막대는 연결된 이슈로부터 계산됩니다 — `done`에 도달한 이슈가 많을수록 더 많이 채워집니다. `cancelled` 상태인 이슈는 집계에서 제외됩니다. `backlog` 상태인 이슈는 분모에는 포함되지만 분자에는 포함되지 않습니다.
## 사이드바에 고정하기
프로젝트 우측 상단의 핀 아이콘을 클릭하면 사이드바의 고정 목록에 추가됩니다. 고정된 프로젝트는 워크스페이스의 어디에 있든 한 번의 클릭으로 접근할 수 있습니다. 팀의 모든 구성원이 각자 독립적으로 고정할 수 있습니다 — 고정은 개인 설정입니다.
사이드바의 **워크스페이스 → 프로젝트** 링크는 워크스페이스의 모든 프로젝트를 항상 보여줍니다. 고정은 그 위에 얹는 개인 단축키일 뿐입니다.
## 리소스 연결하기
각 프로젝트에는 GitHub 저장소를 연결하는 **Resources** 섹션이 있습니다. 연결되고 나면, 이 프로젝트의 이슈에 할당된 [에이전트](/agents)는 작업을 실행할 때 해당 저장소를 읽고 쓸 수 있습니다 — Multica가 저장소 URL을 컨텍스트로 [데몬](/daemon-runtimes)에 전달합니다.
리소스는 프로젝트 단위입니다. 여러 프로젝트가 같은 저장소를 공유하려면 각각에 연결하세요.
## 프로젝트 삭제하기
프로젝트를 삭제해도 **이슈는 삭제되지 않습니다**. 연결된 이슈는 단지 연결이 해제되어 워크스페이스의 평면 이슈 목록으로 되돌아갑니다. 이는 의도된 동작입니다 — 프로젝트의 틀이 바뀌더라도, 프로젝트 범위로 정해졌던 작업은 일회성으로 버려지는 경우가 드뭅니다.
<Callout type="info">
작업도 함께 삭제하고 싶다면, 먼저 이슈를 보관하거나 삭제한 다음 프로젝트를 삭제하세요.
</Callout>
## 프로젝트 리더
리더는 프로젝트에 대해 책임을 지는 사람 — 또는 에이전트 — 입니다. 이는 접근 제어가 아니라 약한 신호입니다. 누가 리더든 상관없이 워크스페이스의 모든 멤버가 프로젝트를 편집할 수 있습니다. 프로젝트 리더는 다음이 될 수 있습니다:
- 워크스페이스 멤버(사람 팀원)
- [에이전트](/agents) — 프로젝트의 작업 대부분을 에이전트에게 위임할 때 유용합니다(예: "주간 버그 분류"를 분류 에이전트가 리드하는 경우).
## 다음
- [이슈](/issues) — 프로젝트 안에 들어 있는 작업 단위
- [프로젝트 리더로서의 에이전트](/agents) — 에이전트가 담당자로 적합한 경우
- [Multica 작동 방식](/how-multica-works) — 더 넓은 그림

View File

@@ -0,0 +1,127 @@
---
title: AI コーディングツール対応表
description: Multica は 12 個の AI コーディングツールをサポートしています。すべて同じインターフェースを実装していますが、機能の詳細は大きく異なります。
---
import { Callout } from "fumadocs-ui/components/callout";
Multica は **12 個の AI コーディングツール**を標準でサポートしています。これらはすべて同じインターフェース(キューへの投入、ディスパッチ、実行、結果の返却)を実装しているため、同じ Multica ボードからどれでも動かすことができます。**しかし機能の詳細は大きく異なります**: セッション再開が実際に動作するか、MCP をサポートするか、スキルファイルがどこに置かれるか、モデルをどう選択するか。このページがその完全な対応表です。
エージェントを作成するときにツールを選ぶ際のガイダンスは、[エージェントの作成と構成](/agents-create)を参照してください。
## 機能対応マトリクス
| ツール | ベンダー | セッション再開 | MCP | スキル注入パス | モデル選択 |
|---|---|---|---|---|---|
| **Antigravity** | Google | ✅ (`--conversation <id>`) | ❌ | `.agents/skills/` | Antigravity CLI 自体の内部で管理 |
| **Claude Code** | Anthropic | ✅ | **✅(実際に使用する唯一のツール)** | `.claude/skills/` | 静的 + flag |
| **Codex** | OpenAI | ⚠️ コードは存在するが到達不可 | ❌ | `$CODEX_HOME/skills/` | 静的 |
| **Copilot** | GitHub | ✅ | ❌ | `.github/skills/` | 静的(アカウントの権限で決定) |
| **Cursor** | Anysphere | ⚠️ コードは存在するが使用不可 | ❌ | `.cursor/skills/` | 動的探索 |
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | 静的 |
| **Hermes** | Nous Research | ✅ | ❌ | `.agent_context/skills/`(フォールバック) | 動的探索 |
| **Kimi** | Moonshot | ✅ | ❌ | `.kimi/skills/` | 動的探索 |
| **Kiro CLI** | Amazon | ✅ | ❌ | `.kiro/skills/` | 動的探索 |
| **OpenCode** | SST | ✅ | ❌ | `.opencode/skills/` | 動的探索 |
| **OpenClaw** | オープンソース | ✅ | ❌ | `.agent_context/skills/`(フォールバック) | エージェントにバインドされ、タスクごとに切り替え不可 |
| **Pi** | Inflection AI | ✅(セッションがファイルパス) | ❌ | `.pi/skills/` | 動的探索 |
## 各ツールの用途
### Antigravity
Google が提供します。CLI バイナリ名は `agy` です。Google の Antigravity サービスと連携し、Gemini ベースのデフォルトモデルが付属しています。**セッション再開が動作します** — `--conversation <id>` を通じて行われ、stdout が構造化されたイベントストリームではなくプレーンテキストであるため、デーモンが CLI のログファイルから conversation UUID をキャプチャします。`--model` flag はありません — モデル選択は Antigravity CLI の設定内にあるため、Multica はこのプロバイダーに対してエージェントごとのモデルピッカーを無効にします。スキルは `.agents/skills/` に配置されますCLI が Gemini CLI のワークスペーススキルレイアウトをそのまま継承します — [Antigravity 移行ドキュメント](https://antigravity.google/docs/gcli-migration)を参照)。
### Claude Code
Anthropic が提供します。**新規ユーザーにとって第一の選択肢**であり、最も完成度の高い機能セットを備えています: セッション再開が実際に動作し、**11 個の中で MCP 構成を本当に読み取る唯一のツール**であり、`--max-turns` や `--append-system-prompt` のような細かな調整 flag をサポートします。Anthropic API キーが必要です。
### Codex
OpenAI が提供します。JSON-RPC 2.0 を使用し、ステートフルな能力がより強く、よりきめ細かい承認メカニズム(`exec_command` および `patch_apply` に対する手動承認)を備えています。**セッション再開のコードは存在しますが、現在は到達できません** — 再開が必要なら、Claude Code または ACP 系のいずれかを選んでください。
### Copilot
GitHub が提供します。モデルルーティングは GitHub アカウントの権限を経由します — ツールが直接モデルを選択するのではなく、GitHub がどのモデルを提供するかを決定します。`.github/skills/` にスキルを置くのは GitHub CLI のネイティブな探索メカニズムです。
### Cursor
Anysphere が提供し、Cursor エディターに対応する CLI です。**セッション再開のコードは存在しますが、実際には動作しません** — Cursor CLI のイベントストリームがセッション ID を返さないため、渡す再開値は常に無効です。再開が必要なら、別のものを選んでください。
### Gemini
Google が提供し、Gemini 2.5 および 3 シリーズをサポートします。**セッション再開も MCP もサポートしません** — 長いコンテキストの記憶が不要なワンショットタスクに適しています。
### Hermes
Nous Research が提供します。ACP プロトコルを使用しますKimi とトランスポート層を共有します)。セッション再開が動作します。しかし**スキル注入パスは専用のものではなく汎用のフォールバック**`.agent_context/skills/`)です — Hermes CLI 自体がこのパスを読み取らない場合、スキルが適用されないことがあります。テストで確認してください。
### Kimi
Moonshot が提供し、中国市場を対象としています。Hermes と ACP プロトコルを共有しますが、スキルパス `.kimi/skills/` は Kimi CLI のネイティブな探索メカニズムであり、Hermes のフォールバックとは異なります。
### Kiro CLI
Amazon が提供します。`kiro-cli acp` を通じて stdio 上で ACP を使用します。セッション再開は ACP `session/load` で動作し、モデル選択は `session/set_model` で動作し、スキルはプロジェクトレベルのネイティブ探索のために `.kiro/skills/` にコピーされます。
### OpenCode
SST が提供するオープンソースです。利用可能なモデルを動的に探索しますCLI の構成ファイルをスキャン)。セッション再開が動作します。**自分のモデルカタログをカスタマイズしたい、いじるのが好きなユーザーに適しています。**
### OpenClaw
オープンソースプロジェクトであり、CLI エージェントオーケストレーターです。**モデルはエージェント層にバインドされます**`openclaw agents add --model` — タスクごとに上書きできません。構成は厳格に制御されます: ユーザーは `--model` や `--system-prompt` を渡せず、エージェント登録時の構成が決定します。
### Pi
Inflection AI が提供し、ミニマルです。**セッション再開の方式が独特です** — セッション ID が文字列 ID ではなく、ディスク上のファイルパス(`~/.pi/...`)です。他のツールでは再開 id は CLI が返す文字列ですが、Pi では再開 id はセッションファイルそのものです。
## セッション再開: 実際にサポートするツール
セッション再開のメカニズムは[タスク](/tasks#can-a-task-continue-from-the-previous-context)で扱います。以下はツールごとの**正確な現在の状態**です。
| 状態 | ツール | 意味 |
|---|---|---|
| ✅ 実際に動作 | Antigravity, Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi | 再開 id を渡すと以前のコンテキストから続行します |
| ⚠️ コードは存在するが到達不可 | Codex, Cursor | コードに再開パスがありますが、実際には到達しませんCodex は静かにフォールバックし、Cursor はセッション id を返しません) — **未サポートとみなしてください** |
| ❌ なし | Gemini | CLI に再開メカニズムがありません |
**意思決定のために**: ワークフローでエージェントがタスク間でコンテキストを保持する必要がある場合(失敗時のリトライ、手動の再実行、対話的な反復)、✅ の行にあるツールだけを選んでください。
## MCP 構成: Claude Code だけが実際に読み取る
**12 個のツールのうち、`mcp_config` を実際に消費するのは Claude Code だけです**。残りの 11 個はこのフィールドを受け取りますが、**完全に無視します** — エラーも警告もなく、構成はただ効果を発揮しません。
<Callout type="warning">
エージェント構成で `mcp_config` を設定しても、Claude Code 以外のツールを選んだ場合、MCP サーバーはそのエージェントに**何の効果**も及ぼしません。現在、MCP 連携は Claude Code のみをカバーしています。
</Callout>
## スキルファイルが置かれる場所
各ツールは**それぞれ独自の**スキル探索パスを使用します。タスクが実行される前に、Multica デーモンがワークスペースのスキルファイルを対応するパスにコピーします。
| ツール | パス | ネイティブ探索か |
|---|---|---|
| Claude Code | `.claude/skills/` | ✅ ネイティブ |
| Codex | `$CODEX_HOME/skills/` | ✅ ネイティブ |
| Copilot | `.github/skills/` | ✅ ネイティブ |
| Cursor | `.cursor/skills/` | ✅ ネイティブ |
| Kimi | `.kimi/skills/` | ✅ ネイティブ |
| Kiro CLI | `.kiro/skills/` | ✅ ネイティブ |
| OpenCode | `.opencode/skills/` | ✅ ネイティブ |
| Pi | `.pi/skills/` | ✅ ネイティブ |
| Antigravity | `.agents/skills/` | ✅ ネイティブGemini CLI のワークスペースレイアウトを継承 — [Antigravity ドキュメント](https://antigravity.google/docs/gcli-migration)を参照) |
| Gemini | `.agent_context/skills/` | ⚠️ 汎用フォールバック |
| Hermes | `.agent_context/skills/` | ⚠️ 汎用フォールバック |
| OpenClaw | `.agent_context/skills/` | ⚠️ 汎用フォールバック |
フォールバックパスを使うツールが実際にこのディレクトリを読み取るかどうかは、そのツール自体のドキュメントによって異なり、保証されません。Gemini / Hermes / OpenClaw でスキルが適用されない場合は、まずこの点を確認してください。
スキルの作成と使用については、[スキル](/skills)を参照してください。
## 次へ
- [エージェントの作成と構成](/agents-create) — エージェントに使うツールを選ぶ
- [タスク](/tasks) — タスクのライフサイクルとセッション再開のメカニズム
- [デーモンとランタイム](/daemon-runtimes) — ツールが実行される場所と Multica への接続方法
- [エージェントランタイムのインストール](/install-agent-runtime) — サポートされる 12 個のツールそれぞれのインストールと認証

View File

@@ -0,0 +1,129 @@
---
title: AI 코딩 도구 대조표
description: Multica는 12개의 AI 코딩 도구를 지원합니다. 모두 동일한 인터페이스를 구현하지만, 기능 세부사항은 크게 다릅니다.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica는 **12개의 AI 코딩 도구**를 기본 지원합니다. 이들은 모두 동일한 인터페이스(대기열 적재, 디스패치, 실행, 결과 반환)를 구현하므로, 같은 Multica 보드에서 어느 것이든 구동할 수 있습니다. **하지만 기능 세부사항은 크게 다릅니다**: 세션 재개가 실제로 동작하는지, MCP를 지원하는지, 스킬 파일이 어디에 위치하는지, 모델을 어떻게 선택하는지. 이 페이지가 전체 대조표입니다.
에이전트를 생성할 때 도구를 고르는 방법은 [에이전트 생성 및 구성](/agents-create)을 참고하세요.
## 기능 대조 매트릭스
| 도구 | 공급사 | 세션 재개 | MCP | 스킬 주입 경로 | 모델 선택 |
|---|---|---|---|---|---|
| **Antigravity** | Google | ✅ (`--conversation <id>`) | ❌ | `.agents/skills/` | Antigravity CLI 자체 내부에서 관리 |
| **Claude Code** | Anthropic | ✅ | ✅ | `.claude/skills/` | 정적 + flag |
| **Codex** | OpenAI | ⚠️ 코드는 존재하지만 도달 불가 | ✅ | `$CODEX_HOME/skills/` | 정적 |
| **Copilot** | GitHub | ✅ | ❌ | `.github/skills/` | 정적 (계정 권한으로 결정) |
| **Cursor** | Anysphere | ⚠️ 코드는 존재하지만 사용 불가 | ❌ | `.cursor/skills/` | 동적 탐색 |
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | 정적 |
| **Hermes** | Nous Research | ✅ | ✅ | `.agent_context/skills/` (fallback) | 동적 탐색 |
| **Kimi** | Moonshot | ✅ | ✅ | `.kimi/skills/` | 동적 탐색 |
| **Kiro CLI** | Amazon | ✅ | ✅ | `.kiro/skills/` | 동적 탐색 |
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 동적 탐색 + variant |
| **OpenClaw** | 오픈소스 | ✅ | ✅ | `.agent_context/skills/` (fallback) | 에이전트에 바인딩되어 작업마다 전환 불가 |
| **Pi** | Inflection AI | ✅ (세션이 파일 경로) | ❌ | `.pi/skills/` | 동적 탐색 |
## 각 도구의 용도
### Antigravity
Google에서 제공합니다. CLI 바이너리 이름은 `agy`입니다. Google의 Antigravity 서비스와 연동되며 Gemini 기반의 기본 모델을 함께 제공합니다. **세션 재개가 동작합니다** — `--conversation <id>`를 통해서이며, stdout이 구조화된 이벤트 스트림이 아니라 일반 텍스트이기 때문에 데몬이 CLI의 로그 파일에서 conversation UUID를 캡처합니다. `--model` flag는 없습니다 — 모델 선택은 Antigravity CLI 설정 안에 있으므로, Multica는 이 제공자에 대해 에이전트별 모델 선택기를 비활성화합니다. 스킬은 `.agents/skills/`에 들어갑니다(CLI가 Gemini CLI의 워크스페이스 스킬 레이아웃을 그대로 따릅니다 — [Antigravity 마이그레이션 문서](https://antigravity.google/docs/gcli-migration) 참고).
### Claude Code
Anthropic에서 제공합니다. **신규 사용자에게 첫 번째 선택지**이며, 가장 완전한 기능 세트를 갖추고 있습니다: 세션 재개가 실제로 동작하고, MCP 구성을 읽으며, `--max-turns`와 `--append-system-prompt` 같은 세부 조정 flag를 지원합니다. Anthropic API 키가 필요합니다.
### Codex
OpenAI에서 제공합니다. JSON-RPC 2.0을 사용하고, 상태 유지 능력이 더 강하며, 더 세밀한 승인 메커니즘(`exec_command` 및 `patch_apply`에 대한 수동 승인)을 갖추고 있습니다. MCP 구성은 작업별 `$CODEX_HOME/config.toml`에 기록됩니다. **세션 재개 코드는 존재하지만 현재 도달할 수 없습니다** — 재개가 필요하다면 Claude Code나 ACP 계열 중 하나를 선택하세요.
### Copilot
GitHub에서 제공합니다. 모델 라우팅은 GitHub 계정 권한을 거칩니다 — 도구가 직접 모델을 선택하지 않고, GitHub가 어떤 모델을 제공할지 결정합니다. `.github/skills/`에 스킬을 두는 것은 GitHub CLI의 기본 탐색 메커니즘입니다.
### Cursor
Anysphere에서 제공하며, Cursor 에디터에 대응하는 CLI입니다. **세션 재개 코드는 존재하지만 실제로는 동작하지 않습니다** — Cursor CLI 이벤트 스트림이 세션 ID를 반환하지 않으므로, 전달하는 재개 값은 항상 무효입니다. 재개가 필요하다면 다른 것을 선택하세요.
### Gemini
Google에서 제공하며, Gemini 2.5 및 3 시리즈를 지원합니다. **세션 재개도 MCP도 지원하지 않습니다** — 긴 컨텍스트 기억이 필요 없는 일회성 작업에 적합합니다.
### Hermes
Nous Research에서 제공합니다. ACP 프로토콜을 사용합니다(Kimi와 전송 계층을 공유합니다). 세션 재개가 동작하고, MCP 구성은 ACP `mcpServers`로 전달됩니다. 하지만 **스킬 주입 경로는 전용 경로가 아니라 범용 fallback**(`.agent_context/skills/`)입니다 — Hermes CLI 자체가 이 경로를 읽지 않으면 스킬이 적용되지 않을 수 있습니다. 테스트로 확인하세요.
### Kimi
Moonshot에서 제공하며, 중국 시장을 겨냥합니다. Hermes와 ACP 프로토콜을 공유하고 MCP 구성도 ACP `mcpServers`로 전달되지만, 스킬 경로 `.kimi/skills/`는 Kimi CLI의 기본 탐색 메커니즘으로 Hermes의 fallback과는 다릅니다.
### Kiro CLI
Amazon에서 제공합니다. `kiro-cli acp`를 통해 stdio 위에서 ACP를 사용합니다. 세션 재개는 ACP `session/load`로 동작하고, MCP 구성은 ACP `mcpServers`로 전달되며, 모델 선택은 `session/set_model`로 동작하고, 스킬은 프로젝트 수준 기본 탐색을 위해 `.kiro/skills/`로 복사됩니다.
### OpenCode
SST에서 제공하는 오픈소스입니다. 사용 가능한 모델과 모델 variant를 동적으로 탐색합니다(CLI의 구성 파일을 스캔). 세션 재개가 동작하고, 에이전트의 `mcp_config` 필드를 소비합니다. Multica는 `OPENCODE_CONFIG_CONTENT` 환경 변수를 통해 이를 인라인으로 주입하므로, 에이전트의 MCP 서버가 작업 디렉터리의 `opencode.json`(에이전트 또는 사용자가 소유하는 파일)을 건드리지 않고 OpenCode에 전달됩니다. 모델이 variant를 노출하면 Multica는 이를 에이전트 thinking selector로 표시하고 선택한 값을 `opencode run --variant`로 전달합니다. **자신의 모델 카탈로그를 커스터마이징하고 싶은, 만지작거리기 좋아하는 사용자에게 적합합니다.**
### OpenClaw
오픈소스 프로젝트이며, CLI 에이전트 오케스트레이터입니다. MCP 구성은 Multica의 작업별 config wrapper를 통해 기록됩니다. **모델이 에이전트 계층에 바인딩됩니다**(`openclaw agents add --model`) — 작업마다 재정의할 수 없습니다. 구성이 엄격하게 통제됩니다: 사용자는 `--model`이나 `--system-prompt`를 전달할 수 없으며, 에이전트 등록 구성이 결정합니다.
### Pi
Inflection AI에서 제공하며, 미니멀합니다. **세션 재개 방식이 특이합니다** — 세션 ID가 문자열 ID가 아니라 디스크상의 파일 경로(`~/.pi/...`)입니다. 다른 도구에서는 재개 id가 CLI가 반환하는 문자열이지만, Pi에서는 재개 id가 세션 파일 그 자체입니다.
## 세션 재개: 실제로 지원하는 도구
세션 재개 메커니즘은 [작업](/tasks#can-a-task-continue-from-the-previous-context)에서 다룹니다. 다음은 도구별 **정확한 현재 상태**입니다:
| 상태 | 도구 | 의미 |
|---|---|---|
| ✅ 실제로 동작 | Antigravity, Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi | 재개 id를 전달하면 이전 컨텍스트에서 이어집니다 |
| ⚠️ 코드는 존재하지만 도달 불가 | Codex, Cursor | 코드에 재개 경로가 있지만 실제로는 도달하지 않습니다(Codex는 조용히 폴백하고, Cursor는 세션 id를 반환하지 않습니다) — **미지원으로 간주하세요** |
| ❌ 없음 | Gemini | CLI에 재개 메커니즘이 없습니다 |
**의사결정을 위해**: 워크플로에서 에이전트가 작업 간에 컨텍스트를 유지해야 한다면(실패 재시도, 수동 재실행, 대화형 반복), ✅ 행에 있는 도구만 선택하세요.
## MCP 구성: 도구별 지원
**12개 도구 중 `mcp_config`를 실제로 소비하는 것은 7개입니다: Claude Code, Codex, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw**. 나머지 5개는 이 필드를 받아들이지만 **무시합니다** — 오류도, 경고도 없으며, 구성이 그저 효과를 내지 못합니다.
각 도구의 연결 방식은 다릅니다: Claude Code는 `--mcp-config`와 `--strict-mcp-config`로 받고, Codex는 데몬이 관리하는 `mcp_servers` 블록을 작업별 `$CODEX_HOME/config.toml`에 기록하며, Hermes/Kimi/Kiro CLI는 ACP `mcpServers`로 받습니다. OpenCode는 `OPENCODE_CONFIG_CONTENT` 환경 변수로 인라인 구성을 받고, OpenClaw는 Multica의 작업별 config wrapper를 통해 `mcp.servers`를 받습니다. OpenCode 경로는 프로젝트의 `opencode.json`을 다시 쓰지 않습니다.
<Callout type="warning">
에이전트 구성에서 `mcp_config`를 설정했더라도 MCP 열에 ✅가 없는 도구를 선택하면, MCP 서버가 해당 에이전트에 **아무런 효과**도 미치지 않습니다. MCP 연동은 도구별로 구현됩니다.
</Callout>
## 스킬 파일이 위치하는 곳
각 도구는 **자체** 스킬 탐색 경로를 사용합니다. 작업이 실행되기 전에 Multica 데몬이 워크스페이스의 스킬 파일을 해당 경로로 복사합니다:
| 도구 | 경로 | 기본 탐색 여부 |
|---|---|---|
| Claude Code | `.claude/skills/` | ✅ 기본 |
| Codex | `$CODEX_HOME/skills/` | ✅ 기본 |
| Copilot | `.github/skills/` | ✅ 기본 |
| Cursor | `.cursor/skills/` | ✅ 기본 |
| Kimi | `.kimi/skills/` | ✅ 기본 |
| Kiro CLI | `.kiro/skills/` | ✅ 기본 |
| OpenCode | `.opencode/skills/` | ✅ 기본 |
| Pi | `.pi/skills/` | ✅ 기본 |
| Antigravity | `.agents/skills/` | ✅ 기본 (Gemini CLI의 워크스페이스 레이아웃을 따름 — [Antigravity 문서](https://antigravity.google/docs/gcli-migration) 참고) |
| Gemini | `.agent_context/skills/` | ⚠️ 범용 fallback |
| Hermes | `.agent_context/skills/` | ⚠️ 범용 fallback |
| OpenClaw | `.agent_context/skills/` | ⚠️ 범용 fallback |
fallback 경로를 쓰는 도구가 실제로 이 디렉터리를 읽는지는 해당 도구 자체의 문서에 따라 달라지며 — 보장되지 않습니다. Gemini / Hermes / OpenClaw에서 스킬이 적용되지 않는다면, 먼저 이 점을 확인하세요.
스킬의 생성과 사용은 [스킬](/skills)을 참고하세요.
## 다음
- [에이전트 생성 및 구성](/agents-create) — 에이전트에 사용할 도구를 선택하세요
- [작업](/tasks) — 작업 생명주기와 세션 재개 메커니즘
- [데몬과 런타임](/daemon-runtimes) — 도구가 실행되는 곳과 Multica에 연결되는 방식
- [에이전트 런타임 설치](/install-agent-runtime) — 지원되는 12개 도구 각각의 설치 및 인증

View File

@@ -14,16 +14,16 @@ For guidance on picking a tool when creating an agent, see [Creating and configu
| Tool | Vendor | Session resumption | MCP | Skill injection path | Model selection |
|---|---|---|---|---|---|
| **Antigravity** | Google | ✅ (`--conversation <id>`) | ❌ | `.agents/skills/` | Managed inside the Antigravity CLI itself |
| **Claude Code** | Anthropic | ✅ | **✅ (the only one that actually uses it)** | `.claude/skills/` | Static + flag |
| **Codex** | OpenAI | ⚠️ Code exists but unreachable | | `$CODEX_HOME/skills/` | Static |
| **Claude Code** | Anthropic | ✅ | | `.claude/skills/` | Static + flag |
| **Codex** | OpenAI | ⚠️ Code exists but unreachable | | `$CODEX_HOME/skills/` | Static |
| **Copilot** | GitHub | ✅ | ❌ | `.github/skills/` | Static (determined by account entitlement) |
| **Cursor** | Anysphere | ⚠️ Code exists but unusable | ❌ | `.cursor/skills/` | Dynamic discovery |
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | Static |
| **Hermes** | Nous Research | ✅ | | `.agent_context/skills/` (fallback) | Dynamic discovery |
| **Kimi** | Moonshot | ✅ | | `.kimi/skills/` | Dynamic discovery |
| **Kiro CLI** | Amazon | ✅ | | `.kiro/skills/` | Dynamic discovery |
| **OpenCode** | SST | ✅ | | `.opencode/skills/` | Dynamic discovery |
| **OpenClaw** | Open source | ✅ | | `.agent_context/skills/` (fallback) | Bound to the agent, can't be switched per task |
| **Hermes** | Nous Research | ✅ | | `.agent_context/skills/` (fallback) | Dynamic discovery |
| **Kimi** | Moonshot | ✅ | | `.kimi/skills/` | Dynamic discovery |
| **Kiro CLI** | Amazon | ✅ | | `.kiro/skills/` | Dynamic discovery |
| **OpenCode** | SST | ✅ | | `.opencode/skills/` | Dynamic discovery + variants |
| **OpenClaw** | Open source | ✅ | | `.agent_context/skills/` (fallback) | Bound to the agent, can't be switched per task |
| **Pi** | Inflection AI | ✅ (session is a file path) | ❌ | `.pi/skills/` | Dynamic discovery |
## What each tool is for
@@ -34,11 +34,11 @@ From Google. CLI binary name is `agy`. Pairs with Google's Antigravity service a
### Claude Code
From Anthropic. **First choice for new users** — the most complete feature set: session resumption actually works, it's the **only one of the 11 that truly reads MCP configuration**, and it supports fine-tuning flags like `--max-turns` and `--append-system-prompt`. Requires an Anthropic API key.
From Anthropic. **First choice for new users** — the most complete feature set: session resumption actually works, it reads MCP configuration, and it supports fine-tuning flags like `--max-turns` and `--append-system-prompt`. Requires an Anthropic API key.
### Codex
From OpenAI. Uses JSON-RPC 2.0, has stronger statefulness, and a finer-grained approve mechanism (manual approval for `exec_command` and `patch_apply`). **Session resumption code exists but is currently unreachable** — if you need resume, pick Claude Code or one of the ACP family.
From OpenAI. Uses JSON-RPC 2.0, has stronger statefulness, and a finer-grained approve mechanism (manual approval for `exec_command` and `patch_apply`). MCP config is materialized into the per-task `$CODEX_HOME/config.toml`. **Session resumption code exists but is currently unreachable** — if you need resume, pick Claude Code or one of the ACP family.
### Copilot
@@ -54,23 +54,23 @@ From Google, supports the Gemini 2.5 and 3 series. **No session resumption and n
### Hermes
From Nous Research. Uses the ACP protocol (shares a transport with Kimi). Session resumption works. But the **skill injection path is the generic fallback** (`.agent_context/skills/`), not a dedicated one — if the Hermes CLI itself doesn't read this path, skills may not take effect. Verify by testing.
From Nous Research. Uses the ACP protocol (shares a transport with Kimi). Session resumption works, and MCP config is passed through ACP `mcpServers`. But the **skill injection path is the generic fallback** (`.agent_context/skills/`), not a dedicated one — if the Hermes CLI itself doesn't read this path, skills may not take effect. Verify by testing.
### Kimi
From Moonshot, aimed at the Chinese market. Shares the ACP protocol with Hermes, but the skill path `.kimi/skills/` is Kimi CLI's native discovery mechanism — different from Hermes's fallback.
From Moonshot, aimed at the Chinese market. Shares the ACP protocol with Hermes, including MCP config through ACP `mcpServers`, but the skill path `.kimi/skills/` is Kimi CLI's native discovery mechanism — different from Hermes's fallback.
### Kiro CLI
From Amazon. Uses ACP over stdio via `kiro-cli acp`. Session resumption works through ACP `session/load`, model selection works through `session/set_model`, and skills are copied into `.kiro/skills/` for native project-level discovery.
From Amazon. Uses ACP over stdio via `kiro-cli acp`. Session resumption works through ACP `session/load`, MCP config is passed through ACP `mcpServers`, model selection works through `session/set_model`, and skills are copied into `.kiro/skills/` for native project-level discovery.
### OpenCode
From SST, open source. Dynamically discovers available models (scans the CLI's configuration file). Session resumption works. **Suitable for tinkerers who want to customize their model catalog.**
From SST, open source. Dynamically discovers available models and model variants (scans the CLI's configuration file). Session resumption works, and it consumes the agent's `mcp_config` field — Multica injects it inline through the `OPENCODE_CONFIG_CONTENT` environment variable, so the agent's MCP servers reach OpenCode without writing anything into the task workdir's `opencode.json` (the agent or the user keep ownership of that file). When a model exposes variants, Multica shows them as the agent thinking selector and passes the selected value through `opencode run --variant`. **Suitable for tinkerers who want to customize their model catalog.**
### OpenClaw
Open-source project, a CLI agent orchestrator. **Model is bound at the agent layer** (`openclaw agents add --model`) — it can't be overridden per task. Configuration is strictly controlled: users can't pass `--model` or `--system-prompt`; the agent-registration config decides.
Open-source project, a CLI agent orchestrator. MCP config is materialized through Multica's per-task OpenClaw config wrapper. **Model is bound at the agent layer** (`openclaw agents add --model`) — it can't be overridden per task. Configuration is strictly controlled: users can't pass `--model` or `--system-prompt`; the agent-registration config decides.
### Pi
@@ -88,12 +88,14 @@ The session resumption mechanism is covered in [Tasks](/tasks#can-a-task-continu
**For your decision**: if your workflow needs agents to preserve context across tasks (failure retries, manual reruns, conversational iteration), pick only from the ✅ row.
## MCP configuration: only Claude Code actually reads it
## MCP configuration: provider-specific support
**Of the 12 tools, only Claude Code actually consumes `mcp_config`**. The other 11 accept the field but **completely ignore it** — no error, no warning, the config just has no effect.
**Of the 12 tools, seven consume `mcp_config`: Claude Code, Codex, Hermes, Kimi, Kiro CLI, OpenCode, and OpenClaw**. The other five accept the field but **ignore it** — no error, no warning, the config just has no effect.
The runtime paths are provider-specific: Claude Code receives it through `--mcp-config` paired with `--strict-mcp-config`; Codex writes a daemon-managed `mcp_servers` block into the per-task `$CODEX_HOME/config.toml`; Hermes, Kimi, and Kiro CLI receive ACP `mcpServers`; OpenCode receives inline config through `OPENCODE_CONFIG_CONTENT`; OpenClaw receives `mcp.servers` through Multica's per-task config wrapper. OpenCode's path does **not** rewrite the project's `opencode.json`.
<Callout type="warning">
If you set `mcp_config` in an agent configuration but pick a tool other than Claude Code, your MCP servers have **no effect** on that agent. MCP integration currently covers Claude Code only.
If you set `mcp_config` in an agent configuration but pick a tool not marked ✅ in the MCP column, your MCP servers have **no effect** on that agent. MCP integration is provider-specific.
</Callout>
## Where skill files go

View File

@@ -14,16 +14,16 @@ Multica 内置支持 **12 款 AI 编程工具**。它们都实现了同一套接
| 工具 | 厂商 | 会话恢复 | MCP | Skill 注入路径 | 模型选择 |
|---|---|---|---|---|---|
| **Antigravity** | Google | ✅(`--conversation <id>`| ❌ | `.agents/skills/` | 由 Antigravity CLI 自己管理 |
| **Claude Code** | Anthropic | ✅ | **✅(唯一真用)** | `.claude/skills/` | 静态 + flag |
| **Codex** | OpenAI | ⚠️ 代码存在但不可达 | | `$CODEX_HOME/skills/` | 静态 |
| **Claude Code** | Anthropic | ✅ | | `.claude/skills/` | 静态 + flag |
| **Codex** | OpenAI | ⚠️ 代码存在但不可达 | | `$CODEX_HOME/skills/` | 静态 |
| **Copilot** | GitHub | ✅ | ❌ | `.github/skills/` | 静态(账号权益决定)|
| **Cursor** | Anysphere | ⚠️ 代码存在但不可用 | ❌ | `.cursor/skills/` | 动态发现 |
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | 静态 |
| **Hermes** | Nous Research | ✅ | | `.agent_context/skills/` fallback| 动态发现 |
| **Kimi** | Moonshot | ✅ | | `.kimi/skills/` | 动态发现 |
| **Kiro CLI** | Amazon | ✅ | | `.kiro/skills/` | 动态发现 |
| **OpenCode** | SST | ✅ | | `.opencode/skills/` | 动态发现 |
| **OpenClaw** | 开源项目 | ✅ | | `.agent_context/skills/` fallback| 绑定在智能体上,不能在任务里切换 |
| **Hermes** | Nous Research | ✅ | | `.agent_context/skills/` fallback| 动态发现 |
| **Kimi** | Moonshot | ✅ | | `.kimi/skills/` | 动态发现 |
| **Kiro CLI** | Amazon | ✅ | | `.kiro/skills/` | 动态发现 |
| **OpenCode** | SST | ✅ | | `.opencode/skills/` | 动态发现 + variant |
| **OpenClaw** | 开源项目 | ✅ | | `.agent_context/skills/` fallback| 绑定在智能体上,不能在任务里切换 |
| **Pi** | Inflection AI | ✅session 为文件路径)| ❌ | `.pi/skills/` | 动态发现 |
## 每款工具的定位
@@ -34,11 +34,11 @@ Google 出品。CLI 二进制名为 `agy`,搭配 Google Antigravity 服务,
### Claude Code
Anthropic 出品。**新用户首选**——功能最完整:会话恢复真用,是 **12 款里唯一真读 MCP 配置**的工具,支持 `--max-turns`、`--append-system-prompt` 等细调参数。需要一个 Anthropic API 密钥。
Anthropic 出品。**新用户首选**——功能最完整:会话恢复真用,读 MCP 配置,支持 `--max-turns`、`--append-system-prompt` 等细调参数。需要一个 Anthropic API 密钥。
### Codex
OpenAI 出品。使用 JSON-RPC 2.0 协议状态化更强approve 机制更细(手动批准 `exec_command` 和 `patch_apply`)。**会话恢复代码存在但当前不可达**——如果你需要 resume选 Claude Code 或 ACP 系列。
OpenAI 出品。使用 JSON-RPC 2.0 协议状态化更强approve 机制更细(手动批准 `exec_command` 和 `patch_apply`)。MCP 配置会写入单次任务的 `$CODEX_HOME/config.toml`。**会话恢复代码存在但当前不可达**——如果你需要 resume选 Claude Code 或 ACP 系列。
### Copilot
@@ -54,23 +54,23 @@ Google 出品,支持 Gemini 2.5 和 3 系列。**不支持会话恢复也不
### Hermes
Nous Research 出品。使用 ACP 协议(和 Kimi 共享传输层)。会话恢复真用。但 **skill 注入路径是通用 fallback**`.agent_context/skills/`),不是专用路径——如果 Hermes CLI 本身不读这路径skill 对它可能不起作用。需要结合实测再确认。
Nous Research 出品。使用 ACP 协议(和 Kimi 共享传输层)。会话恢复真用MCP 配置通过 ACP `mcpServers` 传入。但 **skill 注入路径是通用 fallback**`.agent_context/skills/`),不是专用路径——如果 Hermes CLI 本身不读这路径skill 对它可能不起作用。需要结合实测再确认。
### Kimi
Moonshot 出品,中国市场向。和 Hermes 共享 ACP 协议,但 skill 路径 `.kimi/skills/` 是 Kimi CLI 的原生发现机制——和 Hermes 的 fallback 不一样。
Moonshot 出品,中国市场向。和 Hermes 共享 ACP 协议,MCP 配置同样通过 ACP `mcpServers` 传入;但 skill 路径 `.kimi/skills/` 是 Kimi CLI 的原生发现机制——和 Hermes 的 fallback 不一样。
### Kiro CLI
Amazon 出品。通过 `kiro-cli acp` 使用 ACP stdio 协议。会话恢复走 ACP `session/load`,模型选择走 `session/set_model`skill 会复制到 `.kiro/skills/` 让 Kiro 做项目级原生发现。
Amazon 出品。通过 `kiro-cli acp` 使用 ACP stdio 协议。会话恢复走 ACP `session/load`MCP 配置通过 ACP `mcpServers` 传入,模型选择走 `session/set_model`skill 会复制到 `.kiro/skills/` 让 Kiro 做项目级原生发现。
### OpenCode
SST 出品,开源。动态发现可用模型(扫 CLI 的配置文件)。会话恢复真用。**适合爱折腾、想自定义模型目录**的开发者。
SST 出品,开源。动态发现可用模型和模型 variant(扫 CLI 的配置文件)。会话恢复真用,会消费智能体的 `mcp_config` 字段——Multica 通过 `OPENCODE_CONFIG_CONTENT` 环境变量内联注入,让智能体的 MCP server 直接到达 OpenCode**不会去碰任务工作目录里的 `opencode.json`**(那个文件归智能体或用户所有)。当模型暴露 variant 时Multica 会把它显示成智能体的思考强度选择,并通过 `opencode run --variant` 传给 OpenCode。**适合爱折腾、想自定义模型目录**的开发者。
### OpenClaw
开源项目CLI agent 编排器。**模型绑定在智能体层**`openclaw agents add --model`)——不能在单次任务里覆盖。配置严格受控:用户不能传 `--model` 或 `--system-prompt`,由智能体注册时的配置决定。
开源项目CLI agent 编排器。MCP 配置通过 Multica 的单次任务配置 wrapper 写入。**模型绑定在智能体层**`openclaw agents add --model`)——不能在单次任务里覆盖。配置严格受控:用户不能传 `--model` 或 `--system-prompt`,由智能体注册时的配置决定。
### Pi
@@ -88,12 +88,14 @@ Inflection AI 出品,极简主义。**会话恢复机制特殊**——session
**对你的决策**:如果工作流需要智能体在多次任务之间保持上下文(失败重试、手动重跑、对话式迭代),只选 ✅ 那一行的工具。
## MCP 配置:只有 Claude Code 真的读
## MCP 配置:按工具不同
**12 款工具里Claude Code 实际消费 `mcp_config`**。其他 11 款会接收这个字段但**完全忽略**——不报错、不警告,只是配置不生效。
**12 款工具里有 7 款实际消费 `mcp_config`Claude Code、Codex、Hermes、Kimi、Kiro CLI、OpenCode、OpenClaw**。其他 5 款会接收这个字段但**忽略**——不报错、不警告,只是配置不生效。
各工具的接入方式不同Claude Code 通过 `--mcp-config` 加 `--strict-mcp-config` 接收Codex 会把 daemon 管理的 `mcp_servers` block 写入单次任务的 `$CODEX_HOME/config.toml`Hermes、Kimi、Kiro CLI 通过 ACP `mcpServers` 接收OpenCode 通过 `OPENCODE_CONFIG_CONTENT` 环境变量内联接收OpenClaw 通过 Multica 的单次任务配置 wrapper 接收 `mcp.servers`。OpenCode 这条路径**不会**改写项目里的 `opencode.json`。
<Callout type="warning">
如果你在智能体配置里设置了 `mcp_config`,但选了 Claude Code 之外的工具,你的 MCP server 对这个智能体**没有效果**。目前的 MCP 集成只覆盖 Claude Code
如果你在智能体配置里设置了 `mcp_config`,但选了矩阵 MCP 列没有标 ✅ 的工具,你的 MCP server 对这个智能体**没有效果**。MCP 集成是按工具实现的
</Callout>
## skill 文件该放哪儿

View File

@@ -0,0 +1,275 @@
---
title: セルフホスティングのクイックスタート
description: Docker で自分のサーバーやマシン上に Multica を実行しますKubernetes では Helm が利用できます。所要時間は約10分です。
---
import { Callout } from "fumadocs-ui/components/callout";
このページでは、Docker を使って Multica **サーバー**(バックエンド + フロントエンド + PostgreSQLを自分のマシンやサーバー上で実行する手順を案内します。完了すると、[ワークスペース](/workspaces)、[イシュー](/issues)、[コメント](/comments)、[エージェント](/agents)の構成を含むデータが、完全にあなたの管理下に置かれます。
エージェントの **実行** は、依然としてローカルで動かす[デーモン](/daemon-runtimes)と、そのマシンにインストールされた [AI コーディングツール](/providers)に依存します — Cloud とまったく同じです。セルフホスティングはサーバー層を置き換えるだけで、実行層を置き換えるわけではありません。
## 前提条件
- **Docker** がインストールされ、`docker compose` を実行できること
- **Git**(任意ですが、ソースを取得できるので推奨)
- 常時稼働させられるマシン(ローカル / 内部ネットワーク / クラウドホストのいずれでも可)
- **デーモンを実行するマシン** に AI コーディングツールが最低1つインストールされていることサーバーを実行するマシンである必要はなく、開発用ートパソコンでも構いません
## 1. プロジェクトを取得してバックエンドを起動する
<Callout type="info">
**すでに Kubernetes を使っていますか?** Docker をスキップして代わりに Helm チャートを使ってください — 下記の [Kubernetes デプロイ](#kubernetes-deployment-alternative)へ移動し、初回ログインのために [ステップ4](#4-first-login--create-a-workspace)に戻ってきてください。
</Callout>
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost
```
`make selfhost` は次のことを行います。
1. `.env` がなければ `.env.example` から生成し、**ランダムな JWT_SECRET** を併せて作成します
2. 公式の Docker イメージPostgreSQL、Multica backend、Multica frontendを取得します
3. `docker-compose.selfhost.yml` を使ってすべてのサービスを起動します
4. バックエンドの `/health` エンドポイントが準備できるまで待機します
起動後の継続的なプロダクションプローブには、データベースや migration の問題でチェックが失敗するようにしたい場合は `/readyz` を使ってください。
バックエンドのコンテナは起動時に **データベースの migration を自動で実行します**`docker/entrypoint.sh` がサーバー起動前に `./migrate up` を実行)— バックエンドのログで migration の出力を確認できます。バージョンアップグレードも同じ方法で処理されます。
<Callout type="info">
**イメージがまだ公開されていませんか?** `make selfhost` がイメージを取得できない場合、まだリリースされていないバージョンタグにいる可能性があります。安定リリースに切り替えるか、ソースからビルドしてください: `make selfhost-build`。
</Callout>
起動すると次のようになります。
- **フロントエンド**: [http://localhost:3000](http://localhost:3000)
- **バックエンド**: [http://localhost:8080](http://localhost:8080)
<Callout type="info">
**ポートは `127.0.0.1` でのみ待ち受けます。** `docker-compose.selfhost.yml` は公開されるすべてのポートを loopback にバインドします — `ss -tlnp` には `0.0.0.0:8080` は表示されず、設計上、他のマシンからはサービスにアクセスできません。デフォルトの `JWT_SECRET` と Postgres の認証情報は、公開インターネット上に置いては絶対にいけません。マシン間アクセスが必要な場合は、TLS を終端するリバースプロキシをスタックの前に置いてください — [ステップ5b — マシン間: リバースプロキシを前に置く](#5b-cross-machine-front-with-a-reverse-proxy)を参照してください。
</Callout>
## 2. 重要: プロダクションの安全設定を維持する
<Callout type="warning">
**`docker-compose.selfhost.yml` はデフォルトで `APP_ENV` を `production` に設定し**、`MULTICA_DEV_VERIFICATION_CODE` を空のままにするため、公開インスタンスには固定コードがありません。
`MULTICA_DEV_VERIFICATION_CODE` はローカルまたは非公開のテスト自動化でのみ設定してください。`APP_ENV` が non-production のときに固定コードが有効になっていると、コードをリクエストできる誰もがその固定値でサインインできてしまいます。[認証設定 → 固定のローカルテストコード](/auth-setup#fixed-local-testing-codes)を参照してください。
公開デプロイの前には、`.env` に `APP_ENV=production` が設定され、`MULTICA_DEV_VERIFICATION_CODE` が空であることを必ず確認してください。
</Callout>
## 3. メールサービスを構成する(任意ですが推奨)
メールを構成しないと、ユーザーはメールで認証コードを受け取れず、サーバーは生成したコードを代わりに stdout に出力します。
2種類の配信バックエンドがサポートされています — ネットワークに合うものを選んでください。
**オプション A — Resendクラウド / 公開インターネットのデプロイ):**
1. [Resend](https://resend.com/) にサインアップして API key を取得します
2. 自分が管理する送信用ドメインを認証します
3. `.env` に次を設定します。
```bash
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
**オプション B — SMTP relay内部ネットワーク / オンプレミス):**
デプロイ環境が `api.resend.com` に到達できない場合や、すでに内部メールリレーMicrosoft Exchange、Postfix、オンプレミスの SendGrid など)がある場合に使ってください。両方が設定されている場合は `SMTP_HOST` が Resend より優先されるため、認証メールと招待メールは内部リレーにとどまります。ポート 465SMTPS / 暗黙的 TLSは現在サポートされていません — 25 または 587 を使ってください。
**匿名 Exchange 内部リレー(ポート 25** — ホストが IP で信頼され、認証情報なしで送信する場合:
```bash
SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
```
**認証付き送信(ポート 587、STARTTLS** — リレーがサービスアカウントを必要とし、STARTTLS が広告されると自動的にアップグレードされる場合:
```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false # set true only for private CA / self-signed
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
その後、再起動します: `docker compose -f docker-compose.selfhost.yml restart backend`。再起動時、バックエンドはどのプロバイダーを選んだかを出力します(`EmailService: SMTP relay …` / `Resend API` / `DEV mode`)— 認証情報は決してログに残らないため、この行はヘルプを求めるときに共有しても安全です。
追加の認証構成OAuth、サインアップの許可リストと SMTP 変数の完全なリファレンスは、[認証設定](/auth-setup)と[環境変数 → メール](/environment-variables#email-configuration)を参照してください。
## 4. 初回ログイン + ワークスペースの作成
[http://localhost:3000](http://localhost:3000) を開きます。
- メールアドレスを入力します
- 構成したメールバックエンドResend または SMTP relayから認証コードを受け取ります。どちらも構成していない場合は、サーバーコンテナの stdout からコピーしてください — `[DEV] Verification code` の行を探します
- non-production の非公開インスタンスで `MULTICA_DEV_VERIFICATION_CODE=888888` を明示的に設定した場合を除き、`888888` を使わないでください
- ログインして最初のワークスペースを作成します
## 5. CLI を自分のサーバーに向ける
CLI のインストールは [Cloud クイックスタート → 2. CLI をインストールする](/cloud-quickstart#2-install-the-multica-cli) と同じです — Homebrew / スクリプト / PowerShell のいずれかを選んでください。
### 5a. 同じマシン
CLI とサーバーが同じホストで動作している場合、デフォルト値ですでに動作します。
```bash
multica setup self-host
```
これにより CLI が `http://localhost:8080`(バックエンド)と `http://localhost:3000`フロントエンドを指すようになり、ブラウザログインを案内し、PAT をローカルに保存して、**デーモンを自動的に起動します**。
### 5b. マシン間: リバースプロキシを前に置く
compose スタックは `127.0.0.1` でのみ待ち受けるため、別のマシンにあるデーモンは `http://<server-ip>:8080` に直接接続できません — そして、そうなることを望むべきでもありません。さもなければデフォルトの `JWT_SECRET` が公開インターネットから到達可能になってしまうからです。TLS を終端し、`127.0.0.1:8080`(バックエンド)と `127.0.0.1:3000`フロントエンドへ転送するリバースプロキシをサーバーに置き、CLI を公開 HTTPS URL に向けてください。
```bash
multica setup self-host \
--server-url https://<your-domain> \
--app-url https://<your-domain>
```
単一のホスト名でフロントエンドとバックエンドの両方を前段に置く(デーモンと Web アプリの両方に必要な WebSocket サポートを含む)最小限の Caddyfile は次のとおりです。
```nginx
multica.example.com {
# WebSocket route — must come before the catch-all
@ws path /ws /ws/*
handle @ws {
reverse_proxy 127.0.0.1:8080 {
flush_interval -1
}
}
# Backend API
handle /api/* {
reverse_proxy 127.0.0.1:8080
}
# Everything else → frontend
reverse_proxy 127.0.0.1:3000
}
```
プロキシを立ち上げたら、サーバーの `.env` に `FRONTEND_ORIGIN=https://multica.example.com` を設定してバックエンドを再起動してください — そうしないと WebSocket の origin チェックがブラウザを拒否します([トラブルシューティング → WebSocket が接続できない](/troubleshooting#websocket-cant-connect))。
[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) も堅実な選択肢です — ホストにポートを一切公開せずに TLS と公開ホスト名を提供してくれます。Nginx で同等に構成する方法(`app.` / `api.` を別々のホスト名に分離、WebSocket 用の `proxy_set_header Upgrade`も同じくらいうまく動作します。重要な要件は、TLS の終端と `/ws` での `Upgrade` ヘッダーの転送です。
## 6. エージェントの作成 + 最初のタスクの割り当て
Cloud と同じ流れです — [Cloud クイックスタート → ステップ5-6](/cloud-quickstart#5-create-an-agent)を参照してください。
## 7. 使用量ロールアップのスケジューリング(使用量ダッシュボードに必須)
<Callout type="warning">
使用量 / ランタイムのダッシュボードは、`rollup_task_usage_hourly()` が埋める派生テーブル `task_usage_hourly` からデータを読み取ります。バンドルされた `pgvector/pgvector:pg17` の Postgres イメージには **`pg_cron` が含まれておらず**、バックエンドもロールアップをインプロセスで実行しません。`rollup_task_usage_hourly()` をスケジューリングするものが何もないと、生の `task_usage` 行は届き続けるのに、ダッシュボードは永遠にゼロのままになります。
</Callout>
サポートされているオプションのいずれか1つを選んでください — 1つあれば十分です。
**オプション A — 外部 cron / systemd-timer最もシンプル。** 任意の帯域外スケジューラから5分ごとにロールアップを実行します。冪等でウォーターマーク駆動なので、取りこぼしたティックは追いつきます。
```bash
# /etc/cron.d/multica-rollup — every 5 minutes
*/5 * * * * root docker compose -f /path/to/multica/docker-compose.selfhost.yml \
exec -T postgres psql -U multica -d multica \
-c "SELECT rollup_task_usage_hourly();" >/dev/null
```
**オプション B — Postgres を `pg_cron` を同梱したイメージに置き換える。** `docker-compose.selfhost.yml` の `pgvector/pgvector:pg17` を、`pgvector` と `pg_cron` の両方を備えたイメージ(`supabase/postgres`、またはカスタムビルド)に置き換え、`shared_preload_libraries=pg_cron` を設定して再起動してから、ジョブを一度登録します。
```sql
CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule(
'rollup_task_usage_hourly',
'*/5 * * * *',
$$SELECT rollup_task_usage_hourly()$$
);
```
**オプション C — まず履歴をバックフィルする(アップグレード経路)。** `v0.3.4 → v0.3.5+` へアップグレード中で、既存の `task_usage` 行がある場合、migration `103` は hourly テーブルがシードされるまで `refusing to drop legacy daily rollups: ...` とともに `migrate up` を中断します。バンドルされたバックフィルを一度実行してから、オプション A または B を設定してください。
```bash
docker compose -f docker-compose.selfhost.yml exec backend \
./backfill_task_usage_hourly --sleep-between-slices=2s
```
`--sleep-between-slices=2s` は、忙しい DB での読み取り負荷を調整します。完了後、バックエンドのコンテナを再起動すると(起動時に migration が実行されます)アップグレードが完了します。
完全なリファレンス — Kubernetes の `CronJob` テンプレートとアップグレード順序を含む — は、リポジトリの [`SELF_HOSTING_ADVANCED.md → Usage Dashboard Rollup`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING_ADVANCED.md#usage-dashboard-rollup) にあります。
## Kubernetes デプロイ(代替手段)
すでに Kubernetes クラスターを運用している場合、リポジトリには `deploy/helm/multica/` に Helm チャートも同梱されています。k8s 用の `make selfhost` に相当します — 同じバックエンドイメージ、フロントエンドイメージ、`pgvector/pgvector:pg17` の Postgres を Deployment / Service / Ingress としてパッケージングし、`values.yaml` からレンダリングされた1つの `ConfigMap` を併せて提供します。k3s + Traefik + `local-path` を基準に作成されており、Ingress コントローラーとデフォルトの `ReadWriteOnce` StorageClass があるあらゆるクラスターで動作するはずです。
このチャートは **シークレット値をテンプレート化しません**。`multica-secrets` という名前の Secret を名前で参照するため、実際の JWT / DB / Resend / Google キーが git や `values.yaml` に置かれる必要はまったくありません。ネームスペースと Secret を kubectl で一度作成してください。
```bash
kubectl create namespace multica
kubectl -n multica create secret generic multica-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
--from-literal=RESEND_API_KEY="" \
--from-literal=GOOGLE_CLIENT_SECRET="" \
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""
```
その後、チャートをインストールします。
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica
```
デフォルト値はホスト名 `multica.dev.lan`webと `api.multica.dev.lan`(バックエンド)を想定しています。これらを `/etc/hosts`(またはローカル DNSに追加し、Ingress に到達可能な任意のノード IP を指すようにしてください。別のホスト名を使うには、`deploy/helm/multica/values.yaml` をコピーして `ingress.frontend.host` / `ingress.backend.host` と、それに対応する `backend.config.appUrl` / `frontendOrigin` / `localUploadBaseUrl` / `googleRedirectUri` を編集し、`-f my-values.yaml` でインストールしてください。
コールドクラスターでは、バックエンドが Postgres を待ち、migration を実行する間、数分間 `Running` 状態だが `Ready` ではないことがあります — startupProbe がこれを吸収するため、ポッドは再起動されないはずです。`Ready` になったら:
```bash
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
```
その後 `http://multica.dev.lan` を開き、上記の [ステップ4 — 初回ログイン](#4-first-login--create-a-workspace)から続けてください。CLI を Ingress のホスト名に向けます。
```bash
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lan
```
チャートを変更せずに最新のイメージだけを取得するには、`kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend` を実行してください。特定の Multica リリースに固定するには、values ファイルで `images.backend.tag` / `images.frontend.tag` を設定して `helm upgrade` を実行してください。`helm -n multica uninstall multica` はワークロードを削除しますが、PVC と Secret は保持します。`kubectl delete namespace multica` はすべてを消去します。
完全なリファレンス — 3つのログインモード、web イメージにビルド時に焼き込まれた `REMOTE_API_URL` に対する `backend` ExternalName の回避策、リソース制限、TLS — は、リポジトリの [`SELF_HOSTING.md`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING.md#kubernetes-deployment-alternative) にあります。
## よくある問題
- **バックエンドが起動しない**: `docker compose -f docker-compose.selfhost.yml logs backend` でコンテナのログを確認してください。たいていは `.env` の不正な `DATABASE_URL` または `JWT_SECRET` が原因です
- **認証コードが届かない**: メールバックエンドが構成されていない場合Resend も SMTP もない)→ `docker compose logs backend` で `[DEV] Verification code` を探してください
- **WebSocket が接続できない**: 公開デプロイでは、`FRONTEND_ORIGIN` を実際のフロントエンドのドメインに必ず設定する必要があります。[トラブルシューティング → WebSocket が接続できない](/troubleshooting#websocket-wont-connect)を参照してください
- **使用量 / ランタイムのダッシュボードがゼロのまま**: `rollup_task_usage_hourly()` がスケジューリングされていません — 上記の [ステップ7](#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)と[トラブルシューティング → 使用量ダッシュボードがゼロと表示される](/troubleshooting#usage-dashboard-stays-at-zero)を参照してください
- **`migrate up` が `refusing to drop legacy daily rollups` で失敗する**: `v0.3.4 → v0.3.5+` のアップグレード経路ガードです。まず `backfill_task_usage_hourly` を実行してください — [ステップ7 → オプション C](#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)を参照してください
## 次のステップ
- [環境変数](/environment-variables) — 完全な env リファレンス
- [認証設定](/auth-setup) — Resend / OAuth / サインアップ許可リストの詳細
- [GitHub 連携](/github-integration) — GitHub App を接続して、PR がイシューに自動でリンクされ、マージするとイシューが閉じられるように設定する
- [トラブルシューティング](/troubleshooting) — うまくいかないときはここから始めてください
- [デスクトップアプリ](/desktop-app) — `~/.multica/desktop.json` を通じた任意のデスクトップ設定。Web フロントエンド + CLI が依然として最速のセルフホスティング経路です

View File

@@ -0,0 +1,275 @@
---
title: 자체 호스팅 빠른 시작
description: Docker로 자체 서버나 기기에서 Multica를 실행합니다(Kubernetes에서는 Helm 사용 가능). 약 10분 소요됩니다.
---
import { Callout } from "fumadocs-ui/components/callout";
이 페이지는 Docker로 Multica **서버**(백엔드 + 프런트엔드 + PostgreSQL)를 자체 기기나 서버에서 실행하는 과정을 안내합니다. 완료하면 [워크스페이스](/workspaces), [이슈](/issues), [댓글](/comments), [에이전트](/agents) 구성을 비롯한 데이터가 완전히 본인의 통제하에 놓입니다.
에이전트 **실행**은 여전히 로컬에서 실행하는 [데몬](/daemon-runtimes)과 그 기기에 설치된 [AI 코딩 도구](/providers)에 의존합니다 — Cloud와 완전히 동일합니다. 자체 호스팅은 서버 계층을 교체할 뿐, 실행 계층을 교체하지는 않습니다.
## 사전 요구 사항
- **Docker**가 설치되어 있고 `docker compose`를 실행할 수 있어야 함
- **Git**(선택 사항이지만 소스를 받아올 수 있으므로 권장)
- 계속 켜둘 수 있는 기기(로컬 / 내부 네트워크 / 클라우드 호스트 모두 가능)
- **데몬을 실행하는 기기**에 AI 코딩 도구가 최소 한 개 설치되어 있어야 함(서버를 실행하는 기기일 필요는 없으며, 개발용 노트북도 됩니다)
## 1. 프로젝트 받아오기 및 백엔드 시작하기
<Callout type="info">
**이미 Kubernetes를 쓰고 계신가요?** Docker를 건너뛰고 Helm 차트를 사용하세요 — 아래 [Kubernetes 배포](#kubernetes-deployment-alternative)로 이동한 다음, 첫 로그인을 위해 [4단계](#4-first-login--create-a-workspace)로 돌아오세요.
</Callout>
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost
```
`make selfhost`는 다음을 수행합니다.
1. `.env`가 없으면 `.env.example`로부터 생성하며 **무작위 JWT_SECRET**을 함께 만듭니다
2. 공식 Docker 이미지(PostgreSQL, Multica backend, Multica frontend)를 받아옵니다
3. `docker-compose.selfhost.yml`을 사용해 모든 서비스를 시작합니다
4. 백엔드의 `/health` 엔드포인트가 준비될 때까지 기다립니다
시작 이후 프로덕션 프로브에는, 데이터베이스나 migration 문제 시 검사가 실패하도록 하려면 `/readyz`를 사용하세요.
백엔드 컨테이너는 시작 시 **데이터베이스 migration을 자동으로 실행합니다**(`docker/entrypoint.sh`가 서버 시작 전에 `./migrate up`을 실행) — 백엔드 로그에서 migration 출력을 확인할 수 있습니다. 버전 업그레이드도 같은 방식으로 처리됩니다.
<Callout type="info">
**이미지가 아직 공개되지 않았나요?** `make selfhost`가 이미지를 받아오지 못한다면 아직 릴리스되지 않은 버전 태그에 있을 수 있습니다. 안정 릴리스로 전환하거나 소스에서 빌드하세요: `make selfhost-build`.
</Callout>
시작되면 다음과 같습니다.
- **프런트엔드**: [http://localhost:3000](http://localhost:3000)
- **백엔드**: [http://localhost:8080](http://localhost:8080)
<Callout type="info">
**포트는 `127.0.0.1`에서만 수신합니다.** `docker-compose.selfhost.yml`은 공개된 모든 포트를 loopback에 바인딩합니다 — `ss -tlnp`에서는 `0.0.0.0:8080`이 보이지 않으며, 설계상 다른 기기에서는 서비스에 접근할 수 없습니다. 기본 `JWT_SECRET`과 Postgres 자격 증명이 공개 인터넷에 노출되어서는 절대 안 됩니다. 기기 간 접근이 필요하면 TLS를 종료하는 리버스 프록시를 스택 앞에 두세요 — [5b단계 — 기기 간: 리버스 프록시를 앞에 두기](#5b-cross-machine-front-with-a-reverse-proxy)를 참고하세요.
</Callout>
## 2. 중요: 프로덕션 안전 설정 유지하기
<Callout type="warning">
**`docker-compose.selfhost.yml`은 기본적으로 `APP_ENV`를 `production`으로 설정하고** `MULTICA_DEV_VERIFICATION_CODE`를 비워 두므로, 공개 인스턴스에는 고정 코드가 없습니다.
`MULTICA_DEV_VERIFICATION_CODE`는 로컬 또는 비공개 테스트 자동화에서만 설정하세요. `APP_ENV`가 non-production일 때 고정 코드가 활성화되어 있으면, 코드를 요청할 수 있는 누구나 그 고정 값으로 로그인할 수 있습니다. [인증 설정 → 고정 로컬 테스트 코드](/auth-setup#fixed-local-testing-codes)를 참고하세요.
공개 배포 전에는 `.env`에 `APP_ENV=production`이 설정되어 있고 `MULTICA_DEV_VERIFICATION_CODE`가 비어 있는지 반드시 확인하세요.
</Callout>
## 3. 이메일 서비스 구성하기(선택 사항이지만 권장)
이메일을 구성하지 않으면 사용자가 이메일로 인증 코드를 받을 수 없으며, 서버가 생성된 코드를 대신 stdout에 출력합니다.
두 가지 전송 백엔드를 지원합니다 — 네트워크에 맞는 것을 고르세요.
**옵션 A — Resend(클라우드 / 공개 인터넷 배포):**
1. [Resend](https://resend.com/)에 가입하고 API key를 받습니다
2. 본인이 관리하는 발송 도메인을 인증합니다
3. `.env`에 다음을 설정합니다.
```bash
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
**옵션 B — SMTP relay(내부 네트워크 / 온프레미스):**
배포 환경이 `api.resend.com`에 접근할 수 없거나, 이미 내부 메일 릴레이(Microsoft Exchange, Postfix, 온프레미스 SendGrid 등)가 있는 경우에 사용하세요. 둘 다 설정된 경우 `SMTP_HOST`가 Resend보다 우선하므로, 인증 및 초대 메일이 내부 릴레이에 머무릅니다. 465 포트(SMTPS / 암묵적 TLS)는 현재 지원하지 않습니다 — 25 또는 587을 사용하세요.
**익명 Exchange 내부 릴레이(포트 25)** — 호스트가 IP로 신뢰되며 자격 증명 없이 제출하는 경우:
```bash
SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com # From: 헤더로도 재사용됨
```
**인증 제출(포트 587, STARTTLS)** — 릴레이에 서비스 계정이 필요하며, STARTTLS가 광고될 때 자동으로 업그레이드되는 경우:
```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false # 비공개 CA / 자체 서명 인증서일 때만 true로 설정
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
그런 다음 재시작합니다: `docker compose -f docker-compose.selfhost.yml restart backend`. 재시작 시 백엔드는 어떤 제공자를 선택했는지 출력합니다(`EmailService: SMTP relay …` / `Resend API` / `DEV mode`) — 자격 증명은 절대 로그에 남지 않으므로, 이 줄은 도움을 요청할 때 공유해도 안전합니다.
추가 인증 구성(OAuth, 가입 허용 목록)과 전체 SMTP 변수 레퍼런스는 [인증 설정](/auth-setup)과 [환경 변수 → 이메일](/environment-variables#email-configuration)을 참고하세요.
## 4. 첫 로그인 + 워크스페이스 생성
[http://localhost:3000](http://localhost:3000)을 엽니다.
- 이메일을 입력합니다
- 구성한 이메일 백엔드(Resend 또는 SMTP relay)에서 인증 코드를 받습니다. 둘 다 구성하지 않았다면 서버 컨테이너 stdout에서 복사하세요 — `[DEV] Verification code` 줄을 찾으면 됩니다
- non-production 비공개 인스턴스에서 `MULTICA_DEV_VERIFICATION_CODE=888888`을 명시적으로 설정한 경우가 아니라면 `888888`을 사용하지 마세요
- 로그인하고 첫 워크스페이스를 생성합니다
## 5. CLI를 자체 서버로 연결하기
CLI 설치는 [Cloud 빠른 시작 → 2. CLI 설치](/cloud-quickstart#2-install-the-multica-cli)와 동일합니다 — Homebrew / 스크립트 / PowerShell 중 하나를 고르세요.
### 5a. 같은 기기
CLI와 서버가 같은 호스트에서 실행된다면 기본값으로 이미 동작합니다.
```bash
multica setup self-host
```
이렇게 하면 CLI가 `http://localhost:8080`(백엔드)과 `http://localhost:3000`(프런트엔드)을 가리키고, 브라우저 로그인을 안내하며, PAT를 로컬에 저장하고, **데몬을 자동으로 시작합니다**.
### 5b. 기기 간: 리버스 프록시를 앞에 두기
compose 스택은 `127.0.0.1`에서만 수신하므로, 다른 기기에 있는 데몬은 `http://<server-ip>:8080`에 직접 연결할 수 없습니다 — 그리고 그렇게 되기를 원해서도 안 됩니다. 그렇지 않으면 기본 `JWT_SECRET`이 공개 인터넷에서 접근 가능해지기 때문입니다. 서버에 TLS를 종료하고 `127.0.0.1:8080`(백엔드)과 `127.0.0.1:3000`(프런트엔드)으로 전달하는 리버스 프록시를 두고, CLI를 공개 HTTPS URL로 연결하세요.
```bash
multica setup self-host \
--server-url https://<your-domain> \
--app-url https://<your-domain>
```
단일 호스트네임에서 프런트엔드와 백엔드를 모두 앞단에 두는(데몬과 웹 앱 모두에 필요한 WebSocket 지원 포함) 최소 Caddyfile은 다음과 같습니다.
```nginx
multica.example.com {
# WebSocket route — must come before the catch-all
@ws path /ws /ws/*
handle @ws {
reverse_proxy 127.0.0.1:8080 {
flush_interval -1
}
}
# Backend API
handle /api/* {
reverse_proxy 127.0.0.1:8080
}
# Everything else → frontend
reverse_proxy 127.0.0.1:3000
}
```
프록시를 올린 후에는 서버의 `.env`에 `FRONTEND_ORIGIN=https://multica.example.com`을 설정하고 백엔드를 재시작하세요 — 그렇지 않으면 WebSocket origin 검사가 브라우저를 거부합니다([문제 해결 → WebSocket이 연결되지 않음](/troubleshooting#websocket-cant-connect)).
[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)도 견고한 선택지입니다 — 호스트에 어떤 포트도 노출하지 않고도 TLS와 공개 호스트네임을 제공합니다. Nginx로 동등하게 구성하는 방법(`app.` / `api.`을 별도 호스트네임으로 분리, WebSocket용 `proxy_set_header Upgrade`)도 똑같이 잘 동작합니다. 핵심 요구 사항은 TLS 종료와 `/ws`에서의 `Upgrade` 헤더 전달입니다.
## 6. 에이전트 생성 + 첫 작업 할당
Cloud와 동일한 흐름입니다 — [Cloud 빠른 시작 → 5-6단계](/cloud-quickstart#5-create-an-agent)를 참고하세요.
## 7. 사용량 롤업 스케줄링(사용량 대시보드에 필수)
<Callout type="warning">
사용량 / 런타임 대시보드는 `rollup_task_usage_hourly()`가 채우는 파생 테이블 `task_usage_hourly`에서 데이터를 읽습니다. 번들된 `pgvector/pgvector:pg17` Postgres 이미지에는 **`pg_cron`이 포함되어 있지 않으며**, 백엔드도 롤업을 인프로세스로 실행하지 않습니다. `rollup_task_usage_hourly()`를 스케줄링하는 것이 없으면, 원시 `task_usage` 행은 계속 들어오는데 대시보드는 영원히 0에 머무릅니다.
</Callout>
지원되는 옵션 중 하나를 고르세요 — 하나만 있으면 됩니다.
**옵션 A — 외부 cron / systemd-timer(가장 간단함).** 임의의 외부 스케줄러에서 5분마다 롤업을 실행합니다. 멱등하고 워터마크 기반이므로, 놓친 틱은 따라잡습니다.
```bash
# /etc/cron.d/multica-rollup — every 5 minutes
*/5 * * * * root docker compose -f /path/to/multica/docker-compose.selfhost.yml \
exec -T postgres psql -U multica -d multica \
-c "SELECT rollup_task_usage_hourly();" >/dev/null
```
**옵션 B — Postgres를 `pg_cron`이 포함된 이미지로 교체.** `docker-compose.selfhost.yml`의 `pgvector/pgvector:pg17`을 `pgvector`와 `pg_cron`을 모두 갖춘 이미지(`supabase/postgres` 또는 커스텀 빌드)로 교체하고, `shared_preload_libraries=pg_cron`을 설정한 뒤 재시작하고, 작업을 한 번 등록합니다.
```sql
CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule(
'rollup_task_usage_hourly',
'*/5 * * * *',
$$SELECT rollup_task_usage_hourly()$$
);
```
**옵션 C — 먼저 히스토리 백필(업그레이드 경로).** `v0.3.4 → v0.3.5+`로 업그레이드하는 중이고 기존 `task_usage` 행이 있다면, migration `103`이 hourly 테이블이 시드될 때까지 `refusing to drop legacy daily rollups: ...`와 함께 `migrate up`을 중단합니다. 번들된 백필을 한 번 실행한 다음, 옵션 A 또는 B를 설정하세요.
```bash
docker compose -f docker-compose.selfhost.yml exec backend \
./backfill_task_usage_hourly --sleep-between-slices=2s
```
`--sleep-between-slices=2s`는 바쁜 DB에서 읽기 부하를 조절합니다. 완료된 후 백엔드 컨테이너를 재시작하면(시작 시 migration이 실행됨) 업그레이드가 완료됩니다.
전체 레퍼런스 — Kubernetes `CronJob` 템플릿과 업그레이드 순서 포함 — 는 저장소의 [`SELF_HOSTING_ADVANCED.md → Usage Dashboard Rollup`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING_ADVANCED.md#usage-dashboard-rollup)에 있습니다.
## Kubernetes 배포(대체 방안)
이미 Kubernetes 클러스터를 운영 중이라면, 저장소에는 `deploy/helm/multica/`에 Helm 차트도 포함되어 있습니다. k8s용 `make selfhost`에 해당합니다 — 동일한 백엔드 이미지, 프런트엔드 이미지, `pgvector/pgvector:pg17` Postgres를 Deployment / Service / Ingress로 패키징하고, `values.yaml`로 렌더링한 하나의 `ConfigMap`을 함께 제공합니다. k3s + Traefik + `local-path`를 기준으로 작성되었으며, Ingress 컨트롤러와 기본 `ReadWriteOnce` StorageClass가 있는 모든 클러스터에서 동작합니다.
이 차트는 **시크릿 값을 템플릿화하지 않습니다**. `multica-secrets`라는 이름의 Secret을 이름으로 참조하므로, 실제 JWT / DB / Resend / Google 키가 git이나 `values.yaml`에 들어갈 필요가 전혀 없습니다. 네임스페이스와 Secret을 kubectl로 한 번 생성하세요.
```bash
kubectl create namespace multica
kubectl -n multica create secret generic multica-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
--from-literal=RESEND_API_KEY="" \
--from-literal=GOOGLE_CLIENT_SECRET="" \
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""
```
그런 다음 차트를 설치합니다.
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica
```
기본값은 호스트네임 `multica.dev.lan`(web)과 `api.multica.dev.lan`(백엔드)을 가정합니다. 이것들을 `/etc/hosts`(또는 로컬 DNS)에 추가해, Ingress에 도달 가능한 임의의 노드 IP를 가리키도록 하세요. 다른 호스트네임을 사용하려면 `deploy/helm/multica/values.yaml`을 복사한 뒤 `ingress.frontend.host` / `ingress.backend.host`와 그에 대응하는 `backend.config.appUrl` / `frontendOrigin` / `localUploadBaseUrl` / `googleRedirectUri`를 편집하고, `-f my-values.yaml`로 설치하세요.
콜드 클러스터에서는 백엔드가 Postgres를 기다리고 migration을 실행하는 동안 몇 분간 `Running` 상태이지만 `Ready`는 아닐 수 있습니다 — startupProbe가 이를 흡수하므로 파드는 재시작되지 않습니다. `Ready`가 되면:
```bash
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
```
그런 다음 `http://multica.dev.lan`을 열고 위의 [4단계 — 첫 로그인](#4-first-login--create-a-workspace)에서 이어서 진행하세요. CLI를 Ingress 호스트네임으로 연결합니다.
```bash
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lan
```
차트를 변경하지 않고 최신 이미지만 받아오려면 `kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend`를 실행하세요. 특정 Multica 릴리스를 고정하려면 values 파일에서 `images.backend.tag` / `images.frontend.tag`를 설정하고 `helm upgrade`를 실행하세요. `helm -n multica uninstall multica`는 워크로드를 제거하지만 PVC와 Secret은 유지합니다. `kubectl delete namespace multica`는 모든 것을 삭제합니다.
전체 레퍼런스 — 세 가지 로그인 모드, web 이미지에 빌드 타임에 굳혀진 `REMOTE_API_URL`에 대한 `backend` ExternalName 우회책, 리소스 제한, TLS — 는 저장소의 [`SELF_HOSTING.md`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING.md#kubernetes-deployment-alternative)에 있습니다.
## 자주 발생하는 문제
- **백엔드가 시작되지 않음**: `docker compose -f docker-compose.selfhost.yml logs backend`로 컨테이너 로그를 확인하세요. 보통 `.env`의 잘못된 `DATABASE_URL` 또는 `JWT_SECRET`이 원인입니다
- **인증 코드를 받지 못함**: 이메일 백엔드가 구성되지 않은 경우(Resend도 SMTP도 없음) → `docker compose logs backend`에서 `[DEV] Verification code`를 찾으세요
- **WebSocket이 연결되지 않음**: 공개 배포에서는 반드시 `FRONTEND_ORIGIN`을 실제 프런트엔드 도메인으로 설정해야 합니다. [문제 해결 → WebSocket이 연결되지 않음](/troubleshooting#websocket-wont-connect)을 참고하세요
- **사용량 / 런타임 대시보드가 0에 머무름**: `rollup_task_usage_hourly()`가 스케줄링되지 않고 있습니다 — 위의 [7단계](#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)와 [문제 해결 → 사용량 대시보드가 0으로 표시됨](/troubleshooting#usage-dashboard-stays-at-zero)을 참고하세요
- **`migrate up`이 `refusing to drop legacy daily rollups`로 실패함**: `v0.3.4 → v0.3.5+` 업그레이드 경로 가드입니다. 먼저 `backfill_task_usage_hourly`를 실행하세요 — [7단계 → 옵션 C](#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)를 참고하세요
## 다음 단계
- [환경 변수](/environment-variables) — 전체 env 레퍼런스
- [인증 설정](/auth-setup) — Resend / OAuth / 가입 허용 목록 상세
- [GitHub 연동](/github-integration) — GitHub App을 연결해 PR이 이슈에 자동 연결되고 머지 시 이슈가 닫히도록 설정
- [문제 해결](/troubleshooting) — 문제가 생기면 여기서 시작하세요
- [데스크톱 앱](/desktop-app) — `~/.multica/desktop.json`을 통한 선택적 데스크톱 설정. 웹 프런트엔드 + CLI가 여전히 가장 빠른 자체 호스팅 경로입니다

View File

@@ -82,7 +82,7 @@ Two delivery backends are supported — pick whichever fits your network:
**Option B — SMTP relay (internal networks / on-premise):**
Use this when the deployment can't reach `api.resend.com`, or you already have an internal mail relay (Microsoft Exchange, Postfix, on-prem SendGrid, etc.). `SMTP_HOST` takes priority over Resend when both are set, so verification and invite mail stays on the internal relay. Port 465 (SMTPS / implicit TLS) is not currently supported — use 25 or 587.
Use this when the deployment can't reach `api.resend.com`, or you already have an internal mail relay (Microsoft Exchange, Postfix, on-prem SendGrid, etc.). `SMTP_HOST` takes priority over Resend when both are set, so verification and invite mail stays on the internal relay. STARTTLS is upgraded automatically when advertised; port `465` (SMTPS / implicit TLS) auto-enables an immediate TLS handshake, and `SMTP_TLS=implicit` (aliases: `smtps`, `ssl`) forces it on a non-standard SMTPS port.
For **anonymous Exchange internal relay (port 25)** — the host is trusted by IP and submits without credentials:
@@ -106,7 +106,18 @@ SMTP_TLS_INSECURE=false # set true only for private CA / self-signed
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
Then restart: `docker compose -f docker-compose.selfhost.yml restart backend`. On restart, the backend prints which provider it picked (`EmailService: SMTP relay …` / `Resend API` / `DEV mode`) — credentials are never logged, so this line is safe to share when asking for help.
For **implicit TLS / SMTPS (port 465)** — providers like Aliyun / Tencent enterprise mail that don't advertise STARTTLS. Port `465` auto-enables implicit TLS, so `SMTP_TLS` is optional here:
```bash
SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit # optional on 465; required on a non-standard SMTPS port
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
Then restart: `docker compose -f docker-compose.selfhost.yml restart backend`. On restart, the backend prints which provider it picked and the negotiated TLS mode (`EmailService: SMTP relay <host>:<port> (starttls|implicit-tls) from=…` / `Resend API` / `DEV mode`) — credentials are never logged, so this line is safe to share when asking for help.
For more auth configuration (OAuth, signup allowlist) and the full SMTP variable reference, see [Auth setup](/auth-setup) and [Environment variables → Email](/environment-variables#email-configuration).

View File

@@ -81,7 +81,7 @@ make selfhost
**Option B — SMTP relay内网/自部署):**
适合内网无法访问 `api.resend.com`或已经有内部邮件中继Microsoft Exchange、Postfix、自部署 SendGrid 等)的场景。同时设置时 `SMTP_HOST` 优先级高于 Resend验证码和邀请邮件不会走外部 provider。**暂不支持** 465SMTPS / 隐式 TLS,请使用 25 或 587
适合内网无法访问 `api.resend.com`或已经有内部邮件中继Microsoft Exchange、Postfix、自部署 SendGrid 等)的场景。同时设置时 `SMTP_HOST` 优先级高于 Resend验证码和邀请邮件不会走外部 provider。服务端 advertise STARTTLS 时会自动升级;端口 `465`SMTPS / 隐式 TLS会自动启用连接即握手的 TLS 模式,非标准 SMTPS 端口用 `SMTP_TLS=implicit`(别名:`smtps`、`ssl`)强制开启
**匿名 Exchange 内部 relay端口 25** —— 主机按 IP 被信任,不需要凭据:
@@ -105,7 +105,18 @@ SMTP_TLS_INSECURE=false # 仅在私有 CA / 自签证书时改成 true
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
之后重启:`docker compose -f docker-compose.selfhost.yml restart backend`。重启时 backend 会打印当前选择的 provider`EmailService: SMTP relay …` / `Resend API` / `DEV mode`),密码不会被记录,所以这行截图给同事是安全的。
**隐式 TLS / SMTPS端口 465** —— 适用于阿里云 / 腾讯企业邮箱等不广告 STARTTLS 的服务商。端口 `465` 自动启用隐式 TLS因此这里 `SMTP_TLS` 可省略:
```bash
SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit # 465 上可省略;非标准 SMTPS 端口上必填
RESEND_FROM_EMAIL=noreply@yourdomain.com
```
之后重启:`docker compose -f docker-compose.selfhost.yml restart backend`。重启时 backend 会打印当前选择的 provider 和协商出的 TLS 模式(`EmailService: SMTP relay <host>:<port> (starttls|implicit-tls) from=…` / `Resend API` / `DEV mode`),密码不会被记录,所以这行截图给同事是安全的。
更多 auth 配置OAuth、注册白名单以及完整的 SMTP 变量说明见 [登录与注册配置](/auth-setup) 和 [环境变量](/environment-variables)。

Some files were not shown because too many files have changed in this diff Show More