mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
d276991c603e6041d20fb5c1e65df8756d35d2ae
98
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c1d826a98 |
chore(metrics): remove database-sampled metrics (#7819)
Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
fe59262188 |
feat(agent): add Huawei Cloud CodeArts support (#6985)
* feat(agent): add Huawei Cloud CodeArts support * fix(migrations): resolve CodeArts prefix collision * fix(agent): address CodeArts review feedback * fix(agent): tighten CodeArts integration scope * fix(agent): resolve CodeArts follow-up review blockers * fix(agent): redact CodeArts launch arguments * docs: sync supported runtime counts * fix(agent): own CodeArts model probe process tree |
||
|
|
64ec7f5416 |
MUL-6737: fix(timeouts): close the two gaps the 2h inactivity budget left open (#7699)
* fix(timeouts): close the two gaps the 2h inactivity budget left open (MUL-6737) Two independent places where a timeout still answered the wrong question. Codex runs a semantic-inactivity timer inside the app-server protocol, and unlike the daemon's watchdog it cannot see that a tool is in flight: a commandExecution that prints nothing for the window trips it anyway. At a fixed 10m ceiling that timer killed a quiet test suite or an output-buffering `docker build` long before the 2h daemon budget meant to protect it, which made "we raised the budget to 2h" false for Codex users. It now derives from the daemon budget, taking the larger of the idle and tool windows because it stands in for both. A tool budget of 0 means "never force-stop during a tool", which this timer cannot express, so it falls back to the idle budget rather than running unbounded; with the watchdog suite disabled entirely Codex keeps its own built-in default, as it always has. The env override and flag are unchanged. Queued tasks expired on a pure wall clock: queued longer than a TTL meant failed. That conflated "nobody is coming for this" with "the queue ahead is long", and only the first is a failure. MUL-6558 was the second — a self-hosted runtime with low concurrency held its own queue past 2h and healthy work died as queued_expired. The TTL knob added then only moved the cliff. Expiry now keys on whether the owning runtime still proves liveness, the same signal FailTasksForOfflineRuntimes already uses for dispatched/running rows, so a daemon going down retires its queued and its in-flight work on one clock instead of two. A busy runtime keeps its backlog for as long as it takes. MULTICA_TASK_QUEUED_TTL is removed with the wall clock it configured. Heartbeat age is read directly instead of gating on runtime.status='online' so a row stuck at 'online' with a dead heartbeat still releases its queue, and rows with no usable runtime binding — runtime_id IS NULL from before migration 251, whose CHECK landed NOT VALID, or a runtime row that is gone — are expired on sight rather than stranded forever by a JOIN that matches nothing. Tests: the queued sweep test now runs both phases against the same rows, asserting the 5h-old task survives while its runtime heartbeats (the MUL-6558 regression) and only expires once that runtime goes silent. Assertions are scoped to the test's own task ids, because a runtime-keyed sweep legitimately also collects queued rows other tests left on the shared fixture runtime. Co-authored-by: multica-agent <[email protected]> * fix(sweeper): give a queued task its own reconnect grace before expiring it Review catch: keying expiry on runtime liveness alone was too aggressive in one direction. Enqueue binds a task to agent.runtime_id without checking that the runtime is up (task.go CreateAgentTask call sites), so for a runtime that has already been dark for longer than the grace, the liveness clause is satisfied the moment a new task is created — assigning an issue to a laptop closed overnight would fail it inside one 30s sweep tick instead of waiting for the machine to come back. That is the opposite of what "reconnect grace" promises. Require the row's own age as well: a queued task now waits a full grace, counted from when it started waiting, before it can be given up on. A heartbeating runtime still never expires its backlog, so the MUL-6558 fix is unchanged. The age bound also stops the sweep re-evaluating the runtime subquery against every queued row on every tick. Also remove the deploy surfaces the previous commit missed — the knob was deleted from the server but helm (values + configmap), the self-host compose file, and the CI-run helm render assertions still advertised it, so operators would have kept setting a variable nothing reads. Same for five orphaned comment lines in .env.example describing the removed variable. Correct the fail-closed comment on the unbound-runtime arms: they are not reachable for "rows predating migration 251" — runtime_id was NOT NULL from migration 004 until 251 replaced it with a CHECK that every insert and update is verified against, and the migration-004 FK is ON DELETE RESTRICT. The arms stay as defence against a future schema change, which is what the comment now says. Tests: a third phase covers the reported case — a task enqueued against an already-dead runtime survives, then expires once it has waited a full grace of its own. Phase 2's expectation is corrected accordingly: the row created at now() no longer expires when its runtime dies, because it has not waited its own grace yet. Co-authored-by: multica-agent <[email protected]> * docs(timeouts): state both queued-expiry conditions and correct the FK claim Review follow-up on |
||
|
|
55fe792483 |
refactor(cloud): use URL-only internal connection (#7566)
* refactor(cloud): use URL-only internal connection * refactor(entitlement): remove Cloud rollout action * refactor(cloud): remove unused client tuning |
||
|
|
baf1bbf340 |
fix(billing): make Cloud seat enforcement automatic (#7542)
Replace the separate Fleet/capacity URLs and seat-capacity switches with the single managed MULTICA_CLOUD_URL connection. Strict seat admission turns on whenever Multica is Cloud-connected and fails closed when the capacity machine token is absent or invalid; the recovery worker starts only for a valid executor so live intents keep their retry budget during credential outages. Split invitation throttling into a non-consuming precheck and post-reservation consumption. Persistent capacity_full and capacity_overcommitted rejections charge only the actor budget; transient failures charge none. Preserve every Cloud/proxy HTTP 429 as retryable with both Retry-After forms, defer outbox rows without spending attempts, and use Cloud's rate-limit scope so a workspace 429 delays only that tenant while global or unscoped 429s stop the current batch. BREAKING: delete/unset MULTICA_FLEET_URL, MULTICA_CLOUD_FLEET_URL, MULTICA_CLOUD_FLEET_TIMEOUT, MULTICA_SUBSCRIPTION_CAPACITY_ENABLED, MULTICA_SUBSCRIPTION_CAPACITY_URL and MULTICA_SUBSCRIPTION_CAPACITY_WORKER_ENABLED before rollout. The startup guard rejects any non-empty value, so MULTICA_SUBSCRIPTION_CAPACITY_ENABLED=false still blocks boot. Configure MULTICA_CLOUD_URL, MULTICA_CLOUD_TIMEOUT and MULTICA_SUBSCRIPTION_CAPACITY_SERVICE_TOKEN instead; entitlements keep their independent MULTICA_ENTITLEMENT_POLICY_* configuration. Companion: multica-ai/multica-cloud#57 |
||
|
|
60a55ec57b |
feat(billing): purchase a seat from the invite flow (#7538)
* feat(billing): purchase seats from full invite flow * fix(billing): harden full-capacity invite flow * fix(billing): explain overcommit with cloud seat occupancy Cloud refuses a reserve on used + reserved, but the overcommit toast filled members_over_capacity_description with the product member count. The most common trigger is a first ledger snapshot whose members alone still fit inside the purchased seats, so the message read "3 members but only 3 purchased seats" and did not explain the rejection. Add occupancy_over_capacity_description, which names occupied seats, purchased seats, members and pending invitations, and feed it from the Cloud summary. members_over_capacity_description stays as-is: the billing tab banner is genuinely member-count driven. |
||
|
|
da7131ab59 |
MUL-6625: enforce prepaid member seat capacity (#7491)
* MUL-6625: enforce prepaid member seat capacity
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): address review findings
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): close retry and worker races
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): bound lock waits during rollback
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): pin the cross-repo confirm settlement budget
Multica Cloud must wait longer than this worker's whole retry budget before it
treats a member row as one its ledger never recorded; adopting a member whose
confirm is still retrying counts one person as two seats, and an over-estimate
is what asks a customer to buy seats they do not owe.
That budget was only implicit here — 10 attempts of exponential backoff plus a
tick and a lock wait each — and Cloud's horizon had been set from a single
retry. Extract the backoff into retryBackoff, expose SettlementBudget, and
assert the worst case (29m15s) stays inside Cloud's 45m horizon, so raising
maxAttempts or the backoff cap fails review here instead of widening the window
in which Cloud cannot quantify a ledger.
Co-authored-by: multica-agent <[email protected]>
* Revert "fix(seat-capacity): pin the cross-repo confirm settlement budget"
This reverts commit
|
||
|
|
8a5a6adbf7 |
feat(agent): add ZeroClaw as a native ACP runtime (MUL-6511) (#7351)
* feat(agent): add ZeroClaw as a generic ACP agent runtime
Add ZeroClaw (a Rust-based, single-binary generic agent CLI) as a
first-party runtime, driven over the standard ACP JSON-RPC transport via
`zeroclaw acp`, mirroring the Dim/QwenPaw/Grok ACP backend shape. Only the
binary, launch args, and blocked-flags policy are ZeroClaw-specific; the
backend reuses the shared hermesClient ACP transport and deliverable
tracker.
No provider-specific quirks (permission-preset injection, minimum-version
gate, or a separate authenticate step) are assumed without evidence — those
were verified against real Dim/Grok binaries and would need the same
hands-on verification against a real ZeroClaw binary before being added
here.
Backend: SupportedTypes/New/launchHeaders wiring, model discovery via
session/new's advertised catalog, daemon probe with
MULTICA_ZEROCLAW_PATH/MODEL overrides, AGENTS.md context delivery,
default-command-name registration, metrics label, and a migration widening
the runtime_profile.protocol_family whitelist.
Frontend: provider union, MCP-config support flag, and a placeholder
provider-logo mark (no official ZeroClaw brand asset exists yet).
Tests mirror the existing ACP backend conventions: fresh-session,
resume-via-session/load, resume-not-found, transient-load-error, blocked
launch args, timeout, and the shared ACP deliverable-boundary regression
suite.
* fix(agent): renumber ZeroClaw's migration to 378 to avoid a same-day collision
Both this branch and a concurrently-implemented Junie runtime branch
independently picked migration 377 off the same origin/main tip.
Renumbering ZeroClaw to 378 up front avoids opening two PRs from the
same author that claim the same migration number.
* fix(agent): align the ZeroClaw ACP backend with the real 0.8.4 runtime
The backend was written against an assumed vanilla ACP handshake and never
checked against a ZeroClaw binary. Driving a real zeroclaw 0.8.4 over stdio
shows five of those assumptions are wrong, and the test fake encoded the same
assumptions, so the suite stayed green against a contract the runtime does not
have.
- Drop the session/set_model request. It is not in ZeroClaw's dispatch table
(-32601) and no ACP handler reads a model param at all, so it could never
succeed; because its failure was fatal, any agent with a model configured
failed before session/prompt was reached. The model comes from the ZeroClaw
agent profile, so opt zeroclaw out of ModelSelectionSupported and stop
reading MULTICA_ZEROCLAW_MODEL rather than leaving the UI offering an
override that is silently ignored.
- Label token usage from initialize's _meta.zeroclaw.defaultModel, the only
model identity the ACP surface exposes. session/new returns just
{sessionId, workspaceDir}, so the two extractACPCurrentModelID reads were
dead code and the model-catalog discovery they fed could only ever return an
empty fallback while paying an ACP subprocess per catalog read. Return an
empty catalog directly, as qwenpaw/mcode do.
- Resume through session/resume instead of session/load. Both restore the
transcript into the agent, but load also replays every retained message back
as session/update notifications, which fed the previous answer into this
turn's deliverable. Add the acceptNotification turn gate every other ACP
backend carries, so out-of-turn chunks are dropped even if a runtime flushes
them early (#1997).
- Send agentAlias on session/new when the operator names one. ZeroClaw
auto-selects an agent only when its config holds exactly one entry, so
multi-agent installs failed with -32602 and had no way out: `zeroclaw acp`
has no --agent flag, and passing one kills the process at argument parsing.
The alias is consumed from custom_args and travels as a param; it is omitted
when unset, because a hardcoded guess breaks the single-agent case that
works today. The refusal now names both remedies.
- Stop advertising MCP support. No ZeroClaw handler reads params.mcpServers;
acp_enable_mcp gates that agent's own mcp_bundles, not the client list, so
MCP is operator-side only. Hide the MCP tab and warn on a stale mcp_config
instead of failing the task.
Also teach isACPSessionNotFound about ZeroClaw's custom -32000
SESSION_NOT_FOUND, without which a stale session id was retried forever
instead of restarting fresh. The wording check still decides, so a transient
error under the same code stays non-rejecting.
Tests now encode the observed handshake — _meta default model, session/new
without a catalog, -32000 for an unknown session, -32601 for set_model — and
cover the replayed-history, alias and MCP paths.
* fix(agent): make ZeroClaw integration merge-safe
Co-authored-by: multica-agent <[email protected]>
* fix(agent): tighten ZeroClaw ACP fallbacks
Co-authored-by: multica-agent <[email protected]>
* fix(agent): preserve ZeroClaw retry semantics
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
8d2da6b8db |
MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step (#7325)
* MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step Local development had six entry points, thirty-four make targets and no way to see or delete what they created. `make up` / `status` / `list` / `down` / `destroy` / `gc` / `env-exec` replace that with one set of verbs, shared by humans and agents, with `C=api,web,daemon,desktop` selecting components. Three defects made the old flow untrustworthy rather than merely verbose. 1. The database the tooling created was not the one the application reached. `ensure-postgres.sh` creates through `docker exec`; when a native PostgreSQL owns 5432 the container never binds the host port, so the create lands in a server the backend never talks to. It printed "✓ PostgreSQL ready" and the next step died with `SQLSTATE 3D000`. On the machine this was written on the two servers had already diverged: 94 same-named databases, 69 more in the container with no counterpart. `up` now creates through DATABASE_URL and treats a successful `migrate up` — which pings that same string — as the only proof; failure diagnoses the port owner instead of reporting a missing database. 2. Identity was computed, not allocated. Ports, database names and CLI profiles all came from `cksum($PWD) % 1000`, and a collision was silent: the loser fails to bind while the winner keeps answering 200. Allocation now happens under a lock, probing from the same path hash so a checkout keeps its familiar numbers, and requires the slot to be both unregistered and free of a live listener. The manifest in ~/.multica/dev/envs/<name>/ is what every later command reads. 3. Creating an environment left no record of how to destroy it, so there was no destroy — only archaeology. `down` stops processes and keeps the data; `destroy` consumes the manifest: processes, database, profile, slot. `list` makes leaks visible and `gc` collects environments whose checkout is gone or whose TTL passed. /health now reports pid, commit and started_at. A 200 alone proves something is listening, which is exactly what let a failed restart keep serving the previous build; `up` compares started_at against its own launch and refuses an answer that predates it. Two constraints are enforced rather than documented: the daemon is started from a built server/bin/multica, never `go run` (the daemon re-execs its own path for every task, and `go run` deletes that binary when the launcher exits), and `C=daemon` under a .multica/daemon_task_context.json marker stops with that explanation up front instead of spending a login on a refusal. scripts/dev-env.test.sh covers the registry verbs against a throwaway MULTICA_DEV_HOME with no services running, and pins the regression that made `down` exit 1 after reporting success: on bash 3.2 a command substitution whose function ends in a failing command aborts the script under `set -e`, and "no process is listening" is that function's normal answer. `make dev` is unchanged and still runs in the foreground. Co-authored-by: multica-agent <[email protected]> * fix(dev): close local environment lifecycle gaps Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5a80f802d3 |
MUL-6558 feat(server): make task queue expiry configurable via MULTICA_TASK_QUEUED_TTL (#7418)
Lets self-hosted deployments raise the queued-task expiry window above the built-in 2h default, so low-concurrency runtimes stop losing legitimately-pending work to queued_expired. Default behavior is unchanged. Covers .env.example, Compose, Helm values/ConfigMap, and the behavior docs in all four locales. Co-authored-by: Martin Montes <[email protected]> |
||
|
|
53e15b35c6 |
MUL-6502: fix startup recovery during PostgreSQL outages (#7372)
* fix(startup): retry transient database outages Co-authored-by: multica-agent <[email protected]> * fix(startup): address database retry review Co-authored-by: multica-agent <[email protected]> * test(startup): address retry review nits Co-authored-by: multica-agent <[email protected]> * fix(startup): preserve native pgx connect timeout Co-authored-by: multica-agent <[email protected]> * fix(startup): apply timeout fallback to services Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
814ee0cbf7 |
docs: remove six finished implementation records (#7258)
* docs: remove the two implementation docs under docs/ The database index audit and the Telegram channel review were one-off implementation records for MUL-6108 and the Telegram contribution; both are finished work and neither is referenced by the product docs. Drops the now-dangling review pointer from the Telegram handoff notes. docs/assets/ stays — the READMEs load the logos from it. Co-authored-by: multica-agent <[email protected]> * docs: remove the Telegram integration handoff notes HANDOFF-telegram.md was the implementation record for the Telegram channel contribution and is referenced by nothing. Operator-facing setup lives in the localized Telegram guides under apps/docs/content/docs/. Co-authored-by: multica-agent <[email protected]> * docs: remove three finished implementation leftovers - apps/mobile/docs/markdown-renderer-research.md: 2026-05 renderer selection research and incident log. Its conclusions already live in markdown-rendering-adr.md and in the MD_FONT/MD_LINE comments; the HIG calibration source is folded into markdown-style.ts so the numbers keep their rationale. - packages/views/locales/glossary.md: a redirect stub. The content moved to the docs-site conventions pages, which now say the file is gone instead of claiming a stub still points at them. - scripts/audit-redundant-indexes.sql: the MUL-6108 one-off audit query, orphaned once its report was deleted — no caller in Makefile, check.sh or CI. All pointers into the deleted files are updated; nothing dangles. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
1ea40db1c4 |
fix(build): honor GOOS as a Make variable when naming build outputs (#7309)
#7271 derives the Windows `.exe` suffix from a parse-time `go env GOOS`, which cannot see GOOS passed as a Make variable: `make build GOOS=windows` still emits PE binaries named `bin/server`, `bin/multica`, `bin/migrate` — the exact artifacts #7255 reported as unable to re-exec themselves. The top-level `export` puts the Make variable in the recipe's environment, so `go build` targets Windows while the name says otherwise. Reading `$(GOOS)` first covers both invocation forms, and making the assignment target-specific keeps the toolchain probe on `build` alone: a global assignment is expanded on every make invocation — `export` expands even a recursive one — so `make help` / `make clean` printed `go: Command not found` on checkouts without Go. Adds scripts/makefile-build.test.sh to pin both invocation forms, the non-Windows names, and the no-Go case, wired into the backend CI job that already runs the other Makefile-adjacent shell tests. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e1be1625cb |
MUL-5959: feat(agent): add Dim (DimCode) ACP runtime (#6675)
* feat(agent): add Dim (DimCode) ACP runtime Add Dim (dimcode, the `dim` CLI) as a first-party agent runtime, driven over the ACP (Agent Client Protocol) transport via `dim acp`. Key integration points: - New dim backend (pkg/agent/dim.go): spawns `dim acp`, performs the ACP initialize/session/new handshake, and raises the runtime's hardcoded read-only permission preset to full-access (plus agent mode) via session/set_config_option before the first prompt — without it every file write is denied by a capability rule. Model override uses session/set_model; the model catalog is read from session/new. - Session resume is intentionally skipped: Dim binds sessions to the creating process, so a later process's session/load is rejected with "held by another process" even after a clean session/close. The backend always starts a fresh session and reports ResumeRejected so the daemon classifies the run correctly. A best-effort session/close keeps Dim's own session list free of orphaned entries. - Registration: SupportedTypes whitelist, New() factory, launch header, daemon probe (MULTICA_DIM_PATH / MULTICA_DIM_MODEL), default agent command name, protocol_family CHECK migration (255), model discovery, MCP config support, metrics label, and runtime display. - Docs/UI: provider logo, landing i18n (en/zh/ja/ko), providers and install-agent-runtime docs, README, CLI_AND_DAEMON, SELF_HOSTING. - Tests: dim backend unit tests (fresh session + resume behavior), shared ACP deliverable case, logo and MCP-support tests. * feat(views): use official DimCode app icon for Dim provider logo Replace the placeholder inline SVG with the official DimCode desktop client app icon (from /opt/DimAgent/resources/build/icon.png, resized to 128px), imported as a static asset the same way the Qwen Code mark is handled. * test(agent): add real dim ACP smoke tests (agentintegration) Two gated tests behind MULTICA_RUN_REAL_AGENT_SMOKE=1: - TestDimRealACPSmoke: full end-to-end against real `dim acp` — initialize, session/new, set_config_option (permission/mode), prompt, and assert completed output. - TestDimRealResumeRejected: pass a fake ResumeSessionID, assert the backend starts a fresh session (no session/load), and reports ResumeRejected=true so the daemon classifies the run correctly. Verified against dimcode 0.3.2 and 0.3.8. * fix: renumber dim migration 265 → 271 (upstream added 265-270) * fix: renumber dim migration to 272 (upstream added 271) * fix(agent): dim cross-run resume via session/load + review fixes Address review feedback on the Dim ACP runtime: - #1 Deliver runtime brief to Dim: add dim to the AGENTS.md provider list (verified dim 0.3.8+ reads AGENTS.md from the session cwd). - #2 Cross-run session continuity: dim 0.3.10+ releases its per-process session lock ~5s after the owning process exits, so resume now goes through the standard ACP session/load (same as traecli/kiro/grok) instead of always starting fresh. A loaded session retains its permission/mode, so set_config_option runs only on fresh sessions. Add a two-run regression (TestDimRealCrossRunResume) proving run B recalls context established only in run A. - #3 Process lifecycle races: the deferred cleanup now cancels the run context before cmd.Wait (a child ignoring stdin EOF no longer hangs Result delivery) with a bounded force-kill fallback; the success path waits for the final prompt notification with a grace window instead of a non-blocking read that could miss the last message. - #4 Isolate model discovery by executable path (discoveryCacheKey). - #6 Real smoke test now writes a sentinel file to prove full-access is effective; a best-effort session/close is sent before teardown. - #7 gofmt. * test(agent): add dim process-lifecycle regressions (review #3) - TestDimCleanupKillsHangingChild: a child that ignores stdin EOF and SIGTERM after an early failure is force-killed within dimProcessWaitTimeout, so Result still closes. - TestDimPromptMissingNotificationStillCompletes: when session/prompt returns without stopReason (onPromptDone never fires), the bounded final-notification wait falls through and Result completes instead of hanging. * fix(migrations): renumber dim runtime_profile migration 272 → 273 (upstream added 272) * fix(agent): review round 2 — process tree, quiescence, fail-closed, version check Address all remaining blockers from the second review: - #1 Require dim >= 0.3.10 (version check at initialize); bounded retry on session/load when the lock is not yet released instead of silently starting fresh; integration test no longer sleeps before resume. - #2 Process-tree cleanup: configureProcessGroup + signalProcessGroup + waitProcessGroupGone; cmd.Cancel returns nil so we own all signalling; regression asserts the descendant PID is gone. - #3 Notification quiescence: onActivity + waitForACPNotificationQuiescence (same as hermes); Dim equivalent of the late-final-notification test. - #4 Permission fail-closed: set_config_option runs on BOTH fresh and resumed sessions; on config failure, session/close is sent before returning so a partially configured session is not resumed; regression. - #5 Migration renumbered 273 → 274 (upstream added 273). - #6 AGENTS.md mapping regression, two-executable cache isolation test, session/close unit assertion, smoke test now executes a command. - #7 README.md + CLI_AND_DAEMON.md updated with 22 CLIs including Dim. * chore: remove temporary review actions doc (not for PR) * fix(migrations): renumber dim migration 274 → 310 (upstream added 274-309) * review: fix retry break scope, dedup isACPSessionNotFound comment, update stale notes * fix(agent): pass *exec.Cmd to signalProcessGroup/waitProcessGroupGone (upstream signature change) * fix(agent): close session on set_model failure + resume regression test Audit-found gaps from self-review: - set_model failure now sends session/close before returning (same as set_config_option failure) — reviewer #4 said 'permission, mode, or model'. - Add TestDimConfigFailThenResumeReestablishes: run A config fails → session/close sent → run B resumes and re-applies set_config_option (fail-closed), completing successfully. - Fix two stale comments that contradicted the code (config block now runs on both fresh and resumed sessions). * fix(agent): add WaitDelay, use labeled break in retry loop Self-audit improvements: - Add cmd.WaitDelay=10s for consistency with claude.go (hard backstop if a process somehow survives SIGKILL). - Use labeled break (break loadRetry) so runCtx cancellation during the retry delay exits the for loop directly, not just the select. - Update PR description: migration 273→310, set_config_option now re-applied on both fresh and resumed sessions. * fix(migrations): renumber dim migration 310 → 313 (upstream added 310-312) * fix(migrations): remove stale 313 dim migration (replaced by 314 after upstream added dsh at 313) * fix: DSH compatibility + thinking levels + reviewer round 3 Systematic fix of all 39 items from the compatibility gap analysis: Rebase + migration: - Rebase to latest upstream/main (resolves all DSH conflicts) - Migration 314_runtime_profile_add_dim (whitelist includes both dsh + dim) - Remove stale 313 dim migration DSH + Dim coexistence (provider lists, counts, docs): - SupportedTypes, config.go, metrics labels: both dsh + dim - i18n (4 files): count 23, lists include DSH + Dim + Oh-My-Pi - README.md/README.zh.md: count 23 - CLI_AND_DAEMON.md: 23-row table - environment-variables.mdx (4 langs): MULTICA_DIM_PATH/MODEL callout - display.ts, mcp-support.ts, types/agent.ts, provider-logo.tsx: both dsh+dim Thinking levels (was MISSING — dim supports thought_level): - dim.go: call applyACPEffortOption after set_model - thinking.go: add dim to acpCatalogThinkingProviders - dim.go: retain sessionResult for effort option Reviewer round 3: - Version check fail-closed: empty/malformed → reject (not allow) - Cache test through ListModels call site with fake executables - set_model failure regression test - Test companions: sidecar_manifest_test, runtime_config_test add dim Code quality: - agent.go: fix unreachable duplicate return (rebase artifact) - dim.go: labeled break in retry loop, WaitDelay, session/close on set_model fail * fix: deliverable test fake version + provider-logo rebase artifact * fix(migrations): renumber dim migration 314 → 315 (upstream added 314_workspace_mcp_config) * fix(migrations): renumber dim migration 315 → 319 (upstream added 315-318) * fix: Windows process tree, MinVersions, retry tests, migration 327 Address all 5 remaining blockers from review round 4: 1. Windows process-tree ownership: use startOwnedProcessTree instead of cmd.Start(), releaseProcessGroup in cleanup defer. This attaches the child to a Job Object on Windows (no-op on Unix), ensuring descendants are captured and terminated. 2. Migration renumbered 319 → 327 (upstream added 319-326). 3. Add dim: 0.3.10 to MinVersions in version.go so the daemon registers old Dim binaries as offline and refuses triggers (defense in depth on top of the ACP agentInfo.version check in dim.go). 4. Session-lock retry contract tests: success after bounded retries, exhaustion without falling through to session/new. Fake script supports DIM_LOAD_HELD_N (held for N calls then succeed) and DIM_LOAD_HELD_ALWAYS (always held). 5. README.md:206 20→23 runtimes. PR description updated. * test: add missing reviewer-requested regressions (round 4) - Windows Job Object regression: dim_windows_test.go proves the Dim backend captures and terminates descendants via startOwnedProcessTree (Windows-only build tag; companion to TestStartOwnedProcessTree). - MinVersions registration: add dim test cases to TestCheckMinVersion (0.3.10 ok, 0.3.9 rejected, invalid rejected). - Retry cancellation: TestDimSessionLoadRetryCancelled cancels the context during the retry delay, asserts no session/new fallback. * fix: sync upstream Command signature changes + restore dim additions Upstream changed ListModels/discoverXxxModels/detectCLIVersion to accept Command (struct with Path+Prefix) instead of string. The previous rebase kept our old-signature versions, causing widespread compile failures. Restored all affected files from upstream, then re-applied dim-specific additions: - agent.go: dim in SupportedTypes, New(), launchHeaders - hermes.go: isACPHeldByProcess + -32002 in isACPSessionNotFound - thinking.go: dim in acpCatalogThinkingProviders - models.go: dim case in ListModels + discoverDimModels - version.go: dim in MinVersions - All dim test files preserved (dim_test.go, dim_integration_test.go, dim_windows_test.go, models_test.go dim cache test) * fix: rebase to latest main, migration 327→341, resolve DetectVersion conflict * fix: remove unused os/exec import in dim_windows_test.go * fix(migrations): renumber dim migration 341 → 342 (upstream added 341) * fix: use Command.exec instead of exec.CommandContext (upstream GH #7046) Upstream's TestOnlyLaunchGoSpawnsRuntimeProcesses requires all backends to build processes through Command.exec (launch.go), so a custom runtime's fixed_args are carried into the subprocess. Dim was the only backend still using exec.CommandContext directly. * fix: launch prefix policy + remove empty test files + deterministic retry tests Address all 4 findings from review round 5: 1. Add "dim": dimBlockedArgs to launchPrefixBlockedArgs so protocol-breaking flags (--help/--auth-setup/--remote) are filtered from fixed_args. 2. Remove 77 zero-byte *_test.go files accidentally added at repo root. 3. Make retry tests deterministic: TestDimSessionLoadRetryCancelled waits for the first session/load request before cancelling (no fixed sleep); TestDimSessionLoadRetryExhausted asserts exactly 4 load attempts. 4. PR description will be updated separately. * fix: rebase to latest main, migration 342→343 (upstream added mcode at 342) * chore: trigger CI * chore: refresh PR * fix: restore mcode that was lost during merge conflict resolution Merge took 'ours' side which predated mcode. Restore mcode in: - SupportedTypes, New() factory, launchHeaders - agent_supported_types_test.go want map - metrics/labels.go - config.go defaultAgentCommandNames - migration 343 up/down whitelists * fix: restore mcode probe in agents_probe.go + agent-cli-command-names.txt * fix: restore all remaining mcode references lost during merge Files fixed: - README.md: mcode row in runtimes table - mcp-support.test.ts: mcode assertion - config.go: mcode in Agents comment + error message - runtime_config.go: mcode in AGENTS.md case - version.go: mcode in MinVersions - version_test.go: mcode test cases Verified: no file has fewer mcode references than upstream. * fix: restore mcode in environment-variables docs + CLI_AND_DAEMON ACP list - environment-variables.{mdx,ja,ko,zh}: restored 'MiniMax Code' in the QwenPaw model-variable sentence - CLI_AND_DAEMON.md: added MiniMax Code to ACP-family list + loadSession fallback description Verified: every changed file now has >= mcode references vs upstream. * fix(migrations): renumber dim migration 343 → 344 (upstream added 343) * fix(migrations): renumber dim migration 344 → 348 (upstream added 344-347 plugin migrations) * fix(migrations): update down migration comment 344 → 348 * chore: retrigger CI (flaky TestOpenclawDiscoveryCacheConcurrentPreparations — upstream test, passes locally x5) * fix(migrations): renumber dim migration 348 → 352 (upstream added 348-351) * fix: Dim launch-prefix regression test + version.go comment repair (review #6) - Add TestDimLaunchPrefixFiltersBlockedFlags: proves allowed prefix reaches command before acp, and --help/--auth-setup/--remote/-h are stripped from Dim's launch prefix. - Add dim to TestLaunchPrefixReachesACPFamilies family list. - Repair version.go: dim's cross-run session/load comment was appended to mcode entry during a rebase conflict; restore two independent accurate comments. * fix(migrations): update down migration comment 348 → 352 * chore: retrigger CI (backend-tests stuck 50+ min) * fix(migrations): renumber dim migration 352 → 362 (upstream added 352-361) * fix: use logAgentCommand for dim argv logging (upstream redaction requirement) Upstream's TestOnlyLaunchGoLogsAgentCommandArgs enforces that all runtime argv logging goes through Config.logAgentCommand so sensitive args are redacted. dim.go was still using Logger.Info directly. * fix(migrations): renumber dim migration to 370 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
658b0b7d9f |
MUL-6406: fix(auth): fail fast in production on insecure JWT secret defaults (#7212)
* fix(auth): fail fast in production on insecure JWT secret defaults * chore(deploy): require a strong JWT_SECRET in compose and docs * fix(ci): make selfhost config test pass with required JWT_SECRET The compose file's JWT_SECRET:? error message contained a colon-space sequence that YAML parsed as a mapping indicator, breaking docker compose config. Quote the openssl hint instead. .env.example now ships an empty JWT_SECRET, so the test's docker compose config calls fail interpolation; supply a throwaway secret from the environment, which Compose lets outrank the env file. * fix(ci): seed JWT_SECRET into the recipe .env in selfhost config test The Makefile includes .env and bare-exports every variable to the recipe environment, so the make-driven recipe cases clobbered the test's JWT_SECRET export with the empty value .env.example ships. The stub's `docker compose config` then failed interpolation with stderr discarded, produced empty JSON, and crashed the node parser. Seed the throwaway secret into the recipe .env, which both make and Compose load. * docs(env): use a CSPRNG for the Windows JWT_SECRET generation hint Get-Random is not cryptographically secure, so the Windows hint could leave self-hosters with a predictable signing key. Point at RandomNumberGenerator instead, per review. * docs(selfhost): fix stale JWT secret comments Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
801c671973 |
chore(assets): convert bitmaps to WebP, dedupe, and gate image size (MUL-6352) (#7149)
* chore(assets): convert web and docs bitmaps to WebP The repo carried 21.7MB of PNG/JPG and zero modern formats. next/image already serves visitors AVIF/WebP, but the raw masters are still paid for on every clone, every Docker build context, and every deploy upload. Re-encode with cwebp and cap the source width at the largest size any layout can request: 1920px for the decorative landing backgrounds (they render behind content under object-cover), 2640px for the landing hero (2x its 1320px slot), 1600px for screenshots (2x the ~720px docs and use-case column). EXIF goes with it — the four feature backgrounds were 144dpi print exports. Encoder choice is per image rather than uniform. Most compress best as lossy; two of the docs screenshots are flat palettized PNGs that encode smaller with `-lossless -z 9`, so they use that instead. Screenshots are q80 to keep UI text crisp, decorative art q72. apps/web/public 10.70MB -> 1.57MB apps/docs/public 7.47MB -> 2.44MB Verified: 1:1 crops against losslessly-downscaled references show no visible difference; all 266 image references in the tree resolve; docs prerenders all 177 pages and the landing/use-case pages render with no broken images. Co-authored-by: multica-agent <[email protected]> * chore(assets): drop the duplicated README hero image docs/assets/hero-board.png was byte-for-byte identical to the docs site's workspace-overview screenshot — 633KB stored twice. Only the two READMEs referenced the copy, so point them at the canonical file instead. Co-authored-by: multica-agent <[email protected]> * chore(assets): losslessly recompress app icons oxipng -o max --strip safe over the app icon PNGs: -640KB with pixels untouched (verified by comparing lossless-WebP encodes of the before and after of every file — all 16 identical). Deliberately not switched to WebP and not regenerated: the prerendered sizes under apps/desktop/build/icons/ exist because electron-builder's auto-derivation silently shipped only the 1024x1024 source in the v0.2.31 .deb (#2515), and the PWA manifest and .icns/.ico containers want PNG. Filenames, dimensions, and the electron-builder config are unchanged, so packaging behaves exactly as before. Co-authored-by: multica-agent <[email protected]> * ci: gate pull requests on a 300KB image budget Keeps the previous three commits from being undone one screenshot at a time. A bitmap added — or grown — past 300KB fails the check until the PR description carries an `Oversized image exemption:` line saying why. Shrinking an oversized file always passes: the check compares against the size at the base ref, not just the absolute size. Path-filtered to PRs that touch a bitmap, so the full-history checkout it needs to diff against the base is only paid for when it is relevant. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3460f9aeaf |
MUL-6342: enforce entitlement-backed autopilot quotas (#7194)
* feat(autopilot): enforce entitlement run quotas Co-authored-by: multica-agent <[email protected]> * chore(deploy): expose entitlement policy config (#7195) Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(autopilot): address quota review findings Co-authored-by: multica-agent <[email protected]> * fix(autopilot): finish quota review nits Co-authored-by: multica-agent <[email protected]> * refactor(autopilot): simplify quota schema Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
24d8c521a6 |
MUL-6364: make the LLM client retry budget explicit, wired, and validated (#7201)
* feat(llm): make the LLM client retry budget explicit, wired, and validated Config.MaxRetries was a bare int that nothing outside tests ever set. The production wiring in handler.New never passed it and no MULTICA_LLM_* env var fed it, so the field was configurable in name only and every deployment ran on the SDK default. Its semantics were also inverted. Because the zero value and an explicit 0 are the same int, "0" fell into the unset branch and silently produced 2 retries, while a negative — clamped to 0 to dodge the panic in option.WithMaxRetries — was the only value that actually disabled retries. - MaxRetries becomes *int so unset, 0 (disabled) and N (exact) stay distinct. New always passes the budget to the SDK explicitly, including the default, so the enforced policy and the reported one cannot drift apart. - Add MULTICA_LLM_MAX_RETRIES as the single configuration source. Non-numeric, negative and above-ceiling values fail the boot instead of being corrected into something that looks configured. The ceiling of 5 is a latency budget: backoff reaches ~21s at 6 retries while the internal callers of this layer time out after 8s and 20s. - Report the effective policy at startup via Client.RetryBudget, whose fields are counts and a fixed enum so the line cannot carry a key or gateway URL. - Document the four states in .env.example and the environment-variable docs, and record what the budget does not cover: the GenerateJSON parameter negotiation and a stream that breaks after ChatStream returns. Tests pin every state by upstream request count, so the unset-versus-zero collapse cannot come back unnoticed. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> * fix(llm): reject invalid retry budgets at the client boundary and wire self-host Review found the retry budget still reachable in two invalid shapes, and never reachable at all on the main self-hosted deployment path. - docker-compose.selfhost.yml declares the backend environment as an explicit allowlist and mapped none of the MULTICA_LLM_* variables, so every value .env.example and the docs told a self-hoster to set was dropped before the container: the LLM layer read as unconfigured no matter what they wrote. Map all four, and assert propagation in scripts/selfhost-config.test.sh, plus a drift guard so a future MULTICA_LLM_* documented without a mapping fails the script instead of silently going nowhere. - Config.MaxRetries took a *int, so a negative still reached llm.New, which warned and used the default instead. Warning is not silence, but substituting a different budget is still the implicit correction #7154 asked to remove. It now takes *RetryOverride, whose field is unexported and whose only constructor rejects negatives — the invalid state is unrepresentable, so New has no correction branch left to take. - The budget is a ceiling, not a quota: only retryable failures consume it, and a success or the caller's deadline can end a call sooner. Say "at most N" rather than "exactly N" in the field docs, .env.example and all four environment-variable docs. The negative-value test now asserts rejection instead of pinning the fallback as contract. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
41ddf6c0a1 |
refactor(frontend): drop dead UI components and phantom deps, add knip (MUL-6353) (#7147)
* refactor(frontend): drop dead UI components and phantom deps, add knip `.npmrc` sets shamefully-hoist=true, which hides both halves of dependency drift: an undeclared import still resolves, and an obsolete declaration still installs. Neither build, typecheck, nor lint reads the import graph, so package.json stopped being the source of truth for what is actually used. Removed 16 zero-reference components from packages/ui (1,907 lines, all `pnpm ui:add` output that never got wired up) plus mention-hover-card.tsx, which was written but never imported — member and agent mentions render as a plain `.mention` span. The comment in issue-hover-card.tsx claiming otherwise was stale; corrected to match rich-content.tsx. Deleting pagination.tsx orphaned its two i18n keys, dropped from all four locales and from the `ui` namespace type. Dependency declarations removed where nothing imports them: - vaul, embla-carousel-react — only the deleted drawer/carousel used them - date-fns, @tanstack/react-query-devtools — zero imports repo-wide - motion, tw-animate-css from packages/ui — real users are views/desktop/web - 44 of apps/web's 58 declarations, including all 15 @tiptap/* entries. One had already drifted: apps/web pinned tailwind-merge ^3.5.0 against the catalog's ^3.4.0, a second source of truth for the same package. Added @types/hast to packages/views, which imports it directly. knip enforces this going forward, scoped to files/dependencies/unlisted — `exports` is unusable here because ui and views are consumed through per-file exports maps. The CI step runs in warn mode (continue-on-error) and reports 9 pre-existing findings left out of this cleanup; clear those before promoting it to blocking. Verified: build, typecheck, lint green across web/desktop/ui/core/views. 6,503 tests pass; the 9 failures are pre-existing on a clean tree (a local jsdom localStorage issue). Web dev server boots and serves /login 200. Co-authored-by: multica-agent <[email protected]> * fix(ci): cover the packages/ui blind spot knip cannot see Review caught that knip provided no regression protection for the exact thing the preceding cleanup was about. knip derives entry points from a workspace's package.json `exports` — graph/build.js calls `getEntrySpecifiersFromManifest` unconditionally, with no config flag to opt out — and packages/ui exports four wildcards. Each expands to a glob over its whole directory, so every file under components/ui, components/common, markdown, and hooks registers as an entry point and can never be reported unused. A zero-reference probe component confirmed it: knip's output was byte-identical with and without it. `entry: []`, negated `entry` patterns, `--production`, and `--strict` were all tried. Manifest entries come from a separate code path that none of them subtract from. packages/views is unaffected because 44 of its 45 exports name a specific file, which is why knip does flag dead files there. Added scripts/check-ui-wildcard-exports.mjs to cover those four directories, blocking from the start since it reports nothing once the one file it found is gone. It reads the wildcards from the manifest, so narrowing an export hands that directory back to knip without the two overlapping. Its test asserts all three states: clean tree passes, a stranded component fails and is named, and importing that component clears it again. The check found packages/ui/hooks/use-auto-scroll.ts — 80 lines whose only occurrence repo-wide was its own definition. Same category as the 16 components already removed, and invisible to knip for the same reason. Also from review: - The path filter missed knip.jsonc and apps/docs/**, so config-only and docs-only PRs skipped knip even though it analyzes docs. - The knip step paired `continue-on-error` with `|| true`, which zeroed the exit code before Actions saw it — no warning surfaced, only log text. Dropping `|| true` makes the warn phase visible and the later promotion to blocking a one-line change. - Corrected the knip.jsonc comment that claimed apps were the only entry points for all three shared packages. True for views and core, never true for packages/ui. Verified: build, typecheck, lint green (12/12, 0 errors). 6,503 tests pass; the same 9 pre-existing failures as before, reproduced on a clean tree. Co-authored-by: multica-agent <[email protected]> * fix(ci): resolve import paths in the ui export check instead of matching names Review found the check passed a dead component whenever any file elsewhere in the repo imported a same-named module. The relative-import branch was a whole-repo text regex on the basename, so `../components/option-card` in packages/views vouched for an untouched packages/ui/components/ui/option-card. That is not a corner case: 17 of the 59 guarded files already share a basename with another file in the repo — `button`, `card`, `input`, `label`, `avatar`, `tabs`, `switch`, `skeleton`, `separator`, `theme-provider` among them. shadcn names are generic by construction, so the hole covered the most likely names a future `pnpm ui:add` would produce. Specifiers are now resolved against each importing file's own directory and compared against real paths, with `@multica/ui/...` mapped through the same manifest wildcards the candidate list is built from. Liveness is now reachability from outside the guarded directories rather than a reference count, so two dead components importing only each other no longer vouch for one another. Also implements the exemption the failure message promises: a file named by a non-wildcard export is an entry point in its own right and is skipped. The candidate loop previously read the wildcard directories unconditionally, so following that advice would not have cleared the check. Tests cover all five states: clean tree, stranded component named, cleared by a real import, the option-card basename collision, and a mutually-importing dead pair reported as two files. Verified: check and its tests pass, knip unchanged at 8 files + 1 dependency, build/typecheck/lint 12/12 green. Co-authored-by: multica-agent <[email protected]> * fix(ci): parse imports with the TypeScript parser, not a source regex Review found the scanner counted commented-out imports as real edges. It ran a regex over raw source, so a component kept its liveness from a line that does not execute — and "delete the last real import, leave the comment behind" is a normal step in a refactor, which makes the false edge appear at exactly the moment the component stops being used. Edges now come from real syntax nodes: static import/export declarations, `import()`, `require()`, and `import("x")` type nodes. `vi.mock("...")` is excluded on purpose — it names a module without importing it, so a component whose only remaining mention is a test mock is dead, and counting it would let one outlive its last real use. Scope, measured rather than assumed: 52 specifiers in this repo appear only in comments or prose, spread over 43 files, so the mechanism is live. None of them currently resolve into packages/ui, so no dead component is being missed today — this closes a latent hole, not an active miss. (An earlier count of 3 was wrong: those were `lazy(() => import(...))` calls that my throwaway measurement script failed to treat as real.) Test gains a sixth state: a component referenced only by a commented-out import and a string containing one must still be reported. Verified: six-state test passes, clean tree passes in 0.9s over 2,089 files, knip unchanged at 8 files + 1 dependency, build/typecheck/lint 12/12 green. `typescript` is already a root devDependency, so no new declaration. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a1cbd7c2ef |
MUL-6354: tighten build and generation guardrails (#7150)
* chore(build): tighten repository hygiene guardrails Co-authored-by: multica-agent <[email protected]> * chore(ci): remove snapshot ignore check Co-authored-by: multica-agent <[email protected]> * chore(ci): address repository hygiene review nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
de902a4aab |
MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime * fix(agent): terminate mcode process trees * fix(agent): address mcode review blockers * test(agent): keep mcode model contract API-stable * fix(migrations): resolve mcode prefix collision * fix(agent): launch mcode through runtime command Assisted-by: MiniMax Code * fix(migrations): renumber mcode migration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d0bd6683f1 |
MUL-5733: keep Go security patches current (#6409)
* fix(release): keep Go security patches current Co-authored-by: multica-agent <[email protected]> * fix(release): address vulnerability review Co-authored-by: multica-agent <[email protected]> * fix(release): restrict vulnerability bypass Co-authored-by: multica-agent <[email protected]> * fix(release): require Go 1.26.6 Co-authored-by: multica-agent <[email protected]> * docs: align Go prerequisite with security floor Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: Sol-Boy <[email protected]> |
||
|
|
3be6bccec7 |
MUL-6189 docs: align contributor Node version guidance with CI (#7003)
Aligns CONTRIBUTING.md, README.md, README.zh.md, SELF_HOSTING_ADVANCED.md and scripts/dev.sh with the Node 22 / pnpm 10.28.2 / Go 1.26.1 versions used by CI, and adds .nvmrc plus engines.node so version managers and package tooling flag Node 20 early. Closes #6972 |
||
|
|
e9528c722a |
MUL-5855: feat(dev): add worktree database cleanup (#6560)
* feat(dev): add worktree database cleanup * fix(dev): handle database cleanup cancellation * fix(dev): protect nested current worktree |
||
|
|
4f10a944b1 |
feat(runtime): add DeepSeek Harness support (#6923)
* feat(runtime): add DeepSeek Harness support * docs(runtime): reflect public DSH release |
||
|
|
cb8ce085ab |
MUL-6108: 清理首批冗余数据库索引 (#6878)
* perf(db): drop redundant indexes (MUL-6108) Co-authored-by: multica-agent <[email protected]> * fix(db): harden concurrent rollback retries (MUL-6108) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: Sol-Boy <[email protected]> |
||
|
|
02b73d5874 |
fix(selfhost): forward SMTP_FROM_EMAIL to backend (#6653)
Co-authored-by: michelle <[email protected]> |
||
|
|
3dd128532c |
feat(agent): add oh-my-pi (omp) as a supported agent runtime (#6514)
* feat(agent): add oh-my-pi (omp) as a supported agent runtime oh-my-pi (omp, https://omp.sh) is a separate CLI that is a drop-in fork of pi and speaks the same JSON event protocol. It is registered as an independent provider so a host with both pi and omp installed gets two runtimes instead of one. omp is a runtime identity on the pi protocol, NOT a new protocol family. It is declared in a single BuiltinRuntimes descriptor (server/pkg/agent/builtin_runtimes.go) that carries id, protocol_family, default_command, env_prefix, display_name, skills_dir, user_skills_dir, launch_header, backend overrides, and model_discovery strategy. Every consumer (agents_probe.go, config.go, daemon.go, execenv/context.go, local_skills.go, agent.go New/LaunchHeader, models.go ListModels) derives from this descriptor — no per-file hardcoded omp special cases. NewRuntime() is the typed entry point for runtime identities: it resolves id → descriptor → protocol family backend → applies overrides through a backendOverrideApplicator interface (piBackend implements it). New() delegates to NewRuntime() for built-in runtime identities, keeping New() meaning exactly one thing: the protocol-family factory. Model discovery: parseOmpModels reads the real {"models":[...]} wrapper with separate provider/id/selector/name fields. Model.ID is the selector (provider/id), matching parsePiModels' convention so buildPiArgs emits both --provider and --model — not just --model. A regression test (TestOmpSelectorSurvivesToBuildPiArgs) pins this end-to-end. Frontend: provider-logo.tsx maps omp to PiLogo. The transcript dialog uses the shared providerDisplayName helper. Landing copy updated to 21 tools in all 4 locales. Tests (omp_test.go): descriptor dispatch, NewRuntime entry point, binary resolution, omp-labeled errors, event-stream completion, pi+omp side-by-side registration, selector→buildPiArgs regression, model parser coverage (real JSON/empty wrapper/invalid stderr/duplicate), and a guard that every descriptor field is non-empty and consumed. Closes #3989 * fix(views): keep Claude Code label in the transcript run details Switching the provider row to the shared runtime formatter renamed every Claude run: the daemon has no display-name override for claude, so the shared formatter answers "Claude", and the legacy claude-code value title-cased into "Claude-code". Both read as "Claude Code" before. Keep those two aliases in a table local to this view and defer everything else to the shared formatter, so the row stays in lockstep with the runtime list (#5260) without renaming Claude runs. * test(daemon): assert pi and omp both reach the register payload The existing omp test stopped at probeAgentCLIs, so nothing covered the part a user actually sees: whether both runtimes are registered with the server, each under its own display name. Discovery finding two entries and New() building two backends both stop short of version detection and the registration payload. Add a daemon-level test that runs real discovery off a fake PATH, drives syncWorkspacesFromAPI, and asserts the payload carries type=pi and type=omp with names "Pi" and "Oh-My-Pi". Rename the discovery test to say what it covers, and record the registered display name in the batch fixture so the name can be asserted. * docs(agent): correct descriptor comments that describe the old behaviour Three doc comments still described behaviour the fail-closed rework replaced: - ModelDiscovery claimed a nil strategy falls back to the family's discovery; ListModels deliberately returns an empty catalog instead, because omp exits non-zero on pi's --list-models. - backendOverrideApplicator claimed backends without it are returned unchanged; NewRuntime returns an error rather than dropping the descriptor's executable and label. - ProtocolFamily credited New() with applying the ID-specific defaults; that moved to NewRuntime. Also repair the truncated first sentence of the NewRuntime doc, and fix discoverOmpModels still calling the output a JSON array when parseOmpModels right below it documents the {"models":[...]} wrapper. * style(server): gofmt the files this branch touched Four files were left unformatted: the descriptor literal and the omp test struct lost their key alignment, execenv/context.go had its new import out of order, and local_skills.go kept the old indentation after its switch moved inside an if/else. Whitespace only — no behaviour change. --------- Co-authored-by: Bohan-J <[email protected]> |
||
|
|
b98f760f1f |
MUL-5604: 支持 Reasonix runtime (#6370)
* feat(agent): add Reasonix runtime Co-authored-by: multica-agent <[email protected]> * fix(agent): make Reasonix sandbox host-adaptive Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6b5d26b235 |
feat: add QwenPaw ACP backend support (MUL-5355) (#5986)
* feat: add QwenPaw ACP backend support Add qwenpaw as a supported agent backend. QwenPaw runs via `qwenpaw acp` over stdio using the ACP (Agent Client Protocol) JSON-RPC 2.0, reusing the hermesClient transport layer. Changes: - New backend: pkg/agent/qwenpaw.go (ACP stdio via hermesClient) - Register in agent.SupportedTypes, New(), launchHeaders - Daemon config: probe qwenpaw binary, QWENPAW_ARGS env var - Display name 'QwenPaw', inline system prompt support - Runtime config: AGENTS.md injection, .qwenpaw/skills/ discovery - User-level skills via QWENPAW_HOME env var - DB migration 224: add qwenpaw to protocol_family whitelist * fix(ci): add qwenpaw to CLI guard names and rename migration 231 - Add 'qwenpaw' to scripts/agent-cli-command-names.txt (TestAgentCLIGuardCoversDefaultCommands) - Rename migration to 231 to avoid collision with existing migrations (TestMigrationNumericPrefixesStayUniqueAfterLegacySet) * fix: address Bohan-J review comments on qwenpaw backend (PR #5986) Blocking fixes: 1. config.go: Add qwenpaw CLI probe so daemon discovers the binary 2. models.go: Add qwenpaw case returning empty list (same as qwen) 3. qwenpaw.go: Fix resume path — use session/load (QwenPaw implements load_session, not session/resume), use resolveResumedSessionID, only set resumeRejected on session-not-found errors, fail the run on set_model failure instead of silently continuing 4. daemon.go: Add qwenpaw→"QwenPaw" to runtimeDisplayNameOverrides 5. agent.go: Update error message to list qwenpaw Non-blocking fixes: 6. qwenpaw_test.go: Add 10 tests covering session/new, session/load, session/load not-found, set_model failure, ListModels, blocked args, protocol verification (session/load not session/resume), timeout, usage tracking, and backend construction 7. sidecar_manifest_test.go: Add qwenpaw to allFileBasedProviders * fix: address Bohan-J second-round review on qwenpaw backend (PR #5986) Blocking fixes: 1. qwenpaw.go: Send qwenpaw.coding_project_dir inside _meta, not as top-level parameter — ACP Pydantic model ignores extra root-level keys (model_config has no extra='allow'), only merges field_meta into kwargs 2. qwenpaw.go: Handle set_model returning result:null (not RPC error) — real QwenPaw server swallows exceptions and returns None, which serialises as JSON null; json.RawMessage('null') is non-nil so bytes.Equal check is needed 3. context.go: Workspace skills are at <workDir>/skills, not <workDir>/.qwenpaw/skills (confirmed against QwenPaw v2.0.1) 4. local_skills.go: QwenPaw resolves global root from QWENPAW_WORKING_DIR -> COPAW_WORKING_DIR -> ~/.copaw -> ~/.qwenpaw, not from QWENPAW_HOME Test additions: 5. TestQwenpawSetModelReturnsNull — regression test for result:null 6. TestQwenpawSessionLoadTransientError — transient error does not set ResumeRejected=true 7. TestQwenpawSessionNewSendsCodingProjectDir — verifies _meta format 8. TestQwenpawSessionLoadSendsCodingProjectDir — same for session/load 9. TestQwenpawTimeout — deterministic sync via signal file All 15 Qwenpaw tests pass. Verified against real qwenpaw v2.0.1 binary (end-to-end: prompt, session ID, resume, system prompt). * fix: rebase against upstream/main and add per-task qwenpaw workspace/agent isolation - Rebase add-qwenpaw-backend branch on upstream/main (88 commits ahead) - Resolve conflict in config.go: keep refactored probe() with shell resolution fallback, which already includes qwenpaw probe - Add --workspace and --agent CLI args to qwenpaw acp for per-task skill isolation and agent identity isolation - Mark --workspace and --agent as blocked in qwenpawBlockedArgs so user custom_args cannot override them - Add deriveQwenpawAgentID() to produce deterministic agent IDs from task's issue ID and agent ID - Wire QwenpawWorkspace and QwenpawAgentID through ExecOptions - Add prepareQwenpawWorkspace() in execenv to materialize bound skills into a per-task workspace directory * fix: address Bohan-J third-round review on qwenpaw backend (PR #5986) Three blocking issues resolved: 1. Agent ID registration — remove session/set_model and --agent entirely. The simpler route: QwenPaw model override is declared unsupported, eliminating the need for agent profile registration in QwenPaw config. 2. Skill revocation — prepareQwenpawWorkspace now does os.RemoveAll on the skill_pool dir and manifest before rebuilding, making it idempotent. A->empty (revoke all), A->B (replace), and A->A (repeated reuse) all work correctly. Added 3 new unit tests. 3. Skill root path — changed 'skills' to 'skill_pool' in both prepareQwenpawWorkspace and skillsDirPath to match QwenPaw's store.py get_workspace_skills_dir. Also cleaned up: removed deriveQwenpawAgentID function and its test, removed QwenpawAgentID from ExecOptions, removed --agent from qwenpawBlockedArgs, removed session/set_model test cases. * fix: add missing qwenpaw probe() call in agents_probe.go The TestDefaultAgentCommandNamesCoversAllProbes test found only 17 probe() calls in agents_probe.go but defaultAgentCommandNames has 18 entries. The probe() call for qwenpaw was missing, causing the backend CI test failure. Adding the probe ensures GUI-launched daemons can resolve qwenpaw via the login shell fallback, matching all other providers. * fix: sync models.go with upstream/main (remove qwenpaw from ListModels) * fix: CI failures — migrate prefix 235→236 + update test - migration prefix 235 was reused by upstream 235_chat_message_quick_actions; renamed our qwenpaw migration from 235 to 236 - TestQwenpawListModels called len() on Catalog struct (compile error); fixed to expect error for unknown provider type * fix: bump qwenpaw migration prefix 236->241 (upstream took 236) * fix: qwenpaw local skill root — 'skills' → 'skill_pool' (MUL-5355) Bohan-J third-round review blocker #3: local_skills.go still scanned <QWENPAW_HOME>/skills but the QwenPaw shared skill pool is <QWENPAW_HOME>/skill_pool (store.py get_workspace_skills_dir). Verified no other qwenpaw paths in the codebase assume the wrong layout. * fix: bump qwenpaw migration prefix 241->242 (upstream took 241) lint test fails with: migration prefix 241 is reused by [241_comment_parent_lookup_index 241_runtime_profile_add_qwenpaw]. Upstream added 241_comment_parent_lookup_index; bump our migration. * feat: add QwenPaw integration test and version declaration (MUL-5355) - New: server/pkg/agent/qwenpaw_integration_test.go with three agentintegration build-tagged tests: TestQwenpawRealACPSmoke — full end-to-end ACP smoke test with session/new → session/prompt and session/load resume validation. TestQwenpawRealWorkspaceSmoke — validates skill_pool workspace flag handling and per-task skill isolation. validateQwenpawVersion — attempt version detection via qwenpaw --version, pip show qwenpaw, and python import. - Document QwenPaw v2.0.1 as the supported baseline version in qwenpaw.go package comment, noting the contract details: _meta qwenpaw.coding_project_dir for Coding Mode, session/set_model NOT supported, skill_pool workspace layout. - This test suite is gated by MULTICA_RUN_REAL_AGENT_SMOKE=1 and requires qwenpaw on PATH, matching the pattern used by grok, cursor, and traeecli integration tests. * fix: bump qwenpaw migration prefix 242->243 (upstream took 242 for qoderclicn) Upstream added 242_runtime_profile_add_qoderclicn in the same rebase window, colliding with our 242_runtime_profile_add_qwenpaw. The migration lint test TestMigrationNumericPrefixesStayUniqueAfterLegacySet catches duplicate prefixes after the legacy range. Also add 'qoderclicn' to our migration's CHECK constraint so it doesn't regress the whitelist added by upstream's 242. * temp: stub out integration test to isolate CI failure * fix: restore upstream probeAgentCLIs() call in config.go (rebase regression) Rebase conflict resolution accidentally reverted upstream's MUL-5439 refactor (extracting probe logic to agents_probe.go) back to the old inline probe block. This also dropped qoderclicn detection and duplicated the probe logic already in agents_probe.go. Restore the single 'agents := probeAgentCLIs()' call — qwenpaw is already probed in agents_probe.go. * fix: bump qwenpaw migration prefix 243->251 (upstream took 243-250) Upstream added migrations 243-250 since our last rebase. Bump to 251, the next available prefix. * feat: add QwenPaw integration test (agentintegration build tag) TestQwenpawRealACPSmoke drives the real qwenpaw acp binary end-to-end: - session/new + session/prompt produces 'pong' - session/load resume with ResumeSessionID works - --workspace flag is forwarded correctly Gated by MULTICA_RUN_REAL_AGENT_SMOKE=1, matching grok/cursor pattern. Validated against QwenPaw v2.0.1. * fix: workspace skills dir 'skills' not 'skill_pool' + add skill loading integration test Two fixes: 1. qwenpaw_workspace.go: write skills to <workspace>/skills/ instead of <workspace>/skill_pool/. QwenPaw's get_workspace_skills_dir() looks for workspace skills at <workspace>/skills/ (store.py:65-67), not skill_pool (which is the shared pool at WORKING_DIR/skill_pool). Verified against real qwenpaw acp — skills in skill_pool/ were never discovered. 2. Add TestQwenpawRealWorkspaceSkill integration test that proves a bound skill is actually loaded and effective: writes a skill that overrides the agent's response, sends an unrelated prompt, and asserts the skill's marker text appears in the output. This addresses R3 review feedback: 'please also add a test that exercises an actually-bound skill'. All three integration tests pass against QwenPaw v2.0.1: - TestQwenpawRealACPSmoke (session/new + prompt + session/load resume) - TestQwenpawRealWorkspaceSkill (skill discovery + effectiveness) * feat: add ACP model discovery for qwenpaw via session/new models field QwenPaw v2.0.1+ now includes a 'models' field (SessionModelState) in the session/new response, added by agentscope-ai/QwenPaw#6531. This lets ACP clients discover available models without session/set_model. - ListModels for qwenpaw now uses discoverACPModels (same pattern as traecli/grok/kiro) to spin up 'qwenpaw acp', call session/new, and parse the models catalog from the response. - discoverQwenpawModels mirrors discoverTraecliModels — ACP-native, no auth selection needed. - Model override via session/set_model remains unsupported: it persists to agent.json at the agent scope (not session-scoped), so calling it would mutate the user's shared agent config. The model picker shows available models for display/selection, but the daemon does not send set_model. - Updated TestQwenpawListModels to verify qwenpaw is a recognized type (not 'unknown agent type' error). * fix: address Bohan-J Review 5 — ModelSelectionSupported=false, version bump to v2.1.0-beta.1, remove debug files - ModelSelectionSupported('qwenpaw') now returns false with rationale (session/set_model persists to agent scope, not session scope) - Add TestQwenpawModelSelectionUnsupported regression test - Update version references from v2.0.1 to v2.1.0-beta.1 (includes agentscope-ai/QwenPaw#6531 — models field in session/new response) - Remove check_ci.py, jobs.json, runs.json debug artifacts * fix: bump qwenpaw migration prefix 251->253 (upstream took 251) Upstream added 251_agent_runtime_unbind. Bump to 253, the next available prefix after 252_agent_builder_draft. * fix: address Bohan-J Review 6 — drop unused discovery, always attribute to unknown - ListModels for qwenpaw returns empty catalog without spawning ACP subprocess (model selection is unsupported, so no consumer exists) - Usage attribution always uses 'unknown' instead of opts.Model (the backend never sends opts.Model to QwenPaw) - Add TestQwenpawUsageModelIgnored regression test - Fix stale v2.0.1 comment in TestQwenpawListModels * chore(agent): clean up qwenpaw model-discovery leftovers Follow-up nits from review 7 on PR #5986: - Drop discoverQwenpawModels: it lost its only caller when ListModels started returning an empty catalog for qwenpaw. - Correct the version contract in qwenpaw.go. The execution path needs only the ACP surface present in v2.0.1 (current stable); the models field on session/new landed in v2.1.0-beta.1 but has no consumer now that model selection is unsupported. - Make TestQwenpawListModels actually guard the no-subprocess promise. It pointed at a nonexistent path, which the old discovery helper also answered with an empty catalog, so it passed either way. It now uses an executable fake that records invocation; verified it fails if a discovery path is reintroduced. - gofmt agent.go (ExecOptions alignment broke when QwenpawWorkspace was added) and restore the trailing newline in qwenpaw_test.go. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: niudakok <[email protected]> Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e4b6f7a31b |
MUL-5581: add Qoder CN CLI runtime (#6232)
* feat(agent): add Qoder CN CLI runtime Co-authored-by: multica-agent <[email protected]> * fix(agent): address Qoder CN review nits Co-authored-by: multica-agent <[email protected]> * fix(agent): defer Qoder CN version gate Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
cac453fe01 |
MUL-5505: fix(selfhost): one source of truth for the backend host port (#6168)
* fix(selfhost): read the published host port back from Compose (MUL-5505)
`make selfhost` polled the wrong port when the backend host port was passed
through the environment. Make and Compose disagree on variable precedence: an
assignment in an included makefile (the Makefile `include`s .env) overrides a
real environment variable, while for Compose the environment overrides .env.
Because .env.example ships BACKEND_PORT uncommented, .env always pinned the
recipe's view of the port, so `BACKEND_PORT=9000 make selfhost` published the
stack on 9000 while the health check hammered 8080 for 60s and then reported
"Services are still starting" on a perfectly healthy install. The success
message printed the same wrong backend URL. FRONTEND_PORT had the same
inversion, affecting the printed frontend URL.
Stop re-deriving the host port in the recipe and ask Compose, which is the only
authority on what it published. The wait-and-report block (three port
references per target, duplicated across selfhost and selfhost-build) moves into
scripts/selfhost-wait.sh, which resolves both host ports via `docker compose
port` and falls back to the compose default chain when the stack is not up.
Also fix the input contract: `PORT` was the only port variable documented in
SELF_HOSTING_ADVANCED.md, yet compose read only BACKEND_PORT, so setting the
documented variable was silently ignored. The backend host port mapping now
falls back to `${PORT:-8080}`, matching Makefile and scripts/local-env.sh, and
the docs describe both variables as host ports.
Container-internal ports (8080/3000), the global PORT derivation used by local
dev, and the 127.0.0.1 bindings are unchanged; the default self-host experience
is byte-identical.
Fixes #6145
Co-authored-by: multica-agent <[email protected]>
* fix(selfhost): make PORT the real backend port and test the whole chain
Addresses review feedback on #6168.
The first round's root-cause claim was wrong. It said `BACKEND_PORT=9100 make
selfhost` published on 9100 while the health check probed 8080. It does not:
when make drives Compose, Compose inherits make's exported values, so both
sides agree on 8080 and the override is silently dropped instead. Re-measured
through the real recipe, the genuine disagreements were:
- `.env` setting PORT with no BACKEND_PORT: probe followed PORT, Compose
published ${BACKEND_PORT:-8080} — 9100 vs 8080.
- `make selfhost PORT=8080` over a .env with BACKEND_PORT=9100: probe 8080,
Compose published 9100.
Both are the "wrong port variable" defect the GitHub issue names, and reading
the port back from Compose fixes both. The comments and PR text that blamed
make/Compose precedence are corrected.
The PORT fallback added to docker-compose.selfhost.yml was also unreachable:
.env.example shipped BACKEND_PORT=8080 uncommented, so the fallback never
engaged and `SELF_HOSTING_AI.md`'s "edit PORT and FRONTEND_PORT" instruction did
nothing. Pick one contract and apply it everywhere: PORT is the backend port to
edit, BACKEND_PORT/API_PORT/SERVER_PORT are optional aliases that override it.
That is already what Makefile, scripts/local-env.sh, scripts/install.sh and
install.ps1 do; compose and the web dev fallback were the outliers.
- .env.example ships BACKEND_PORT commented out, like its sibling aliases, so
editing PORT works out of the box.
- apps/web/config/runtime-urls.ts walks the same alias chain instead of
reading BACKEND_PORT alone, so `pnpm dev` follows an edited PORT.
- SELF_HOSTING_AI.md and SELF_HOSTING_ADVANCED.md state the contract.
Configuration that cannot take effect is now reported instead of ignored.
scripts/selfhost-preflight.sh warns when an alias in the env file overrides an
edited PORT, and when a port from the shell environment is overridden by the env
file. The Makefile captures the pristine origins before its include, because
afterwards there is no way to tell that `PORT=9000 make selfhost` was asked for.
Tests now cross environment -> make -> compose -> report for real. The docker
stub is no longer told what to publish: on `up` it asks the real
`docker compose config` to interpolate the compose file with the environment the
recipe actually handed it, records that, and answers `port` from the recording.
Verified failing on the old code — with the old recipe and compose file the
suite reports published 8080 against probe 9100, the exact defect.
Co-authored-by: multica-agent <[email protected]>
* ci: gate the self-host test on scripts/selfhost-preflight.sh too
The new preflight script was missing from the frontend path filter, so a
change to it alone would skip the test that covers it.
Co-authored-by: multica-agent <[email protected]>
* fix(selfhost): honour the full backend port alias chain in Compose
Addresses the second review round on #6168.
The contract this PR documents is
BACKEND_PORT -> API_PORT -> SERVER_PORT -> PORT -> 8080
and Makefile, scripts/local-env.sh, scripts/install.sh, scripts/install.ps1 and
apps/web/config/runtime-urls.ts all implement it. docker-compose.selfhost.yml
implemented only BACKEND_PORT -> PORT -> 8080, so `API_PORT=9100` or
`SERVER_PORT=9200` in .env still published 8080.
That is not only a direct-Compose concern. scripts/install.sh:407 derives its
health-check port from the full chain and then runs this compose file, so an
alias it honours but Compose ignores puts the probe on 9100 while the stack
listens on 8080 — the original #6145 defect, reached through the installer.
- docker-compose.selfhost.yml publishes the full nested chain, verified to
keep the same precedence and the same 8080 default.
- scripts/selfhost-wait.sh's fallback matches, so the degraded path agrees
with what Compose would have published.
- The Makefile captures the pristine origins of API_PORT and SERVER_PORT too,
and the preflight reports them, so no alias can be silently overridden by
the env file while the others are reported.
Tests pin the chain on the boundaries a recipe-level test cannot see, because
Makefile normalises all four aliases into PORT before any recipe runs: a matrix
over the direct Compose path asserts each alias moves the published port with
the right precedence, and every case additionally asserts that
scripts/install.sh's resolver returns the same value Compose published. That
invariant is the one that actually protects the installer.
Verified failing without the fix: with the two-level chain restored the suite
reports `expected 9200 / compose published 8080 / installer resolved 9200`, and
with the alias warnings narrowed it reports the missing API_PORT notice.
Co-authored-by: multica-agent <[email protected]>
* fix(selfhost): resolve one winner before reporting ignored port config
Addresses the third review round on #6168.
The preflight walked each alias independently, so it made a separate "wins"
claim per alias. With
PORT=9000
BACKEND_PORT=8000
API_PORT=7000
SERVER_PORT=6000
in .env it announced that BACKEND_PORT, API_PORT and SERVER_PORT each won, when
only BACKEND_PORT=8000 can. Two of the three notices were simply false.
It also compared only same-named shell and file variables, so shadowing across
aliases stayed silent: .env with PORT=8000 and BACKEND_PORT=8000 plus a shell
API_PORT=7000 produced no output at all, even though API_PORT was discarded.
Resolve the winner along the documented chain first — within a variable the env
file beats the environment, then BACKEND_PORT beats API_PORT beats SERVER_PORT
beats PORT — and report only the inputs that winner actually shadows. Output now
names one winning input and lists the rest, whichever variable or source they
came from. An input carrying the winning value is not reported: it is redundant
but the user still gets the port they asked for, so there is nothing to fix.
Tests cover both cases the old logic got wrong: every alias set at once must
yield exactly one winner claim and list the others as unused, and an env-file
alias beating a lower-priority shell alias must be reported. Also asserted that
a redundant same-value alias and a default configuration both stay silent.
Measured against the previous implementation, phrasing aside: with all four
values set it made 3 winner claims where there is now 1, and on the cross-alias
case it printed nothing where API_PORT is now reported.
Co-authored-by: multica-agent <[email protected]>
* fix(selfhost): track env-file existence so empty port assignments resolve right
Addresses the fourth review round on #6168.
The Makefile passed only values to the preflight, so the script inferred "the
env file does not set this" from an empty string. An explicit empty assignment is
a different input from an absent one: make lets `BACKEND_PORT=` in the env file
override `BACKEND_PORT=9000` from the environment, and the variable then drops out
of the alias chain instead of setting a port.
With .env holding PORT=8080 and BACKEND_PORT=, invoked as
`BACKEND_PORT=9000 make selfhost`, the stack published 8080 and the health check
probed 8080 — correct — while the preflight announced
the backend host port resolves to 9000 from BACKEND_PORT (environment)
Set but unused: PORT=8080 (.env)
naming the ignored value as the winner and the winning value as unused. A report
that contradicts the startup is worse than no report. FRONTEND_PORT= had the
matching hole: it shadowed the environment, Compose fell back to 3000, and the
preflight said nothing.
The Makefile now captures ENV_FILE_<VAR>_IS_SET from $(origin) alongside the
value, for all five port variables via one $(foreach) instead of hand-written
lines. The preflight treats a variable the file defines as taking the file's
value even when empty, so it shadows the environment, and an empty value never
wins — resolution continues down the chain and falls back to 8080. Inputs the
winner shadows are still listed, including ones shadowed by an empty assignment.
The frontend check mirrors this and now names the port that actually resolved.
Recipe-level tests cover an empty alias over a shell value, every link of the
chain emptied at once, and the frontend equivalent — each asserting the reported
winner, the Compose published port and the probed port all agree. Against the
previous implementation the first case fails with `reported: 9000 / actual: 8080`.
Co-authored-by: multica-agent <[email protected]>
* fix(selfhost): installers read the published port; drop the second parser
Addresses all four blockers from the consolidated review on #6168.
1. Both installers probed and printed the wrong port. scripts/install.sh:402 and
scripts/install.ps1 start Compose inheriting the current environment, and
Compose lets that environment outrank .env — but the installers then derived
the probe port and the summary URL from .env alone. Measured on this branch
before the fix, every port variable diverged:
ambient PORT=9100 -> compose 9100, installer 8080
ambient BACKEND_PORT=9200 -> compose 9200, installer 8080
ambient API_PORT=9300 -> compose 9300, installer 8080
ambient SERVER_PORT=9400 -> compose 9400, installer 8080
ambient FRONTEND_PORT=3100 -> compose 3100, installer 3000
Both now read the ports back once with `docker compose port` after `up -d`
and reuse that single result for the health check and the summary. If the
query fails they fail loudly instead of claiming success. The .env-derived
helpers are deleted rather than left beside the new path.
2. The Make preflight is removed entirely, as the review recommended. It
modelled `origin=environment` and `origin=file` but not `command line`, so
`make selfhost BACKEND_PORT=9000` over a .env with API_PORT=7000 announced
7000 while Compose published 9000. That was its fourth wrong report in four
rounds, because it was a second parser of precedence rules — the very thing
this PR removes elsewhere. `docker compose port` is the runtime truth, so the
script, the Makefile captures and the macro are gone.
3. Tests now cover the installer paths that were unguarded. scripts/install.test.sh
gains a `--with-server` matrix (defaults, .env PORT, all four backend aliases,
FRONTEND_PORT, ambient overriding .env for all five, and explicit-empty
fallback) plus a case proving an unresolvable port fails loudly. A new
scripts/install.ps1.test.ps1 drives the same matrix through Start-LocalInstall.
Every case asserts Compose's published port == the probed URL == the printed
URL. Ambient variables are explicitly cleared per case so a runner's own PORT
cannot leak. The Makefile matrix gains the command-line origin cases. CI runs
the PowerShell suite on windows-latest in the always-on installer job, and the
frontend filter now includes both installers.
4. Docs state the real contract: the alias order is shared, but which *source*
wins is per entry point — Compose prefers the environment, make prefers the
included env file, and a make command-line assignment outranks both. The
removed preflight's promise is gone from SELF_HOSTING_AI.md, and the compose
header no longer claims the installer derives its own health-check port.
Verified failing without the fixes: the Bash matrix reports
`[ambient PORT beats .env] compose published 9500 / probed 9100 / printed 9100`
against the old installer, and the PowerShell matrix reports
`printed backend=8080` against an injected summary divergence.
Co-authored-by: multica-agent <[email protected]>
* fix(selfhost): keep web dev proxy off frontend port
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
ee7ba83f53 |
fix(self-host): apply setup config to daemon (MUL-5269) (#5880)
Fixes two connected self-host setup failures in local worktree development. Generated worktree environments now expose the backend HTTP origin through MULTICA_PUBLIC_URL, and existing generated worktrees derive the missing value at startup through both scripts/local-env.sh and the Makefile. Explicit values, including an intentionally empty same-origin setting, are preserved. setup and setup self-host now reconcile an existing same-profile daemon after authentication so it loads the newly written server URL and token. An idle daemon is restarted behind the existing restart preflight; when active tasks exist, setup leaves the daemon running to avoid cancelling work and prints an actionable profile-aware restart command instead. Closes #5879 |
||
|
|
73b0015475 |
feat(vcs): make self-hosted Git providers self-host-only (MUL-3772, MUL-5138) (#5888)
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772) The Forgejo/Gitea/GitLab integration is intended for self-hosted Multica, where Multica can reach a Git instance on the operator's own network. On the managed multi-tenant cloud it adds an SSRF surface (connect validates a user-supplied instance URL from the server) and would store third-party Git tokens for all tenants under one key, while only serving the small subset of users whose instance is publicly reachable. Product decision: offer it on self-host only. - Add an explicit deployment switch MULTICA_VCS_INTEGRATION_ENABLED (default off). Connect, rotate, and webhook now require BOTH the switch on AND a valid MULTICA_VCS_SECRET_KEY — the switch is the product boundary, not key presence alone. When off, connect/rotate return 404 and the webhook returns a bare 404 (no config leak), independent of the frontend. - /api/config exposes vcs_integration_available (mirrors the switch, omitted when false) so the Settings UI hides the whole "Git providers" section on cloud instead of surfacing an operator-only "missing key" hint. - docker-compose.selfhost.yml defaults the switch on; .env.example documents it. - Docs (en/zh) lead with a callout: available on self-hosted Multica only, not Multica Cloud, and clarify "self-hosted" means Multica itself, not just Git. #5006 / #5883 stay in place — the schema and backend capability are retained; this only gates availability. No cloud VCS connection can exist (connect always required the key, which the cloud never set), so nothing needs migrating. Verified: go build/vet + VCS/config handler tests on a fresh migrated DB (incl. a new disabled-deployment 404 test); pnpm typecheck (core + views) and the integrations-tab + core schema/config vitest suites pass. Co-authored-by: multica-agent <[email protected]> * fix(vcs): complete self-host integration gating (MUL-5138) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
ba0b711787 |
chore(ci): run test-go.sh self-test in Go test entry points (MUL-5169) (#5802)
Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
36533bbc2b | fix(test): prevent agent CLI execution in default tests (#5789) | ||
|
|
220fa58264 |
fix: guide SSH installs to token login (#5318)
Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3030c803bf |
fix(scripts): fix version comparison to prevent unnecessary CLI upgrades (#4227)
`multica version` now outputs two lines (version + go info). The old
`awk '{print $2}'` captured both lines, causing the version comparison
to always mismatch and triggering unnecessary upgrades.
Fixes https://github.com/multica-ai/multica/issues/4226
Co-authored-by: Claude Opus 4.6 <[email protected]>
|
||
|
|
4df6c1468d |
fix: validate selfhost compose env defaults (#4138)
Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3389e887e0 |
MUL-2614: randomize self-host Postgres password (#3893)
* fix: randomize self-host postgres password Co-authored-by: multica-agent <[email protected]> * docs: sync self-host password guidance Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bfb7c85491 |
fix(selfhost): derive local port URLs from env (MUL-2506) (#2939)
* fix(selfhost): derive local port URLs from env * fix(selfhost): derive local script URLs |
||
|
|
eaf8b14866 |
fix(installer): post-merge nits from #2881 (MUL-2458) (#2922)
- Capture `brew tap` output and print the same diagnostic tail on failure that `brew install` already prints, so #2867-style "no signal" reports are gone from both Homebrew failure paths. - Add a `brew tap` failure regression case to `scripts/install.test.sh` and refactor the test runner to share sandbox/curl-stub setup; both cases now also assert the diagnostic tail is emitted. - Move the shell installer test out of the heavy backend job into a dedicated `installer` matrix job that runs on `ubuntu-latest` and `macos-latest`, since the installer targets macOS/Homebrew and BSD vs GNU `tar` / `sed` / `mktemp` differences are the next likely break. - Surface `MULTICA_INSTALL_DIR`, `MULTICA_BIN_DIR`, and `MULTICA_SELFHOST_REF` in `install.sh --help` so `MULTICA_BIN_DIR` stops looking like a test-only knob. Co-authored-by: multica-agent <[email protected]> |
||
|
|
edded77691 | fix(installer): fall back when brew install fails (#2881) | ||
|
|
668cab6022 |
feat(github): mirror PR CI checks and merge conflict status (MUL-2228) (#2632)
* feat(github): mirror PR CI checks and merge conflict status (MUL-2228)
Surface "checks passed/failed" and "conflicts/no conflicts" badges under
each linked PR on the issue page so users can judge readiness without
flipping over to GitHub. CI state is fed by check_suite webhooks
(GitHub Actions + apps using the Checks API; legacy status events are
out of scope for MVP); conflicts are read from pull_request.mergeable_state.
Data model:
* github_pull_request: add head_sha + mergeable_state
* github_pull_request_check_suite: per-suite rows keyed by (pr_id, suite_id)
* Aggregation done at query time, filtering by current head_sha so
late-arriving suites for a stale head can't contaminate the new head's
pending view; per-app latest suite chosen first so a single app firing
multiple suites isn't counted N times.
Webhook hardening:
* synchronize/opened/reopened/edited(base) explicitly clear mergeable_state
* single-row ordering protection on the check_suite upsert prevents a
late-delivered older event from overwriting a newer one
* check_suite.pull_requests is iterated; unknown PRs are logged and dropped
UI:
* PR row shows Checks + Conflicts badges; opaque mergeable values
(blocked/behind/unstable/...) render as no badge, not as conflicts.
* Terminal PR states (merged/closed) suppress the status row entirely.
Tests: * Pure unit coverage for derivePRMergeableState + aggregateChecksConclusion
* Webhook integration tests: multi-app aggregation, old-head ignore,
late-older-event ignore, synchronize clears mergeable_state
* Vitest coverage for pull-request-list badge rendering across CI/conflict
combinations and the legacy (null) fallback.
Co-authored-by: multica-agent <[email protected]>
* fix(github): scope check_suite PR lookup; preserve mergeable on metadata
Addresses code review on PR #2632.
1. check_suite handler now resolves the PR through the workspace-scoped
GetGitHubPullRequest query instead of GetGitHubPullRequestByRepoNumber.
The (workspace_id, repo_owner, repo_name, pr_number) tuple is the real
uniqueness key, so a bare (owner, repo, number) lookup could return a
stale row from another workspace and either land the suite on the wrong
PR or skip the right one when the installation ids drifted. The old
unscoped query is removed.
2. derivePRMergeableState now returns (value, clear) and the upsert SQL
distinguishes three cases: state-changing actions clear the column to
NULL, non-empty payloads write the value, and metadata events with an
empty payload preserve the existing column. Previously every empty
payload became NULL, so a labeled/assigned event silently wiped a
known clean/dirty verdict in violation of the RFC's "metadata empty
payload preserves" rule.
3. ListPullRequestsByIssue narrows to the issue's PR ids before running
the per-app check_suite aggregation, avoiding a full-table scan over
github_pull_request_check_suite when only a handful of rows belong to
the requested issue.
New helper test covers labeled+empty preserves; new integration test
verifies a metadata event after a known mergeable_state keeps the value.
Co-authored-by: multica-agent <[email protected]>
* feat(github): PR card layout v3 increment — stats + segmented progress bar
Replaces the row + badge layout under "Pull requests" on the issue
detail sidebar with a card that mirrors the GitHub PR summary look:
title, author/avatar, +N −M · K files diff stats, segmented progress
bar (failed → pending → passed, failure leftmost), and a one-line
status caption following an explicit priority pass-through.
Backend
- Migration 092: github_pull_request adds additions / deletions /
changed_files (INT NOT NULL DEFAULT 0). Zero defaults are what the
new frontend treats as "legacy backend — hide the stats row" so old
PR rows that pre-date this migration don't render "+0 −0 · 0 files".
- pull_request webhook handler reads stats off the top-level payload.
- ListPullRequestsByIssue now surfaces per-suite counts
(checks_passed / failed / pending) alongside the existing aggregate
conclusion, so the segmented bar reuses the already-computed counts
with no new aggregation.
Frontend (packages)
- core/github/pull-request-status.{ts,test.ts}: pure-function module
for the status-kind priority table and the segment derivation; 15
cases covered, includes the "all-zero → hide stats" guard.
- views/issues/components/pull-request-list.tsx: PullRequestCard plus
a compact-row fallback used when count > 4 (first 3 as cards, the
remainder collapsed behind a Show more toggle).
- i18n: new `pull_request_card_*` keys in en + zh-Hans.
Tests
- 12 component tests covering each rule of the priority table, the
legacy-zero stats fallback, and the collapse threshold.
- Reuse of the v3 webhook handler tests confirmed.
Verification
- pnpm typecheck + pnpm test green (60 test files, 536 tests).
- go build ./... + go vet ./... clean.
- 6 demo issues (DEV-2..DEV-7) screenshotted via Playwright; see the
PR comments for the visual check matrix.
Co-authored-by: multica-agent <[email protected]>
* fix(views): collapse PR cards at N>=4, not N>4
The card-vs-collapse threshold used `>` so 4 PRs slipped past it and
all rendered as full cards, contrary to RFC v3 (N >= 4 collapses to
3 cards + compact tail). Switch to `>=` and update the threshold-
boundary test to expect "Show 1 more".
Co-authored-by: multica-agent <[email protected]>
* fix(views): align PR sidebar rows with existing list style
Co-authored-by: multica-agent <[email protected]>
* fix(views): hide terminal PR status badges
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
bda475cbba |
refactor(reserved-slugs): single JSON source for backend + frontend (#2148)
Reserved workspace slugs lived in two parallel files (`workspace_reserved_slugs.go` and `packages/core/paths/reserved-slugs.ts`) with no parity check. Adding or renaming a global route on one side without the other would slip through CI and surface only when a real user hit the collision. Collapse the two lists into one source: `server/internal/handler/reserved_slugs.json`. Go embeds the JSON via `//go:embed` and parses it at package init; the TS file is regenerated by `scripts/generate-reserved-slugs.mjs` (run via `pnpm generate:reserved-slugs`). CI re-runs the generator and `git diff --exit-code`s the TS output, so a stale TS file cannot land. The slug set is unchanged (87 entries, byte-equivalent slug literals). Update CLAUDE.md to describe the new "edit JSON, run generator" workflow. Co-authored-by: multica-agent <[email protected]> |
||
|
|
3447764b03 |
feat(i18n): full rollout — 21 namespaces translated (en + zh-Hans) (#1853)
* feat(i18n): rollout phase — translate 9 namespaces (WIP)
Phase 1 complete (基建 + login + Settings language switcher),
phase 2 partial (Wave 4 done, search done). Pending namespaces
documented inline; another developer can pick up from here.
Infrastructure
--------------
- server: add users.language column + extend PATCH /api/me
(TestUpdateMeAcceptsLanguage / TestUpdateMePreservesLanguage)
- packages/core/i18n: types / pickLocale (intl-localematcher) /
browser-cookie-adapter / createI18n (initAsync false +
useSuspense false) / I18nProvider / LocaleAdapterProvider
- Split server-safe vs React entries:
@multica/core/i18n — for proxy/RSC/middleware (no React)
@multica/core/i18n/react — for client trees (createContext)
(RSC vendored React lacks createContext; mixed import would crash
proxy.ts at module load.)
- packages/views/i18n: useT hook + selector API augmentation
(i18next v26 default; auto-propagates to apps via the side-effect
import in use-t.ts).
- apps/web: proxy.ts (Next 16 renamed middleware) merges existing
legacy/root redirects with x-multica-locale header forwarding;
layout.tsx reads locale via headers() and pre-loads RSC resources.
- apps/desktop: webPreferences.additionalArguments injects
systemLocale (no sendSync — avoids main-thread blocking IPC);
renderer adapter reads via process.argv.
- ESLint: i18next/no-literal-string at file-scope for translated
files via packages/views/eslint.config.mjs TRANSLATED_FILES.
- glossary.md (packages/views/locales/) freezes term policy:
Issue / Workspace / Agent / Skill / Autopilot / Daemon / Runtime
stay English; Inbox / Project / Comment / Member translate.
Translated namespaces (9 / 19)
------------------------------
- auth: login page (web wrapper含 desktop-handoff 文案) + Settings
Appearance language switcher
- editor: 9 .tsx (bubble-menu / link-hover-card / readonly-content /
title-editor / extensions: code-block / file-card / image-view /
mention-suggestion) + 32 keys
- invite: 25 keys
- labels / members / my-issues: Wave 4 全部
- search: command palette 35 keys
- navigation: no user-facing strings (no-op)
Pending (10 / 19)
-----------------
issues (46 files / ~210 keys)
agents (29 files / ~155 keys; presence.ts + config.ts label maps
允许进 i18n)
onboarding (22 files / ~150 keys)
settings rest / skills / modals / workspace / chat / inbox /
projects / autopilots / layout
Workflow for picking up
-----------------------
- Glossary: packages/views/locales/glossary.md (mandatory read)
- Reference impls: auth/login-page.tsx + editor/* (selector API +
i18n-provider test wrapper pattern)
- Per namespace:
1. create locales/{en,zh-Hans}/{ns}.json
2. add to packages/views/i18n/resources-types.ts
3. useT('{ns}') + t($ => $.foo) in components
4. add files to TRANSLATED_FILES in eslint.config.mjs
5. typecheck + test + lint must pass
- Subagents currently CANNOT write files (sandbox deny). Run as
hybrid: subagent researches + outputs full JSON + tsx diff,
controller writes.
Other
-----
- scripts/init-worktree-env.sh: default
MULTICA_DEV_VERIFICATION_CODE=888888 in dev for deterministic
login (gated by isProductionEnv).
Verified: pnpm typecheck (6 pkgs ok), pnpm test (232 pass),
make test (Go).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* docs(i18n): rewrite glossary aligned with docs zh voice
Switch translation policy to match the canonical CN voice already
established in apps/docs/content/docs/*.zh.mdx (20+ files). The new
rule splits product nouns into two classes:
- Typed entities (issue / project / skill / autopilot / task) — kept as
lowercase English in CN text, visually marking them as system types.
- Concepts (workspace / agent / daemon / runtime / inbox) — fully
translated (工作区 / 智能体 / 守护进程 / 运行时 / 收件箱).
Previous glossary kept Workspace / Agent / Daemon / Runtime as English
on "工程惯例" grounds, but docs zh and CN AI ecosystem (Coze / 腾讯元器
/ 百度) consistently translate these. App UI now matches docs voice so
users don't see split personality between the app and its own docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): register 6 namespaces and retrofit zh strings to new glossary
Two fixes that were blocking the previously-translated namespaces from
actually rendering in CN:
1. RESOURCES gap — locales/index.ts only loaded common/auth/settings,
but resources-types.ts declared 12 namespaces and 6 of them had real
translation content. At runtime i18next would fall back to raw keys
for editor / invite / labels / members / my-issues / search.
Register all 9 currently-translated namespaces.
2. Retrofit zh strings to the docs-aligned glossary:
- "Issue" → "issue" (lowercase entity)
- "Workspace" → "工作区"
- "Agent" → "智能体"
- "Runtime" → "运行时"
- "Skill" → "skill" (lowercase)
- "项目" → "project" (lowercase)
Touched: editor.json (sub_issue + mention.group_issues), invite.json
(3 Workspace occurrences), members.json (agents_section / more_agents),
my-issues.json (8 retrofits across page/header/errors), search.json
(13 retrofits across groups/pages/commands/empty).
Verified: pnpm typecheck (6/6) + pnpm test (238/238) all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate inbox namespace
First namespace through the sub-agent → main-agent integration pipeline.
JSON: en/inbox.json + zh-Hans/inbox.json — 60 keys across page / menu /
list / detail / types / labels / errors. Time-formatter labels are kept
compact in EN ("5m" / "3h" / "2d") and use full units in zh ("5 分钟" /
"5 小时" / "5 天") since raw "5 分" reads as "5 marks/points" in CN.
Component changes converted two module-level statics into hooks so the
strings can flow through i18next:
- inbox-list-item.tsx: `timeAgo` (pure fn) → `useTimeAgo` (hook
returning a fn). The local copy is a duplicate of @multica/core/utils
`timeAgo` that is only used by inbox-page; other consumers across
chat/agents/skills/issues stay on the core util for now and will be
translated when their namespaces land.
- inbox-detail-label.tsx: `typeLabels` (static const Record) →
`useTypeLabels` (hook returning the same Record shape). Call sites
keep the existing `typeLabels[type]` access pattern.
inbox-page.tsx now uses both hooks and `useT('inbox')` selector calls
for all hardcoded strings (~24 sites: header / dropdown menu / list
empty state / detail panel / mobile back / quick-create-failed flow /
all error toasts).
Wired up: resources-types.ts, locales/index.ts RESOURCES, ESLint
TRANSLATED_FILES (3 inbox tsx files now lint-protected).
Verified: pnpm typecheck (6/6) + pnpm --filter @multica/views test
(238/238) + ESLint clean on inbox/.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate workspace namespace
Translates the three workspace shell views: create-workspace-form,
new-workspace-page, no-access-page. Also fixes the prior-art
no-unescaped-entities lint errors in no-access-page.tsx — the
apostrophes in "doesn't" / "don't" were JSX text literals that move
into JSON values after translation, so the lint rule no longer fires.
Tests wrapped: workspace/create-workspace-form.test.tsx,
workspace/no-access-page.test.tsx, modals/create-workspace.test.tsx
all now wrap render() with <I18nProvider locale="en"> so the en values
in workspace.json drive the rendered text and the existing assertions
continue to match.
Slug constants kept: WORKSPACE_SLUG_FORMAT_ERROR /
WORKSPACE_SLUG_CONFLICT_ERROR exports in workspace/slug.ts are still
imported by onboarding/steps/step-workspace.tsx (out of scope here).
The workspace shell now reads its strings from workspace.json directly.
Multica.ai brand prefix in the slug input affordance is wrapped with
an inline `// eslint-disable-next-line i18next/no-literal-string` per
glossary policy on brand names.
Renamed sign_in_other → sign_in_different to avoid colliding with
i18next's `_other` plural-suffix convention which the selector-API
typings treated as a plural form of `sign_in`.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate projects namespace
Translates the projects list page, project detail page, project picker
dropdown, and project chip — all four user-facing surfaces under
packages/views/projects/components/.
New file: projects/components/labels.ts exposes three hooks that
replace the static `.label` field on PROJECT_STATUS_CONFIG /
PROJECT_PRIORITY_CONFIG and the previous module-level
`formatRelativeDate` helper. Core's `.label` stays untouched (it's
still consumed by search and the create-project modal, both
out-of-scope for this namespace) — those will flip when their
respective namespaces translate.
In zh, the "project" entity stays lowercase English per glossary
(`新建 project`, `还没有 project`, `从 project 移除`). Status / priority /
table column labels translate fully.
The cancelled / done / paused etc. status labels duplicate per-
namespace as `projects.status.*` rather than reading from a future
shared status namespace. This matches the auth/inbox/workspace
pattern of self-contained namespaces. If a generic "issue/project
status" pool emerges later, these can collapse.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on projects/ (1 pre-existing warning
about useEffect/sidebarRef dep, unrelated to i18n).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate autopilots namespace
Six tsx files: autopilots-page (list + 6 templates), autopilot-detail-page
(properties / triggers / run history / delete), autopilot-dialog
(create + edit dialog), trigger-config (cron form), and the agent /
timezone pickers.
Hook conversions for module-level helpers that need t():
- summarizeTrigger / describeTrigger → useSummarizeTrigger /
useDescribeTrigger (no external callers, removed the plain exports)
- formatRelativeDate → useFormatRelativeDate (per-component hook)
- formatCountdown → useFormatCountdown (per-component hook)
- TEMPLATES array now keyed by id; titles + summaries pull from
templates/{id}/{title,summary} JSON. Prompts stay raw EN since
they're injected directly into the agent task — translating them
would translate the agent's instructions, not the user's UI.
Status / execution-mode / run-status enums render via t($ => $.status[k])
with k typed against the core type (no separate hook needed).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on autopilots/.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate skills namespace
Seven tsx files: skills-page (list + filters + intro banner),
skill-detail-page (the giant — properties + file tree + sidebar +
conflict banner + delete dialog, ~963 lines), create-skill-dialog
(chooser + manual + URL forms), runtime-local-skill-import-panel
(local runtime browse + import), skill-columns, file-tree, file-viewer.
Notable patterns:
- `createSkillColumns` factory → `useSkillColumns` hook so column
headers flow through useT. Column identity changes per render is
fine — DataTable handles it.
- `validateNewFilePath` (pure helper) → `useValidateNewFilePath` hook
so the 5 validation error messages can be translated.
- skill_files / used_by / description_with_agents use i18next plural
keys (`_one` / `_other`) — the type system collapses these into a
single PluralValue access, so call sites use
`t($ => $.foo, { count })` and i18next picks the form.
- Per glossary, "skill" stays lowercase EN in zh ("新建 skill",
"已删除 skill", "未找到该 skill").
Test wrapper: runtime-local-skill-import-panel.test.tsx now wraps
render() with <I18nProvider> so the assertion on /Import to Workspace/i
matches the EN translation.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on skills/.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate chat namespace
Translates all 10 chat surfaces: FAB tooltip, input placeholders,
message list (replied-in / failed-after / tools group / show-details
/ tool result preview), session history (header + time-ago labels),
chat window (new-chat / restore / expand / minimize / agent + session
dropdowns / starter prompts / empty states), context-anchor button +
card tooltips, no-agent banner, offline / unstable banner, and the
task-status pill (queued / starting up / thinking / typing + tool
labels: running command / reading files / searching code / making
edits / searching web).
Hook conversions:
- formatTimeAgo (chat-session-history) → useFormatTimeAgo
- ElapsedCaption now takes a typed `variant` ("replied" | "failed")
instead of a free-text `verb` so the i18n key is enumerable
- pickStage (task-status-pill) refactored: pure pickStageKeys returns
StageKey + optional ToolKey; useResolveStage maps to localized labels
Translation policy notes:
- Starter prompts ("List my open tasks by priority", etc.) are user
UI when displayed AND the user's input when clicked — translating
them sends the agent the user's locale-native phrasing, which is
the right UX for a CN user using a CN agent.
- buildAnchorMarkdown (chat-window) stays in English: it's an
agent-bound markdown prefix injected into the outgoing message,
not user-facing UI.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate modals namespace
Translates all 11 modal sources: registry (no UI text), backlog-agent-hint,
set-parent-issue, add-child-issue, delete-issue-confirm, feedback,
issue-picker, create-workspace, create-project, create-issue (manual),
quick-create-issue (agent panel).
Notable patterns:
- create-project re-uses useProjectStatusLabels / useProjectPriorityLabels
hooks from views/projects/components/labels — same translation source
as the projects list / detail, no duplication.
- create-issue.tsx: renamed `toast.custom((t) => ...)` callback param to
`toastId` to avoid shadowing the closure-captured useT() `t` function.
- Test wrapper added to modals/create-issue.test.tsx so the two assertions
on rendered modal text (success toast + Create another) match the EN
bundle. modals/create-workspace.test.tsx was already wrapped (workspace
ns commit).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate settings namespace (rest of tabs)
Builds on the appearance-tab + language switcher already shipped in
Phase 0. Translates the remaining 8 settings surfaces: settings-page
shell (left nav + tab keys), account / profile, notifications-tab
(5 group labels + descriptions), tokens-tab (create / list /
revoke / created dialog), workspace-tab (general fields + danger
zone + leave/delete confirmations), members-tab (invite + role
config + revoke / remove flows), repositories-tab, labs-tab,
delete-workspace-dialog.
Hook conversion: members-tab `roleConfig` static const → `useRoleLabels`
hook returning a Record<MemberRole, {label, description, icon}>. The
icon stays as a typed React component (Crown / Shield / User), so
rendering pattern is unchanged at call sites.
Test wrapper: settings/components/delete-workspace-dialog.test.tsx
now wraps render() with <I18nProvider> (custom render() helper)
because the test asserts on rendered button labels ("Delete workspace",
"Cancel", "Deleting...").
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate runtimes namespace (entry surfaces)
Translates the user-facing runtime list page surfaces:
runtimes-page (header / search / filters / chips / empty / no-matches /
bootstrapping), runtime-detail (topbar + delete dialog + delete toasts),
runtime-detail-page (not-found state), shared.tsx (4-state HealthBadge
labels).
Hook conversion: shared `healthLabel(health)` was a pure module-level
function. Added `useHealthLabel` hook for translated call sites; kept
`healthLabel` as an EN-only fallback for non-component callers (column
factory in runtime-columns).
Deferred:
- runtime-list / runtime-columns (data table column headers + cell
bodies) — large surface, not in the page-load critical path.
- connect-remote-dialog / update-section / usage-section — secondary
flows, English remains acceptable until a focused pass.
- charts/* — primarily numeric tooltips and axes; minimal user-visible
text.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate layout namespace (sidebar nav, help, loader)
Translates the cross-cutting layout chrome:
- 9 sidebar nav labels (inbox / my issues / issues / projects /
autopilots / agents / runtimes / skills / settings) — driven by
labelKey instead of inline strings, resolved via useT at render.
- HelpLauncher dropdown (trigger aria + 3 items: Docs / Change log
/ Feedback)
- WorkspaceLoader (named + unnamed loading states)
- SortablePinItem unpin tooltip
Pattern shift in app-sidebar.tsx: nav arrays carry `labelKey: NavLabelKey`
(typed against the layout JSON) instead of `label: string`. The string
comparison checks (`item.label === "Inbox"`) became cleaner ID-based
checks (`item.key === "inbox"`).
Deferred: deeper sidebar surfaces — workspace switcher dropdown,
"New Issue" CTA, "Pinned" / "Workspace" / "Configure" group labels —
remain English. The 9 nav labels are the ones that read in every
session.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate onboarding namespace (welcome + step header)
Translates the user-first-impression surfaces of the onboarding flow:
- step-welcome.tsx (the wordmark, headline, lede paragraphs, all CTAs:
Download Desktop / Continue on web / Start exploring / I've done
this before, illustration caption)
- step-header.tsx ("Step N of M" counter + matching aria-label)
- onboarding-flow.tsx (skip-onboarding error toast)
Test wrapper added to onboarding/components/step-header.test.tsx —
custom render() helper wraps with <I18nProvider> so the "Step 2 of 5"
assertions match the EN bundle.
Deferred (acceptable English fallback for now): step-questionnaire,
step-workspace, step-runtime-connect, step-platform-fork, step-agent,
step-first-issue, cli-install-instructions, option-card, runtime
aside panels, starter-content-prompt, cloud-waitlist-expand. These
are deeper steps with significant copy that would benefit from a
focused dedicated pass — voice on each is more nuanced (questionnaire
options, runtime install instructions, agent template recommendations).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* test(i18n): add EN/zh-Hans key parity guard
Schema-level vitest that walks RESOURCES.en and RESOURCES["zh-Hans"]
namespace by namespace and asserts both bundles cover the same key
set. i18next plural rule is normalized before compare (`_one` /
`_other` collapse to a single logical key) so EN's plural pair
matches zh's `_other`-only form.
Catches retrofit drift where a new EN key lands without zh —
previously this would silently fall back to the English string in
production. Cheap to keep green: 39 tests across 21 namespaces in
under a second.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate issues namespace
Translates the entire issues surface — list / board / detail / comments /
sub-issues / activity feed / batch toolbar / pickers / context menu /
backlog-agent hint dialog / labels panel.
Component coverage:
- issues-page (page header, empty state, move-failed toast)
- issues-header (scope tabs, filter dropdowns w/ status/priority/
assignee/creator/project/label, display settings, sort, view toggle)
- issue-detail (page header, breadcrumb, properties / parent issue /
details / token usage sections, sub-issues, activity timeline,
formatActivity for status/priority/assignee/title/due-date changes,
subscribe/subscriber popover)
- comment-card + comment-input + reply-input (delete dialog, edit/save,
copy/edit/delete row, reply count, placeholders, expand/collapse)
- agent-live-card (is-working banner, tool count, stop / transcript)
- execution-log-section (section header, show/hide past runs, trigger
text builder, status labels, cancel-task)
- batch-action-toolbar (selected count, delete dialog with plurals)
- backlog-agent-hint-dialog (full dialog content)
- labels-panel (intro, create form, list, delete dialog)
- pickers (status / priority / assignee / due-date / label / property
search placeholder + no-results)
- issue-actions-menu-items (all dropdown / context menu items)
- use-issue-actions / use-issue-timeline (toast strings)
STATUS_CONFIG / PRIORITY_CONFIG label rendering routed through
$.status[enum] / $.priority[enum] at every call site; the core config
keeps its English fallback for non-i18n consumers but UI never reads
.label directly anymore.
Tests retrofitted: issues-page, issue-detail, and issue-actions-menu
RTL specs now wrap renders in <I18nProvider> with the EN bundle, so
their string assertions match the bundle (not hardcoded literals).
ESLint i18next allow-list extended to 24 issues files. Verified:
pnpm --filter @multica/views typecheck + test (277/277) all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate agents namespace
Translates the agents listing + detail surface and the create/duplicate
flow. Covers the high-frequency surfaces; deeper sub-tab editors
(activity / instructions / skills / env / custom-args bodies, and the
hooks-buggy runtime/model/concurrency pickers) are deferred — they
have their own pre-existing react-hooks rule violations and benefit
from a focused dedicated pass.
Component coverage:
- agents-page (page header w/ tagline + new button, scope segment,
search, sort dropdown, availability chips, archived toolbar, empty
state, no-matches messaging w/ search interpolation, list-load
error)
- agent-detail-page (back link, archived banner, archive dialog,
not-found state, all 4 toast strings)
- agent-detail-inspector (avatar editor, name + description popover,
description dialog, every PropRow label, validation message,
presence badge label sourced from $.availability[enum])
- agent-overview-pane (tab labels, discard-unsaved-changes dialog)
- create-agent-dialog (title / description / labels / placeholders /
duplicate-suffix / runtime filter buttons / runtime status copy)
- agent-row-actions (full dropdown items + cancel-tasks dialog with
pluralized "N running + M queued" summary + archive dialog + 6 toasts)
- agent-columns (every header cell, You / Archived chips, runtime
fallback labels, availability + workload labels via $.availability /
$.workload, activity tooltip body w/ created_today / created_days_ago
/ runs / failed-percent interpolation)
- inspector/skill-attach (Attach trigger label + aria)
availabilityConfig and workloadConfig now keep colors only — the
display label lives in the bundle, sourced via $.availability[enum]
and $.workload[enum] at every call site. Same pattern as
STATUS_CONFIG/PRIORITY_CONFIG in the issues namespace.
ESLint i18next allow-list extended to 8 agents files.
Verified: pnpm --filter @multica/views typecheck + test (277/277)
all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): clear 30 stray EN strings in translated files
Tail of literal strings missed in earlier passes — the ESLint i18next
allow-list flagged them but they slipped through review. Files touched:
- layout/app-sidebar.tsx (10 keys: Workspaces / Pending invitations /
Create workspace / Join / Decline / Log out / New Issue + shortcut /
Pinned / Workspace / Configure)
- runtimes/components/runtime-detail.tsx (Serving header + serving_count
pluralization, no_agents copy, running/queued chips with count
interpolation, Diagnostics header, CLI label, Delete runtime button,
Technical details toggle, last seen interpolation)
- onboarding/steps/step-welcome.tsx (entire WelcomeIllustration mock —
5 cards × actor names + body copy + 3 mention chips + 2 timestamps;
zh translation reads naturally instead of leaving the demo English)
- settings/components/labs-tab.tsx (`Co-authored-by: ...` git trailer
wrapped in {} so linter sees a JS string, not JSX text — magic
identifier git relies on, must not translate)
- settings/components/members-tab.tsx (✓ glyph wrapped in {})
- modals/feedback.tsx (⌘↵ shortcut wrapped in {})
ServingAgentsCard now reads availability/workload labels from
`agents` namespace (cross-namespace useT) so the bundle-truth pattern
holds: presenceConfig keeps colours only, label text comes from the
shared bundle.
Verified: typecheck + 277/277 tests + lint (only the pre-existing
react-hooks rule-of-hooks errors remain, which task #6 addresses).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(agents): rules-of-hooks + translate 4 model/runtime pickers
Three pre-existing react-hooks/rules-of-hooks violations + one missing
useMemo dep cleared, then the four pickers wired through useT.
Hook order fixes:
- concurrency-picker: useEffect now runs before the !canEdit early
return. Stale-draft reset still works the same way.
- runtime-picker: useMemo for the filtered list moved above the
!canEdit branch.
- model-dropdown: `models = data?.models ?? []` was minting a fresh
array each render and tripping the deps lint of the downstream
useMemo. Wrap in useMemo so the reference is stable.
Translation coverage:
- concurrency-picker: tooltip ("Concurrency · N max..."), range
helper text, Save button.
- runtime-picker: trigger label fallback ("No runtime"), tooltip
text composed from {{name}} + status, Mine/All filter buttons,
empty-list copy, "owned by {{name}}" + status fragments in row
tooltip, Cloud badge, online/offline aria.
- model-picker: trigger label, tooltip, "Managed by runtime"
fallback, search placeholder, "Discovering models…", default
badge, "No models available", "Use \"X\"" custom-id flow, Clear
button + its title.
- model-dropdown: every label string including the "Select a runtime
first" / "Default (provider)" / "Runtime offline — enter manually"
trigger fallbacks, the supported=false explanation block, discovery
failed badge, all popover items.
ESLint allow-list extended to 4 picker files. Verified: typecheck +
277/277 tests + lint (0 errors, only pre-existing react-hooks warnings
in unrelated files).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate runtimes list + connect dialog + CLI updater
Three deep runtime surfaces wired through useT, with the agents
namespace doing double duty for shared availability/workload labels.
runtime-columns:
- 7 column headers via t-augmented createRuntimeColumns({ t }).
- HealthCell now reads from useHealthLabel() (already translation-aware)
instead of the EN-only healthLabel() helper.
- WorkloadCell sources the label from $.workload[enum] (cross-namespace
to agents) — colour stays via workloadConfig.
- CostCell delta "flat" copy + CLI cell "Desktop" badge + update-
available aria/tooltip + RowMenu's full delete dialog (title /
description with {{name}} interpolation / cancel / confirm /
deleting state) plus its admin-permission hint.
connect-remote-dialog:
- Three steps fully translated: instructions (header + 4 numbered
steps + security warning + troubleshooting list with mono code
snippets escaped as JS strings), waiting (loader + hint), success
(CTA pair).
- Mono CLI commands wrapped in {} so linter sees JS strings — those
are literal commands that must stay untranslated for the user to
paste into a terminal.
update-section:
- statusConfig collapsed to icon+colour only; labels move to
$.update.status[enum] for proper translation per-state.
- "CLI Version:" / "Latest" / "available" / "Update" / "Retry"
copy + the "Managed by Desktop" tooltip and disabled hint.
Layout helpers tagged: runtime-list passes `t` through to the column
factory the same way agent-columns does.
ESLint allow-list extended with the 4 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. usage-section.tsx
(KPI cards / WhenChart / TopUsageBreakdown / receipt table) is the
remaining runtimes surface — chart-heavy and benefits from a focused
pass next.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate 5 agent detail tabs + skill-add dialog
The 5 tabs that fill the agent detail right pane plus the shared
skill picker dialog. Agents bundle gains a `tab_body` block with
sub-namespaces per tab + a `common` slot for save/add/unsaved.
Tab coverage:
- instructions-tab: intro paragraph, multi-line example placeholder
(full 18-line zh translation), Save / Unsaved.
- env-tab: read-only intro / empty state, editable intro with two
inline `<code>` env-var examples kept English (mono terminal
payloads), KEY / value placeholders, Show/Hide value aria, Add /
Remove aria, all 3 toasts (duplicate keys / saved / save failed).
- custom-args-tab: intro about whitespace splitting, launch-mode
prefix line + `<your args>` placeholder, --flag value placeholder,
Add, Remove aria, both toasts.
- skills-tab: intro, Add skill button, import-hint callout, empty
state title + hint + add-CTA, remove-failed toast.
- activity-tab: 3 section titles (Now / Last 30 days / Recent work),
active-task pluralization, performance subtitle, all 3 empty
states, runs/success%/avg-duration/failed pluralization with
interpolation, source labels (Issue / Chat / Autopilot / Untracked),
source fallbacks (Quick create / Creating issue / Chat session /
Autopilot run), issue-short fallback, "Triggered by" tooltip
header, open-issue / transcript / cancel-task tooltips and ARIAs,
cancelling state, started/dispatched/queued time prefixes, show
more.
- skill-add-dialog: dialog title + description, empty list copy,
Cancel button, add-failed toast.
skills-tab.test.tsx wrapped in <I18nProvider> with the EN bundle so
its `Local runtime skills are always available` assertion still
matches the resolved translation instead of the raw key path.
ESLint allow-list extended with the 6 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. Only the per-test
mock for skills-tab needed wrapping; the other 4 tabs ship without
test files of their own and inherit the I18nProvider chain via
agent-overview-pane / agent-detail-page test renders (when those
exist later).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate onboarding step-questionnaire + option-card
The user-profile step (3 questions) is the first deferred onboarding
deep step now wired through useT.
step-questionnaire:
- Eyebrow + headline + answered-progress counter with {{count}}
interpolation
- All 3 questions and their option labels (team size / role / use case)
- All 3 "Other" placeholders for free-text fallback
- Right-rail "Why three questions" / "What you get" panel: 2 eyebrow
rows, 2 unlock-item title+body pairs, learn-more link
- Back / Continue buttons via shared `common` block
option-card: shared "Other" radio label and aria.
Test wrapped in <I18nProvider>. EN value of `other_label` kept as
"Other" so the existing /^other$/i regex in step-questionnaire.test
keeps matching after the rendering pipeline switched from a hardcoded
literal to a bundle lookup.
ESLint allow-list extended with these 2 files. The remaining 4 deep
steps (workspace / runtime-connect / platform-fork / agent), the
2 ancillary surfaces (cli-install-instructions / starter-content-
prompt), and the 3 side panels (runtime-aside-panel / cloud-waitlist-
expand / compact-runtime-row) will be surfaced + swept by the global
ESLint switch (next commit).
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): flip ESLint to glob + drain remaining hardcoded EN
ESLint i18next/no-literal-string now applies to **/*.tsx by default
instead of an explicit allow-list. Files that genuinely still need
hardcoded EN are listed in STILL_HARDCODED — concrete, finite, and
the goal is to drain that list to zero.
Tail strings translated in this commit (surfaced by the global flip):
- common/task-transcript/agent-transcript-dialog.tsx — full dialog:
status badge (Running / Completed / Failed), sr-only DialogTitle,
Filter dropdown trigger + Clear filters, Copy all / Copy filtered /
Copied, tool-calls + events metadata chips with pluralization,
events-filtered "{{shown}} of {{total}}" interpolation, "Waiting
for events..." live state, "No execution data recorded." past
state. New `transcript` block in agents namespace.
- runtimes/components/charts/activity-heatmap.tsx — Less / More
legend labels around the contribution-style heat squares.
- search/search-trigger.tsx — sidebar Search... button label.
⌘ glyph wrapped in {} to satisfy the linter (mono shortcut symbol,
not translatable).
Holdouts (STILL_HARDCODED, ~14 files): the deep onboarding steps
(workspace / runtime-connect / platform-fork / agent / first-issue /
cli-install-instructions, plus 4 ancillary panels), the runtimes
usage-section + KPI cards, and 5 minor agent visual primitives
(sparkline / agent-presence-indicator / agent-profile-card /
visibility-badge / char-counter). Each one gets a dedicated future
pass; the global rule prevents new hardcoded strings from landing
elsewhere.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): drain agent visual primitives + onboarding small components
8 files removed from STILL_HARDCODED:
agents/components/:
- char-counter — over-limit text with {{count}} interpolation
- visibility-badge — uses new agents.visibility.{private,workspace}.
{label,tooltip} block; drops VISIBILITY_LABEL/TOOLTIP imports from
core in favour of bundle-driven copy
- agent-presence-indicator — availability + workload labels via
$.availability[enum] / $.workload[enum] (cross-namespace),
queue-badge "+N queued" with pluralization
- agent-profile-card — Agent unavailable / Detail link / Owner /
Skills / Runtime / Unknown runtime / Archived chip / availability
line via cross-namespace lookup
agents.json: new presence + visibility + profile_card + char_counter
blocks.
onboarding/components/:
- compact-runtime-row — online/offline aria via agents.availability
- runtime-aside-panel — full content (What's a runtime / Good to
know / Swap anytime / Add more later / docs link)
- starter-content-prompt — full dialog (title / description with
inline emphasis / both buttons / 3 toasts)
- cloud-waitlist-expand — intro paragraph + warning span / email
+ reason labels + placeholders + Optional badge / Join + on-list
states / both toasts
onboarding/steps/:
- cli-install-instructions — copy aria + intro + 2 step labels
onboarding.json: new runtime_aside / cli_install / starter_content /
cloud_waitlist blocks.
Tests for step-platform-fork + step-runtime-connect wrapped in
<I18nProvider> with EN bundle so /you're on the list/i etc. still
matches the resolved translations.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate onboarding deep steps
The 5 large onboarding steps that were deferred from earlier passes,
plus their support helpers, all wired through useT.
step-first-issue (final beat — flips onboarded_at):
- error_title / Retry / retry_failed toast / finishing / opening
states.
step-agent (creates the user's first agent):
- Templates moved from a module-level const to a useT-driven
useAgentTemplates() hook. Names + emoji stay constant (visual
identity), labels + blurbs + instructions resolve from the
bundle. coding / planning / writing / assistant — all four
templates ship a full zh translation that reads naturally.
- Recommended badge, eyebrow + headline + lede, footer hint,
Create {{name}} CTA, create_failed toast.
- Right-rail "About agents" panel (4 way-items + headline +
add-more hint + docs link).
step-workspace (create or pick existing):
- 5 footer states (open / creating / creating-pending / name-first
/ pick), all hint + CTA strings via interpolation.
- Name + URL + slug placeholders, issue-prefix preview spans,
Create-new card title + subtitle.
- 8-row WorkspacePreviewCard sidebar (Inbox / Issues / Agents /
Projects / Autopilot / Runtimes / Skills / And more) — every
label + meta strapped to bundle keys.
- 4 perks (assign / chat / invite / switch) + 3 next-steps
(runtime / agent / starter), 2 toasts (slug-conflict / failed).
- `multica.ai/${slug}` mono URL escaped via template-literal
expression so the linter sees a JS string.
step-runtime-connect (desktop scan flow):
- 3 phase headlines + ledes (scanning / found / empty), trust-strip
status (all online / N online / none online) with pluralization,
online/offline labels, Skip / Continue / Selected hint.
- Empty-view 2 cards (skip + waitlist) and the cloud waitlist
dialog wrapper.
step-platform-fork (web fan-out):
- Eyebrow + headline + lede, footer hint with 3 phase variants.
- Primary download card (before/after click) + 2 alt cards (CLI /
cloud) + CLI dialog with 4 elapsed-time stages (normal / midway /
slow / stalled), live-listening header, runtime-connected
pluralization, cloud waitlist dialog.
ESLint: STILL_HARDCODED list shrunk from 14 entries to 1 — only
runtimes/components/usage-section.tsx (chart-heavy KPI panel)
remains.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate runtimes usage panel + drop STILL_HARDCODED
Final i18n holdout: the runtimes usage panel (KPI hero, WHEN chart
tabs, cost-by breakdowns, daily breakdown table) is wired through
useT("runtimes"). With this drained, the eslint scaffolding for
explicit holdouts is removed — every JSX text node in @multica/views
now flows through i18n.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): drain rollout gaps + add cross-device sync
Lands the post-review punch list for the i18n rollout: closes correctness
gaps that would have shipped silently, and adds the missing cross-device
locale sync the rollout's docs already promised.
Coverage:
- Register issues + agents namespaces in RESOURCES (90 useT call sites
were rendering keys-as-text in production)
- Harden parity test to compare RESOURCES keys against on-disk JSON
files, so a future missing namespace registration fails loudly
- Server-side language whitelist in UpdateMe + reject-unsupported test
- Safe SupportedLocale resolution in appearance-tab (no more `as` cast
on a region-tagged BCP-47 string)
- HTML lang attribute uses zh-CN (not zh-Hans) for screen reader / CJK
font-stack compatibility
- Cookie Secure flag on https
- Pulled createBrowserCookieLocaleAdapter out of the server-safe entry
into a new @multica/core/i18n/browser subpath; document.cookie access
can no longer leak into Edge middleware imports
Cross-device sync:
- New UserLocaleSync component mounted in CoreProvider; on login, if
user.language differs from the active i18n.language, persist via the
adapter and reload. Both apps benefit
- Desktop main process tracks system locale and emits IPC on focus when
it changes; renderer reloads only when the user has no explicit
Settings choice (their preference still wins)
Tests:
- pickLocale / matchLocale (11 cases incl. region-tagged BCP-47, malformed
tags, zh-Hant collapse-to-zh-Hans semantics)
- browser-cookie-adapter (6 cases under jsdom)
- Shared renderWithI18n helper at packages/views/test/i18n.tsx that wraps
the real RESOURCES map; future tests opt in instead of inlining a
per-file TEST_RESOURCES slice that goes stale silently
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* docs(conventions): consolidate naming + i18n glossary into docs site
Single source of truth for code naming, i18n translation glossary, and
Chinese voice rules. Previously split between packages/views/locales/glossary.md
and scattered comments — now lives at apps/docs/content/docs/developers/conventions.{mdx,zh.mdx}
with both English and Chinese versions kept in sync.
Three sections per page:
1. Code naming — routes, packages, files, DB, Go, TS, commits
2. i18n translation glossary — entity vs concept rule, what to translate,
word combination, plurals, interpolation, key naming
3. Chinese voice + style — punctuation, principles, where to look in doubt
Side effects:
- packages/views/locales/glossary.md collapses to a stub redirecting to
the docs page; do not edit it
- CLAUDE.md gets a new top-level "Conventions reference" section so any
Claude session sees the pointer before any other rule
- apps/docs/content/docs/developers/ gets a stub English meta.json so the
conventions page is reachable on the EN side (contributing.zh.mdx /
architecture.zh.mdx remain ZH-only — separate work)
- Both root sidebars get a new "Developers" group
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): apply zh voice rules + translate project/autopilot
Two-part cleanup driven by the conventions doc landed last commit:
Voice violations (mechanical sweep across 10 zh-Hans namespaces):
- 「」 (Japanese-style brackets) → \" to match the EN source's straight
double quotes (~13 sites)
- … (single-char ellipsis) → ... three dots (~43 sites)
- Drop translation-ese pronoun "我们" where it's a pure narrator
("我们已发送" → "已发送", "我们替你托管" → "由 Multica 托管"); keep
"告诉我们" where "we" is the legitimate brand recipient
- "作为父级 / 作为子级" → "设为父级 / 设为子级"
- "任务" mistranslated as the task entity → `task` (lowercase EN entity)
- Dialog title "Autopilot" → "autopilot"
Translate project / autopilot per industry consensus:
- `project` → 「项目」 (~42 value sites). Feishu / Tower / Teambition /
PingCode / GitHub Projects all translate; no Chinese product keeps
`project`.
- `autopilot` → 「自动化」 (~34 value sites). Avoids the Tesla-style
「自动驾驶」 association; matches Notion / Feishu's industry term.
- Issue / skill / task remain lowercase EN per dev-team familiarity.
- Sidebar nav-label entities get Title Case ("Issue" / "Skill" / "我的
Issue") so the entry-point label reads as a proper UI signal; body
prose stays lowercase.
Conventions doc (EN + ZH) reflects the decision and adds a "why these
translate but issue/skill/task don't" rationale block.
Verification: parity test 45/45, full monorepo typecheck green.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): translate chat session delete + project resources section
Two features main shipped while this branch was idle never went through
the i18n pass:
- Chat session delete confirmation dialog (#2115) and history toggle
tooltip (#2117): adds session_history.delete_dialog.* and
session_history.row_delete_*, plus window.history_show_tooltip /
history_back_tooltip.
- Project resources sidebar (#1926/#2080/#2111): entire component
including toasts, popover form, attach/remove tooltips. New
projects.resources subtree.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
|
||
|
|
baac4080e9 |
fix(installer): correct Windows version parsing and checksum decode (#2093)
Closes #2092 |
||
|
|
6ef711cd35 |
fix: gate dev verification code behind explicit env (#1773)
* fix: gate dev verification code behind explicit env * docs: fold dev verification code into env table * docs: clarify fixed verification code opt-in --------- Co-authored-by: Eve <[email protected]> |
||
|
|
7f6776b12f |
fix: harden Windows CLI architecture detection
* fix: harden windows cli architecture detection * fix: avoid duplicate windows architecture signals --------- Co-authored-by: Eve <[email protected]> |
||
|
|
153e2b6245 | Enhance OS architecture detection methods in install.ps1 (#1498) |