Compare commits

...

58 Commits

Author SHA1 Message Date
Lambda
199942ee1e feat(web): refine newhome landing demos
Co-authored-by: multica-agent <github@multica.ai>
2026-06-07 19:21:05 +08:00
Lambda
1f7447ad10 feat(web): rebuild value #2 delegate demo as the real delegation flow
Replace the static conversation with a scripted, looping flow that plays the actual delegation loop: compose a new issue and pick Claude Code from the Assignee dropdown -> create (assigned) -> the issue page with the agent working (reads/edits + spinner) -> the agent posts its result as a comment and the status moves to In Review. Adds newhome-fade / newhome-pop scene+popover entrances.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 16:19:29 +08:00
Lambda
10123ee41c feat(web): add values #2-#4 with alternating left/right layout
Build the remaining three value cards, each with its own focused auto-playing demo (same approach as value #1): #2 Delegation — a delegation conversation (person @mentions an agent, it works, then replies with a PR); #3 Accountability — a streaming run transcript (thinking/reads/edits/tests); #4 Leverage — a skills library with a highlight cycling across agents. Cards alternate sides (text/demo -> demo/text -> ...) via a ValueCard reverse prop. The content-light demos share a scale frame (ValueDemoFrame) at the same DEMO_ZOOM and are sized to fit the demo half without bleeding.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:56:28 +08:00
Lambda
c7bdf30d07 style(web): give controls their own radius (8px), keep surfaces at 6px
Radius is role-based, not one global value. Restore an 8px control radius (the design system's --radius-md) on buttons, nav links, the GitHub chip, and the demo browser tabs; keep surfaces (cards, demo windows, panel rows) at the restrained 6px. Flattening buttons to the container radius made them read boxy and off-brand versus the real product, whose Button uses rounded-lg.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:35:59 +08:00
Lambda
3c928abfeb style(web): tighten landing border radii to one restrained 6px tier
The value card's 24px corner was too large. Unify all landing-authored chrome (value card, demo window frames, buttons, nav links, chips, browser tabs, panel rows) to a single small 6px radius. The embedded real-product component radii (board columns/cards) are left as-is so the live demo still matches the actual product.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:30:55 +08:00
Lambda
d51d6628cd feat(web): make value section a self-contained bordered card
Drop the big section heading and the hero->features divider/background tint; each value is now a rounded, tinted, bordered card that supplies its own framing. The card's overflow-hidden border is the boundary that clips the demo, so it bleeds to the card edge instead of past the browser edge. Keep the value title on one line (lg:whitespace-nowrap).

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:26:44 +08:00
Lambda
064cbf80ff feat(web): side-by-side value layout (text left, demo bleeds right)
Replace the stacked text-above-board value layout with a two-column row: a compact claim on the left (vertically centered) and the live demo on the right at its real shared-zoom size, bleeding off the right page edge (section uses overflow-x-clip so there's no horizontal scrollbar). Matches the requested reference layout.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:15:49 +08:00
Lambda
6cfe42a4f2 feat(web): add /newhome landing V2 sandbox with interactive demos
Sandbox landing at /newhome (production / untouched): hero with an embedded, interactive product demo (real board/issue-detail/create-issue/transcripts over mock data) and a Values section whose first pillar is an auto-playing board built from the real product components. All embedded demos render at one shared DEMO_ZOOM, and popups portal into the scaled box so they share the zoom.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 03:17:43 +08:00
Lambda
6a5150bb01 feat(ui): add opt-in PortalContainerProvider for portaled popups
Popups (DropdownMenu, Popover, Dialog, HoverCard, Tooltip) portal to document.body by default. Add a PortalContainerProvider/usePortalContainer context (default undefined -> body, so production behavior is unchanged) and wire each Portal to it, so an embedded surface inside a CSS transform can redirect popups into its own scaled box.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 03:17:43 +08:00
LinYushen
de900b2ba6 feat(server): funnel/community/commercial business metrics + PostHog pairing (MUL-2949) (#3698)
* feat(server): funnel/community/commercial business metrics + PostHog pairing (MUL-2949)

PR3 of the Grafana board metrics split (parent MUL-2328).

Adds 23 new Prometheus counter/histogram families to the PR2 BusinessMetrics
collector covering the activation/community/commercial funnels, and binds
every PostHog event emission to a matching metric increment so the two sides
cannot drift.

Funnel: signup, workspace_created, team_invite_sent/accepted, onboarding_*,
cloud_waitlist_joined.
Content: issue_created, chat_message_sent, agent_created, squad_created,
autopilot_created, issue_executed.
Runtime: runtime_registered/ready/failed/offline + ready_seconds histogram,
daemon_ws_message_received_total.
Autopilot: autopilot_run_started/terminal/skipped.
Webhook/GitHub: webhook_delivery_total, github_event_received_total,
github_pr_review_total, github_pr_merge_seconds histogram.
CloudRuntime: cloudruntime_request_total + duration histogram, wired through
a small RequestRecorder interface so the cloudruntime package stays decoupled
from metrics.
Commercial: feedback_submitted, contact_sales_submitted.

The pairing helper metrics.RecordEvent(client, m, ev) emits the PostHog
event AND increments the matching counter via IncForEvent dispatch, reading
labels from the analytics event Properties. Every existing
h.Analytics.Capture(analytics.X(...)) call site has been migrated to the
helper across handler/, service/, and cmd/server/runtime_sweeper.go.

Lint enforcement (server/internal/metrics/business_pairing_test.go):
- TestEveryAnalyticsEventHasPrometheusCounter: every Event* constant in
  analytics/events.go either dispatches via IncForEvent or is in the
  taskMetricEvents allow-list (PR2 typed RecordTask* methods).
- TestNoNakedAnalyticsCaptureInHandlersOrServices: AST-walks handler/
  service/cmd-server for direct Analytics.Capture(...) calls — only
  service/task.go's captureTaskEvent helper is allow-listed.
- TestEveryAnalyticsRecordEventTakesAnalyticsHelper: validates the third
  arg of every metrics.RecordEvent call is built from analytics.*.

Cardinality protection: all new label values pass through fixed allow-lists
in labels_pr3.go; unknown values collapse to 'other'/'unknown'/'error'.

Refs:
- Spec MUL-2328 / MUL-2949.
- Builds on PR2 (MUL-2948) — collectors registered through the same
  BusinessMetrics struct, no separate Registry.
- Uses PR1's taskfailure.Reason (MUL-2946) for runtime_failed's failure_reason
  label via NormalizeFailureReason.

Out of scope: Sampler-class metrics (PR4 / MUL-2947), pr_review_total
emission point (no review event handler exists yet — counter is defined,
TODO to wire up when /api/webhooks/github grows pull_request_review handling).

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

* fix(server): tighten PR3 review items — signup_source bucket, fill platform/kind/form_source enums, onboarding_started server emission, lint scope (MUL-2949)

Addresses 张大彪's review on #3698:

1. signup_source: NormalizeSignupSource added to labels_pr3.go with a
   fixed allow-list bucket (direct/google/twitter/linkedin/.../other).
   Parses JSON cookie payload for utm_source/source/referrer fields,
   strips URL schemes, maps well-known hostnames to channel buckets.
   PostHog event still ships the raw cookie value for analytics; only
   the Prometheus label is bucketed.

2. Filled the unknown/other label gaps:
   - analytics.IssueCreated and analytics.ChatMessageSent now take a
     platform parameter sourced from middleware.ClientMetadataFromContext
     (X-Client-Platform header) at the handler. Autopilot-originated
     issues stamp PlatformServer.
   - analytics.FeedbackSubmitted now takes a kind parameter; CreateFeedback
     reads req.Kind (default "general") so the picker selection lights up
     the metric's kind label instead of long-term "other".
   - analytics.ContactSalesSubmitted now takes a formSource (page /
     onboarding / agents_page); CreateContactSales reads req.Source.
     The metric reads ev.Properties["form_source"] so the analytics
     CoreProperties.Source ("marketing_contact_sales") stays
     backward-compat for PostHog dashboards.

3. analytics.OnboardingStarted helper added; server-side emission lives
   in PatchOnboarding, fired exactly once per user on the first PATCH
   that carries a non-empty questionnaire payload (firstTouch logic
   compares prior bytes against {} / null). Frontend onboarding_started
   keeps firing on page open; the server emission is what guarantees the
   Prometheus counter exists so Grafana can be cross-checked against the
   PostHog funnel without depending on the SDK roundtrip.

4. business_pairing_test.go tightened:
   - TestNoNakedAnalyticsCaptureInHandlersOrServices now allow-lists at
     function granularity (just captureTaskEvent in service/task.go), not
     whole-file. Any future naked Capture in the same file fails CI.
   - TestEveryAnalyticsRecordEventTakesAnalyticsHelper now does def-use
     tracking inside the enclosing FuncDecl: when RecordEvent's third
     arg is an *ast.Ident, the test walks the function body for the
     assignment that defined it and confirms the RHS is an
     analytics.<Helper>(...) call. Bare local idents that didn't
     originate from analytics are now caught.

5. gofmt -w applied across the touched files; gofmt -l clean.

Tests: go test ./internal/metrics/... ./internal/analytics/... pass.
Pre-existing TestClaimTask_/TestWebhook_MergedPR/TestDeleteIssueByIdentifier
failures on origin/main are DB-environment-dependent and not regressions
from this change.

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

* fix(server): normalise onboarding_started platform label + regression test (MUL-2949)

Addresses 张大彪's last review nit:

- IncForEvent's EventOnboardingStarted case now wraps the platform
  property with NormalizePlatform, matching every other platform-bearing
  metric. A misbehaving frontend can no longer leak a raw X-Client-Platform
  header value into the multica_onboarding_started_total{platform=...}
  series.

- New labels_pr3_test.go covers every PR3 normalizer with both a happy-path
  value and an unknown value, asserting the unknown collapses to the
  documented fallback bucket. Includes a focused regression for
  onboarding_started: emits one event with an attacker-shaped platform
  string and asserts the metric only exposes web + unknown label values
  (no raw header bleed).

- testutil.go gains a small GatherForTest helper so the regression test
  can pull the typed MetricFamily map without re-implementing the
  registry-walk dance.

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

* fix(server): NormalizeTaskSource on workspace_created + document lint limitations (MUL-2949)

Final review touch-ups before merge:

- IncForEvent's EventWorkspaceCreated case wraps source through
  NormalizeTaskSource, matching the other source-bearing dispatches
  (issue_created, agent_created, issue_executed). Closes the last raw
  property leak in the dispatcher table.

- business_pairing_test.go inline docstrings now spell out the two
  known limitations of the lint gate that 张大彪 / Eve flagged:
  analyticsBackedIdents matches by ident NAME (not SSA def-use, so a
  nested-scope shadow could pass) and isMetricsRecordEvent hard-codes
  the import alias set. PR description carries a Follow-ups section
  with the same two items so the work is visible after merge.

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

---------

Co-authored-by: 魏和尚 <agent+wei@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 16:39:06 +08:00
LinYushen
24ea169d89 fix(migrate): serialize startup migrations with pg advisory lock (#3658)
cmd/migrate previously ran a check-then-apply loop on a *pgxpool.Pool
with no locking, so two backend pods starting at the same time (multi-
replica Deployment, scale-up, or a manual run overlapping with pod
startup) could both pass the EXISTS check on a pending migration and
race on the DDL or the schema_migrations INSERT, crashing the loser.

Take a single connection from the pool, hold a session-level
pg_advisory_lock for the entire migration loop, and release it on the
way out. We use the blocking variant so a late arriver queues behind
the current runner and then no-ops on the EXISTS checks instead of
crash-looping. The loop deliberately stays outside a transaction so
existing CREATE INDEX CONCURRENTLY migrations keep working.

Also refresh the values.yaml / backend.yaml comments next to
backend.replicas: the chart still ships replicas: 1 by default, but
that is now a recommendation (Recreate strategy, no leader split), not
a correctness requirement.

Refs https://github.com/multica-ai/multica/issues/3647

Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 15:51:03 +08:00
Bohan Jiang
5900d8b637 fix(issues): make start_date/due_date timezone-stable calendar days (#3618) (#3692)
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925)

These fields are calendar days (the pickers offer no time-of-day), but were
stored as TIMESTAMPTZ. A client serializing local midnight via toISOString()
folded its timezone into the instant, so the day shifted by the local offset
(GH #3618). Migrate the columns to DATE and parse/serialize date-only
"YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the
UTC day) so older clients keep working.

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

* fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925)

Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of
toISOString(), and every read formats via the shared @multica/core/issues/date
helpers with timeZone:"UTC" so the day never shifts with the viewer's offset.
The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers,
quick-set menu, list/board/detail/activity, and the mobile due-date picker.

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

* fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925)

Review follow-ups on #3692:
- ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339
  to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight
  instant and rejects ambiguous ones loudly. Adds util unit tests.
- migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via
  AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too.
- Convert remaining date-change display sites to formatDateOnly: inbox detail
  label (web) and mobile activity + inbox labels (were new Date()+local format).
- CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339.

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

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 14:34:01 +08:00
Multica Eve
a72fb020de Add business metrics collectors (#3695)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 14:32:44 +08:00
Multica Eve
10afd1af1b feat(server): introduce pkg/taskfailure classifier and switch in-flight failure_reason writes (MUL-2946) (#3693)
Lift MUL-1949's offline backfill failure_reason taxonomy into a shared
in-flight classifier so the agent_task_queue.failure_reason column is
written with refined values (provider_auth_or_access, context_overflow,
provider_capacity_or_rate_limit, …) at write time rather than waiting on
SQL backfill to re-classify after the fact. PR1 of the Grafana board
plan in MUL-2328 — the upcoming PR2 reuses pkg/taskfailure.AllReasons()
to pre-warm the Prometheus failure_reason label set.

* server/pkg/taskfailure: new package with the canonical 21 Reason
  constants (7 platform-side + 14 agent_error.* sub-reasons),
  AllReasons() returning a defensive copy, IsAgentError() prefix check,
  and Classify(rawError) Reason mirroring the SQL CASE rules from
  MUL-1949 (db-boy's analysis). 100% statement coverage.
* server/internal/daemon/daemon.go: route the 'agent_error' coarse
  fallback paths (StartTask error, runTask early-return error, CompleteTask
  permanent rejection, reportTaskResult default branch) and the
  executeAndDrain default error case (chained after classifyPoisonedError)
  through taskfailure.Classify so blocked / timeout / unknown-status
  results all carry a refined reason on the wire.
* server/internal/service/task.go: FailTask classifies errMsg when the
  daemon-supplied failureReason is empty, eliminating the legacy
  COALESCE(.., 'agent_error') landing.
* server/internal/daemon/poisoned.go: alias FailureReasonIterationLimit
  and FailureReasonAPIInvalidRequest to the canonical taskfailure
  constants. agent_fallback_message and codex_semantic_inactivity are
  pre-existing operational reasons not in the canonical 21 — kept as
  literals for now and revisited in a follow-up PR.

Backfill SQL from MUL-1949 stays as the authoritative offline source of
truth; this PR keeps the in-flight classifier in lock-step with the SQL
CASE expression so historical and future rows share the same taxonomy.
No behavior change for the platform-side reasons (queued_expired,
runtime_offline, runtime_recovery, timeout, etc.) which already align
with the canonical set.

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 13:52:56 +08:00
Naiyuan Qing
f2f17e3355 Optimize chat message loading (#3685)
* Optimize chat message loading

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

* Fix chat history cursor pagination

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

* Fix chat session list remount key

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

* fix(chat): fall back to legacy /messages when paged endpoint 404s

Deployment-order compatibility: a backend deployed before the
/messages/page endpoint existed returns 404 for the unknown route.
The cursorless initial page now falls back to the legacy full-list
/messages endpoint and wraps it in a single has_more:false page, so
chat never white-screens regardless of which side deploys first. A 404
on a cursor request still propagates to avoid duplicating the full list.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:47:30 +08:00
Bohan Jiang
fcb5099ec5 fix(agent): raise opencode model-discovery timeout to 15s (MUL-2888) (#3689)
Newer opencode (1.15+) syncs its hosted free-model catalog over the
network on `opencode models`, which can take ~6s. The previous 5s cap
killed the command, discoverOpenCodeModels returned an empty list, and
the daemon reported it as a successful empty result — so the runtime
showed online but the model picker was empty ("暂无可用模型").

Fixes #3627

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 12:18:43 +08:00
Naiyuan Qing
0dd30c544c fix(editor): close suggestion popups on outside focus (#3683)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 10:11:26 +08:00
Jiayuan Zhang
0d51614c9c feat(editor): text highlight (==text==) in description & comments [MUL-2934] (#3661)
* feat(editor): support text highlight (==text==) in description & comments

Adds a single-color (yellow) text highlight mark to the shared rich-text
editor, round-tripped through stored Markdown as ==text==.

- HighlightExtension: @tiptap/extension-highlight + @tiptap/markdown hooks
  (markdownTokenizer/parseMarkdown/renderMarkdown) so ==text== <-> <mark>
  round-trips; inner inline formatting preserved via inlineTokens.
- Bubble menu: highlight toggle button (Mod-Shift-H), i18n in 4 locales.
- Read-only renderer: highlightToHtml lowers ==text== -> <mark> (skips code
  and math); rehype-sanitize schema whitelists <mark>. Nested Markdown inside
  a highlight still parses via the existing rehype-raw step.
- prose.css: single yellow <mark> style, legible in light/dark.

Pinned @tiptap/extension-highlight to exact 3.22.1 to match @tiptap/core
(>=3.23 expects a getStyleProperty export core 3.22.1 doesn't have).

Web/desktop only. Mobile (native md4c, no == syntax, no custom renderers)
is tracked as a follow-up. MUL-2934.

Tests: editor round-trip (cross-process serialization protocol), readonly
<mark> rendering + sanitize, and the ==->mark transform incl. code-skip.

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

* fix(editor): align highlight boundary rules across editor & readonly

Addresses two boundary bugs from review (PR #3661):

1. A == inside inline code/math could close a highlight when the opening
   == was outside the literal span (e.g. ==a `b==c` d== wrongly became
   <mark>a `b</mark>c` d==). Both the editor tokenizer's lazy regex and the
   readonly transform only guarded the opening fence, not the closing one.
2. The readonly transform matched across blank lines (==a\n\nb==) while the
   editor lexes those as two literal paragraphs — a storage↔editor↔readonly
   mismatch.

Fix: extract one shared matcher (utils/highlight-match.ts) used by BOTH the
editor tokenizer and the readonly lowering, so the rules can't drift. It skips
fences that fall inside code/math literal ranges (open or close) and caps the
inner span at the first blank line.

Tests: shared-matcher unit tests + both repros covered on the editor
(round-trip/HTML) and readonly (transform + rendered DOM) sides.

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

* fix(editor): handle CRLF in highlight blank-line boundary

BLANK_LINE_RE only matched LF, so a CRLF blank line (==a\r\n\r\nb==) was not
recognized as a block boundary and got highlighted. Widen to \r?\n[ \t]*\r?\n.

Tests: CRLF blank-line (no highlight) + CRLF soft-break (still highlights) on
the matcher, readonly transform, and editor sides.

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:24:55 +02:00
Bohan Jiang
44feb3d06d fix(skill): canonicalize reserved SKILL.md path check across daemon + API (#3660)
A skill_file row whose path is the skill's own SKILL.md (persisted by
older builds or direct create/update API calls) collides with the
primary content the daemon writes itself, failing task prep with
errPathPreExists on every non-codex local runtime (#3489).

#3526 guarded this with strings.EqualFold(path, "SKILL.md") at the
daemon write site and the three API ingress points, but the stored path
is not canonicalized: "./SKILL.md" or "sub/../SKILL.md" slip past the
exact-match guard while filepath.Join still resolves them onto the same
SKILL.md, so prep can still break.

Extract one canonical helper, skill.IsReservedContentPath, that cleans
the path before the case-insensitive compare, and use it at all four
sites (execenv writeSkillFiles, skill create, update, single-file
upsert). Add a daemon-side regression test for writeSkillFiles ignoring
a bundled SKILL.md (exact + "./" spellings) — the load-bearing fix
previously had only API-layer coverage — plus a unit test for the helper.

Existing poisoned rows are intentionally left in place (skipped at prep)
per the decision on MUL-2928.

MUL-2928
Follow-up to #3526; supersedes #3560.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 18:23:57 +08:00
Multica Eve
48044cc918 docs: add June 2 changelog entry (#3656)
* docs: add June 2 changelog entry

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

* docs: simplify June changelog feature copy

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-02 18:04:28 +08:00
MSandro
996eb07dc5 fix(daemon): skip duplicate SKILL.md in supporting files to prevent task prep failures (#3526)
Fixes #3489

MUL-2928
2026-06-02 17:53:20 +08:00
Bohan Jiang
888186b183 fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904) (#3654)
* fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904)

Agents inlining a backtick-wrapped token into `multica issue comment add
--content "..."` had the shell run it as a command substitution, silently
deleting the token; the stored comment never matched the model's intent, so
it retried forever — spamming OKK-497 with duplicate comments.

The corruption is shell-driven, not provider-driven, so extend the
"never inline --content; use --content-file / quoted-HEREDOC --content-stdin"
rule from Codex-only to ALL providers:

- BuildCommentReplyInstructions: collapse the Linux/macOS non-Codex inline
  branch into the unified quoted-HEREDOC stdin template.
- buildMetaSkillContent: rename "Codex-Specific Comment Formatting" ->
  "Comment Formatting" and emit it for every provider; strengthen the
  Available Commands entry and the assignment step-6 examples to steer away
  from inline --content.
- Windows behavior unchanged (file-only; avoids PowerShell ASCII drop).

Tests: flip the non-Codex Linux reply test into a MUL-2904 regression,
broaden the stdin-emphasis test across providers, and pin the
provider-agnostic guardrail.

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

* fix(daemon): keep Windows assignment brief file-only (address review)

Review catch on #3654: the previous commit added platform-agnostic prose
recommending "--content-file or --content-stdin" in the Available Commands
entry and the assignment-triggered step-6 example. The assignment path has
no BuildCommentReplyInstructions OS override, so on Windows an agent following
step 6 literally would pipe its final comment through PowerShell and drop
non-ASCII bytes (#2198 / #2236 / #2376) — contradicting this PR's own
Windows file-only rule in the ## Comment Formatting section.

Make the platform-agnostic surfaces defer to the OS-aware ## Comment
Formatting section (the single source of truth) instead of naming stdin.
The flag synopsis still lists all three modes.

Add TestInjectRuntimeConfigWindowsAssignmentBriefStaysFileOnly: a Windows
assignment-triggered brief must not contain any prescriptive "... or
--content-stdin" recommendation.

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 17:47:09 +08:00
Anderson Shindy Oki
92309cf5e2 fix: autopilot page and modal mobile responsive (MUL-2929) (#3471)
* fix: autopilot page and modal mobile responsive

* fix(autopilots): label icon-only action buttons and keep desktop padding

- Add aria-label to Edit/Run now buttons so they have an accessible
  name on mobile where the text label is hidden via 'hidden sm:inline'.
- Change button padding 'px-2 sm:px-3' -> 'px-2 sm:px-2.5' so the
  size="sm" default (px-2.5) is preserved on desktop (no visual diff).

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

---------

Co-authored-by: J (Multica Agent) <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:32:12 +08:00
Naiyuan Qing
e1a5310780 feat(cli): add skill content file and stdin input (#3652)
* feat(cli): add skill content file and stdin input

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

* test(cli): set skill server env for flag validation

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:25:37 +08:00
LinYushen
a6247ad714 helm: gate uploads PVC behind backend.uploads.persistence.enabled (#3655)
Adds a value (default true for backward compatibility) that gates the
uploads PersistentVolumeClaim, the backend container's volumeMount, and
the pod-spec volume. Operators who serve uploads from S3 (S3_BUCKET set)
can now set backend.uploads.persistence.enabled=false to drop the PVC
entirely, removing the ReadWriteOnce Multi-Attach barrier on the storage
side for replicas > 1.

Also makes the PVC accessModes configurable (default [ReadWriteOnce]) so
operators with a ReadWriteMany-capable StorageClass can share the
uploads volume across replicas without object storage.

Documentation: values.yaml comments and the SELF_HOSTING.md resource
list are updated to describe the new toggle.

Refs: https://github.com/multica-ai/multica/issues/3646

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:21:26 +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
421 changed files with 28445 additions and 1647 deletions

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

@@ -144,7 +144,7 @@ If you already run a Kubernetes cluster, you can deploy Multica there instead of
The chart creates the following resources in the target namespace:
- `multica-postgres``pgvector/pgvector:pg17` backed by a 10Gi PVC
- `multica-backend` — Go API/WS server backed by a 5Gi uploads PVC
- `multica-backend` — Go API/WS server. Backed by a 5Gi `ReadWriteOnce` uploads PVC by default; set `backend.uploads.persistence.enabled=false` when you have configured S3 (`backend.config.s3Bucket`) and don't want the chart to declare the PVC at all.
- `multica-frontend` — Next.js standalone server
- Two `Ingress` resources: one for the web host, one for the backend host
- `multica-config` ConfigMap (rendered from `values.yaml`)

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

@@ -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,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

@@ -4,6 +4,7 @@ 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));
@@ -19,6 +20,23 @@ function expectChineseFontsBeforeKoreanFonts(source: string) {
}
}
// 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(
@@ -28,4 +46,13 @@ describe("CJK font fallback order", () => {
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

@@ -31,6 +31,26 @@
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

@@ -26,11 +26,11 @@ import { SquadsPage, SquadDetailPage as SquadDetailPageView } from "@multica/vie
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" },
},
{

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,21 +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",
"Apple SD Gothic Neo",
"Malgun Gothic",
"Noto Sans CJK KR",
"sans-serif",
],
variable: "--font-inter",
});
const geistMono = Geist_Mono({

View File

@@ -17,6 +17,24 @@ 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: {
@@ -26,6 +44,15 @@ export const { GET } = createFromSource(source, {
},
},
},
ja: {
components: {
tokenizer: {
language: "english",
normalizationCache: new Map(),
tokenize: tokenizeJapanese,
},
},
},
zh: {
components: {
tokenizer: {

View File

@@ -4,6 +4,7 @@ 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));
@@ -19,13 +20,38 @@ function expectChineseFontsBeforeKoreanFonts(source: string) {
}
}
// 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 layoutSource = readFileSync(
resolve(process.cwd(), "app/[lang]/layout.tsx"),
const cssSource = readFileSync(
resolve(process.cwd(), "app/global.css"),
"utf8",
);
expectChineseFontsBeforeKoreanFonts(layoutSource);
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,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,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,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,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,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,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,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

@@ -88,7 +88,7 @@ CI나 headless 환경에서는 브라우저 플로우를 건너뛰세요. 웹
| `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 member list/add/remove/set-role <squad-id>` | 스쿼드 멤버 관리 및 역할 직접 업데이트 |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 스쿼드 리더 에이전트가 매 턴마다 평가를 기록할 때 사용 |
전체 모델은 [스쿼드](/squads)를 참고하세요.

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,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,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,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,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,4 @@
{
"title": "開発者",
"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,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,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,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,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,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,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,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,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,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,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,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,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,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

@@ -22,7 +22,7 @@ Multica는 **12개의 AI 코딩 도구**를 기본 지원합니다. 이들은
| **Hermes** | Nous Research | ✅ | ✅ | `.agent_context/skills/` (fallback) | 동적 탐색 |
| **Kimi** | Moonshot | ✅ | ✅ | `.kimi/skills/` | 동적 탐색 |
| **Kiro CLI** | Amazon | ✅ | ✅ | `.kiro/skills/` | 동적 탐색 |
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 동적 탐색 |
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 동적 탐색 + variant |
| **OpenClaw** | 오픈소스 | ✅ | ✅ | `.agent_context/skills/` (fallback) | 에이전트에 바인딩되어 작업마다 전환 불가 |
| **Pi** | Inflection AI | ✅ (세션이 파일 경로) | ❌ | `.pi/skills/` | 동적 탐색 |
@@ -66,7 +66,7 @@ Amazon에서 제공합니다. `kiro-cli acp`를 통해 stdio 위에서 ACP를
### OpenCode
SST에서 제공하는 오픈소스입니다. 사용 가능한 모델 동적으로 탐색합니다(CLI의 구성 파일을 스캔). 세션 재개가 동작하고, 에이전트의 `mcp_config` 필드를 소비합니다. Multica는 `OPENCODE_CONFIG_CONTENT` 환경 변수를 통해 이를 인라인으로 주입하므로, 에이전트의 MCP 서버가 작업 디렉터리의 `opencode.json`(에이전트 또는 사용자가 소유하는 파일)을 건드리지 않고 OpenCode에 전달됩니다. **자신의 모델 카탈로그를 커스터마이징하고 싶은, 만지작거리기 좋아하는 사용자에게 적합합니다.**
SST에서 제공하는 오픈소스입니다. 사용 가능한 모델과 모델 variant를 동적으로 탐색합니다(CLI의 구성 파일을 스캔). 세션 재개가 동작하고, 에이전트의 `mcp_config` 필드를 소비합니다. Multica는 `OPENCODE_CONFIG_CONTENT` 환경 변수를 통해 이를 인라인으로 주입하므로, 에이전트의 MCP 서버가 작업 디렉터리의 `opencode.json`(에이전트 또는 사용자가 소유하는 파일)을 건드리지 않고 OpenCode에 전달됩니다. 모델이 variant를 노출하면 Multica는 이를 에이전트 thinking selector로 표시하고 선택한 값을 `opencode run --variant`로 전달합니다. **자신의 모델 카탈로그를 커스터마이징하고 싶은, 만지작거리기 좋아하는 사용자에게 적합합니다.**
### OpenClaw

View File

@@ -22,7 +22,7 @@ For guidance on picking a tool when creating an agent, see [Creating and configu
| **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 |
| **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 |
@@ -66,7 +66,7 @@ From Amazon. Uses ACP over stdio via `kiro-cli acp`. Session resumption works th
### OpenCode
From SST, open source. Dynamically discovers available models (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). **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

View File

@@ -22,7 +22,7 @@ Multica 内置支持 **12 款 AI 编程工具**。它们都实现了同一套接
| **Hermes** | Nous Research | ✅ | ✅ | `.agent_context/skills/` fallback| 动态发现 |
| **Kimi** | Moonshot | ✅ | ✅ | `.kimi/skills/` | 动态发现 |
| **Kiro CLI** | Amazon | ✅ | ✅ | `.kiro/skills/` | 动态发现 |
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 动态发现 |
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 动态发现 + variant |
| **OpenClaw** | 开源项目 | ✅ | ✅ | `.agent_context/skills/` fallback| 绑定在智能体上,不能在任务里切换 |
| **Pi** | Inflection AI | ✅session 为文件路径)| ❌ | `.pi/skills/` | 动态发现 |
@@ -66,7 +66,7 @@ Amazon 出品。通过 `kiro-cli acp` 使用 ACP stdio 协议。会话恢复走
### OpenCode
SST 出品,开源。动态发现可用模型(扫 CLI 的配置文件)。会话恢复真用,会消费智能体的 `mcp_config` 字段——Multica 通过 `OPENCODE_CONFIG_CONTENT` 环境变量内联注入,让智能体的 MCP server 直接到达 OpenCode**不会去碰任务工作目录里的 `opencode.json`**(那个文件归智能体或用户所有)。**适合爱折腾、想自定义模型目录**的开发者。
SST 出品,开源。动态发现可用模型和模型 variant(扫 CLI 的配置文件)。会话恢复真用,会消费智能体的 `mcp_config` 字段——Multica 通过 `OPENCODE_CONFIG_CONTENT` 环境变量内联注入,让智能体的 MCP server 直接到达 OpenCode**不会去碰任务工作目录里的 `opencode.json`**(那个文件归智能体或用户所有)。当模型暴露 variant 时Multica 会把它显示成智能体的思考强度选择,并通过 `opencode run --variant` 传给 OpenCode。**适合爱折腾、想自定义模型目录**的开发者。
### OpenClaw

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,67 @@
---
title: スキル
description: エージェントに「ナレッジパック」を取り付けましょう — Anthropic Agent Skills オープン標準と互換性があります。
---
import { Callout } from "fumadocs-ui/components/callout";
スキルは[エージェント](/agents)のための**ナレッジパック**です — `SKILL.md` 1 つと、任意の補助ファイルスクリプト、設定、参照テンプレートで構成され、エージェントに「この種のタスクに出くわしたら、こう考えてこう動け」と伝えます。Multica は [Anthropic Agent Skills](https://agentskills.io) オープン標準を採用しているため、Anthropic 公式リポジトリ、ClawHub、skills.sh などから取得した、標準に準拠したどのスキルでも直接インポートできます。
## ワークスペーススキルとローカルスキル
Multica は 2 つのスキルソースに対応しています。
- **ワークスペーススキル** — Multica のクラウドに保存されます。エージェントに取り付けると、タスク実行時にあなたのデーモンへ同期されます。これが**チーム全体でスキルを共有する標準的な方法**です。
- **ローカルスキル** — あなたのマシン上のディレクトリに存在します(各 AI コーディングツールには慣例的なデフォルトパスがあります。例: Claude Code の `~/.claude/skills/`)。あなたが要求すると、[デーモン](/daemon-runtimes)がマシンをスキャンし、どれをワークスペースに取り込むかを手動で選びます。
ほとんどの場合は**ワークスペーススキル**が望ましいでしょう。一度インポートすれば、すべてのチームメイトのエージェントが使えるからです。ローカルスキルは、まずローカルでテストしたい場合や、内容に機密性の高いローカル資料が含まれる場合に適しています。
## スキルをインポートする
ワークスペーススキルは 4 つのソースから取得します。
- **新規作成** — UI で `SKILL.md` と関連ファイルを直接作成します
- **GitHub から** — リポジトリ URL を貼り付けると(例: `https://github.com/owner/repo/tree/main/skills/my-skill`、Multica がそのディレクトリの `SKILL.md` とすべてのファイルを取得します
- **ClawHub から** — [ClawHub](https://clawhub.io) 公開マーケットプレイスで検索し、バージョンを選択してインポートします
- **ローカルから** — デーモンがあなたのマシンのスキルディレクトリをスキャンし、ワークスペースに取り込むものを選びます
個々のファイルもスキルパック全体も容量の上限がありますGitHub からインポートする際の単一ファイルの上限は約 1 MB。正確なルールはインポートダイアログに表示され、上限を超えるとエラーが返されます。
## エージェントに取り付ける
インポートしたスキルは、**特定のエージェントに取り付けて**初めて効果を発揮します。1 つのエージェントに複数のスキルを取り付けられ、1 つのスキルを複数のエージェントに取り付けることもできます。
取り付けた後は、エージェントが次にタスクを開始するときにスキルを取り込みます — 各 AI コーディングツールには固有のスキル探索パスがありClaude Code は `.claude/skills/`、Cursor は `.cursor/skills/`、Antigravity は `.agents/skills/` などを使用、Multica が正しい場所にファイルを自動的に配置します。**ただし、3 つのツールGemini、Hermes、OpenClawは現在、汎用のフォールバックパス `.agent_context/skills/` を使用しており、これらのツールが実際にそのパスからスキルを読み込むかどうかはツール自体に依存します。** 完全なパスマッピングと、ネイティブ探索とフォールバックの区別は [AI コーディングツール比較 → スキルファイルが置かれる場所](/providers#where-skill-files-go)にあります。
スキルの内容を編集した後は、**新しく作成されたタスクだけが新しいバージョンを取り込みます** — すでに実行中のタスクは以前のスキルをそのまま使い続けます。
## サードパーティスキルの安全性
GitHub や ClawHub からインポートしたスキルには、スクリプトや実行可能なコンテンツが含まれることがあります。Multica 自体はそれらを**署名も、監査も、サンドボックス化もしません** — スキルの内容はそのまま対応する AI コーディングツールに渡され、ツールがそれを実行可能なものとして扱うかどうかはツール次第です。
<Callout type="warning">
**サードパーティスキルをインポートする前に、`SKILL.md` と同梱されるすべてのファイルを確認してください。**
2026 年 2 月に発生した「ClawHavoc」事件では、人気のスキルパックに仕込まれた悪意ある指示が、影響を受けたユーザーの API キーを盗み出しました。ClawHub はその後 VirusTotal スキャンを追加しましたが、**自動スキャンはあなた自身の確認の代わりにはなりません。**
**信頼できるソースからのみインポートしてください。** 機密データが関わるプロジェクトでは、自分で書いたローカルスキルだけを使うことを検討してください。
</Callout>
## スキルと MCP
どちらもエージェントができることを拡張しますが、方向が異なります。
- **スキル** = 構造化された**ナレッジパック**(静的なコンテンツ + 指示)。エージェントはスキルを読んで「問題 X を見たら、こう考えてこう行動する」を学びます。
- **MCP**Model Context Protocol= **ツールチャネル**。エージェントは MCP を使って外部サービス(データベース、ファイルシステム、サードパーティ APIに接続し、それらを**呼び出します**。
この 2 つは相互補完的です。現在の Multica では、MCP のサポートを**実際に使うのは Claude Code だけ**です — 他のツールは MCP 設定を受け取りはしますが、実際には使いません。MCP 専用のセクションは今後のリリースで追加される予定です。
---
これで、エージェントとは何か、どう作るか、スキルをどう取り付けるかが分かりました。次の問いはこれです。**エージェントは実際にどこで実行され、なぜ自分のエージェントはときどき止まってしまうのか?** 次の章では実行アーキテクチャ — デーモン、ランタイム、そしてタスクがどう連携するか — を扱います。
## 次のステップ
- [デーモンとランタイム](/daemon-runtimes) — エージェントが実際に実行される場所、そしてオンラインとオフラインの見分け方
- [タスクの実行](/tasks) — 1 回の「エージェント作業セッション」の全ライフサイクル
- [AI コーディングツール比較](/providers) — 12 ツール全体の比較(各ツールのスキル注入パスを含む)

View File

@@ -0,0 +1,136 @@
---
title: スクワッド
description: "スクワッドは、1 人の指定されたリーダーエージェントが率いるエージェント(そして任意で人間のメンバー)のグループです。スクワッドにイシューを割り当てると、リーダーが誰が引き受けるかを決定します。"
---
import { Callout } from "fumadocs-ui/components/callout";
スクワッドは、1 人の指定された**リーダーエージェント**を擁する、**[エージェント](/agents)と人間の[メンバー](/members-roles)の名前付きグループ**です。スクワッド自体が一級の担当者です。どの **Assignee** ピッカーからでもスクワッドを選ぶと、リーダーがトリガーを受け取り、イシューを読んだ上で、その作業に最も適したスクワッドメンバーを `@` でメンションします。スクワッドを使えば、専門家を一度組み立てておいて、**名前ではなくトピックで**作業を割り振れます — チームが大きくなっても、ルーティングはそのまま維持されます。
## スクワッドの動作原理
- **リーダー 1 人、メンバー多数。** リーダーは必ずエージェントでなければならず、メンバーはエージェントでも人間のメンバーでも構いません。リーダーだけのスクワッドも許可されますリーダーブリーフィングに「no other members」と表示されます。同じエージェントが複数のスクワッドに所属することもできます。
- **人を選べるあらゆる場所で割り当て可能。** スクワッドは Assignee ピッカー、@メンションピッカー、クイック作成モーダルに表示されます — エージェントやメンバーを選べる場所ならどこでも、スクワッドを選べます。
- **アーカイブによるソフト削除。** スクワッドをアーカイブすると、ピッカーや一覧から消えます。現在そのスクワッドに割り当てられているイシューはすべて**リーダーエージェントに移管され**、作業が途切れないようにします。アーカイブされたスクワッドに新しいイシューを割り当てることはできません。
## スクワッドと単一エージェントのどちらを使うか
| スクワッドを選ぶ場合… | 単一エージェントを選ぶ場合… |
|---|---|
| 複数の専門家がいるが、このイシューに誰が合うか事前にわからないとき | 作業の範囲が 1 つの専門分野に明確で、誰がやるべきかわかっているとき |
| 実際の応答者はイシューごとに変わっても、担当者(スクワッド)は安定して保ちたいとき | イシューにエージェントの名前を残し、明確な個人の説明責任を持たせたいとき |
| コメントで `@FrontendTeam` のようなルーティング先がほしいとき | 一対一の `@agent-name` だけで十分なとき |
スクワッドは能力を加えません。**ルーティング**を加えます。メンバーは依然として普通のエージェントであり、リーダーの唯一の役割は適切な人を選ぶことです。
## 権限
| 操作 | 実行できる人 |
|---|---|
| スクワッドの作成 / 更新 / アーカイブ | ワークスペースの **owner** または **admin** |
| メンバーの追加・削除、ロールの変更 | ワークスペースの **owner** または **admin** |
| スクワッドにイシューを割り当て | すべてのワークスペースメンバー(エージェントへの割り当てと同じ) |
| コメントでスクワッドを `@` でメンション | すべてのワークスペースメンバー |
| スクワッドリーダーの評価を記録 | スクワッドリーダーエージェントのみCLI 経由) |
完全なロールマトリクスは[メンバーとロール](/members-roles)にあります。
## スクワッドを作成する
サイドバーで **スクワッド → 新しいスクワッド** を開き、次を入力してください。
- **名前Name** — 例: `Frontend Team`、`Bug Triage`。ワークスペース内で一意である必要はありません。
- **説明Description、任意** — スクワッドカードと詳細ページに表示される短い紹介文。
- **リーダーLeader** — 既存のエージェントを選びます。リーダーは `leader` ロールで自動的にスクワッドに追加されます。
作成後、スクワッドの詳細ページを開いて次を行えます。
- **メンバーを追加** — エージェントや人間のメンバーを選び、任意で各自に短いロールの説明(例: 「owns the migrations」、「reviewer of last resort」を付けます。リーダーは誰に委任するかを決めるときにこれらのロールを使います。
- **指示を書く** — リーダーが実行のたびに見るスクワッドレベルのガイダンスです(詳しくは後述)。
- **アバターを設定** — エージェントに使うのと同じピッカーから選びます。
CLI での同等のコマンド:
```bash
multica squad create --name "Frontend Team" --leader frontend-lead-agent
multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agent --role "Owns Tailwind / shadcn surface"
```
## スクワッドに割り当てられたイシューの実行方法
Backlog でないイシューがスクワッドに割り当てられると、Multica はただちに**リーダーエージェント**のための `task` をキューに入れます(すべてのメンバーのためではありません)。その後のフローは次のようになります。
1. **リーダーがタスクを引き受けます。** エージェントランタイムが次のポーリングでタスクを引き受けます。これは他のエージェント割り当てと同じです。
2. **リーダーがブリーフィングを受けます。** 引き受けた時点で、Multica はリーダーのシステムプロンプトに 3 つのセクションを追記します — 下記の[リーダーが毎ターン見る内容](#what-the-leader-sees-on-every-turn)を参照してください。
3. **リーダーが 1 つの委任コメントを投稿します。** そのコメントは、名簿rosterにある正確なメンションマークダウンを使って、選んだメンバーを `@` でメンションします。このメンションが、メンションされた各エージェントのための新しい `task` をトリガーします。
4. **リーダーが評価を記録します** — `multica squad activity <issue-id> action --reason "..."` を通じて記録します。これはイシューのアクティビティタイムラインにエントリを書き込み、リーダーが実際にトリガーを評価したことを人が確認できるようにします。
5. **リーダーが停止します。** リーダーは実装作業を自分では行いません。委任されたメンバーが返信を投稿すると、リーダーが再びトリガーされて更新を読み、次のステップを委任するか、エスカレーションするか、沈黙を保ちます。
イシューが **Backlog** の状態であれば、リーダーはトリガーされません — Backlog は駐車場であり、エージェントへ直接割り当てる場合と同じルールが適用されます。
### リーダーが毎ターン見る内容
スクワッドリーダーが実行されるたびに、3 つのブロックがリーダーの指示に追記されます。
- **Squad Operating Protocol** — ハードコードされたルール集です: イシューを読み、`@` メンションで委任し、簡潔に(イシュー本文を繰り返さない — 担当者が自分で読めます)書き、毎ターン評価を記録し、**ディスパッチ後に停止する**。このプロトコルはシステムが管理しており、編集できません。
- **Squad Roster** — リーダー自身の行と、アーカイブされていないメンバーごとに 1 行ずつで構成されます。各行には、リーダーが貼り付けるべき正確なメンションマークダウン(`[@Name](mention://agent/<uuid>)` または `[@Name](mention://member/<uuid>)`)が含まれています — 単なるテキストの `@name` を入力しても誰もトリガーされません。
- **Squad Instructions** — このスクワッドのためのカスタムガイダンスです(スクワッドの詳細ページで設定するか、`multica squad update --instructions` で設定。ルーティングルール「DB 作業は Alice に、フロントエンドは Bob に」)、エスカレーションポリシー、その他イシュー自体にはない、リーダーが知っておくべき事柄を書くのに使ってください。
## リーダーが再トリガーされる場合
最初のディスパッチの後、リーダーはイシューの**ほとんどの後続コメント**によって自動的に起こされます。正確なルールは次のとおりです。
| イベント | リーダーがトリガーされるか? |
|---|---|
| 非メンバー(人間のレポーター、外部エージェント)がコメントを投稿 | **はい** |
| スクワッドメンバーが `@mention` なしで進捗の更新を投稿 | **はい** — リーダーが次のステップが必要かどうかを再評価します |
| 誰かが別のエージェント / メンバー / スクワッド / `@all` を明示的に `@` メンションするコメントを投稿 | **いいえ** — 明示的な `@` がルーティングシグナルであり、リーダーは身を引きます |
| リーダー自身のコメント(自己トリガー) | **いいえ** — ループを防ぐためにガードされています |
| イシューの相互参照(`[MUL-123](mention://issue/...)`)のみを含むコメント | **はい** — イシュー参照はルーティングではありません |
これらのルールの上に重複排除が適用されます。リーダーがこのイシューにすでに `queued` または `dispatched` 状態のタスクを持っている場合、新しいトリガーが重複したタスクをキューに入れることはありません。
<Callout type="info">
**メンバーが `@` メンションを投稿したときにリーダーがトリガーされない理由。** スクワッドメンバーが誰かを直接 `@` したら、そのコメントは意図的な引き継ぎです — リーダーを起こしてルーティングを「観察」させても、何もしないターンを生み出してタイムラインを散らかすだけです。エージェントが書いたコメントは例外です。あるエージェントが別のエージェントを `@` する結果を投稿すると、リーダーは依然として起き上がり、スレッドを調整できます。
</Callout>
## コメントでスクワッドを `@` でメンションする
スクワッドはメンバーやエージェントと並んで `@` ピッカーに表示されます。スクワッドをメンションすると `[@SquadName](mention://squad/<uuid>)` が挿入され、イシューをスクワッドに割り当てたかのように**スクワッドリーダー**をトリガーします — ただし担当者やステータスは変わりません。現在の所有者をそのまま保ちながら、スクワッドに質問やサブタスクを担当する人を選ばせたいときに使ってください。
同じアンチループのルールが適用されます。リーダーは自分自身をスキップし、同じコメント内に明示的なメンバーの `@` メンションがあれば、そのメンバーに直接ルーティングされます。
## スクワッドの再割り当てまたはアーカイブ
**イシューをスクワッドから別の担当者に再割り当てする**のは、他のあらゆる担当者変更と同じように動作します。イシューのアクティブなタスク(リーダーのものを含む)がすべてキャンセルされ、新しい担当者(エージェント、メンバー、または別のスクワッド)がキューに入ります。「担当者を変えずにスクワッドだけを外す」という別個の操作はありません。別の担当者を選んでください。
**スクワッドのアーカイブ**`multica squad delete <id>`、または詳細ページの Archive ボタン):
1. **現在スクワッドに割り当てられているイシューをリーダーエージェントに移管し**、作業が途切れる代わりに具体的なエージェントを相手に継続するようにします。
2. スクワッドに `archived_at` / `archived_by` を記録します。行は保存されるため過去のアクティビティエントリは引き続き解決されますが、スクワッドは一覧、ピッカー、@メンションのドロップダウンから消えます。
3. このスクワッドへの**今後の割り当てを拒否**し、`cannot assign to an archived squad` を返します。
現在アーカイブ解除のコマンドはありません。ルーティングを復活させる必要がある場合は、新しいスクワッドを作成してください。
## CLI からのスクワッド操作
| コマンド | 用途 |
|---|---|
| `multica squad list` | ワークスペースのスクワッド一覧を表示 |
| `multica squad get <id>` | 1 つのスクワッドの名前、リーダー、説明、指示を表示 |
| `multica squad create --name "..." --leader <agent>` | スクワッドを作成owner / admin |
| `multica squad update <id> [--name X] [--description X] [--instructions X] [--leader Y] [--avatar-url Z]` | 1 つ以上のフィールドを更新 |
| `multica squad delete <id>` | アーカイブ(ソフト削除) — 割り当てられたイシューをリーダーに移管 |
| `multica squad member list <id>` | スクワッドのメンバー一覧を表示 |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | メンバーを追加owner / admin |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | メンバーを削除(リーダーは削除できません — 先にリーダーを変更してください) |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | リーダーエージェントが毎ターン終了時に記録 |
`--leader` はエージェント名または UUID を受け付けます。それ以外の ID は `multica agent list --output json`、`multica workspace member list --output json`、`multica squad list --output json` から取得します。
## 次に
- [エージェントにイシューを割り当てる](/assigning-issues) — 同じフローで、スクワッド担当者にも適用されます
- [コメントでエージェントを `@` でメンションする](/mentioning-agents) — `@` ピッカーにはスクワッドも表示されます
- [エージェント](/agents) — エージェントとは何か、すべてのスクワッドの構成要素
- [メンバーとロール](/members-roles) — owner / admin / member の完全な権限マトリクス

View File

@@ -123,6 +123,7 @@ multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agen
| `multica squad delete <id>` | 보관(소프트 삭제) — 할당된 이슈를 리더에게 이전 |
| `multica squad member list <id>` | 스쿼드의 멤버 목록 표시 |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | 멤버 추가(owner / admin) |
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | 멤버를 제거하지 않고 역할 변경 |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | 멤버 제거(리더는 제거할 수 없습니다 — 먼저 리더를 변경하세요) |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 리더 에이전트가 매 턴 종료 시 기록 |

View File

@@ -123,6 +123,7 @@ There is currently no unarchive command; create a new squad if you need the rout
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
| `multica squad member list <id>` | List a squad's members |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | Add a member (owner / admin) |
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | Change a member's role without removing it |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | Remove a member (the leader cannot be removed — change leader first) |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Recorded by the leader agent at the end of every turn |

View File

@@ -123,6 +123,7 @@ multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agen
| `multica squad delete <id>` | 归档(软删除)——同时把当前分配给小队的 issue 转给队长 |
| `multica squad member list <id>` | 列出小队成员 |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | 加成员owner / admin|
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | 不移除成员,直接修改 role |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | 移除成员(**不能移除队长**——先换队长)|
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 队长每次结束前由它自己调用 |

View File

@@ -0,0 +1,117 @@
---
title: タスク
description: すべてのエージェント実行の作業単位であり、明確なステートマシン、タイムアウト、リトライルールを備えています。
---
import { Callout } from "fumadocs-ui/components/callout";
import { Mermaid } from "@/components/mermaid";
**タスク**はすべての[エージェント](/agents)実行の単位です — [エージェントへのイシューの割り当て](/assigning-issues)、[コメントでのエージェントの @-メンション](/mentioning-agents)、[チャット](/chat)でのメッセージ送信、または[オートパイロット](/autopilots)が予定時刻に発火することは、いずれもタスクを生成します。Multica はそれをキューに入れ、[デーモン](/daemon-runtimes)が取得して対応する [AI コーディングツール](/providers)に引き渡し、完了するとその結果をサーバーに書き戻します。
タスクと[イシュー](/issues)は異なる 2 つのオブジェクトです。1 つのイシューは何度も割り当てられたり、@-メンションされたり、手動で再実行されたりでき — そのたびに**新しい**タスクが生成されます。
## タスクが経るステート
<Mermaid chart={`
graph LR
Q["Queued<br/>queued"] -->|daemon picks up| D["Dispatched<br/>dispatched"]
D -->|agent starts| R["Running<br/>running"]
R -->|success| C["Completed<br/>completed"]
R -->|error or timeout| F["Failed<br/>failed"]
Q -->|user cancels| X["Cancelled<br/>cancelled"]
D -->|user cancels| X
R -->|user cancels| X
F -.retryable reason.-> Q
`} />
- **Queuedキュー待ち** — タスクが作成されたばかりで、デーモンが取得するのを待っている状態
- **Dispatchedディスパッチ済み** — デーモンがタスクを占有し、AI コーディングツールを起動中
- **Running実行中** — AI コーディングツールが実際に作業を実行中
- **Completed完了** — 正常に終了し、成果物(コメント、コードコミット、ステータス変更)がサーバーに書き戻されます
- **Failed失敗** — エラーまたはタイムアウトで中断。失敗理由がリトライ可能な場合、タスクは自動的に `queued` 状態に戻り、再試行されます
- **Cancelledキャンセル済み** — ユーザーがキャンセルした場合
## タスクがタイムアウトしたときに起きること
Multica サーバーは 30 秒ごとにスキャンします。2 種類のタイムアウトが失敗を引き起こします。
| 状況 | タイムアウト |
|---|---|
| ディスパッチされたが開始されない(デーモンが取得したが AI ツールを起動しなかった) | **5 分** |
| 実行が長すぎる | **2.5 時間** |
どちらのタイムアウトも失敗理由として `timeout` を使用し、**自動的にリトライされます**(次のセクション)。関連するランタイム欠落チェックについては、[デーモンとランタイム → ランタイムがオフラインとマークされるタイミング](/daemon-runtimes#when-a-runtime-is-marked-offline)を参照してください。
## どの失敗が自動的にリトライされ、どの失敗がされないか
失敗は 2 つのカテゴリに分かれます: **リトライ可能**と**リトライ不可**です。
**リトライ可能**Multica が自動的に再キューイング):
- `runtime_offline` — タスクがディスパッチされた後にデーモンがいなくなった
- `runtime_recovery` — デーモンがクラッシュして再起動し、終わらせなかったタスクを回収した
- `timeout` — ランタイムまたはディスパッチのタイムアウト
**リトライ不可**(タスクは失敗状態のまま):
- `agent_error` — AI コーディングツール自体がエラーを報告したAPI エラー、クォータ超過、内部バグ)。根本的な問題はリトライされません — 無限ループになってしまうためです。
自動リトライにはさらに 2 つの追加条件もあります。
1. **最大 2 回の試行** — オリジナル 1 回 + リトライ 1 回。リトライも失敗した場合は、理由がリトライ可能であってもそれ以上リトライしません。
2. **イシューおよびチャットでトリガーされたタスクのみ** — オートパイロットでトリガーされたタスクは自動的にリトライ**されません**。
<Callout type="warning">
**オートパイロットのタスクは自動的にリトライされません** — 意図された設計です。オートパイロットは独自の発火サイクル(例: 毎日)を持っており、失敗時に自動リトライが起きると次の予定実行と重なってしまいます。失敗後すぐに再実行が必要なら、手動の再実行を使用してください(次のセクション)。
**オートパイロットのタスクが失敗したことを知る方法**: [インボックス](/inbox)に通知が届き、関連するイシューのステータスが `in_progress` から `todo` に戻ります。[オートパイロット](/autopilots)ページでも、オートパイロットごとの最新の実行結果を確認できます。
</Callout>
## 手動の再実行 vs. 自動リトライ
**手動の再実行**は、CLI または API`POST /api/issues/{id}/rerun`)から自分でトリガーするものです。
```bash
multica issue rerun <issue-id>
```
動作:
- デフォルトでは、イシューの**現在のエージェント担当者**を対象とします — 以前のタスクを誰が実行したかに関わらず、再実行が現在の割り当てに従うようにしたいときに便利です。
- 実行ログの特定の行にあるリトライボタンは、その行のタスク ID も一緒に送信するため、再実行は**現在の担当者ではなく、まさにそのタスクを実行したエージェント**を対象とします。これにより、スクワッドワーカー、並列の @-メンションエージェント、または再割り当てによってエージェントが入れ替わった行に対しても、行単位のリトライが意味を持つようになります。
- このイシューに対する対象エージェントのキュー待ちまたは実行中のタスクを**キャンセルします**(ある場合)。同じイシューで他のエージェントが所有するタスク(例: 並列の @-メンション実行)はそのまま残します。
- **まったく新しい**タスクを作成します — オリジナルのタスクが試行回数の上限に達していても、試行回数は 1 にリセットされます。
- **新しいエージェントセッション**を開始します — 以前のセッション ID は**継承されません**。手動の再実行は、以前の成果物が悪かったと判断したことを意味するため、同じ会話を続けると汚染された状態をそのまま再生してしまいます。(一方、自動リトライはセッションを継承します — そのパスは悪い成果物ではなくインフラ障害のためのものです。)
比較:
| 項目 | 自動リトライ | 手動の再実行 |
|---|---|---|
| トリガー | システム、失敗理由に基づく | ユーザー、手動 |
| 上限 | 2 回の試行 | 制限なし |
| 適用できるソース | イシュー、チャット | エージェント担当者があるイシュー |
| 選択されるエージェント | 失敗したタスクと同じエージェント | ソースタスクのエージェントUI 行単位リトライまたはイシューの現在の担当者CLI / task_id なし) |
| セッション継承 | あり(以前のセッションを再開) | なし(新しいセッション) |
## 失敗したタスクがイシューのステータスに与える影響
イシューがエージェントに割り当てられていてトリガーされたタスクが失敗すると(そして自動リトライが成功しないと)、**イシューのステータスが `in_progress` から `todo` に自動的に戻ります** — そのため、ボードを開くと「これはもう一度見る必要がある」とすぐに分かります。[イシューとプロジェクト](/issues)を参照してください。
## タスクが以前のコンテキストから続行できるか
できます — AI コーディングツールがセッション再開をサポートしている限りは。
Multica はタスク中にセッション ID を**2 回**固定します: 開始時に 1 回AI ツールが最初のシステムメッセージを返したとき)、終了時に 1 回完了または失敗時。1 回目はデーモンが実行の途中でクラッシュしても復旧できるようにし、2 回目は次の**自動リトライ**のために予約され、その ID を渡し返すことでエージェントが以前の会話とファイル状態を引き継げるようにします。**手動の再実行は意図的にこのステップをスキップし**、新しいセッションを開始します — [手動の再実行 vs. 自動リトライ](#manual-rerun-vs-automatic-retry)を参照してください。
ただし、**実際にどの AI コーディングツールがこれをサポートするか**は大きく異なります。
- ✅ **実際にサポート** — Antigravity, Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi
- ⚠️ **コードはあるが使用不可** — Codex, Cursor
- ❌ **サポートなし** — Gemini
[プロバイダー対応表 → セッション再開](/providers#session-resumption-who-really-supports-it)を参照してください。
## 次へ
- [プロバイダー対応表](/providers) — 12 個の AI コーディングツール間の機能差(正確なセッション再開の状態を含む)
- [エージェントへのイシューの割り当て](/assigning-issues) / [コメントでのエージェントの @-メンション](/mentioning-agents) / [チャット](/chat) / [オートパイロット](/autopilots) — タスクをトリガーする 4 つの方法

View File

@@ -0,0 +1,279 @@
---
title: トラブルシューティング
description: Multica をセルフホストする際によく遭遇する問題 — 症状、原因、診断方法、解決方法。
---
import { Callout } from "fumadocs-ui/components/callout";
症状から問題を探してください。各項目では**症状 / 考えられる原因 / 診断方法 / 解決方法**を提供します。お使いの状況が一覧にない場合は、[GitHub](https://github.com/multica-ai/multica/issues) にイシューを登録してください。
## デーモンがサーバーに接続できない
**症状**: [`multica daemon`](/cli) の `status` コマンドが `offline` または `connection refused` を表示します。サーバーログに `/api/daemon/register` や `/api/daemon/heartbeat` のリクエストが見当たりません。デーモンの仕組みについては[デーモンとランタイム](/daemon-runtimes)を参照してください。
**考えられる原因**:
1. **`MULTICA_SERVER_URL` が誤ったアドレスを指している** — デフォルト値は `ws://localhost:8080/ws` で、セルフホスト時は自分のサーバーアドレスに変更する必要があります
2. **ネットワーク / ファイアウォールによるブロック** — デーモンとサーバーが同じネットワークにいない、またはアウトバウンドトラフィックがブロックされている
3. **トークンが期限切れまたは無効** — `multica login` を一度も実行していない、または PAT が取り消された
4. **サーバーが登録を拒否した** — ログインしたアカウントが対象のワークスペースに所属していないregister が 403 を返す)
5. **DNS 解決の失敗** — デーモンのマシンでホスト名が解決されない
**診断方法**:
```bash
multica daemon logs --lines 100 # look for daemon-side errors
echo $MULTICA_SERVER_URL # confirm the address is set
curl -i http://<server-host>:8080/health # hit the server directly
curl -i http://<server-host>:8080/readyz # include DB + migration readiness
cat ~/.multica/config.json # verify api_token exists
multica workspace list # confirm you're a member of the target workspace
```
**解決方法**: 上記の各原因を 1 つずつ対処してください。最もよくある 2 つの解決策は、**`MULTICA_SERVER_URL` を変更してデーモンを再起動する**こと(`multica daemon restart`)と、**ログインし直す**こと(`multica logout && multica login`)です。
## タスクが `queued` で止まる
**症状**: エージェントにイシューを割り当てた後、イシューの状態はすぐに `in_progress` に変わりますが、長時間経ってもページにエージェント実行の兆候が見えません。`multica daemon status` はデーモンを `online` と表示しています。
**考えられる原因**(頻度順):
1. **エージェントの同時実行上限に到達** — このエージェントの `max_concurrent_tasks`(デフォルト 6が、他の実行中タスクですでに埋まっている
2. **同じイシューで同じエージェントの別タスクがまだ実行中** — 同じエージェント × 同じイシューは順次実行が強制されます(重複実行の防止)
3. **エージェントがアーカイブされている** — アーカイブ後も新しいタスクはキューに入りますが、クレームできず、5 分後にタイムアウトしますcode-issue G-01
4. **デーモンが現在のワークスペースにこのランタイムを登録していない** — デーモンを再起動するか、UI でランタイムを選択し直してください
5. **デーモンの接続が切れた** — 直近 45 秒間ハートビートがありません。`daemon status` が `online` と表示されるのは、ごく最近切断された状態を反映している可能性があります
**診断方法**:
```bash
multica daemon status --output json # runtime list + last_seen_at
multica agent list # check agent archived state
multica issue show <issue-id> # inspect task history
```
サーバー側(セルフホスト)では、`"no_tasks"` / `"no_capacity"` を grep してクレームの結果を確認できます。
**解決方法**:
- 同時実行が満杯 → 実行中のタスクが終わるのを待つか、`multica agent update <id> --max-concurrent-tasks 10` で上限を引き上げてください
- 同一イシューの順次実行 → 前のタスクが終わるのを待つか、別のエージェントに割り当て直してください
- エージェントがアーカイブされている → `multica agent restore <id>`
- ランタイム未登録 → `multica daemon restart` するとデーモンが再登録します
## WebSocket が接続できない
**症状**: ブラウザのコンソールに `WebSocket is closed` が記録されます。ページにリアルタイム更新(タスクの進捗、コメント、インボックス)が表示されず、再読み込みしないと見えません。バックエンドのタスクは引き続き実行されます。
**考えられる原因**:
1. **Origin チェックの失敗** — フロントエンドのドメインがサーバーの CORS 許可リストにありません。デフォルトの許可リストには `localhost:3000/5173/5174` のみが含まれ、公開インターネットでセルフホストするには `FRONTEND_ORIGIN` が必要です
2. **プロトコルの不一致** — `https://` のフロントエンドには `wss://` が必要で、HTTP は `ws://` を使います
3. **リバースプロキシが WebSocket アップグレードを有効にしていない** — Nginx / Envoy / HAProxy はデフォルトでは `Upgrade` ヘッダーを転送しません
4. **JWT クッキーの期限切れまたは欠落** — 30 日の有効期限後にログインし直していない
**診断方法**:
- ブラウザの DevTools → Network → 「WS」でフィルタリングし、接続状態とステータスコードを確認してください
- サーバーログで `"rejected origin"` / `"websocket"` を grep してください — origin の問題であれば明示的に表示されます
- `curl -i http://<server-host>:8080/ws` は(`Upgrade` ヘッダー付きで)`101 Switching Protocols` を返すはずです
**解決方法**:
- Origin エラー → サーバーの `.env` に `FRONTEND_ORIGIN=https://multica.yourdomain.com` を設定(またはカンマ区切りの `CORS_ALLOWED_ORIGINS`)し、サーバーを再起動してください
- プロトコルの不一致 → `FRONTEND_ORIGIN` のプロトコルがフロントエンドと一致しているか確認してください
- リバースプロキシ → Nginx に `proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";` を追加してください
- クッキーの期限切れ → ページを再読み込みしてログインし直してください
## メールが届かない
**症状**: ログインまたは招待の受諾中にメールアドレスを送信したのに、インボックスにもスパムフォルダにも認証コードがありません。
**まず、サーバーがどのプロバイダーをアクティブと認識しているかを確認してください。** 起動時にバックエンドは次のいずれかを出力します。
- `EmailService: SMTP relay <host>:<port> from=<addr>` — SMTP を使用(`SMTP_HOST` が空でなければ Resend より優先)
- `EmailService: Resend API from=<addr>` — Resend を使用
- `EmailService: DEV mode — codes printed to stdout …` — プロバイダーが構成されていない
```bash
docker compose -f docker-compose.selfhost.yml logs backend | grep "EmailService:"
```
期待していた行が見当たらない場合は、環境変数がプロセスに届いていません。`.env` と `docker compose -f docker-compose.selfhost.yml exec backend env | grep -E 'RESEND_|SMTP_'` を確認してください。この起動ログの行には認証情報は一切記録されません。
### Resend がアクティブなプロバイダーの場合
**考えられる原因**:
1. **`RESEND_API_KEY` が設定されていない** — サーバーは静かにフォールバックし、エラーを出さずに**コードを自身の stdout に書き込みます**。プロダクションで陥りやすい落とし穴です
2. **Resend API キーが無効、またはクォータ超過** — サーバーログに `"failed to send verification code"` が表示されます
3. **`RESEND_FROM_EMAIL` のドメインが Resend で検証されていない** — Resend が送信を拒否します
4. **メールは送信されたが受信者の ISP にスパムと判定された** — Resend ダッシュボードとスパムフォルダを確認してください
**診断方法**:
- サーバーログで `"[DEV] Verification code for"` を grep してください — これがある場合、Resend が構成されておらず、コードが stdout に書き込まれたことを意味します
- [Resend ダッシュボード](https://resend.com/) → Emails で送信履歴を確認してください
- `RESEND_FROM_EMAIL` のドメインが Resend コンソールの「Verified Domains」リストに表示されるか確認してください
**解決方法**:
- API キーの欠落 → [サインインとサインアップの構成 → メールの仕組み](/auth-setup#how-email--verification-code-sign-in-works)に従って構成し、サーバーを再起動してください
- ドメイン未検証 → Resend コンソールで DNS 検証フローを実行してくださいSPF / DKIM レコードを追加)
- 緊急時(内部テスト)→ サーバーログの `[DEV]` の下に出力されたコードをコピーしてください
### SMTP がアクティブなプロバイダーの場合
SMTP の経路はすべての失敗を失敗した段階とともにラップするため、サーバーログがすでにどの段階で relay がセッションを拒否したかを教えてくれます。`"failed to send verification email"` / `"failed to send invitation email"` を grep し、ラップされたエラーを確認してください。
| 記録されたエラー | 意味 | 解決方法 |
|---|---|---|
| `smtp dial <host>:<port>: dial tcp …: connect: connection refused` / `i/o timeout` | バックエンドコンテナが relay に到達できない — host が誤っている、port が誤っている、ファイアウォール、または relay が待ち受けていない | コンテナ内部から `SMTP_HOST` / `SMTP_PORT` が解決されるか確認してください(`docker compose -f docker-compose.selfhost.yml exec backend nslookup <host>` および `nc -vz <host> <port>`。Multica を実行するホストから relay へのファイアウォールを開放してください |
| `smtp starttls: x509: certificate signed by unknown authority`(または `certificate is not valid for any names` | relay がプライベート CA / 自己署名証明書を使用しており、コンテナの信頼ストアがそれを拒否している | CA をコンテナにインストールするか、relay が信頼できるネットワークセグメント上で到達可能であることを確認したうえでのみ `SMTP_TLS_INSECURE=true` を設定してください |
| `smtp auth: 535 5.7.8 Authentication credentials invalid`(または `534`/`530` | `SMTP_USERNAME` / `SMTP_PASSWORD` が誤っている、または relay が `PLAIN` 以外の認証方式を要求している | メール管理者にサービスアカウントの認証情報を再確認してください。Exchange の匿名内部 relay の場合は両方を空のままにします(`SMTP_USERNAME=`、`SMTP_PASSWORD=` |
| `smtp MAIL FROM: 550 5.7.1 Client does not have permissions to send as this sender` | relay が `RESEND_FROM_EMAIL` をエンベロープ送信者として受け入れない — 典型的な Exchange の「anonymous users not allowed」または DMARC アラインメントの問題 | `RESEND_FROM_EMAIL` を relay が受け入れるドメインに設定してください。Exchange では receive connector で送信元 IP に `ms-Exch-SMTP-Accept-Any-Sender` を付与してください |
| `smtp RCPT TO <addr>: 550 5.7.1 Unable to relay` | relay の receive connector が、あなたのサブネットから外部の受信者への中継を許可していない(外部ドメインと通信する匿名内部 relay で最も多い) | 招待を内部の受信者に制限するか、Multica ホストのサブネットを Exchange の「Anonymous Users → Relay」権限リストに追加してください |
| `smtp DATA` / `smtp write body` / `smtp end data` | セッションは受け入れられたが relay が本文を破棄した — 通常はメッセージサイズ制限、コンテンツフィルタリング、または送信途中の接続リセットが原因 | relay のログで同じ `Message-ID`(ログには `<unixnano>@<host>` 形式)を確認してください。必要であればメッセージサイズの上限を引き上げてください |
`MAIL FROM`、`RCPT TO`、`DATA` のエラーは常に relay の応答コードとともに記録されるため、反対側の Exchange / Postfix のログと突き合わせることができます。認証コードと招待トークンは、ラップされたエラーに**決して**含まれません。
**診断方法**:
- 起動時に `"EmailService: SMTP relay"` を一度 grep し、ランタイムの失敗については `"failed to send"` を grep してください
- バックエンドコンテナ内部から接続性を点検してください: `docker compose -f docker-compose.selfhost.yml exec backend sh -c 'nc -vz $SMTP_HOST $SMTP_PORT'`
- 環境変数がプロセスに届いたか確認してください: `docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP_`(出力にパスワードが含まれるため、信頼できるシェルでのみ実行してください)
**解決方法**:
- host / port の誤り → `SMTP_HOST` / `SMTP_PORT` を調整してバックエンドを再起動してください。サポートされる relay モードは[認証設定 → Option B: SMTP relay](/auth-setup)を参照してください
- 証明書の不一致 → relay の CA をコンテナにインストールするか、信頼できるネットワークセグメントで一時的に `SMTP_TLS_INSECURE=true` を設定してください
- 認証の失敗 → 認証情報を再確認してください。匿名内部 relay の場合は `SMTP_USERNAME` と `SMTP_PASSWORD` を空のままにしてください
- `Unable to relay` → 内部の受信者に制限するか、Exchange の receive connector で Multica ホストの IP に中継権限を付与してください
## 固定のローカルテストコードが動作しない
**症状**: セルフホストのインスタンスで `888888` のような固定のローカルテストコードでログインしようとしたところ、`invalid or expired code` で拒否されます。
**考えられる原因**(相互に排他的):
1. **`MULTICA_DEV_VERIFICATION_CODE` が空** — 固定コードはデフォルトで無効です
2. **`APP_ENV=production`** — これは**正しい**プロダクション構成です。固定のローカルテストコードはプロダクションでは無視されます
3. **構成されたコードが 6 桁でない** — このショートカットは 6 桁の値のみを受け付けます
**診断方法**:
```bash
cat .env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
docker exec <container> env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
```
インボックス(スパムを含む)で実際の認証コードを確認してください。
**解決方法**:
- プロダクションでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにし、Resend を構成して実際のコードを使用してください
- ローカル開発や内部テストの場合は、サーバーログから生成されたコードをコピーするか、`APP_ENV=development` と `MULTICA_DEV_VERIFICATION_CODE=888888` を設定してください — 公開インスタンスでは固定コードを絶対に有効にしないでください(詳細は[サインインとサインアップの構成 → 固定のローカルテストコード](/auth-setup#fixed-local-testing-codes)を参照)
## 使用量ダッシュボードが 0 のままになる
**症状**: エージェントはタスクを完了し、生のトークン使用量はデータベースに記録されていますが、**設定 → 使用量**と**設定 → ランタイム**で入力 / 出力 / コストがすべて 0 と表示されます。これは静かに発生する現象で、バックエンドログにエラーはありません。
**考えられる原因**:
1. **`rollup_task_usage_hourly()` が一切スケジュールされていない** — 使用量 / ランタイムのダッシュボードは派生テーブル `task_usage_hourly` から読み取り、このテーブルはその関数によって埋められます。同梱の `pgvector/pgvector:pg17` イメージには `pg_cron` が含まれておらず、バックエンドもプロセス内で rollup を実行しません。外部スケジューラのない新規セルフホストインストールでは、これがデフォルトの状態です。
2. **`pg_cron` はインストールされているが誤ったデータベースを指している** — `pg_cron.database_name` のデフォルト値は `postgres` です。Multica のデータベース名が異なる場合、スケジュールされたジョブは `rollup_task_usage_hourly()` を一切見つけられません。
3. **スケジューラは動作しているが rollup が静かにエラーを出している** — 例えば cron エントリ内部の DB ロール / search_path が誤っている。
**診断方法**:
```sql
-- Confirm raw events exist but the hourly table is empty.
SELECT count(*) AS raw_rows FROM task_usage;
SELECT count(*) AS hourly_rows FROM task_usage_hourly;
-- Confirm pg_cron is (or isn't) available.
SELECT * FROM pg_available_extensions WHERE name = 'pg_cron';
SHOW shared_preload_libraries;
-- If pg_cron is installed, check the schedule + last run.
SELECT jobname, schedule, database, active FROM cron.job;
SELECT jobname, status, return_message, start_time, end_time
FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;
-- Watermark — if this is 1970-01-01, the rollup has never run.
SELECT watermark_at FROM task_usage_hourly_rollup_state;
```
**解決方法**:
- rollup を手動で一度呼び出して動作するか確認してください: `SELECT rollup_task_usage_hourly();` — ダッシュボードを再読み込みしてください。数値が表示されれば、欠けているのはスケジューラだけです。
- [セルフホストクイックスタート → 使用量 rollup のスケジューリング](/self-host-quickstart#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)からサポートされる方式のいずれかを選んでください: 外部 cron / systemd-timer / Kubernetes CronJob、または Postgres を `pg_cron` を含むイメージに置き換える。
- スケジュール設定より前の履歴がすでにある場合は、バックエンドコンテナ内部で `backfill_task_usage_hourly` を実行し、ウォーターマーク以前のバケットを埋めてください。
## マイグレーション `103` が `refusing to drop legacy daily rollups` で失敗する
**症状**: `v0.3.4` から `v0.3.5+` にアップグレードする際、バックエンドコンテナが起動しない(または `migrate up` が中断する)とともに、次のエラーが発生します。
```text
ERROR: refusing to drop legacy daily rollups:
task_usage_hourly_rollup_state.watermark_at (1970-01-01 ...) trails
task_usage latest event (...) by more than 01:00:00 — backfill is
incomplete or pg_cron is not running. Run cmd/backfill_task_usage_hourly
(and let pg_cron catch up) before re-running migrate
```
**考えられる原因**: これはマイグレーション `103` の fail-closed ガードです。`task_usage_hourly` が生の `task_usage` に追いつくまで、レガシーの daily rollup の削除を拒否します。既存の行が存在し、rollup のウォーターマークがまだ epoch に留まっているとき — つまり、まだどの履歴も hourly テーブルに rollup されていないとき — にこのガードが発動します。
**解決方法**:
1. 同じデータベースに対して backfill を実行してください(冪等であり、中断しても安全で、再実行しても安全です):
```bash
# Docker Compose
docker compose -f docker-compose.selfhost.yml exec backend \
./backfill_task_usage_hourly --sleep-between-slices=2s
# Kubernetes
kubectl -n multica exec deploy/multica-backend -- \
./backfill_task_usage_hourly --sleep-between-slices=2s
```
2. アップグレードを再実行してください — バックエンドコンテナを再起動するだけで十分で、マイグレーションは起動時に実行されます。これでガードが最新のウォーターマークを確認し、`103` の適用を許可します。
3. ウォーターマークが進み続けるように、継続的な rollup スケジュールcron / `pg_cron`)を設定してください — [セルフホストクイックスタート → 使用量 rollup のスケジューリング](/self-host-quickstart#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)を参照してください。
`--sleep-between-slices=2s` は、数年分の履歴を持つプロダクションデータベースにとって控えめなデフォルト値です。直近 N か月のみを保持し、それより古いバケットを永久に放棄してもかまわない場合は `--months-back N --force-partial` を使用してください。
## ポートの競合
**症状**: `multica server` や `multica daemon start` が `address already in use` で失敗します。
**考えられる原因**:
1. **サーバーポートが使用中**(デフォルト `8080`
2. **デーモンの health ポートが使用中**(デフォルト `19514`、プロファイルごとにハッシュでオフセット)
3. **Web 開発サーバーのポート競合**`3000` / `5173`
4. **ポートに対する権限不足**`< 1024` の特権ポートのバインドには sudo が必要)
**診断方法**:
```bash
lsof -i :8080 # macOS / Linux
netstat -ano | findstr :8080 # Windows
```
**解決方法**:
- 競合しているプロセスを終了する(`kill -9 <PID>`)か、`PORT=9000` でポートを変更してください
- 80 / 443 を使うには → 直接バインドせず、前段にリバースプロキシNginx / Caddyを置いて高位ポートへ転送してください
## ログの場所
| 構成要素 | 場所 | コマンド |
|---|---|---|
| **デーモン** | `~/.multica/daemon.log`(バックグラウンドモード)またはフォアグラウンドの stdout | `multica daemon logs -f --lines 100` |
| **サーバーDocker** | コンテナの stdout | `docker logs -f <container>` |
| **サーバーsystemd** | journal | `journalctl -u multica-server -f` |
| **フロントエンドdev** | `pnpm dev` を実行中のターミナル | 直接確認 |
| **フロントエンド(ブラウザ)** | DevTools → Console | `F12` を押す |
より詳細なデーモンログが必要な場合は、デーモンをバックグラウンドからフォアグラウンドに移してください: `multica daemon stop && multica daemon start --foreground`。

View File

@@ -0,0 +1,56 @@
---
title: ワークスペース
description: ワークスペースはグループが協働する独立した空間で、すべてのイシュー、メンバー、コメント、エージェントが 1 つのワークスペースに属します。
---
import { Callout } from "fumadocs-ui/components/callout";
ワークスペースは **Multica でグループが協働する独立した空間**で、すべての[イシュー](/issues)、[メンバー](/members-roles)、[コメント](/comments)、[エージェント](/agents)が 1 つのワークスペースに属します。ログイン後に表示されるイシュー一覧、メンバー名簿、エージェント設定はすべて現在のワークスペースに限定されており、**ワークスペースを切り替えると画面全体が入れ替わります**。
## ワークスペースの作成
ワークスペースを作成するとき、3 つのことが決まります。
- **ワークスペース名** — メンバーに表示される表示名です。空白や非 ASCII 文字を使用できます。後から変更できます。
- **Slug** — ワークスペース URL に使われる文字列です。小文字と数字のみが使用でき(`-` で連結)、**作成後は変更できないため**、慎重に選んでください。slug がすでに使用中だったり、システム予約語と重なったりする場合、作成画面で別の値を選ぶよう求められます。
- **イシュー接頭辞** — ワークスペース内のすべてのイシュー番号の接頭辞です(`MUL-123` の `MUL`)。大文字と数字のみが使用でき、最大 10 文字です。
<Callout type="warning">
**イシュー接頭辞は変更しないでください。** イシュー番号は現在の接頭辞でレンダリングされるため、接頭辞を変更すると `MUL-5` がただちに `NEW-5` になります。すべての外部リンク、Slack のメンション、コメント内の過去の参照が古い番号と合わなくなります。イシュー接頭辞は「作成時に決め、決して触らない」値として扱ってください。
</Callout>
ワークスペースは Web UI から作成することも、コマンドラインから作成することもできます。
```bash
multica workspace create
```
## イシュー番号
ワークスペースで作成されるすべてのイシューには、`<接頭辞>-<数字>` 形式の番号が自動的に割り当てられます — `MUL-1`、`MUL-2`、`MUL-3`。いくつかの特性は次のとおりです。
- **ワークスペース内で連番かつ一意** — 各ワークスペースは独自のカウンターを保持し、ワークスペース同士は互いに干渉しません。
- **手動で指定できない** — イシューを作成するときはタイトルのみを入力し、番号はシステムが割り当てます。
- **削除しても再利用されない** — `MUL-5` を削除しても、次の新しいイシューは `MUL-5` ではなく `MUL-6` です。
## ワークスペースの削除
ワークスペースの owner のみが、ワークスペース全体を削除できます。削除は**取り消せません**。
<Callout type="warning">
ワークスペースを削除すると、次の項目が一度にすべて消去されます。
- すべてのイシュー、プロジェクト、コメント、リアクション
- すべての添付ファイル
- すべてのメンバーシップと保留中の招待
- すべてのエージェント設定とそのタスク履歴
**データは復旧できません。** 削除する前に、保管しておきたい項目をエクスポートしてください。
</Callout>
ワークスペースの最後の owner であり、そのワークスペースから手を引きたい場合は、まず owner の役割を別のメンバーに移譲したうえで、新しい ownerまたは本人が削除するかどうかを決定するようにしてください。[メンバーと役割](/members-roles)を参照してください。
## 次へ
- [メンバーと役割](/members-roles) — ワークスペースに人を追加する方法と、3 つの役割がそれぞれ何をできるか
- [イシューとプロジェクト](/issues) — ワークスペース内部の中核となる作業オブジェクト

View File

@@ -1,11 +1,11 @@
import { defineI18n } from "fumadocs-core/i18n";
// English is the default; Chinese (/zh/) and Korean (/ko/) are available.
// hideLocale: 'default-locale' keeps English URLs prefix-free
// English is the default; Chinese (/zh/), Korean (/ko/), and Japanese (/ja/)
// are available. hideLocale: 'default-locale' keeps English URLs prefix-free
// (`/docs/`) while translated locales live under `/docs/<lang>/...`.
// parser: 'dot' picks up `page.zh.mdx` / `page.ko.mdx` and `meta.<lang>.json`.
// parser: 'dot' picks up `page.zh.mdx` / `page.ko.mdx` / `page.ja.mdx` and `meta.<lang>.json`.
export const i18n = defineI18n({
languages: ["en", "zh", "ko"],
languages: ["en", "zh", "ko", "ja"],
defaultLanguage: "en",
hideLocale: "default-locale",
parser: "dot",

View File

@@ -5,6 +5,7 @@ describe("prefixLocale", () => {
it("prefixes root-relative paths with the active non-default locale", () => {
expect(prefixLocale("/workspaces", "zh")).toBe("/zh/workspaces");
expect(prefixLocale("/workspaces", "ko")).toBe("/ko/workspaces");
expect(prefixLocale("/workspaces", "ja")).toBe("/ja/workspaces");
expect(prefixLocale("/agents-create", "zh")).toBe("/zh/agents-create");
});
@@ -30,6 +31,7 @@ describe("prefixLocale", () => {
expect(prefixLocale("/zh/workspaces", "zh")).toBe("/zh/workspaces");
expect(prefixLocale("/en/workspaces", "zh")).toBe("/en/workspaces");
expect(prefixLocale("/ko/workspaces", "zh")).toBe("/ko/workspaces");
expect(prefixLocale("/ja/workspaces", "zh")).toBe("/ja/workspaces");
});
it("leaves external URLs alone", () => {

View File

@@ -13,9 +13,11 @@ const pages = new Map<string, { url: string }>([
["en:", { url: "/" }],
["zh:", { url: "/zh" }],
["ko:", { url: "/ko" }],
["ja:", { url: "/ja" }],
["en:agents", { url: "/agents" }],
["zh:agents", { url: "/zh/agents" }],
["ko:agents", { url: "/ko/agents" }],
["ja:agents", { url: "/ja/agents" }],
]);
vi.mock("@/lib/source", () => ({
@@ -69,6 +71,21 @@ describe("docsAlternates", () => {
});
});
it("includes Japanese hreflang when a real *.ja.mdx page exists", async () => {
existingDocs.add("agents.ja.mdx");
const { docsAlternates } = await import("./site");
expect(docsAlternates(["agents"])).toEqual({
canonical: "https://www.multica.ai/docs/agents",
languages: {
en: "https://www.multica.ai/docs/agents",
zh: "https://www.multica.ai/docs/zh/agents",
ja: "https://www.multica.ai/docs/ja/agents",
"x-default": "https://www.multica.ai/docs/agents",
},
});
});
it("keeps the locale root alternates limited to real localized MDX pages", async () => {
const { docsAlternates } = await import("./site");

View File

@@ -19,6 +19,8 @@ describe("docsSlugStaticParams", () => {
{ lang: "zh", slug: ["agents"] },
{ lang: "ko", slug: ["agents"] },
{ lang: "ko", slug: ["cli", "reference"] },
{ lang: "ja", slug: ["agents"] },
{ lang: "ja", slug: ["cli", "reference"] },
];
expect(docsSlugStaticParams(params)).toEqual([
@@ -27,6 +29,8 @@ describe("docsSlugStaticParams", () => {
{ lang: "zh", slug: ["agents"] },
{ lang: "ko", slug: ["agents"] },
{ lang: "ko", slug: ["cli", "reference"] },
{ lang: "ja", slug: ["agents"] },
{ lang: "ja", slug: ["cli", "reference"] },
]);
});

View File

@@ -28,6 +28,18 @@ export const uiTranslations: Partial<Record<Lang, Partial<Translations>>> = {
chooseTheme: "테마 변경",
editOnGithub: "GitHub에서 편집",
},
ja: {
search: "検索",
searchNoResult: "結果が見つかりません",
toc: "このページの内容",
tocNoHeadings: "見出しなし",
lastUpdate: "最終更新",
chooseLanguage: "言語を選択",
nextPage: "次のページ",
previousPage: "前のページ",
chooseTheme: "テーマを変更",
editOnGithub: "GitHub で編集",
},
};
// Display name shown in the LanguageToggle dropdown.
@@ -35,6 +47,7 @@ export const localeLabels: Record<Lang, string> = {
en: "English",
zh: "简体中文",
ko: "한국어",
ja: "日本語",
};
// Copy for the welcome page (Hero + Byline). Pages are translated as MDX;
@@ -58,4 +71,10 @@ export const homeCopy = {
titleAccent: "한곳에서.",
byline: ["시작하기", "2026년 4월 업데이트", "약 6분 읽기"],
},
ja: {
eyebrow: "Multica ドキュメント",
titleLead: "人とエージェントが、",
titleAccent: "一つの場所に。",
byline: ["はじめに", "2026年4月更新", "約6分で読めます"],
},
} as const satisfies Record<Lang, unknown>;

View File

@@ -17,6 +17,7 @@ import type {
IssueStatus,
IssuePriority,
} from "@multica/core/types";
import { formatDateOnly } from "@multica/core/issues/date";
import { Text } from "@/components/ui/text";
import { StatusIcon } from "@/components/ui/status-icon";
import { PriorityIcon } from "@/components/ui/priority-icon";
@@ -64,12 +65,9 @@ const TYPE_LABEL: Record<InboxItemType, string> = {
quick_create_failed: "Quick-create failed",
};
// due_date is a calendar day — format timezone-safely (no offset day shift).
function shortDate(dateStr: string): string {
if (!dateStr) return "";
return new Date(dateStr).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
return formatDateOnly(dateStr, { month: "short", day: "numeric" }, "en-US");
}
function singleLine(value: string | null | undefined): string {

View File

@@ -23,6 +23,7 @@ import type {
Issue,
IssuePriority,
} from "@multica/core/types";
import { formatDateOnly } from "@multica/core/issues/date";
import { Text } from "@/components/ui/text";
import { StatusIcon } from "@/components/ui/status-icon";
import { PriorityIcon } from "@/components/ui/priority-icon";
@@ -67,11 +68,11 @@ const ISSUE_PICKER_PATHNAMES = {
"due-date": "/[workspace]/issue/[id]/picker/due-date",
} as const satisfies Record<IssuePickerField, string>;
// due_date is a calendar day — format timezone-safely so the day never shifts
// with the viewer's offset. Mirrors web's formatDate in list-row/board-card.
function formatDueDate(iso: string | null): string | null {
if (!iso) return null;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return null;
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
return formatDateOnly(iso, { month: "short", day: "numeric" }, "en-US") || null;
}
export function AttributeRow({ issue }: { issue: Issue }) {

View File

@@ -18,6 +18,7 @@ import { ActorAvatar } from "@/components/ui/actor-avatar";
import { PriorityIcon } from "@/components/ui/priority-icon";
import { ProjectIcon } from "@/components/ui/project-icon";
import { StatusIcon } from "@/components/ui/status-icon";
import { formatDateOnly } from "@multica/core/issues/date";
import { useActorLookup } from "@/data/use-actor-name";
import { useNewIssueDraftStore } from "@/data/stores/new-issue-draft-store";
import { useWorkspaceStore } from "@/data/workspace-store";
@@ -131,8 +132,7 @@ export function CreateFormAttributeRow() {
);
}
// due_date is a calendar day — format timezone-safely (no offset day shift).
function formatDueDate(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "Due date";
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
return formatDateOnly(iso, { month: "short", day: "numeric" }) || "Due date";
}

View File

@@ -3,40 +3,40 @@
* (a formSheet route) renders the Done / Clear actions in its own header
* area — this body only handles the picker spinner + the local draft state.
*
* Backend (server/internal/handler/issue.go CreateIssue / UpdateIssue) parses
* with time.Parse(time.RFC3339, ...) — strict. Mirrors web's
* packages/views/issues/components/pickers/due-date-picker.tsx which sends
* d.toISOString().
* due_date is a calendar day (date-only "YYYY-MM-DD", no time/timezone — see
* @multica/core/issues/date and GH #3618). Mirrors web's
* packages/views/issues/components/pickers/due-date-picker.tsx: read the stored
* day into a local-midnight Date for the spinner, write back the picked local
* day as a date-only string.
*/
import { useState, useEffect, useImperativeHandle, forwardRef } from "react";
import { View } from "react-native";
import DateTimePicker from "@react-native-community/datetimepicker";
import { toDateOnly, dateOnlyToLocalDate } from "@multica/core/issues/date";
interface Props {
value: string | null;
}
export interface DueDatePickerBodyHandle {
/** Returns the currently-displayed date as an ISO 8601 string. */
/** Returns the currently-displayed day as a date-only "YYYY-MM-DD" string. */
getIso: () => string;
}
function isoToDate(iso: string | null): Date {
if (!iso) return new Date();
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? new Date() : d;
function toLocalDay(value: string | null): Date {
return dateOnlyToLocalDate(value) ?? new Date();
}
export const DueDatePickerBody = forwardRef<DueDatePickerBodyHandle, Props>(
function DueDatePickerBody({ value }, ref) {
const [draft, setDraft] = useState<Date>(() => isoToDate(value));
const [draft, setDraft] = useState<Date>(() => toLocalDay(value));
useEffect(() => {
setDraft(isoToDate(value));
setDraft(toLocalDay(value));
}, [value]);
useImperativeHandle(ref, () => ({
getIso: () => draft.toISOString(),
getIso: () => toDateOnly(draft),
}));
return (

View File

@@ -29,6 +29,9 @@ const DOT_CLASS: Record<AgentAvailability, string> = {
online: "bg-success",
unstable: "bg-warning",
offline: "bg-muted-foreground/40",
// Retired agent (agent.archived_at set) — gray, mirrors web's archived dot
// in packages/views/agents/presence.ts.
archived: "bg-muted-foreground/40",
};
export function PresenceDot({ availability, size = 8 }: Props) {

View File

@@ -467,6 +467,7 @@ export const WorkspaceSchema: z.ZodType<Workspace> = z.object({
settings: z.record(z.string(), z.unknown()).default({}),
repos: z.array(z.object({ url: z.string() }).loose()).default([]),
issue_prefix: z.string().default(""),
avatar_url: z.string().nullable().default(null),
created_at: z.string().default(""),
updated_at: z.string().default(""),
}).loose();

View File

@@ -15,6 +15,7 @@ import type {
IssueStatus,
TimelineEntry,
} from "@multica/core/types";
import { formatDateOnly } from "@multica/core/issues/date";
const STATUS_LABEL: Record<IssueStatus, string> = {
backlog: "Backlog",
@@ -44,12 +45,11 @@ function priorityName(p: string | undefined): string {
return p ?? "?";
}
// start_date / due_date are calendar days — format timezone-safely (no offset
// day shift). Mirrors web's formatActivity in issue-detail.tsx.
function shortDate(date: string | undefined): string {
if (!date) return "?";
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
return formatDateOnly(date, { month: "short", day: "numeric" }, "en-US");
}
export function formatActivity(

View File

@@ -122,8 +122,7 @@ function LoginPageContent() {
return;
}
const list = qc.getQueryData<Workspace[]>(workspaceKeys.list()) ?? [];
const dest = await resolveLoggedInDestination(qc, onboarded, list);
router.push(dest);
router.push(await resolveLoggedInDestination(qc, onboarded, list));
};
// Build Google OAuth state: encode platform + next URL so the callback

View File

@@ -1,4 +1,4 @@
import { Instrument_Serif, Noto_Serif_SC } from "next/font/google";
import { Instrument_Serif, Noto_Serif_SC, Caveat } from "next/font/google";
import { LocaleProvider } from "@/features/landing/i18n";
import { getRequestLocale } from "@/lib/request-locale";
@@ -14,6 +14,15 @@ const notoSerifSC = Noto_Serif_SC({
variable: "--font-serif-zh",
});
// Handwritten face for the newhome demo's "this is live, try it" hint. Kept in
// this server layout (not the client newhome component) — next/font in a client
// component fails to resolve @swc/helpers under pnpm.
const caveat = Caveat({
subsets: ["latin"],
weight: "600",
variable: "--font-hand",
});
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
@@ -52,7 +61,7 @@ export default async function LandingLayout({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div className={`${instrumentSerif.variable} ${notoSerifSC.variable} landing-light h-full overflow-x-hidden overflow-y-auto bg-white`}>
<div className={`${instrumentSerif.variable} ${notoSerifSC.variable} ${caveat.variable} landing-light h-full overflow-x-hidden overflow-y-auto bg-white`}>
<LocaleProvider initialLocale={initialLocale}>{children}</LocaleProvider>
</div>
</>

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { NewHomeLanding } from "@/features/landing/newhome/newhome-landing";
export const metadata: Metadata = {
title: "Multica — Landing V2 (sandbox)",
description: "Work-in-progress rebuild of the Multica landing page.",
robots: { index: false, follow: false },
alternates: {
canonical: "/newhome",
},
};
export default function NewHomePage() {
return <NewHomeLanding />;
}

View File

@@ -18,13 +18,18 @@ const {
mockSetQueryData: vi.fn(),
}));
const makeUser = (overrides: Partial<{ onboarded_at: string | null }> = {}) => ({
const makeUser = (
overrides: Partial<{
onboarded_at: string | null;
onboarding_questionnaire: Record<string, unknown>;
}> = {},
) => ({
id: "user-1",
name: "Test",
email: "test@multica.ai",
avatar_url: null,
onboarded_at: null,
onboarding_questionnaire: {},
onboarding_questionnaire: { source: ["search"] },
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
...overrides,
@@ -73,6 +78,15 @@ import CallbackPage from "./page";
describe("CallbackPage", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset the source-backfill dismiss counter so a test that writes
// it doesn't leak state into the next test (and the next test
// doesn't inherit a cap-reached state from a previous run).
for (let i = window.localStorage.length - 1; i >= 0; i--) {
const k = window.localStorage.key(i);
if (k && k.startsWith("multica.source_backfill.dismiss.")) {
window.localStorage.removeItem(k);
}
}
// Snapshot keys before deleting — forEach + delete skips entries because
// the iteration index advances while the underlying list shrinks.
Array.from(mockSearchParams.keys()).forEach((k) =>
@@ -134,6 +148,7 @@ describe("CallbackPage", () => {
settings: {},
repos: [],
issue_prefix: "ACME",
avatar_url: null,
created_at: "",
updated_at: "",
},
@@ -181,4 +196,34 @@ describe("CallbackPage", () => {
expect(mockPush).toHaveBeenCalledWith(paths.onboarding());
});
});
it("onboarded users with missing source land in the workspace; the source-backfill modal is mounted there", async () => {
// Source attribution backfill is now an in-workspace modal — see
// `<SourceBackfillModal />` mounted inside `DashboardLayout`. The
// callback page is intentionally agnostic about it.
mockLoginWithGoogle.mockResolvedValue(
makeUser({
onboarded_at: "2026-01-01T00:00:00Z",
onboarding_questionnaire: {},
}),
);
mockListWorkspaces.mockResolvedValue([
{
id: "ws-1",
name: "Acme",
slug: "acme",
description: null,
context: null,
settings: {},
repos: [],
issue_prefix: "ACME",
created_at: "",
updated_at: "",
},
]);
render(<CallbackPage />);
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(paths.workspace("acme").issues());
});
});
});

View File

@@ -100,7 +100,10 @@ function CallbackContent() {
// 3. Default: hand off to the resolver (onboarding for first-timers,
// first workspace for returning users, /workspaces/new for
// onboarded users with zero workspaces).
// onboarded users with zero workspaces). Source-attribution
// backfill for onboarded users with no recorded source is
// handled by `<SourceBackfillModal />` inside the dashboard
// shell — not a route detour, so we route straight to dest.
router.push(resolvePostAuthDestination(wsList, onboarded));
})
.catch((err) => {

View File

@@ -43,3 +43,205 @@
--scrollbar-thumb-hover: oklch(0 0 0 / 18%);
--scrollbar-track: transparent;
}
/* newhome value #1 board demo: a card softly "lands" when it advances a
* column, so the auto-play reads as movement rather than a teleport. */
@keyframes newhome-card-land {
from {
opacity: 0;
transform: translateY(-8px) scale(0.97);
}
to {
opacity: 1;
transform: none;
}
}
.newhome-card-land {
animation: newhome-card-land 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@media (prefers-reduced-motion: reduce) {
.newhome-card-land {
animation: none;
}
}
/* "Agent is working" typing dots (value #2 delegate demo). */
@keyframes newhome-typing {
0%,
60%,
100% {
opacity: 0.25;
}
30% {
opacity: 0.9;
}
}
.newhome-typing > span {
animation: newhome-typing 1.1s ease-in-out infinite;
}
.newhome-typing > span:nth-child(2) {
animation-delay: 0.18s;
}
.newhome-typing > span:nth-child(3) {
animation-delay: 0.36s;
}
@media (prefers-reduced-motion: reduce) {
.newhome-typing > span {
animation: none;
}
}
/* Scene / popover entrances for the scripted delegate demo (value #2). */
@keyframes newhome-fade {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: none;
}
}
.newhome-fade {
animation: newhome-fade 0.4s ease-out;
}
@keyframes newhome-pop {
from {
opacity: 0;
transform: scale(0.96) translateY(-3px);
}
to {
opacity: 1;
transform: none;
}
}
.newhome-pop {
transform-origin: top left;
animation: newhome-pop 0.18s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.newhome-fade,
.newhome-pop {
animation: none;
}
}
/* newhome "this is live, try it" hint: the arrow draws itself in once, then the
* whole annotation gently floats to draw the eye. Respects reduced-motion. */
@keyframes newhome-hint-bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-7px);
}
}
.newhome-hint {
animation: newhome-hint-bob 2.8s ease-in-out infinite;
}
@keyframes newhome-hint-draw {
from {
stroke-dashoffset: 90;
}
to {
stroke-dashoffset: 0;
}
}
.newhome-hint-arrow path {
stroke-dasharray: 90;
animation: newhome-hint-draw 0.9s ease-out 0.25s both;
}
@media (prefers-reduced-motion: reduce) {
.newhome-hint {
animation: none;
}
.newhome-hint-arrow path {
animation: none;
stroke-dashoffset: 0;
}
}
/* newhome demo: darken --brand so the selected "working" chip (white text on
* bg-brand) has readable contrast. Scoped to the embedded product demo only. */
.landing-demo {
--brand: oklch(0.46 0.17 256);
}
/* Hide every scrollbar inside the embedded product demo — on a marketing page
* native scrollbars read as jarring. Scroll/drag still works; only the bar is
* hidden. Scoped to the demo so the rest of the site keeps its scrollbars. */
.landing-demo,
.landing-demo * {
scrollbar-width: none;
-ms-overflow-style: none;
}
.landing-demo::-webkit-scrollbar,
.landing-demo *::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
/* Suppress the board's "Hidden columns" side panel in the demo. We hide the
* backlog/blocked columns via the status allowlist, which would otherwise list
* them in this panel (w-[240px] is unique to that panel — columns use an inline
* width). Keeps the demo board to a clean four columns. */
.landing-demo [class~="w-[240px]"] {
display: none;
}
/* Popups (menus, dialogs, hover cards, tooltips) portal INTO the scaled demo
* box (see PortalContainerProvider) so they inherit the demo's zoom instead of
* rendering at 1:1 over the page. Full-screen modals like the transcript are
* sized in viewport units (100vh/100vw), which overflow the small demo window;
* cap them to the window (their fixed containing block is the transformed box)
* so the header/footer stay visible. Auto-height modals stay smaller than the
* cap, so they are unaffected.
*
* Scoped into @layer utilities so this wins the cascade against Tailwind's own
* `!`-important sizing utilities (e.g. the transcript's !h-[calc(100vh-4rem)]):
* same layer + higher specificity. Height is the only axis that overflows the
* window; width already fits, so leave each modal's own max-width intact. */
@layer utilities {
.landing-demo [data-slot="dialog-content"] {
max-height: calc(100% - 1.5rem) !important;
}
}
/* newhome (Landing V2 sandbox) — auto-scrolling agent marquee. Pure CSS, no
* motion library: the track holds two identical groups and slides left by one
* group width for a seamless loop. The wrapper is overflow-hidden, so there is
* no horizontal scrollbar. Pauses on hover; respects reduced-motion. */
@keyframes newhome-marquee {
to {
transform: translateX(-50%);
}
}
.newhome-marquee {
-webkit-mask-image: linear-gradient(
to right,
transparent,
#000 6%,
#000 94%,
transparent
);
mask-image: linear-gradient(
to right,
transparent,
#000 6%,
#000 94%,
transparent
);
}
.newhome-marquee-track {
animation: newhome-marquee 45s linear infinite;
}
.newhome-marquee:hover .newhome-marquee-track {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.newhome-marquee-track {
animation: none;
}
}

View File

@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
const repoRoot = resolve(process.cwd(), "../..");
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));
@@ -20,14 +21,41 @@ function expectChineseFontsBeforeKoreanFonts(source: string) {
}
}
// Japanese Kanji share the Han Unicode block with Chinese, so the Korean
// "append after Chinese" tactic would render Japanese with Chinese glyph
// shapes. The Japanese CJK chain must therefore be (a) gated behind a lang
// selector so zh/en keep Chinese-first ordering, and (b) ordered Japanese
// fonts BEFORE the Chinese families inside that scoped stack.
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 web Chinese font fallbacks before Korean font fallbacks", () => {
const layoutSource = readFileSync(
resolve(repoRoot, "apps/web/app/layout.tsx"),
const cssSource = readFileSync(
resolve(repoRoot, "apps/web/app/globals.css"),
"utf8",
);
expectChineseFontsBeforeKoreanFonts(layoutSource);
expectChineseFontsBeforeKoreanFonts(cssSource);
});
it("scopes the Japanese-first CJK stack to html[lang|='ja'] (web)", () => {
const cssSource = readFileSync(
resolve(repoRoot, "apps/web/app/globals.css"),
"utf8",
);
expectJapaneseScopedOverride(cssSource);
});
it("keeps desktop Chinese font fallbacks before Korean font fallbacks", () => {
@@ -38,4 +66,13 @@ describe("CJK font fallback order", () => {
expectChineseFontsBeforeKoreanFonts(desktopCss);
});
it("scopes the Japanese-first CJK stack to html[lang|='ja'] (desktop)", () => {
const desktopCss = readFileSync(
resolve(repoRoot, "apps/desktop/src/renderer/src/globals.css"),
"utf8",
);
expectJapaneseScopedOverride(desktopCss);
});
});

View File

@@ -10,3 +10,36 @@
@source "../../../packages/ui/**/*.{ts,tsx}";
@source "../../../packages/core/**/*.{ts,tsx}";
@source "../../../packages/views/**/*.{ts,tsx}";
/* Font stack. `--font-inter` is the next/font Inter family (+ its synthetic
size-adjusted fallback), set on <html> by inter.variable in app/layout.tsx.
We compose `--font-sans` here in static CSS — rather than baking the CJK tail
into next/font's `fallback` — so it can be overridden per `<html lang>` and
stays CSP-safe (no inline <style>). Tailwind's `font-sans` utility resolves
`var(--font-sans)` (see packages/ui/styles/tokens.css `@theme inline`).
Default (en / zh / ko): Latin chars render with Inter; CJK chars fall through
to the platform Chinese fonts, then Korean. Chinese MUST stay before Korean so
zh users never get Korean Hanja glyph shapes (Hangul is a separate Unicode
block, so ko users still get Korean fonts for Hangul). Kept in sync with
apps/desktop/src/renderer/src/globals.css. */
: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 are Han ideographs sharing the same Unicode block as Chinese,
and CSS font-fallback order is NOT affected by `<html lang>` — so the
Chinese-first default above would hand Japanese users Chinese glyph shapes
for shared ideographs. Promote a Japanese-first CJK chain only for Japanese.
`[lang|="ja"]` is the BCP-47 language-range selector: it matches exactly `ja`
or `ja-<region>` (layout.tsx emits `ja-JP`), never unrelated 3-letter subtags
such as `jam`. Inter still leads for Latin; zh/ko remain as a deep fallback. */
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;
}

View File

@@ -9,30 +9,19 @@ import { RESOURCES } from "@multica/views/locales";
import { getRequestLocale } from "@/lib/request-locale";
import "./globals.css";
// Font stack: Inter for Latin UI text + system CJK fonts for localized content.
// Desktop app uses the same stack via apps/desktop/src/renderer/src/globals.css
// 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.
// 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.
// Inter is the Latin UI face. next/font produces a hashed family (`__Inter_xxx`)
// plus a synthetic size-adjusted fallback face to prevent FOUT layout shift
// both are exposed under the `--font-inter` CSS variable.
//
// The full `--font-sans` stack (Inter + the per-locale CJK fallback chain) is
// assembled in static CSS in ./globals.css, not here: it must be overridable per
// `<html lang>` (Japanese Kanji are Han ideographs and need a Japanese-first CJK
// stack), and a hashed family name can only be referenced from CSS via a variable.
// Keeping the CJK chain in CSS also keeps it CSP-safe and in sync with the desktop
// app, which defines the same chain in apps/desktop/src/renderer/src/globals.css.
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
fallback: [
"-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",
],
variable: "--font-inter",
});
// 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
@@ -109,6 +98,7 @@ const HTML_LANG: Record<SupportedLocale, string> = {
en: "en",
"zh-Hans": "zh-CN",
ko: "ko-KR",
ja: "ja-JP",
};
export default async function RootLayout({

View File

@@ -0,0 +1,102 @@
---
title: Multica に24時間体制のシニアデータアナリストを置いた方法
description: db-boy は私たちのワークスペースの24時間体制のシニアデータアナリストです。毎週月曜に週次メトリクスを自動投稿し、アドホックな質問に数分で答え、データが欠けていれば自分でトラッキング用の Pull request を作ってフロントエンドエージェントに割り当てます。
category: 分析の自動化
updated_at: 2026-05-22
hero_image: /usecases/auto-data-analysis/hero.png
---
db-boy は私たちのワークスペースの中にいる24時間体制のシニアデータアナリストです。
毎週月曜の午前9時、db-boy は先週の数字を表にまとめて `#weekly-metrics` イシューに貼り付け、各項目の担当者を @メンションします。
<Screenshot
src="/usecases/auto-data-analysis/hero.png"
alt="db-boy が月曜の週次メトリクスを @メンション付きで投稿した Multica のイシューコメント"
width={2098}
height={1548}
caption="月曜9:00 — db-boy が先週の主要な数字をイシューに残します(デモデータ)。"
priority
/>
勤務時間中は常に待機しています。データに関する質問が @メンションで飛んでくると、数分以内に Markdown の表や HTML のチャートを添えて答えます。トラッキングされていないイベントや曖昧な定義に途中でぶつかると、待つのではなく、適切な担当者を巻き込むためのイシューをその場で作ります。
[CTA: 無料で始める →]
---
## Multica とは
Multica は AI エージェントを従業員として扱うワークスペースプラットフォームです。一般的なコラボレーションツールと違う点は大きく2つあります。
**エージェントはツールではなく、ワークスペースのメンバーです。** db-boy はメンバー一覧の中に自分のアバターとプロフィールページ、オープン中のイシューのキューを持っています。イシューを割り当てられ、@メンションされ、プロジェクトのオーナーに指名されることもあり、自分でイシューを作って他の人に渡すこともできます。
**コンテキストはワークスペース全体で共有されます。** イシューのコメント、添付ファイル、HTML レポートを、人もエージェントも検索してリンクできます。スキルはワークスペース全体で使うプレイブックです。一度書いておけば、そのスキルを身につけたすべてのエージェントが同じ定義を共有するので、db-boy も DAU の計算方法や決済データがどのテーブルにあるかを毎回覚え直す必要がありません。
---
## どう組み上げたか
オフィスには Multica デーモンをインストールした Mac mini が1台あります。デーモンは起動時にローカルの AI コーディングツール(Claude Code、Codex などの系統)をスキャンし、それぞれを利用可能なランタイムとして登録します。そのうえでワークスペースに新しいエージェントを作り、@db-boy という名前を付けました。
この Mac mini には db-boy が仕事をするのに必要なものがあらかじめ揃っています。EKS の認証情報が入った `kubectl`、`posthog-cli`、そして読み取り専用アカウントで接続する `psql` です。ホストマシンで実行できるものなら、db-boy もそのまま呼び出せます。データベース接続は分析用のリードレプリカ(分析用に切り出したプロダクションの読み取り専用コピー)を向いており、`reader` アカウントには SELECT 権限しかありません。レプリカを分離してあるおかげでクエリがプロダクションのトラフィックに触れることはなく、読み取り専用なのでプロンプトインジェクションがあっても何も書き込めません。
最後に「データ分析」スキルを彼に紐付けました。このスキルには、DAU の定義、ファネルの各ステップの数え方、アプリケーションの状態は PG に・ユーザー行動は PostHog にあること、決済データが入っているテーブル、HTML レポートを残すイシュー、計測が欠けているときに @メンションする担当者が書かれています。一度書いてワークスペース全体で共有します。定義を変えたいときはスキルの1行を直すだけです。次の実行から反映され、デプロイもコード変更も要りません。
<Screenshot
src="/usecases/auto-data-analysis/db-boy-profile.png"
alt="Multica の db-boy エージェントのプロフィールページ。左にランタイムとスキル、右に system prompt と読み取り専用の警告が書かれた指示があります。"
width={2596}
height={1712}
caption="db-boy のプロフィール — アイデンティティ、データ定義、安全装置がすべて指示の中にあります。"
/>
---
## 仕事を任せる方法と、彼が自分で拾う仕事
いちばんよく使うのはイシューを割り当てる方法です。質問を書いて Assign を押し、@db-boy を選びます。数秒で Claude Code が起動し、スキルに書かれた経路(posthog-cli と HogQL、または psql)をたどって作業し、数分後にはイシューに Markdown の表と HTML レポートが添付されます。
毎回新しいイシューを開く必要はありません。既存のスレッドで続きの質問があれば、その場で @メンションしてください。同じスレッドの中で文脈をそのまま保ったまま返してくれます。ちょっとした数字が欲しいだけなら、同僚に聞くように直接チャットを送れば大丈夫です。繰り返しのレポート(週次メトリクス、月次の投資家向け資料、日次のトークン使用量トップ10など)はオートパイロットに一度設定しておけば、スケジュールどおりに実行され、該当するイシューに結果を残し、購読者にも通知が届きます。
最初に詰まったところで手を止めることもありません。トラッキングされていないイベントを見つけると、自分で新しいイシューを開きます。たとえば `Y ページの X ボタンのクリックにトラッキングを追加` のようなタイトルで、フィールド、イベント名、必要な理由を埋めて @frontend-agent に割り当てます。フロントエンドエージェントが Pull request を出し、エンジニアがレビューしてマージすれば、次に同じ質問が来たときにはもうデータが揃っています。
<Screenshot
src="/usecases/auto-data-analysis/ad-hoc-question.png"
alt="@naiyuan がコメントで db-boy に North Star 指標の質問をし、db-boy が分析結果と、frontend-agent のために作ったトラッキング用 Pull request のリンクで答えています。"
width={1726}
height={840}
caption="naiyuan がスレッドで db-boy を @メンションします。db-boy は答えを残し、フロントエンドエージェントに割り当てたトラッキングの後続作業をそっと開きます(デモデータ)。"
/>
誰もチケットを別に切りません。誰も他人を追い回しません。エージェント同士が仕事を渡し合い、人とエージェントが同じイシューで議論し、イシューそのものが共有の作業単位になります。
---
## チームで変わったこと
db-boy を迎えてから、データを見るリズムそのものが変わりました。
**即時性。** どんな質問でも @メンションすれば数分で答えが返ってきます。深夜2時に retention curve が必要でも大丈夫です。db-boy はいつもそこにいます。「メモしておいて来週アナリストに聞こう」といった遅れがなくなりました。
**自動化。** 月曜の週次レポート、月次の投資家向け資料、日次のトークン使用量トップ10が、すべてオートパイロットの上で回っています。結果は該当イシューのコメントに残り、購読者にも通知が届きます。繰り返しのレポートに人の時間がもうかかりません。
**可視化。** 既定の成果物は文章の段落ではなく、チャート入りの HTML ダッシュボードです。数分でチャートが返ってくるので、トレンドグラフ1つのために sprint を丸ごと待つことはありません。議論も「なんとなく X な気がする」から抜け出します。
**主体性。** 分析の流れで詰まった仕事を自分で拾います。欠けている計測はフロントエンドエージェントに割り当てたイシューになり、曖昧な定義は適切な担当者とのスレッドになり、コンパイルできないクエリは別のアプローチでの再試行になります。「誰かを待っている」列に何も残りません。
---
## あなたのワークスペースにも1人迎える
Multica をダウンロードし、エージェントを登録し、データ分析スキルを与えて、最初のイシューを割り当てましょう。
[CTA: 無料で始める →] [Secondary CTA: セットアップガイドを読む →]
---
## 関連リンク
- [エージェント](/docs/ja/agents)
- [オートパイロット](/docs/ja/autopilots)
- [スキル](/docs/ja/skills)

View File

@@ -6,12 +6,14 @@ describe("changelog date labels", () => {
expect(monthYearLabel(2026, 1, "en")).toBe("January 2026");
expect(monthYearLabel(2026, 1, "zh-Hans")).toBe("2026年1月");
expect(monthYearLabel(2026, 1, "ko")).toBe("2026년 1월");
expect(monthYearLabel(2026, 1, "ja")).toBe("2026年1月");
});
it("formats full dates for each landing locale", () => {
expect(fullDateLabel("2026-01-15", "en")).toBe("January 15, 2026");
expect(fullDateLabel("2026-01-15", "zh-Hans")).toBe("2026年1月15日");
expect(fullDateLabel("2026-01-15", "ko")).toBe("2026년 1월 15일");
expect(fullDateLabel("2026-01-15", "ja")).toBe("2026年1月15日");
});
it("keeps invalid release dates unchanged", () => {

View File

@@ -8,10 +8,48 @@ import { captureDownloadIntent } from "@multica/core/analytics";
import { XMark, GitHubMark, githubUrl, twitterUrl } from "./shared";
import { useLocale, locales, localeLabels } from "../i18n";
export type LandingFooterGroup = {
label: string;
links: Array<{
label: string;
href: string;
}>;
};
export function LandingFooter() {
const { t, locale, setLocale } = useLocale();
const user = useAuthStore((s) => s.user);
const groups = Object.values(t.footer.groups);
return (
<LandingFooterContent
ctaHref={user ? "/" : "/login"}
ctaLabel={user ? t.header.dashboard : t.footer.cta}
locale={locale}
setLocale={setLocale}
/>
);
}
export function LandingFooterContent({
brandHref = "#product",
ctaHref,
ctaLabel,
groups,
locale,
setLocale,
}: {
brandHref?: string;
ctaHref: string;
ctaLabel?: string;
groups?: LandingFooterGroup[];
locale?: ReturnType<typeof useLocale>["locale"];
setLocale?: ReturnType<typeof useLocale>["setLocale"];
}) {
const localeContext = useLocale();
const activeLocale = locale ?? localeContext.locale;
const setActiveLocale = setLocale ?? localeContext.setLocale;
const footerGroups = groups ?? Object.values(localeContext.t.footer.groups);
const footerCtaLabel = ctaLabel ?? localeContext.t.footer.cta;
return (
<footer className="bg-[#0a0d12] text-white">
@@ -20,14 +58,14 @@ export function LandingFooter() {
<div className="flex flex-col gap-12 border-b border-white/10 py-16 sm:py-20 lg:flex-row lg:gap-20">
{/* Left — newsletter / CTA */}
<div className="lg:w-[340px] lg:shrink-0">
<Link href="#product" className="flex items-center gap-3">
<Link href={brandHref} className="flex items-center gap-3">
<MulticaIcon className="size-5 text-white" noSpin />
<span className="text-[18px] font-semibold tracking-[0.04em] lowercase">
multica
</span>
</Link>
<p className="mt-4 max-w-[300px] text-[14px] leading-[1.7] text-white/50 sm:text-[15px]">
{t.footer.tagline}
{localeContext.t.footer.tagline}
</p>
<div className="mt-4 flex items-center gap-3">
<Link
@@ -49,17 +87,17 @@ export function LandingFooter() {
</div>
<div className="mt-6">
<Link
href={user ? "/" : "/login"}
href={ctaHref}
className="inline-flex items-center justify-center rounded-[11px] bg-white px-5 py-2.5 text-[13px] font-semibold text-[#0a0d12] transition-colors hover:bg-white/88"
>
{user ? t.header.dashboard : t.footer.cta}
{footerCtaLabel}
</Link>
</div>
</div>
{/* Right — link columns */}
<div className="grid flex-1 grid-cols-2 gap-8 sm:grid-cols-4">
{groups.map((group) => (
{footerGroups.map((group) => (
<div key={group.label}>
<h4 className="text-[12px] font-semibold uppercase tracking-[0.1em] text-white/40">
{group.label}
@@ -92,7 +130,7 @@ export function LandingFooter() {
{/* Bottom: copyright + language switcher */}
<div className="flex items-center justify-between py-6">
<p className="text-[13px] text-white/36">
{t.footer.copyright.replace(
{localeContext.t.footer.copyright.replace(
"{year}",
String(new Date().getFullYear()),
)}
@@ -102,10 +140,10 @@ export function LandingFooter() {
<button
type="button"
key={l}
onClick={() => setLocale(l)}
onClick={() => setActiveLocale(l)}
className={cn(
"px-1.5 py-1 text-[12px] font-medium transition-colors",
l === locale
l === activeLocale
? "text-white/70"
: "text-white/30 hover:text-white/50",
i > 0 && "border-l border-white/16",

View File

@@ -12,6 +12,7 @@ import { useRouter } from "next/navigation";
import { useConfigStore } from "@multica/core/config";
import { createBrowserCookieLocaleAdapter } from "@multica/core/i18n/browser";
import { createEnDict } from "./en";
import { createJaDict } from "./ja";
import { createKoDict } from "./ko";
import { createZhDict } from "./zh";
import {
@@ -26,6 +27,7 @@ const dictionaryFactories: Record<
(allowSignup: boolean) => LandingDict
> = {
en: createEnDict,
ja: createJaDict,
ko: createKoDict,
zh: createZhDict,
};

View File

@@ -292,6 +292,61 @@ export function createEnDict(allowSignup: boolean): LandingDict {
fixes: "Bug Fixes",
},
entries: [
{
version: "0.3.14",
date: "2026-06-02",
title: "Japanese Support and /skill Command",
changes: [],
features: [
"Multica now supports Japanese across the app, site, and docs",
"Chat now supports a /skill command for choosing an agent Skill",
"Workspaces can now show a custom logo",
"Teams can add Skills to an agent without replacing existing Skills",
"OpenCode agents now support thinking variants",
],
improvements: [
"Existing users who skipped the onboarding source question can now answer it later without restarting onboarding",
"Retired agents now appear as Archived everywhere instead of looking offline or still active",
"Chat history and Issue execution rows have cleaner hover actions with less truncation and overlap",
"Project Issue filters now apply the agents-working filter consistently across list, board, and timeline views",
],
fixes: [
"Users without access can no longer trigger private squad leaders through indirect Issue or comment paths",
"Project progress counters and reopened agent work now refresh more reliably",
"Desktop and web recover better from blank workspace states, no-access pages, renderer errors, and renderer crashes",
"Images and file cards keep rendering when names contain Markdown characters",
"Chat, labels, and invitations refresh correctly after a realtime reconnect",
"Run-only autopilot tasks, quick-create tasks, and their retries can be cancelled from the activity view",
"Skill descriptions with multi-line frontmatter now import and display correctly",
"Windows Copilot runs preserve multiline prompts and clean up shell-style custom arguments",
],
},
{
version: "0.3.13",
date: "2026-06-01",
title: "Skill Search and CLI Updates",
changes: [],
features: [
"The CLI can now search Skills and list pull requests linked to an Issue, making release checks and automation audits easier from the terminal",
"Teams can change squad member roles from the CLI without opening the app",
"Agent lists can be filtered by runtime machine, so teams can quickly find the agents tied to a device or local service",
"SMTP relays now support secure SMTPS connections on port 465",
"OpenCode runtimes can use MCP settings saved on an agent",
"OpenCode agents now expose model variants as thinking controls and pass the selected effort through the runtime",
],
improvements: [
"Mobile Issue headers now use cleaner controls that stay easier to reach on small screens",
"Chat history rows show running states and actions more predictably",
"Importing the same Skill twice now reports a clear result instead of interrupting the flow",
],
fixes: [
"Replies stay attached to the exact comment they were sent under",
"Claude runs are less likely to stall while prompts are being sent",
"Self-hosted local runtime setup links now point users to the right addresses",
"MCP setup guidance and runtime support checks now match what the product can run",
"Execution logs now clear active-row highlights after the related work finishes",
],
},
{
version: "0.3.12",
date: "2026-05-29",

File diff suppressed because it is too large Load Diff

View File

@@ -267,6 +267,61 @@ export function createKoDict(allowSignup: boolean): LandingDict {
fixes: "버그 수정",
},
entries: [
{
version: "0.3.14",
date: "2026-06-02",
title: "일본어 지원과 /skill command",
changes: [],
features: [
"Multica가 앱, 사이트, 문서에서 일본어를 지원합니다.",
"채팅에서 /skill command로 에이전트의 스킬을 선택할 수 있습니다.",
"워크스페이스에 사용자 지정 로고를 표시할 수 있습니다.",
"기존 스킬을 유지한 채 에이전트에 스킬을 추가할 수 있습니다.",
"OpenCode 에이전트에서 thinking variant를 선택할 수 있습니다.",
],
improvements: [
"초기 온보딩에서 유입 경로 질문을 건너뛴 사용자도 나중에 짧은 안내로 답할 수 있습니다.",
"사용 중지된 에이전트는 오프라인이나 작업 중처럼 보이지 않고, 모든 화면에서 Archived로 표시됩니다.",
"채팅 기록과 이슈 실행 기록의 hover 작업이 더 깔끔해져 텍스트 잘림과 겹침이 줄었습니다.",
"프로젝트 이슈의 agents-working 필터가 목록, 보드, 타임라인에서 일관되게 적용됩니다.",
],
fixes: [
"권한이 없는 사용자가 간접적인 이슈나 댓글 경로로 private 스쿼드 리더를 실행할 수 없게 했습니다.",
"프로젝트 진행률 집계와 다시 시작된 에이전트 작업 상태가 더 안정적으로 새로고침됩니다.",
"데스크톱과 웹에서 빈 워크스페이스 상태, 접근 불가 페이지, 화면 오류, 충돌 이후 더 잘 복구됩니다.",
"이미지와 파일 카드 이름에 Markdown 문자가 있어도 올바르게 표시됩니다.",
"실시간 연결이 다시 이어진 뒤 채팅, 라벨, 초대 데이터가 올바르게 새로고침됩니다.",
"run-only 오토파일럿 작업, quick-create 작업, 그리고 재시도 작업을 Activity 화면에서 취소할 수 있습니다.",
"여러 줄 스킬 설명이 가져오기 이후에도 올바르게 표시됩니다.",
"Windows Copilot 실행에서 여러 줄 프롬프트가 유지되고, 따옴표가 있는 사용자 지정 인수도 더 잘 처리됩니다.",
],
},
{
version: "0.3.13",
date: "2026-06-01",
title: "Skill 검색과 CLI 업데이트",
changes: [],
features: [
"CLI에서 Skill을 검색하고 이슈에 연결된 pull request를 확인할 수 있어 릴리스 확인과 자동화 점검이 더 쉬워졌습니다.",
"스쿼드 구성원의 역할을 CLI에서 바로 변경할 수 있습니다.",
"에이전트 목록을 런타임 머신별로 필터링해 특정 기기나 로컬 서비스에 연결된 에이전트를 더 빨리 찾을 수 있습니다.",
"메일 발송 설정에서 465번 포트의 보안 SMTP 연결을 사용할 수 있습니다.",
"OpenCode 런타임이 에이전트에 저장된 MCP 설정을 사용할 수 있습니다.",
"OpenCode 에이전트는 모델 variant를 thinking control로 표시하고 선택한 값을 런타임에 전달합니다.",
],
improvements: [
"모바일 이슈 상단의 조작 버튼이 더 정돈되어 작은 화면에서도 다루기 쉽습니다.",
"채팅 기록의 실행 상태와 작업 버튼이 더 예측 가능하게 표시됩니다.",
"같은 Skill을 다시 가져올 때 흐름을 끊지 않고 분명한 결과를 보여 줍니다.",
],
fixes: [
"댓글 답글이 사용자가 선택한 정확한 댓글 아래에 유지됩니다.",
"Claude 작업이 프롬프트를 보내는 중 멈출 가능성이 줄었습니다.",
"셀프 호스팅 로컬 런타임 설정 링크가 올바른 주소를 안내합니다.",
"MCP 설정 안내와 런타임 지원 여부가 제품 동작과 맞게 표시됩니다.",
"작업이 끝난 뒤 실행 로그의 활성 표시가 올바르게 정리됩니다.",
],
},
{
version: "0.3.12",
date: "2026-05-29",

View File

@@ -2,20 +2,22 @@ import type { SupportedLocale } from "@multica/core/i18n";
export { docsHrefForLocale } from "@/lib/docs-href";
export type Locale = SupportedLocale;
export type LandingDictionaryLocale = "en" | "zh" | "ko";
export type LandingDictionaryLocale = "en" | "zh" | "ko" | "ja";
export const locales: Locale[] = ["en", "zh-Hans", "ko"];
export const locales: Locale[] = ["en", "zh-Hans", "ko", "ja"];
export const localeLabels: Record<Locale, string> = {
en: "EN",
"zh-Hans": "\u4e2d\u6587",
ko: "\ud55c\uad6d\uc5b4",
ja: "\u65e5\u672c\u8a9e",
};
export function toLandingDictionaryLocale(
locale: Locale,
): LandingDictionaryLocale {
if (locale === "ko") return "ko";
if (locale === "ja") return "ja";
return locale === "zh-Hans" ? "zh" : "en";
}

View File

@@ -292,6 +292,61 @@ export function createZhDict(allowSignup: boolean): LandingDict {
fixes: "问题修复",
},
entries: [
{
version: "0.3.14",
date: "2026-06-02",
title: "日语支持和 /skill command",
changes: [],
features: [
"Multica 现在支持日语界面、官网和文档",
"聊天现在支持 /skill command用来选择智能体技能",
"工作区现在可以显示自定义 Logo",
"可以给智能体追加技能,不会覆盖已有技能",
"OpenCode 智能体现在支持思考强度 variant",
],
improvements: [
"早期用户如果没有填写入门来源问题,现在可以在之后补充,不需要重新走完整入门流程",
"已停用的智能体现在会在各处显示为已归档,不再看起来像离线或仍在工作",
"聊天历史和 Issue 执行记录的悬浮操作更清晰,减少文字截断和按钮重叠",
"项目里的 Issue 筛选现在会在列表、看板和时间线视图中一致应用智能体工作中筛选",
],
fixes: [
"无权限用户不能再通过间接的 Issue 或评论路径触发私有小队负责人",
"项目进度统计和重新进入工作的智能体状态刷新更可靠",
"桌面端和网页端在空白工作区、无访问权限页面、渲染错误和崩溃后恢复更稳定",
"图片和文件卡片的名称包含 Markdown 字符时也能正常显示",
"实时连接重连后,聊天、标签和邀请数据会正确刷新",
"仅运行自动任务、快速创建任务及其重试任务现在可以从活动视图取消",
"多行技能描述现在可以正确导入和展示",
"Windows 上的 Copilot 运行会保留多行提示词,并正确处理带引号的自定义参数",
],
},
{
version: "0.3.13",
date: "2026-06-01",
title: "Skill 搜索与命令行更新",
changes: [],
features: [
"命令行现在可以搜索 Skill也可以列出某个 Issue 关联的合并请求,发布检查和自动化排查更方便",
"团队可以直接在命令行调整小队成员角色,不用进入应用界面",
"智能体列表可以按运行机器筛选,更快找到绑定到某台设备或本机运行服务的智能体",
"邮件发送服务现在支持安全的 465 端口连接",
"OpenCode 运行环境可以使用智能体里保存的 MCP 设置",
"OpenCode 智能体现在会把模型 variant 显示为思考强度控制,并把选择结果传给运行时",
],
improvements: [
"移动端 Issue 顶部操作更清晰,小屏幕上也更容易点击",
"聊天历史里的运行状态和操作按钮更稳定,减少误点和状态混乱",
"重复导入同一个 Skill 时,会给出清楚结果,不再打断当前流程",
],
fixes: [
"评论回复会保留在用户实际回复的那条评论下面",
"Claude 任务发送提示词时更不容易卡住",
"自托管本机运行服务的设置链接会指向正确地址",
"MCP 设置说明和运行环境支持判断现在保持一致",
"执行日志在任务结束后会正确清理活跃状态",
],
},
{
version: "0.3.12",
date: "2026-05-29",

View File

@@ -0,0 +1,190 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { ArrowLeft, LayoutList, Bot, Sparkles } from "lucide-react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { cn } from "@multica/ui/lib/utils";
import { PortalContainerProvider } from "@multica/ui/lib/portal-container";
import { setApiInstance } from "@multica/core/api";
import { I18nProvider } from "@multica/core/i18n/react";
import { WorkspaceSlugProvider } from "@multica/core/paths";
import { workspaceListOptions } from "@multica/core/workspace/queries";
import { useIssueViewStore } from "@multica/core/issues/stores/view-store";
import { RESOURCES } from "@multica/views/locales";
import {
NavigationProvider,
type NavigationAdapter,
} from "@multica/views/navigation";
import { IssuesPage, IssueDetail } from "@multica/views/issues/components";
import { ModalRegistry } from "@multica/views/modals/registry";
import { createMockApi } from "./mock-api";
import { WORKSPACE } from "./mock-data";
import { DemoErrorBoundary } from "./demo-error-boundary";
import { AgentsPanel, SkillsPanel } from "./demo-panels";
// Install the mock client globally (the @multica/core/api singleton). This
// module is only ever imported client-side via a dynamic ssr:false import,
// and only on the landing page, so overriding the singleton here is safe.
setApiInstance(createMockApi());
const ISSUE_PATH = /\/issues\/([^/?#]+)/;
const TABS = [
{ id: "issues", label: "Issues", Icon: LayoutList },
{ id: "agents", label: "Agents", Icon: Bot },
{ id: "skills", label: "Skills", Icon: Sparkles },
] as const;
type TabId = (typeof TABS)[number]["id"];
export function DemoBoard() {
const [tab, setTab] = useState<TabId>("issues");
const [detailId, setDetailId] = useState<string | null>(null);
// Keep the latest setter in a ref so the navigation adapter is stable.
const setDetailRef = useRef(setDetailId);
setDetailRef.current = setDetailId;
// Portal mount for popups (menus, dialogs, hover cards, tooltips). It lives
// inside the scaled demo box, so every portaled popup inherits the same zoom
// instead of rendering at 1:1 over the page.
const portalRef = useRef<HTMLDivElement>(null);
const queryClient = useMemo(() => {
const qc = new QueryClient({
defaultOptions: {
queries: { retry: false, refetchOnWindowFocus: false, staleTime: 30_000 },
mutations: { retry: false },
},
});
qc.setQueryData(workspaceListOptions().queryKey, [WORKSPACE]);
return qc;
}, []);
// No status filter — show every column (backlog … blocked). Reset on mount in
// case a previous session persisted a filter.
useEffect(() => {
useIssueViewStore.setState({ statusFilters: [] });
}, []);
const adapter = useMemo<NavigationAdapter>(() => {
const openFromPath = (path: string) => {
const m = path.match(ISSUE_PATH);
if (m?.[1]) setDetailRef.current(decodeURIComponent(m[1]));
};
return {
push: openFromPath,
replace: openFromPath,
back: () => setDetailRef.current(null),
pathname: "/demo/issues",
searchParams: new URLSearchParams(),
getShareableUrl: (p) => p,
};
}, []);
const resources = useMemo(() => ({ en: RESOURCES.en }), []);
return (
<DemoErrorBoundary>
<QueryClientProvider client={queryClient}>
<I18nProvider locale="en" resources={resources}>
<NavigationProvider value={adapter}>
<WorkspaceSlugProvider slug="demo">
<PortalContainerProvider container={portalRef}>
{/* `landing-demo` darkens --brand so the selected "working" chip
stays readable (white-on-brand). */}
<div className="landing-demo flex h-full w-full flex-col bg-background text-foreground">
<BrowserBar
tab={tab}
onTab={(t) => {
setTab(t);
setDetailId(null);
}}
/>
<div className="min-h-0 flex-1">
{tab === "issues" ? (
detailId ? (
<div className="flex h-full flex-col">
<button
type="button"
onClick={() => setDetailId(null)}
className="flex shrink-0 items-center gap-1.5 px-4 py-2.5 text-[13px] font-medium text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-4" aria-hidden />
Back to board
</button>
<div className="min-h-0 flex-1 overflow-auto [scrollbar-width:thin]">
<IssueDetail
issueId={detailId}
onDone={() => setDetailId(null)}
onDelete={() => setDetailId(null)}
/>
</div>
</div>
) : (
// Hide IssuesPage's own PageHeader — the browser tabs
// above already serve as the app header.
<div className="flex h-full w-full flex-col [&>div>div:first-child]:hidden">
<IssuesPage />
</div>
)
) : tab === "agents" ? (
<AgentsPanel />
) : (
<SkillsPanel />
)}
</div>
{/* Popups (menus, dialogs, hover cards, tooltips) portal here
via PortalContainerProvider, so they share the demo's zoom
instead of rendering at 1:1 over the page. */}
<div ref={portalRef} />
</div>
{/* Real create-issue dialog host — opened by the board's "+"
buttons via the global modal store. Portals into the scaled
box (see PortalContainerProvider) so it matches the demo zoom. */}
<DemoErrorBoundary fallback={null}>
<ModalRegistry />
</DemoErrorBoundary>
</PortalContainerProvider>
</WorkspaceSlugProvider>
</NavigationProvider>
</I18nProvider>
</QueryClientProvider>
</DemoErrorBoundary>
);
}
function BrowserBar({
tab,
onTab,
}: {
tab: TabId;
onTab: (t: TabId) => void;
}) {
return (
<div className="flex h-11 shrink-0 items-center gap-3 border-b border-[#0a0d12]/8 bg-[#f7f8fa] px-3.5">
<div className="flex shrink-0 items-center gap-1.5">
<span className="size-2.5 rounded-full bg-[#0a0d12]/12" />
<span className="size-2.5 rounded-full bg-[#0a0d12]/12" />
<span className="size-2.5 rounded-full bg-[#0a0d12]/12" />
</div>
<div className="flex items-center gap-0.5">
{TABS.map(({ id, label, Icon }) => (
<button
key={id}
type="button"
onClick={() => onTab(id)}
className={cn(
"inline-flex h-7 items-center gap-1.5 rounded-[8px] px-2.5 text-[13px] font-medium transition-colors",
tab === id
? "bg-white text-[#0a0d12] shadow-[0_1px_2px_rgba(10,13,18,0.08)] ring-1 ring-[#0a0d12]/8"
: "text-[#0a0d12]/55 hover:bg-[#0a0d12]/[0.04] hover:text-[#0a0d12]/80",
)}
>
<Icon className="size-3.5" aria-hidden />
{label}
</button>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
"use client";
import { Component, type ReactNode } from "react";
import { RotateCcw } from "lucide-react";
interface Props {
children: ReactNode;
// When provided, render this on error instead of the default reset panel.
// Pass `null` to fail silently (used for the portaled modal host).
fallback?: ReactNode;
}
interface State {
hasError: boolean;
}
/**
* Isolates the live product demo. If any interaction throws during render,
* we show a small reset panel instead of letting the error bubble up and
* white-screen the whole landing page. "Reset" remounts the demo subtree.
*/
export class DemoErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch() {
// Swallow — the demo is non-critical marketing UI. Nothing to report.
}
private reset = () => this.setState({ hasError: false });
render() {
if (this.state.hasError) {
if ("fallback" in this.props) return this.props.fallback ?? null;
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-white text-center">
<p className="text-[14px] text-[#0a0d12]/55">
The demo hit a snag.
</p>
<button
type="button"
onClick={this.reset}
className="inline-flex items-center gap-1.5 rounded-[8px] border border-[#0a0d12]/14 bg-white px-3.5 py-2 text-[13px] font-semibold text-[#0a0d12] transition-colors hover:bg-[#0a0d12]/[0.04]"
>
<RotateCcw className="size-3.5" aria-hidden />
Reset demo
</button>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,64 @@
"use client";
import { useEffect, useRef } from "react";
import { IssueDetail } from "@multica/views/issues/components";
export function DemoIssueDetail({
issueId,
initialScrollTop = 0,
}: {
issueId: string;
initialScrollTop?: number;
}) {
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (initialScrollTop <= 0) return;
let cancelled = false;
const applyScroll = () => {
if (cancelled) return;
const root = rootRef.current;
if (!root) return;
const scrollables = Array.from(root.querySelectorAll<HTMLElement>("div"))
.filter((el) => el.scrollHeight - el.clientHeight > 80)
.sort(
(a, b) =>
b.scrollHeight - b.clientHeight - (a.scrollHeight - a.clientHeight),
);
const target =
scrollables.find((el) => {
const className =
typeof el.className === "string" ? el.className : "";
return (
className.includes("relative") &&
className.includes("flex-1") &&
className.includes("overflow-y-auto")
);
}) ?? scrollables[0];
if (!target) return;
const maxScroll = Math.max(0, target.scrollHeight - target.clientHeight);
target.scrollTop = Math.min(initialScrollTop, maxScroll);
};
const frame = window.requestAnimationFrame(applyScroll);
const timers = [
window.setTimeout(applyScroll, 250),
window.setTimeout(applyScroll, 800),
];
return () => {
cancelled = true;
window.cancelAnimationFrame(frame);
timers.forEach((timer) => window.clearTimeout(timer));
};
}, [initialScrollTop, issueId]);
return (
<div ref={rootRef} className="h-full overflow-auto [scrollbar-width:thin]">
<IssueDetail issueId={issueId} onDone={() => {}} onDelete={() => {}} />
</div>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
// Lightweight panels for the Agents / Skills tabs in the demo browser. These
// are bespoke presentational views over the same mock data — not the heavy
// product pages — so the tabs feel real without extra coupling or risk.
import { AGENTS, ISSUES, RUNNING_TASKS, SKILLS } from "./mock-data";
export function AgentsPanel() {
return (
<div className="h-full overflow-auto px-5 py-5 [scrollbar-width:thin]">
<div className="mx-auto grid max-w-[760px] grid-cols-1 gap-2.5 sm:grid-cols-2">
{AGENTS.map((a) => {
const task = RUNNING_TASKS.find((t) => t.agent_id === a.id);
const issue = task && ISSUES.find((i) => i.id === task.issue_id);
return (
<div
key={a.id}
className="flex items-center gap-3 rounded-[6px] border border-[#0a0d12]/8 bg-white px-3.5 py-3"
>
{a.avatar_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={a.avatar_url} alt="" className="size-8 shrink-0 rounded-full" />
) : (
<span className="size-8 shrink-0 rounded-full bg-[#0a0d12]/10" />
)}
<div className="min-w-0">
<div className="truncate text-[14px] font-semibold text-[#0a0d12]">
{a.name}
</div>
{issue ? (
<div className="flex items-center gap-1.5 truncate text-[12.5px] text-[#0a0d12]/55">
<span className="inline-block size-1.5 shrink-0 rounded-full bg-emerald-500" />
Working on {issue.identifier}
</div>
) : (
<div className="text-[12.5px] text-[#0a0d12]/45">Idle</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}
export function SkillsPanel() {
return (
<div className="h-full overflow-auto px-5 py-5 [scrollbar-width:thin]">
<div className="mx-auto grid max-w-[760px] grid-cols-1 gap-2.5 sm:grid-cols-2">
{SKILLS.map((s) => (
<div
key={s.name}
className="rounded-[6px] border border-[#0a0d12]/8 bg-white px-3.5 py-3"
>
<div className="text-[14px] font-semibold text-[#0a0d12]">{s.name}</div>
<div className="mt-0.5 text-[12.5px] leading-5 text-[#0a0d12]/55">
{s.description}
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,165 @@
"use client";
import { useEffect, useMemo, useRef, type ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
BookOpen,
Bot,
LayoutList,
Server,
Sparkles,
UsersRound,
} from "lucide-react";
import { setApiInstance } from "@multica/core/api";
import { I18nProvider } from "@multica/core/i18n/react";
import { useIssueViewStore } from "@multica/core/issues/stores/view-store";
import { WorkspaceSlugProvider } from "@multica/core/paths";
import { agentTaskSnapshotKeys } from "@multica/core/agents";
import { runtimeKeys } from "@multica/core/runtimes";
import {
workspaceKeys,
workspaceListOptions,
} from "@multica/core/workspace/queries";
import { PortalContainerProvider } from "@multica/ui/lib/portal-container";
import { cn } from "@multica/ui/lib/utils";
import { RESOURCES } from "@multica/views/locales";
import {
NavigationProvider,
type NavigationAdapter,
} from "@multica/views/navigation";
import { ModalRegistry } from "@multica/views/modals/registry";
import { createMockApi } from "./mock-api";
import {
AGENTS,
MEMBERS,
RUNTIMES,
RUNNING_TASKS,
SKILLS,
SQUADS,
WORKSPACE,
} from "./mock-data";
import { DemoErrorBoundary } from "./demo-error-boundary";
setApiInstance(createMockApi());
export type DemoProductTab = "issues" | "runtimes" | "agents" | "squads" | "skills";
const PRODUCT_TABS = [
{ id: "issues", label: "Issues", Icon: LayoutList },
{ id: "runtimes", label: "Runtimes", Icon: Server },
{ id: "agents", label: "Agents", Icon: Bot },
{ id: "squads", label: "Squads", Icon: UsersRound },
{ id: "skills", label: "Skills", Icon: BookOpen },
] as const;
export function DemoProductFrame({
activeTab,
pathname,
children,
className,
}: {
activeTab: DemoProductTab;
pathname: string;
children: ReactNode;
className?: string;
}) {
const portalRef = useRef<HTMLDivElement>(null);
const queryClient = useMemo(() => {
const qc = new QueryClient({
defaultOptions: {
queries: { retry: false, refetchOnWindowFocus: false, staleTime: 30_000 },
mutations: { retry: false },
},
});
seedDemoQueryData(qc);
return qc;
}, []);
const resources = useMemo(() => ({ en: RESOURCES.en }), []);
useEffect(() => {
useIssueViewStore.setState({ statusFilters: [] });
}, []);
const adapter = useMemo<NavigationAdapter>(
() => ({
push: () => {},
replace: () => {},
back: () => {},
pathname,
searchParams: new URLSearchParams(),
getShareableUrl: (p) => p,
}),
[pathname],
);
return (
<DemoErrorBoundary>
<QueryClientProvider client={queryClient}>
<I18nProvider locale="en" resources={resources}>
<NavigationProvider value={adapter}>
<WorkspaceSlugProvider slug="demo">
<PortalContainerProvider container={portalRef}>
<div
className={cn(
"landing-demo flex h-full w-full flex-col bg-background text-foreground",
className,
)}
>
<DemoBrowserBar activeTab={activeTab} />
<div className="min-h-0 flex-1 overflow-hidden">{children}</div>
<div ref={portalRef} />
</div>
<DemoErrorBoundary fallback={null}>
<ModalRegistry />
</DemoErrorBoundary>
</PortalContainerProvider>
</WorkspaceSlugProvider>
</NavigationProvider>
</I18nProvider>
</QueryClientProvider>
</DemoErrorBoundary>
);
}
export function DemoBrowserBar({ activeTab }: { activeTab: DemoProductTab }) {
return (
<div className="flex h-11 shrink-0 items-center gap-3 border-b border-[#0a0d12]/8 bg-[#f7f8fa] px-3.5">
<div className="flex shrink-0 items-center gap-1.5">
<span className="size-2.5 rounded-full bg-[#0a0d12]/12" />
<span className="size-2.5 rounded-full bg-[#0a0d12]/12" />
<span className="size-2.5 rounded-full bg-[#0a0d12]/12" />
</div>
<div className="flex min-w-0 items-center gap-0.5 overflow-hidden">
{PRODUCT_TABS.map(({ id, label, Icon }) => (
<span
key={id}
className={cn(
"inline-flex h-7 shrink-0 items-center gap-1.5 rounded-[8px] px-2.5 text-[13px] font-medium transition-colors",
activeTab === id
? "bg-white text-[#0a0d12] shadow-[0_1px_2px_rgba(10,13,18,0.08)] ring-1 ring-[#0a0d12]/8"
: "text-[#0a0d12]/55",
)}
>
<Icon className="size-3.5" aria-hidden />
{label}
</span>
))}
</div>
<Sparkles
className="ml-auto hidden size-3.5 shrink-0 text-[#0a0d12]/30 sm:block"
aria-hidden
/>
</div>
);
}
function seedDemoQueryData(qc: QueryClient) {
const wsId = WORKSPACE.id;
qc.setQueryData(workspaceListOptions().queryKey, [WORKSPACE]);
qc.setQueryData(workspaceKeys.members(wsId), MEMBERS);
qc.setQueryData(workspaceKeys.agents(wsId), AGENTS);
qc.setQueryData(workspaceKeys.squads(wsId), SQUADS);
qc.setQueryData(workspaceKeys.skills(wsId), SKILLS);
qc.setQueryData(runtimeKeys.list(wsId), RUNTIMES);
qc.setQueryData(agentTaskSnapshotKeys.list(wsId), RUNNING_TASKS);
}

View File

@@ -0,0 +1,171 @@
// A stand-in ApiClient for the landing-page product demo. Returns canned,
// server-shaped responses from mock-data so the real product components run
// with zero backend. Installed via setApiInstance() (the global injection
// seam in @multica/core/api). Any method not implemented here resolves to
// `undefined` via the Proxy fallback, so unanticipated calls never throw.
import type { ApiClient } from "@multica/core/api";
import {
AGENTS,
EXEC_LOG,
ISSUES,
MEMBERS,
PULL_REQUESTS,
RUNTIME_USAGE,
RUNTIME_USAGE_BY_AGENT,
RUNTIME_USAGE_BY_HOUR,
RUNTIMES,
RUNNING_TASKS,
SKILLS,
SQUAD_MEMBER_STATUS,
SQUAD_MEMBERS,
SQUADS,
TIMELINE,
TRANSCRIPT_BY_ISSUE,
WORKSPACE,
createMockIssue,
patchIssue,
} from "./mock-data";
// Every task (snapshot + per-issue execution log), so a transcript request for
// any task id can resolve which issue (and thus which transcript) it belongs to.
const ALL_TASKS = [...RUNNING_TASKS, ...Object.values(EXEC_LOG).flat()];
type AnyParams = Record<string, unknown> | undefined;
const handlers: Record<string, (...args: any[]) => Promise<unknown>> = {
// Keep the demo logged-out: the landing's auth init must not think a user
// is signed in (that would redirect away from the marketing page).
getMe: () => Promise.reject(new Error("demo: unauthenticated")),
getBaseUrl: () => "" as unknown as Promise<unknown>,
listWorkspaces: () => Promise.resolve([WORKSPACE]),
getWorkspace: () => Promise.resolve(WORKSPACE),
listMembers: () => Promise.resolve(MEMBERS),
listAgents: () => Promise.resolve(AGENTS),
listSquads: () => Promise.resolve(SQUADS),
getSquad: (id: string) =>
Promise.resolve(SQUADS.find((s) => s.id === id) ?? SQUADS[0]),
listSquadMembers: (id: string) => Promise.resolve(SQUAD_MEMBERS[id] ?? []),
getSquadMemberStatus: (id: string) =>
Promise.resolve(SQUAD_MEMBER_STATUS[id] ?? { members: [] }),
listRuntimes: () => Promise.resolve(RUNTIMES),
getRuntimeUsage: (runtimeId: string) =>
Promise.resolve(RUNTIME_USAGE.filter((row) => row.runtime_id === runtimeId)),
getRuntimeUsageByAgent: (runtimeId: string) => {
const agentIds = new Set(
AGENTS.filter((agent) => agent.runtime_id === runtimeId).map((agent) => agent.id),
);
return Promise.resolve(
RUNTIME_USAGE_BY_AGENT.filter((row) => agentIds.has(row.agent_id)),
);
},
getRuntimeUsageByHour: () => Promise.resolve(RUNTIME_USAGE_BY_HOUR),
getWorkspaceAgentRunCounts: () =>
Promise.resolve(
AGENTS.map((agent, index) => ({
agent_id: agent.id,
run_count: 12 + index * 4,
})),
),
getWorkspaceAgentActivity30d: () =>
Promise.resolve(
AGENTS.flatMap((agent, agentIndex) =>
Array.from({ length: 7 }, (_, i) => ({
agent_id: agent.id,
bucket_at: new Date(Date.now() - (6 - i) * 24 * 60 * 60 * 1000).toISOString(),
task_count: 1 + ((i + agentIndex) % 4),
failed_count: i === 1 && agentIndex === 1 ? 1 : 0,
})),
),
),
getAgent: (id: string) =>
Promise.resolve(AGENTS.find((a) => a.id === id) ?? AGENTS[0]),
// Agent transcript (live run log) — resolve the task's issue, return its
// transcript with seq / task_id / issue_id stamped on each message.
listTaskMessages: (taskId: string) => {
const task = ALL_TASKS.find((t) => t.id === taskId);
const tmpl = task ? TRANSCRIPT_BY_ISSUE[task.issue_id] : undefined;
if (!task || !tmpl) return Promise.resolve([]);
return Promise.resolve(
tmpl.map((m, i) => ({
...m,
seq: i,
task_id: task.id,
issue_id: task.issue_id,
})),
);
},
listSkills: () => Promise.resolve(SKILLS),
getSkill: (id: string) =>
Promise.resolve(SKILLS.find((skill) => skill.id === id) ?? SKILLS[0]),
getAssigneeFrequency: () => Promise.resolve([]),
listIssues: (params: AnyParams) => {
const status = params?.status as string | undefined;
const issues = status ? ISSUES.filter((i) => i.status === status) : [...ISSUES];
return Promise.resolve({ issues, total: issues.length });
},
listGroupedIssues: () => Promise.resolve({ groups: [] }),
getIssue: (id: string) => {
const issue = ISSUES.find((i) => i.id === id);
return issue
? Promise.resolve(issue)
: Promise.reject(new Error("demo: issue not found"));
},
getChildIssueProgress: () => Promise.resolve({ progress: [] }),
listChildIssues: () => Promise.resolve({ issues: [] }),
listChildrenByParents: () => Promise.resolve({ issues: [] }),
listTimeline: (issueId: string) => Promise.resolve(TIMELINE[issueId] ?? []),
listComments: () => Promise.resolve([]),
listIssueSubscribers: () => Promise.resolve([]),
listAttachments: () => Promise.resolve([]),
getIssueUsage: () =>
Promise.resolve({ total_tokens: 0, total_cost_usd: 0, runs: 0 }),
listProjects: () => Promise.resolve({ projects: [] }),
listLabels: () => Promise.resolve({ labels: [] }),
listLabelsForIssue: () => Promise.resolve({ labels: [] }),
listIssuePullRequests: (issueId: string) =>
Promise.resolve({ pull_requests: PULL_REQUESTS[issueId] ?? [] }),
listTasksByIssue: (issueId: string) =>
Promise.resolve(EXEC_LOG[issueId] ?? []),
listAgentTasks: (agentId: string) =>
Promise.resolve(ALL_TASKS.filter((task) => task.agent_id === agentId)),
getAgentTaskSnapshot: () => Promise.resolve(RUNNING_TASKS),
getActiveTasksForIssue: (issueId: string) =>
Promise.resolve({
tasks: RUNNING_TASKS.filter((t) => t.issue_id === issueId),
}),
// Writes: mutate the in-memory issue so drag-to-change-status persists
// across the refetch that react-query fires after a mutation settles.
updateIssue: (id: string, data: AnyParams) => {
const updated = patchIssue(id, (data ?? {}) as Record<string, never>);
return Promise.resolve(updated ?? ISSUES.find((i) => i.id === id));
},
// Create-issue flow: build a card from the dialog input and add it.
createIssue: (data: AnyParams) =>
Promise.resolve(createMockIssue((data ?? {}) as { title: string })),
quickCreateIssue: (data: AnyParams) => {
const d = (data ?? {}) as { prompt?: string; agent_id?: string };
createMockIssue({
title: (d.prompt || "New agent task").slice(0, 80),
status: "todo",
assignee_type: d.agent_id ? "agent" : undefined,
assignee_id: d.agent_id,
});
return Promise.resolve({ task_id: "task-new" });
},
};
export function createMockApi(): ApiClient {
const target = {} as Record<string, unknown>;
return new Proxy(target, {
get(_t, prop: string) {
if (prop in handlers) return handlers[prop];
// Unknown method → resolve to undefined so no call ever throws.
return () => Promise.resolve(undefined);
},
}) as unknown as ApiClient;
}

View File

@@ -0,0 +1,796 @@
// Mock data for the interactive product demo embedded in the landing hero.
// All ids/shapes follow the real backend contracts so the real product
// components render unchanged. Issues are a MUTABLE module array so demo
// interactions (drag to change status) persist across refetches.
import type {
Agent,
AgentRuntime,
AgentTask,
RuntimeUsage,
RuntimeUsageByAgent,
RuntimeUsageByHour,
Skill,
} from "@multica/core/types/agent";
import type { TaskMessagePayload } from "@multica/core/types/events";
import type { TimelineEntry } from "@multica/core/types/activity";
import type { GitHubPullRequest } from "@multica/core/types/github";
import type { Issue, IssueStatus, IssuePriority } from "@multica/core/types/issue";
import type { MemberWithUser, Workspace } from "@multica/core/types/workspace";
import type {
Squad,
SquadMember,
SquadMemberStatusListResponse,
} from "@multica/core/types/squad";
const NOW = "2026-06-01T09:00:00Z";
export const WORKSPACE = {
id: "ws-demo",
name: "Acme",
slug: "demo",
created_at: NOW,
updated_at: NOW,
} as unknown as Workspace;
export const MEMBERS: MemberWithUser[] = [
{
id: "m-alex",
workspace_id: "ws-demo",
user_id: "u-alex",
role: "admin",
created_at: NOW,
name: "Alex Rivera",
email: "alex@acme.dev",
avatar_url: null,
},
{
id: "m-sam",
workspace_id: "ws-demo",
user_id: "u-sam",
role: "member",
created_at: NOW,
name: "Sam Chen",
email: "sam@acme.dev",
avatar_url: null,
},
] as unknown as MemberWithUser[];
// Each agent carries its provider mark as a data-URI avatar so the real
// ActorAvatar renders the right icon on cards / detail / the working chip.
const svgUri = (svg: string) => `data:image/svg+xml,${encodeURIComponent(svg)}`;
const CLAUDE_SVG = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='-1 -1 18 18'><rect x='-1' y='-1' width='18' height='18' fill='#F4EBE5'/><path fill='#D97757' d='m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z'/></svg>`;
const CODEX_SVG = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='-1 -1 18 18'><rect x='-1' y='-1' width='18' height='18' fill='#111827'/><path fill='#ffffff' d='M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057L5.29 12.09a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872v.024Zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z'/></svg>`;
const GEMINI_SVG = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='-3 -3 30 30'><rect x='-3' y='-3' width='30' height='30' fill='#EFEBF6'/><path fill='#8E75B2' d='M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81'/></svg>`;
const KIMI_SVG = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' fill='#1F1147'/><path fill='#ffffff' d='M7.2 6h2.4v5.1l4.3-5.1h2.9l-4.4 5.1L17 18h-2.9l-3.2-5.2-1.3 1.5V18H7.2V6z'/></svg>`;
export const RUNTIMES: AgentRuntime[] = [
{
id: "rt-local-claude",
workspace_id: "ws-demo",
daemon_id: "daemon-local",
name: "Claude Code (Jiayuan MacBook Pro)",
runtime_mode: "local",
provider: "claude",
launch_header: "claude",
status: "online",
device_info: "Jiayuan MacBook Pro · multica 0.3.14 (Claude Code)",
metadata: { cli_version: "v0.3.14", launched_by: "Desktop" },
owner_id: "u-alex",
visibility: "private",
last_seen_at: new Date().toISOString(),
created_at: NOW,
updated_at: NOW,
},
{
id: "rt-local-codex",
workspace_id: "ws-demo",
daemon_id: "daemon-local",
name: "Codex (Jiayuan MacBook Pro)",
runtime_mode: "local",
provider: "codex",
launch_header: "codex",
status: "online",
device_info: "Jiayuan MacBook Pro · multica 0.3.14 (Codex)",
metadata: { cli_version: "v0.3.14", launched_by: "Desktop" },
owner_id: "u-alex",
visibility: "private",
last_seen_at: new Date().toISOString(),
created_at: NOW,
updated_at: NOW,
},
{
id: "rt-mini-gemini",
workspace_id: "ws-demo",
daemon_id: "daemon-office-mini",
name: "Gemini CLI (Office Mac mini)",
runtime_mode: "local",
provider: "gemini",
launch_header: "gemini",
status: "online",
device_info: "Office Mac mini · multica 0.3.13 (Gemini CLI)",
metadata: { cli_version: "v0.3.13", launched_by: "daemon" },
owner_id: "u-sam",
visibility: "public",
last_seen_at: new Date().toISOString(),
created_at: NOW,
updated_at: NOW,
},
{
id: "rt-cloud-kimi",
workspace_id: "ws-demo",
daemon_id: null,
name: "Kimi Cloud Runtime",
runtime_mode: "cloud",
provider: "kimi",
launch_header: "kimi",
status: "online",
device_info: "us-east-1 · autoscaled cloud node",
metadata: {},
owner_id: "u-alex",
visibility: "public",
last_seen_at: new Date().toISOString(),
created_at: NOW,
updated_at: NOW,
},
] as unknown as AgentRuntime[];
function runtimeIdForAgent(agentId: string): string {
switch (agentId) {
case "a-claude":
return "rt-local-claude";
case "a-codex":
return "rt-local-codex";
case "a-gemini":
return "rt-mini-gemini";
case "a-kimi":
return "rt-cloud-kimi";
default:
return "rt-local-claude";
}
}
export const AGENTS: Agent[] = [
{ id: "a-claude", name: "Claude Code", avatar_url: svgUri(CLAUDE_SVG) },
{ id: "a-codex", name: "Codex", avatar_url: svgUri(CODEX_SVG) },
{ id: "a-gemini", name: "Gemini CLI", avatar_url: svgUri(GEMINI_SVG) },
{ id: "a-kimi", name: "Kimi", avatar_url: svgUri(KIMI_SVG) },
].map(
(a) =>
({
...a,
workspace_id: "ws-demo",
description: "Autonomous coding agent — assign it an issue and it runs.",
instructions: "",
// Fields read by the agent hover-card (AgentProfileCard). Without these
// (esp. `skills`) hovering an agent throws.
skills: [],
runtime_id: runtimeIdForAgent(a.id),
runtime_mode: RUNTIMES.find((r) => r.id === runtimeIdForAgent(a.id))?.runtime_mode ?? "local",
runtime_config: {},
custom_args: [],
owner_id: "u-alex",
owner_type: "member",
visibility: "workspace",
archived_at: null,
created_at: NOW,
updated_at: NOW,
}) as unknown as Agent,
);
export const SQUADS: Squad[] = [
{
id: "squad-api",
workspace_id: "ws-demo",
name: "API Squad",
description: "Coordinates API hardening work across implementation, tests, docs, and review.",
instructions:
"Follow the API hardening playbook: inspect the affected route, split implementation and verification, request a human checkpoint when behavior changes, then open a PR with docs and tests.",
avatar_url: null,
leader_id: "a-claude",
creator_id: "u-alex",
created_at: NOW,
updated_at: NOW,
archived_at: null,
archived_by: null,
member_count: 4,
member_preview: [
{ member_type: "agent", member_id: "a-claude", role: "Lead / planner" },
{ member_type: "agent", member_id: "a-codex", role: "Implementation" },
{ member_type: "agent", member_id: "a-gemini", role: "Tests and docs" },
{ member_type: "member", member_id: "u-alex", role: "Human checkpoint" },
],
},
] as unknown as Squad[];
export const SQUAD_MEMBERS: Record<string, SquadMember[]> = {
"squad-api": [
{
id: "sm-api-claude",
squad_id: "squad-api",
member_type: "agent",
member_id: "a-claude",
role: "Lead / planner",
created_at: NOW,
},
{
id: "sm-api-codex",
squad_id: "squad-api",
member_type: "agent",
member_id: "a-codex",
role: "Implementation",
created_at: NOW,
},
{
id: "sm-api-gemini",
squad_id: "squad-api",
member_type: "agent",
member_id: "a-gemini",
role: "Tests and docs",
created_at: NOW,
},
{
id: "sm-api-alex",
squad_id: "squad-api",
member_type: "member",
member_id: "u-alex",
role: "Human checkpoint",
created_at: NOW,
},
] as unknown as SquadMember[],
};
export const SQUAD_MEMBER_STATUS: Record<string, SquadMemberStatusListResponse> = {
"squad-api": {
members: [
{
member_type: "agent",
member_id: "a-claude",
status: "idle",
active_issues: [],
last_active_at: NOW,
},
{
member_type: "agent",
member_id: "a-codex",
status: "idle",
active_issues: [],
last_active_at: NOW,
},
{
member_type: "agent",
member_id: "a-gemini",
status: "working",
active_issues: [
{
issue_id: "issue-137",
identifier: "MUL-137",
title: "Add rate limiting to the public API",
issue_status: "in_review",
},
],
last_active_at: NOW,
},
{
member_type: "member",
member_id: "u-alex",
status: null,
active_issues: [],
last_active_at: null,
},
],
},
};
type Seed = {
n: number;
title: string;
status: IssueStatus;
priority: IssuePriority;
at: "member" | "agent" | "squad";
aid: string;
due?: string;
};
const SEEDS: Seed[] = [
{ n: 160, title: "Add 2FA / TOTP support", status: "backlog", priority: "medium", at: "member", aid: "u-sam" },
{ n: 162, title: "Investigate p95 latency on /search", status: "backlog", priority: "high", at: "agent", aid: "a-codex" },
{ n: 165, title: "Spike: vector search over issues", status: "backlog", priority: "low", at: "agent", aid: "a-gemini" },
{ n: 168, title: "Audit npm dependencies for CVEs", status: "backlog", priority: "medium", at: "member", aid: "u-alex" },
{ n: 142, title: "Design pricing page v2", status: "todo", priority: "high", at: "member", aid: "u-alex" },
{ n: 151, title: "Add SSO (SAML) to enterprise plan", status: "todo", priority: "low", at: "member", aid: "u-sam" },
{ n: 156, title: "Refactor billing webhooks handler", status: "todo", priority: "medium", at: "agent", aid: "a-kimi" },
{ n: 129, title: "Implement OAuth login flow", status: "in_progress", priority: "high", at: "agent", aid: "a-claude", due: "2026-06-08" },
{ n: 133, title: "Migrate analytics events to new schema", status: "in_progress", priority: "medium", at: "agent", aid: "a-gemini" },
{ n: 137, title: "Add rate limiting to the public API", status: "in_review", priority: "high", at: "squad", aid: "squad-api" },
{ n: 138, title: "Fix flaky checkout E2E test", status: "in_progress", priority: "medium", at: "agent", aid: "a-codex" },
{ n: 147, title: "Polish onboarding empty states", status: "in_progress", priority: "medium", at: "member", aid: "u-alex" },
{ n: 124, title: "Weekly dependency upgrade sweep", status: "in_review", priority: "low", at: "agent", aid: "a-claude" },
{ n: 119, title: "Write API docs for webhooks", status: "in_review", priority: "medium", at: "member", aid: "u-sam" },
{ n: 112, title: "Triage inbound bug reports", status: "done", priority: "low", at: "agent", aid: "a-codex" },
{ n: 108, title: "Ship dark-mode polish", status: "done", priority: "medium", at: "member", aid: "u-alex" },
{ n: 103, title: "Nightly DB backup health check", status: "done", priority: "low", at: "agent", aid: "a-gemini" },
{ n: 170, title: "Enable SSO on the staging environment", status: "blocked", priority: "high", at: "member", aid: "u-sam" },
{ n: 172, title: "Migrate CI to the new build runners", status: "blocked", priority: "medium", at: "agent", aid: "a-gemini" },
];
function makeIssue(seed: Seed, index: number): Issue {
return {
id: `issue-${seed.n}`,
workspace_id: "ws-demo",
number: seed.n,
identifier: `MUL-${seed.n}`,
title: seed.title,
description:
seed.at === "agent"
? `Assigned to an agent. ${seed.title}. The agent picks this up, runs it, and reports back here.`
: seed.at === "squad"
? "API hardening playbook: split implementation, verification, docs, and PR review across the squad."
: `${seed.title}.`,
status: seed.status,
priority: seed.priority,
assignee_type: seed.at,
assignee_id: seed.aid,
creator_type: "member",
creator_id: "u-alex",
parent_issue_id: null,
project_id: null,
position: index,
start_date: null,
due_date: seed.due ?? null,
metadata: {},
created_at: NOW,
updated_at: NOW,
};
}
// Mutable so updateIssue (drag) persists across refetches.
export const ISSUES: Issue[] = SEEDS.map(makeIssue);
// Agents currently working — drives the "N working" header chip + avatar
// stack and the agents-working filter. Each points at an in-progress,
// agent-assigned issue above.
const WORKING: { agent: string; issue: string }[] = [
{ agent: "a-claude", issue: "issue-129" },
{ agent: "a-gemini", issue: "issue-133" },
{ agent: "a-gemini", issue: "issue-137" },
{ agent: "a-codex", issue: "issue-138" },
];
// A few minutes ago, so the "agent is working" timers read naturally (e.g.
// "6m") and tick up live, instead of an absurd elapsed value from a fixed date.
const startedAt = (minsAgo: number) =>
new Date(Date.now() - minsAgo * 60_000).toISOString();
export const RUNNING_TASKS: AgentTask[] = WORKING.map(
({ agent, issue }, i) =>
({
id: `task-${i}`,
agent_id: agent,
runtime_id: runtimeIdForAgent(agent),
issue_id: issue,
status: "running",
priority: 0,
dispatched_at: startedAt(4 + i * 3 + 1),
started_at: startedAt(4 + i * 3),
completed_at: null,
result: null,
error: null,
created_at: NOW,
updated_at: NOW,
}) as unknown as AgentTask,
);
// Create-issue flow: build a fresh issue from the dialog input, drop it at the
// top of its column, and return it so the board shows the new card.
let nextNumber = 200;
export function createMockIssue(
input: Partial<Issue> & { title: string },
): Issue {
const n = nextNumber++;
const now = new Date().toISOString();
const issue: Issue = {
id: `issue-new-${n}`,
workspace_id: "ws-demo",
number: n,
identifier: `MUL-${n}`,
title: input.title || "Untitled issue",
description: input.description ?? null,
status: input.status ?? "todo",
priority: input.priority ?? "none",
assignee_type: input.assignee_type ?? null,
assignee_id: input.assignee_id ?? null,
creator_type: "member",
creator_id: "u-alex",
parent_issue_id: input.parent_issue_id ?? null,
project_id: input.project_id ?? null,
position: -1,
start_date: input.start_date ?? null,
due_date: input.due_date ?? null,
metadata: {},
created_at: now,
updated_at: now,
};
ISSUES.unshift(issue);
return issue;
}
export function patchIssue(id: string, patch: Partial<Issue>): Issue | undefined {
const i = ISSUES.findIndex((x) => x.id === id);
if (i === -1) return undefined;
ISSUES[i] = { ...ISSUES[i]!, ...patch, updated_at: NOW };
return ISSUES[i];
}
// ---------------------------------------------------------------------------
// Richer issue detail: comments/discussion, linked PRs, execution history.
// ---------------------------------------------------------------------------
const mins = (m: number) => new Date(Date.now() - m * 60_000).toISOString();
function comment(
id: string,
actorType: "member" | "agent",
actorId: string,
content: string,
minsAgo: number,
parentId: string | null = null,
reactions: TimelineEntry["reactions"] = [],
): TimelineEntry {
return {
type: "comment",
id,
actor_type: actorType,
actor_id: actorId,
content,
comment_type: "comment",
parent_id: parentId,
reactions,
attachments: [],
created_at: mins(minsAgo),
updated_at: mins(minsAgo),
resolved_at: null,
} as unknown as TimelineEntry;
}
function thumbsUp(commentId: string): NonNullable<TimelineEntry["reactions"]>[number] {
return {
id: `r-${commentId}-1`,
comment_id: commentId,
actor_type: "member",
actor_id: "u-alex",
emoji: "👍",
created_at: mins(1),
};
}
// Comment / activity threads, keyed by issue id. Issues without an entry just
// render an empty Activity feed (the real component handles that).
export const TIMELINE: Record<string, TimelineEntry[]> = {
// One conversation thread (root + nested replies) reads cleaner than four
// separate top-level comments each with its own reply box.
"issue-129": [
comment("c-129-1", "member", "u-alex", "Let's use the existing session store for the token refresh — no new tables.", 38),
comment("c-129-2", "agent", "a-claude", "Done. Implemented the redirect flow and token refresh against the session store, and opened a PR (linked on the right). Working through the edge cases now.", 22, "c-129-1"),
comment("c-129-3", "member", "u-alex", "Make sure we validate the state param to prevent CSRF.", 12, "c-129-1"),
comment("c-129-4", "agent", "a-claude", "Good catch — added state validation + a regression test. Re-running CI.", 5, "c-129-1"),
],
"issue-133": [
comment("c-133-1", "member", "u-sam", "Which events are in scope for v1 of the migration?", 90),
comment("c-133-2", "agent", "a-gemini", "Starting with page_view, signup, and checkout; the long tail follows once the new schema is verified in staging.", 64, "c-133-1"),
],
"issue-138": [
comment("c-138-1", "agent", "a-codex", "Reproduced the flake — it's a race on the cart fixture between the checkout poll and the seed step. Adding an explicit wait + idempotent seed.", 30),
comment("c-138-2", "member", "u-alex", "Nice, that's been haunting CI for weeks.", 18, "c-138-1"),
],
"issue-137": [
comment("c-137-1", "member", "u-alex", "Route this through the API Squad. Use the hardening playbook: implementation, tests, docs, and a PR summary.", 52),
comment("c-137-2", "agent", "a-claude", "I split the work: Codex owns the limiter implementation, Gemini owns tests and docs, and I will review the PR before marking it ready.", 38, "c-137-1"),
comment("c-137-3", "agent", "a-codex", "Draft PR is up with the token bucket middleware. Waiting on route-specific limits before final review.", 24, "c-137-1"),
comment("c-137-4", "member", "u-alex", "Please add per-route overrides and document the Retry-After header.", 16, "c-137-1"),
comment("c-137-5", "agent", "a-gemini", "Done. Added route overrides, docs, and regression coverage. PR is ready for review.", 4, "c-137-1", [thumbsUp("c-137-5")]),
],
"issue-124": [
comment("c-124-1", "agent", "a-claude", "Bumped 14 dependencies, 2 majors held back behind a follow-up. Lockfile + changelog in the PR.", 140),
],
};
// Real pull requests from github.com/multica-ai/multica, linked to issues.
function pr(
id: string,
number: number,
title: string,
state: "open" | "merged",
authorLogin: string,
opts: { mergedMinsAgo?: number; checks?: "passed" | "pending" | "failed"; add?: number; del?: number; files?: number } = {},
): GitHubPullRequest {
return {
id,
workspace_id: "ws-demo",
repo_owner: "multica-ai",
repo_name: "multica",
number,
title,
state,
html_url: `https://github.com/multica-ai/multica/pull/${number}`,
branch: null,
author_login: authorLogin,
author_avatar_url: `https://github.com/${authorLogin}.png`,
merged_at: state === "merged" ? mins(opts.mergedMinsAgo ?? 120) : null,
closed_at: null,
pr_created_at: mins(600),
pr_updated_at: mins(opts.mergedMinsAgo ?? 60),
mergeable_state: state === "open" ? "clean" : null,
checks_conclusion: opts.checks ?? (state === "merged" ? "passed" : "pending"),
checks_passed: opts.checks === "failed" ? 11 : 12,
checks_failed: opts.checks === "failed" ? 1 : 0,
checks_pending: opts.checks === "pending" ? 2 : 0,
additions: opts.add ?? 180,
deletions: opts.del ?? 40,
changed_files: opts.files ?? 7,
} as unknown as GitHubPullRequest;
}
export const PULL_REQUESTS: Record<string, GitHubPullRequest[]> = {
"issue-129": [
pr("pr-1", 3717, "refactor(server/lark): collapse HTTP_ENABLED + WS_ENABLED into the SECRET_KEY gate", "merged", "Bohan-J", { mergedMinsAgo: 80, add: 96, del: 120, files: 5 }),
],
"issue-138": [
pr("pr-2", 3712, "test(migrate): concurrent migration race test using real Postgres (MUL-2956)", "open", "ldnvnbl", { checks: "pending", add: 210, del: 8, files: 3 }),
],
"issue-133": [
pr("pr-3", 3716, "fix(execenv): refresh skills in place on reuse instead of accumulating duplicate dirs", "merged", "Bohan-J", { mergedMinsAgo: 200, add: 64, del: 31, files: 4 }),
],
"issue-137": [
pr("pr-5", 3721, "feat(api): add per-route rate limiting middleware", "open", "multica-bot", { checks: "passed", add: 184, del: 22, files: 6 }),
],
"issue-124": [
pr("pr-4", 3718, "fix(lark): use named import for react-qr-code to survive electron-vite interop", "open", "Bohan-J", { checks: "passed", add: 12, del: 6, files: 1 }),
],
};
// Execution-log history per issue (api.listTasksByIssue) — running task(s)
// plus a couple of completed past runs, so the panel isn't empty.
function task(
id: string,
agentId: string,
issueId: string,
status: AgentTask["status"],
summary: string,
startMinsAgo: number,
endMinsAgo: number | null,
): AgentTask {
return {
id,
agent_id: agentId,
runtime_id: runtimeIdForAgent(agentId),
issue_id: issueId,
status,
priority: 0,
dispatched_at: mins(startMinsAgo + 1),
started_at: mins(startMinsAgo),
completed_at: endMinsAgo == null ? null : mins(endMinsAgo),
result: null,
error: null,
trigger_summary: summary,
created_at: mins(startMinsAgo + 1),
updated_at: mins(endMinsAgo ?? 0),
} as unknown as AgentTask;
}
export const EXEC_LOG: Record<string, AgentTask[]> = {
"issue-129": [
task("t-129-run", "a-claude", "issue-129", "running", "Implement OAuth login flow", 4, null),
task("t-129-1", "a-claude", "issue-129", "completed", "Scaffold OAuth routes", 95, 78),
task("t-129-0", "a-claude", "issue-129", "failed", "Initial run", 180, 150),
],
"issue-133": [
task("t-133-run", "a-gemini", "issue-133", "running", "Migrate analytics events to new schema", 7, null),
task("t-133-1", "a-gemini", "issue-133", "completed", "Draft migration plan", 120, 96),
],
"issue-138": [
task("t-138-run", "a-codex", "issue-138", "running", "Fix flaky checkout E2E test", 10, null),
task("t-138-1", "a-codex", "issue-138", "completed", "Reproduce the flake", 60, 44),
],
"issue-137": [
task("t-137-3", "a-gemini", "issue-137", "running", "Verify docs and final PR summary", 3, null),
task("t-137-2", "a-codex", "issue-137", "completed", "Implement token bucket limiter", 34, 20),
task("t-137-1", "a-claude", "issue-137", "completed", "Plan API hardening playbook", 48, 40),
],
"issue-124": [
task("t-124-1", "a-claude", "issue-124", "completed", "Weekly dependency upgrade sweep", 160, 138),
],
"issue-112": [
task("t-112-1", "a-codex", "issue-112", "completed", "Triage inbound bug reports", 1400, 1380),
],
};
// ---------------------------------------------------------------------------
// Agent transcripts (the "running" agent's live log, opened via the transcript
// button). Keyed by issue; listTaskMessages resolves a task id to its issue.
// ---------------------------------------------------------------------------
type Msg = Omit<TaskMessagePayload, "task_id" | "issue_id" | "seq">;
const think = (content: string): Msg => ({ type: "thinking", content });
const say = (content: string): Msg => ({ type: "text", content });
const tool = (t: string, input: Record<string, unknown>): Msg => ({ type: "tool_use", tool: t, input });
const result = (t: string, output: string): Msg => ({ type: "tool_result", tool: t, output });
export const TRANSCRIPT_BY_ISSUE: Record<string, Msg[]> = {
"issue-129": [
think("Let me see how auth is wired today — the session store and the existing login route — so I can reuse them instead of adding new tables."),
tool("Read", { file_path: "server/internal/auth/session.go" }),
result("Read", "// session.go — NewSession, Refresh, store-backed cookie sessions (138 lines)"),
say("Got it. I'll add an OAuth redirect handler and store the token in the existing session store. Writing it now."),
tool("Edit", { file_path: "server/internal/auth/oauth.go", summary: "add Authorize + Callback handlers, token exchange" }),
result("Edit", "Created server/internal/auth/oauth.go (+96 0)"),
think("Need to validate the `state` param on the callback to prevent CSRF."),
tool("Edit", { file_path: "server/internal/auth/oauth.go", summary: "validate state param against the signed cookie" }),
result("Edit", "Updated server/internal/auth/oauth.go (+14 1)"),
tool("Bash", { command: "go test ./internal/auth/..." }),
result("Bash", "ok \tmultica/internal/auth\t1.82s"),
say("Tests pass. Opened a PR with the redirect flow + token refresh + the state-validation test. Working through the last edge cases (expired token re-auth)."),
],
"issue-133": [
think("Mapping the old analytics events to the new schema. Let me read the current event definitions first."),
tool("Read", { file_path: "packages/analytics/events.ts" }),
result("Read", "// 41 event types; page_view / signup / checkout are the high-volume ones"),
say("I'll migrate page_view, signup, and checkout first, then backfill the long tail once staging looks clean."),
tool("Bash", { command: "pnpm migrate:analytics --events page_view,signup,checkout --dry-run" }),
result("Bash", "dry-run: would migrate 3 event types · 1,243,902 rows · est. 4m12s"),
say("Dry-run looks right. Running it against staging now and will diff the row counts before touching prod."),
],
"issue-138": [
think("Reproducing the flake first — running the checkout E2E in a tight loop to catch it."),
tool("Bash", { command: "pnpm exec playwright test checkout --repeat-each=20" }),
result("Bash", "18 passed, 2 failed — timeout waiting for [data-testid=cart-total]"),
say("It's a race between the checkout poll and the cart-seed step. Adding an explicit wait + making the seed idempotent."),
tool("Edit", { file_path: "e2e/tests/checkout.spec.ts", summary: "await cart-ready, dedupe seed" }),
result("Edit", "Updated e2e/tests/checkout.spec.ts (+9 3)"),
tool("Bash", { command: "pnpm exec playwright test checkout --repeat-each=30" }),
result("Bash", "30 passed (0 flaky)"),
say("30/30 green now. Pushing the fix and linking the PR."),
],
"issue-137": [
think("The implementation is ready. I am checking the docs and route override cases before the squad marks the PR ready."),
tool("Read", { file_path: "server/internal/gateway/ratelimit.go" }),
result("Read", "Token bucket middleware, per-route override table, and Retry-After header handling."),
tool("Bash", { command: "go test ./internal/gateway/..." }),
result("Bash", "ok \tmultica/internal/gateway\t2.14s"),
say("Verified the limiter, docs, and regression tests. PR #3721 is ready for review."),
],
};
// Workspace skills — full server-shaped rows so the real SkillsPage and agent
// profile cards can render source, creator, files, and agent assignments.
export const SKILLS: Skill[] = [
{
id: "skill-oauth-flow",
workspace_id: "ws-demo",
name: "OAuth integration checklist",
description: "Repeat the proven OAuth flow: reuse sessions, validate state, test refresh, and open a PR.",
config: {
origin: {
type: "runtime_local",
runtime_id: "rt-local-claude",
provider: "claude",
source_path: "~/.claude/skills/oauth-integration",
},
},
created_by: "u-alex",
content: "# OAuth integration checklist\n\nReuse the existing session store, validate state, cover refresh, and include PR notes.",
files: [
{
id: "sf-oauth-1",
skill_id: "skill-oauth-flow",
path: "fixtures/oauth-state.test.md",
content: "Regression checklist for OAuth state validation.",
created_at: NOW,
updated_at: NOW,
},
],
created_at: NOW,
updated_at: NOW,
},
{
id: "skill-pr-review",
workspace_id: "ws-demo",
name: "PR Review",
description: "Read a diff, flag bugs and style issues, then leave review comments.",
config: {},
created_by: "u-alex",
content: "# PR Review\n\nInspect changed files, run targeted checks, and summarize blocking issues.",
files: [],
created_at: NOW,
updated_at: NOW,
},
{
id: "skill-bug-repro",
workspace_id: "ws-demo",
name: "Bug Repro",
description: "Turn a bug report into a minimal reproduction and a failing test.",
config: {},
created_by: "u-sam",
content: "# Bug Repro\n\nReproduce, minimize, write failing coverage, then attach logs.",
files: [],
created_at: NOW,
updated_at: NOW,
},
{
id: "skill-dependency-sweep",
workspace_id: "ws-demo",
name: "Dependency Sweep",
description: "Bump dependencies, run the suite, and open a PR with the lockfile diff.",
config: {},
created_by: "u-alex",
content: "# Dependency Sweep\n\nUpgrade in batches, run tests, and document held-back majors.",
files: [],
created_at: NOW,
updated_at: NOW,
},
] as unknown as Skill[];
for (const agent of AGENTS) {
if (agent.id === "a-claude") agent.skills = [SKILLS[0]!, SKILLS[1]!];
if (agent.id === "a-codex") agent.skills = [SKILLS[1]!, SKILLS[2]!];
if (agent.id === "a-gemini") agent.skills = [SKILLS[0]!, SKILLS[3]!];
if (agent.id === "a-kimi") agent.skills = [SKILLS[2]!];
}
const USAGE_DATES = Array.from({ length: 21 }, (_, i) => {
const d = new Date(Date.now() - (20 - i) * 24 * 60 * 60 * 1000);
return d.toISOString().slice(0, 10);
});
export const RUNTIME_USAGE: RuntimeUsage[] = RUNTIMES.flatMap((runtime, runtimeIndex) =>
USAGE_DATES.map((date, dayIndex) => ({
runtime_id: runtime.id,
date,
provider: runtime.provider,
model:
runtime.provider === "codex"
? "gpt-5.4"
: runtime.provider === "gemini"
? "gemini-2.5-pro"
: "claude-sonnet-4-5",
input_tokens: 18_000 + runtimeIndex * 4_200 + dayIndex * 900,
output_tokens: 4_200 + runtimeIndex * 1_100 + dayIndex * 240,
cache_read_tokens: 9_000 + runtimeIndex * 1_600 + dayIndex * 520,
cache_write_tokens: 1_500 + runtimeIndex * 280 + dayIndex * 80,
})),
) as RuntimeUsage[];
export const RUNTIME_USAGE_BY_AGENT: RuntimeUsageByAgent[] = AGENTS.map(
(agent, index) =>
({
agent_id: agent.id,
model:
agent.id === "a-codex"
? "gpt-5.4"
: agent.id === "a-gemini"
? "gemini-2.5-pro"
: "claude-sonnet-4-5",
input_tokens: 82_000 + index * 18_000,
output_tokens: 18_000 + index * 4_200,
cache_read_tokens: 38_000 + index * 7_200,
cache_write_tokens: 6_000 + index * 1_200,
task_count: 8 + index * 3,
}) as RuntimeUsageByAgent,
);
export const RUNTIME_USAGE_BY_HOUR: RuntimeUsageByHour[] = Array.from(
{ length: 12 },
(_, index) =>
({
hour: 8 + index,
model: index % 3 === 0 ? "gpt-5.4" : "claude-sonnet-4-5",
input_tokens: 12_000 + index * 1_300,
output_tokens: 2_400 + index * 280,
cache_read_tokens: 5_200 + index * 420,
cache_write_tokens: 900 + index * 90,
task_count: 1 + (index % 4),
}) as RuntimeUsageByHour,
);

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