25 Commits

Author SHA1 Message Date
Bohan Jiang
ad803b86ec fix(skills): shared-state runtime local-skill stores (MUL-1288) (#1557)
* fix(skills): shared-state runtime local-skill stores (MUL-1288)

Fixes the bug Bohan surfaced on MUL-1288: behind prod's multi-node API the
runtime-local-skill list/import flow would intermittently time out or 404.
Root cause: LocalSkillListStore and LocalSkillImportStore were per-process
sync.Mutex+map, so when the frontend POST, the daemon heartbeat and the
frontend GET landed on different API instances, each saw a different
pending set. Confirmed against production daemon logs — the failed
request_id never showed up in the daemon's "runtime local skills
requested" log, even though other requests around the same window worked.

Per Yushen's guidance (server must stay stateless; state lives in
storage), migrate both stores to Redis so every node agrees on the same
pending set.

What changed
- LocalSkillListStore / LocalSkillImportStore are now interfaces. Methods
  take context.Context and return error.
- InMemoryLocalSkill{List,Import}Store — renamed from the existing types,
  kept as the default for single-node dev and the in-process test suite.
- RedisLocalSkill{List,Import}Store — new. Keyed on
  mul:local_skill:{list,import}:<id> (JSON record, TTL = retention), with
  a per-runtime ZSET mul:local_skill:{list,import}:pending:<runtime_id>
  (score = created_at UnixNano) providing cross-node ordering. PopPending
  wins the claim via ZREM == 1, so concurrent pops from different nodes
  never return the same request twice.
- NewRouter gets an optional *redis.Client; when non-nil it swaps in the
  Redis-backed stores. main.go hoists the existing Redis client (already
  used by the realtime relay) so both subsystems share one client.
- Handler fields flip to interface types; handler.New still constructs
  in-memory stores by default.
- Daemon heartbeat's PopPending call sites thread r.Context() through so
  Redis operations inherit request cancellation. Errors warn instead of
  poisoning the heartbeat response.

Tests
- Existing in-memory tests updated for the new signatures (ctx + error).
- New runtime_local_skills_redis_store_test.go covers:
  - Create/Get/Complete round trip preserves skills payload
  - PopPending across two *store instances sharing one rdb (the exact
    regression: node A creates, node B pops)
  - N concurrent PopPending on one record => exactly one winner
  - Pending-timeout threshold transitions the record and removes the zset
    member so a later PopPending doesn't return a timed-out request
  - Import store round-trips CreatorID (which is json:"-" on the public
    struct — needs a Redis envelope so ReportLocalSkillImportResult can
    still attribute the created Skill)
  - Per-runtime isolation — a PopPending for runtime B does not disturb
    A's pending zset
- Tests skip gracefully if REDIS_TEST_URL is unset; CI now spins up a
  redis:7-alpine service and exports the URL so the suite actually runs
  there.

Out of scope
PingStore / UpdateStore / ModelListStore have the same shape and the
same latent bug (they just fire rarely enough to have gone unnoticed).
Migrating them to Redis is a follow-up — MUL-1288 is specifically the
local-skills break Bohan is blocked on.

* fix(skills): atomic Redis claim + surface store write failures (PR #1557 review)

Two real gaps GPT-Boy flagged:

1. RedisLocalSkill{List,Import}Store.PopPending was doing ZREM then SET as
   two separate round-trips. If the SET failed for any reason — transient
   Redis error, context cancellation, pod getting SIGKILL'd mid-call — the
   request was already gone from the pending zset but the stored record
   still said "pending", and no subsequent PopPending would re-dispatch
   it. Exactly the "request disappears" class of bug this PR is supposed
   to kill.

   Fix: push the claim into a Lua script so Redis runs ZREM + SET as one
   atomic unit. If ZREM returns 0 (another node won the race), SET is
   skipped and the caller retries.

2. ReportLocalSkill{List,Import}Result handlers were logging Complete/Fail
   store failures at Warn and still returning 200 OK. That made the
   daemon think the report landed when it hadn't, leaving the request
   stuck in "running" until the server-side timeout and — worse for the
   import flow — leaving the just-created Skill row orphaned in Postgres
   so every retry collided with the unique-name constraint.

   Fix: escalate to Error + return 500 so the daemon (and monitoring) can
   see the write failed. For the import flow, Complete failure after the
   Skill row is already committed also triggers a best-effort DeleteSkill
   so a daemon retry lands on a clean slate instead of hitting
   "a skill with this name already exists" forever.

Tests
- New TestRedisLocalSkillListStore_PopPendingAtomicClaim asserts the
  happy-path invariant: after one PopPending the record is "running"
  AND a second PopPending returns nothing. Deliberately does NOT poke
  Redis internals directly so the test survives any future key-layout
  refactor.
- Existing cross-instance / concurrent / timeout / per-runtime tests
  continue to pass against the Lua-based claim path (verified locally
  against a scratch redis-server; 8/8 Redis tests green).
2026-04-23 17:07:34 +08:00
LinYushen
f98a67dd90 ci(release): build docker images natively per arch and merge manifests (#1507)
Multi-arch images were built on a single amd64 runner with QEMU
emulating arm64. The Next.js build (Dockerfile.web) under emulation
took 30+ minutes per release and was the long pole of the workflow.

Split each image build across two native runners (amd64 on
ubuntu-latest, arm64 on ubuntu-24.04-arm), push by digest, then
merge into a manifest list with docker buildx imagetools. QEMU is
no longer needed.

Backend and web each become a (matrix build + merge) pair, replacing
the previous single docker-images job. Per-platform GHA cache scopes
avoid cross-arch cache eviction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 18:28:54 +08:00
devv-eve
fbf41bde73 feat(selfhost): ship public GHCR deployment flow
Publish stable GHCR self-host images, switch self-host deploys to official image pulls with a source-build fallback, and move self-host signup / Google OAuth config onto runtime /api/config.
2026-04-22 16:58:42 +08:00
yushen
20c9d985f5 ci: clarify release tag filters 2026-04-21 17:24:20 +08:00
yushen
6ecf15e62c ci: add desktop smoke build workflow 2026-04-21 16:44:19 +08:00
LinYushen
6f63fae41a feat(desktop): support macOS cross-platform packaging (#1262)
* feat(desktop): support macOS cross-platform packaging

* fix(desktop): use releaseType instead of publishingType in electron-builder publish config

publishingType is not a valid electron-builder key; the correct GitHub
provider option is releaseType. The previous value was silently ignored,
causing uploads to be skipped and breaking auto-update.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(release): standardize artifact naming across desktop and CLI

Unified scheme: `multica-<kind>-<version>-<platform>-<arch>.<ext>` so a
filename alone reveals kind, version, platform, and CPU arch.

Desktop (apps/desktop/electron-builder.yml):
  mac     → multica-desktop-<v>-mac-<arch>.{dmg,zip}
  linux   → multica-desktop-<v>-linux-<arch>.{deb,AppImage}
    (fixes `\${name}` expanding the scoped `@multica/desktop` into a
    broken `@multica/desktop-*` filename path)
  windows → multica-desktop-<v>-windows-<arch>.exe

CLI (.goreleaser.yml):
  multica_<os>_<arch>.tar.gz → multica-cli-<v>-<os>-<arch>.tar.gz
  (adds `-cli` marker + version; switches `_` to `-` for consistency)

Matrix update in apps/desktop/scripts/package.mjs `--all-platforms`:
  - drop mac x64 (Intel not a target yet)
  - add linux arm64
  Final: mac arm64, win x64/arm64, linux x64/arm64.

Downstream updates so install paths match the new CLI names:
  - scripts/install.sh
  - scripts/install.ps1 (URL + checksum regex)
  - CLI_INSTALL.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(release): use multica_{os}_{arch} CLI archive naming

Standardize on the GoReleaser default 'multica_{os}_{arch}.{tar.gz|zip}'
asset names. Install scripts and the desktop CLI bootstrap now resolve
assets via checksums.txt so they work without hardcoding versions.

The Go self-update path queries the GitHub release API and accepts
either the new or legacy 'multica-cli-<version>-...' names so existing
releases keep updating cleanly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(release): ship both legacy and versioned CLI archive names

GoReleaser now produces both 'multica_{os}_{arch}.{ext}' (legacy) and
'multica-cli-{version}-{os}-{arch}.{ext}' (versioned) archives in every
release. The legacy name keeps already-released CLIs self-updating; the
versioned name is what new clients should use going forward.

Self-update / install paths flipped to prefer the versioned name and
fall back to legacy:
  - server/internal/cli/update.go (multica update)
  - apps/desktop/src/main/cli-release-asset.ts (desktop CLI bootstrap)
  - scripts/install.sh, scripts/install.ps1 (fresh install)

Homebrew formula is pinned to the versioned archive via 'ids: [versioned]'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(desktop): also build Linux .rpm packages

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(release): build Linux/Windows Desktop installers in CI; detect Windows ARM64 in install.ps1

Address review feedback on PR #1262:

- .github/workflows/release.yml: add a 'desktop' job that runs after the
  CLI 'release' job and packages the Desktop installers for Linux
  (AppImage/deb/rpm) and Windows (NSIS) on x64 and arm64, then publishes
  them to the same GitHub Release via electron-builder. macOS Desktop
  continues to ship through the manual release-desktop skill so it can
  be signed and notarized with Apple Developer credentials.

- scripts/install.ps1: detect Windows ARM64 hosts via
  RuntimeInformation::OSArchitecture so the new windows-arm64 CLI
  archive is downloaded on ARM64 machines instead of always falling
  back to amd64.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(release): split Windows arm64 auto-update channel to avoid latest.yml collision

electron-builder's update metadata file is hardcoded to `latest.yml` for
Windows regardless of arch (only Linux gets an arch-suffixed name; see
app-builder-lib's getArchPrefixForUpdateFile). With two separate
electron-builder invocations for Windows x64 and arm64, both publish
`latest.yml` to the same GitHub Release and the second upload silently
overwrites the first — leaving one of the two architectures with auto-
update metadata pointing at the other arch's installer.

Route Windows arm64 to its own `latest-arm64` channel:

* scripts/package.mjs appends `-c.publish.channel=latest-arm64` only
  for the Windows arm64 invocation, so x64 keeps producing `latest.yml`
  and arm64 produces `latest-arm64.yml` alongside it.
* updater.ts pins `autoUpdater.channel = 'latest-arm64'` on Windows
  arm64 clients so they fetch the matching metadata file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 23:33:41 -07:00
LinYushen
133a1f1c16 ci(release): restrict tag pattern to semver and reject -dirty tags (#1280)
The release workflow previously triggered on 'v*', which matched a
stray 'v0.2.5-dirty' tag pushed to the repository. GoReleaser ran
again and overwrote the Homebrew formula with a 0.2.5-dirty version
whose tarball URLs 404.

Tighten the trigger to semver-shaped tags and add an explicit guard
that fails the job if the tag name contains '-dirty' (which can come
from 'git describe --tags --dirty').

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 07:34:25 -07:00
Bohan Jiang
b1b66ab05d ci: exclude apps/docs from frontend build/typecheck/test (#1279)
Docs site is no longer auto-deployed via Vercel (disabled in
dashboard), so building it on every PR adds friction without
catching anything actionable. Use turbo's negative filter to
skip @multica/docs across all three tasks.
2026-04-17 22:00:23 +08:00
LinYushen
fc0ef0fcd8 fix(ci): update backend Go version from 1.24 to 1.26.1 (#337)
Align the CI backend job with the Go version declared in server/go.mod
and used in the Dockerfile (golang:1.26-alpine).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:11:05 +08:00
Naiyuan Qing
26d3ecd06d fix(ci): replace turbo commands with direct pnpm calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:01:24 +08:00
yushen
22f1005537 fix(release): add test step, fix checkout version, improve brew formula
- Fix actions/checkout@v6 → v4 (v6 doesn't exist)
- Add Go test step before GoReleaser to prevent shipping broken binaries
- Add license and test block to Homebrew formula

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:32:47 +08:00
yushen
4c4f6c4a60 fix(release): use brews instead of homebrew_casks for CLI formula
homebrew_casks is for macOS .app bundles; brews is the correct
GoReleaser v2 key for CLI binaries. Also read Go version from go.mod
instead of hardcoding it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:26:34 +08:00
yushen
872d2d10bd fix(release): match Go version to go.mod (1.26)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:21:23 +08:00
yushen
2f54665833 feat(release): add GoReleaser config and Homebrew tap support
Enable `brew install multica-cli` via multica-ai/homebrew-tap.
GoReleaser builds cross-platform binaries (macOS/Linux, amd64/arm64)
and auto-updates the Homebrew formula on tagged releases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:15:09 +08:00
Jiayuan Zhang
6b6e8ac4e4 fix(ci): provision postgres for backend tests 2026-03-24 14:29:56 +08:00
Jiayuan Zhang
d4f5c5b16f feat: pivot to AI-native task management platform (#232)
Replace the agent framework codebase with a new monorepo structure
for an AI-native Linear-like product where agents are first-class citizens.

New architecture:
- server/ — Go backend (Chi + gorilla/websocket + sqlc)
  - API server with REST routes for issues, agents, inbox, workspaces
  - WebSocket hub for real-time updates
  - Local daemon entry point for agent runtime connection
  - PostgreSQL migration with 13 tables (issue, agent, inbox, etc.)
  - WebSocket protocol types for server<->daemon communication
- apps/web/ — Next.js 16 frontend
  - Dashboard layout with sidebar navigation
  - Route skeleton: inbox, issues, agents, board, settings
- packages/ui/ — Preserved shadcn/ui design system (26+ components)
- packages/types/ — Full API contract types (Issue, Agent, Workspace, Inbox, Events)
- packages/sdk/ — REST ApiClient + WebSocket WSClient
- packages/store/ — Zustand stores (issue, agent, inbox, auth)
- packages/hooks/ — React hooks (useIssues, useAgents, useInbox, useRealtime)
- packages/utils/ — Shared utilities

Removed: apps/cli, apps/desktop, apps/mobile, apps/gateway,
packages/core, skills/, and all agent-framework code.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 17:55:49 +08:00
Jiayuan Zhang
8c5ff8c6ff Merge pull request #186 from multica-ai/dependabot/github_actions/actions/cache-5
chore(deps): Bump actions/cache from 4 to 5
2026-02-15 02:40:44 +08:00
Jiayuan Zhang
828c32f5e5 Merge pull request #185 from multica-ai/dependabot/github_actions/actions/setup-node-6
chore(deps): Bump actions/setup-node from 4 to 6
2026-02-15 02:40:10 +08:00
dependabot[bot]
40be100c1c chore(deps): Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 18:15:23 +00:00
dependabot[bot]
2a7cbfa45d chore(deps): Bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 18:15:20 +00:00
dependabot[bot]
72249254bc chore(deps): Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 18:15:17 +00:00
Jiayuan Zhang
1a85cc104a fix(ci): use pnpm --filter for lint instead of turbo
turbo lint fails for all packages due to eslint binary resolution
issues. Fall back to pnpm --filter which handles node_modules/.bin
PATH correctly. Scope matches original CI (web + desktop).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 02:09:47 +08:00
Jiayuan Zhang
8c51cf9f9a ci: add test step to CI pipeline
Add test task to turbo.json and include it in the build-and-typecheck
job. Turbo handles the dependency graph: builds deps first, then runs
typecheck and test in parallel for each package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 02:03:33 +08:00
Jiayuan Zhang
2727ecd803 ci: parallelize jobs and add turbo cache persistence
Split single CI job into parallel lint + build-and-typecheck jobs.
Add turbo cache via actions/cache for faster subsequent runs.
Use single turbo invocation for build+typecheck to optimize task
scheduling. Add concurrency group to cancel stale PR runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 01:32:19 +08:00
Jiayuan
a86774e7b5 chore(ci): add GitHub Actions workflow (#28) 2026-01-30 13:51:05 +08:00