From 0f179ab7b494d60ac1b9b4a1b340a9ec2e8dce5e Mon Sep 17 00:00:00 2001 From: jeffrey Date: Thu, 12 Nov 2026 21:58:32 +0800 Subject: [PATCH 1/3] ci: reject CJK ideographs in changed files The repository is English-only. The repository gate now fails when a changed file contains CJK ideographs. Kana, Hangul, and full-width punctuation stay allowed, so tests can still use unspaced scripts. Co-Authored-By: Claude Code --- scripts/ci/check-repository-gate.sh | 10 ++++++++++ scripts/ci/check-repository-gate.test.ts | 23 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/ci/check-repository-gate.sh b/scripts/ci/check-repository-gate.sh index 5bfe1275..811ec189 100755 --- a/scripts/ci/check-repository-gate.sh +++ b/scripts/ci/check-repository-gate.sh @@ -40,6 +40,7 @@ pnpm env:inventory:check # Skill versions are immutable and never reformatted. prettier_files=() eslint_files=() +changed_files=() while IFS= read -r -d '' file; do [[ "$file" =~ ^skills/builtin/[^/]+/[^/]+/ ]] && continue [[ -f "$file" ]] || continue @@ -51,6 +52,7 @@ while IFS= read -r -d '' file; do case "$file" in *.cjs | *.cts | *.js | *.jsx | *.mjs | *.mts | *.ts | *.tsx) eslint_files+=("$file") ;; esac + changed_files+=("$file") done < <(git -c core.quotePath=false diff -z --name-only --no-renames --diff-filter=ACMRT "$base" "$head_sha") if ((${#prettier_files[@]} > 0)); then pnpm exec prettier --check "${prettier_files[@]}" @@ -59,4 +61,12 @@ if ((${#eslint_files[@]} > 0)); then pnpm exec eslint "${eslint_files[@]}" fi +# The repository is English-only. Only CJK ideographs are rejected, so tests +# may still use kana, Hangul, and full-width punctuation as unspaced scripts. +if ((${#changed_files[@]} > 0)) && + LC_ALL=C git grep -nIP '(*UTF)[\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{F900}-\x{FAFF}]' -- "${changed_files[@]}"; then + echo "Changed files contain Chinese characters; the repository is English-only (see AGENTS.md)." >&2 + exit 1 +fi + make check-scripts-fast check-docs diff --git a/scripts/ci/check-repository-gate.test.ts b/scripts/ci/check-repository-gate.test.ts index e8662672..ef7871e6 100644 --- a/scripts/ci/check-repository-gate.test.ts +++ b/scripts/ci/check-repository-gate.test.ts @@ -20,6 +20,7 @@ const cleanEnv = Object.fromEntries( interface GateOptions { changedPath?: string; + changedContent?: string; // Delete this base file in the head commit. deletedPath?: string; // Pass a target branch that moved past the merge base, as a local run does. @@ -57,7 +58,10 @@ function runGate(options: GateOptions = {}) { if (options.deletedPath) git("rm", "-q", options.deletedPath); const changed = options.changedPath ?? "apps/web/src/example.ts"; mkdirSync(join(root, changed, ".."), { recursive: true }); - writeFileSync(join(root, changed), "export {};\n"); + writeFileSync( + join(root, changed), + options.changedContent ?? "export {};\n", + ); git("add", "."); git("commit", "-q", "-m", "head"); const head = git("rev-parse", "HEAD"); @@ -137,6 +141,23 @@ describe("local repository gate", { timeout: 60_000 }, () => { ]); }); + it("rejects changed files with CJK ideographs and allows kana", () => { + const ideographs = runGate({ + changedContent: 'export const greeting = "\u4f60\u597d";\n', + }); + expect(ideographs.status).not.toBe(0); + expect(ideographs.stderr).toContain("English-only"); + expect(ideographs.commands).not.toContain( + "make check-scripts-fast check-docs", + ); + + const kana = runGate({ + changedContent: + 'export const sample = "\u3072\u3089\u304c\u306a\u3002";\n', + }); + expect(kana.status).toBe(0); + }); + it("formats and lints only existing changed files outside published Skill versions", () => { const skill = runGate({ changedPath: "skills/builtin/example/1.0.0/scripts/run.js", From c6f0b265cc97039eae1187f6cfd85e52786705e3 Mon Sep 17 00:00:00 2001 From: jeffrey Date: Thu, 12 Nov 2026 21:58:37 +0800 Subject: [PATCH 2/3] docs: slim agent instructions and move desktop packaging details Claude Code injects apps/desktop/AGENTS.md whenever a desktop file is read, so the Source Package and Release sections move verbatim to the Packaging Details section of docs/desktop-release.md, with a pointer left behind. The root AGENTS.md names one term, fast checks, for what it called cheap gates and fast boundary checks; states the English-only rule once; and describes the shared-configuration and cleanup rules as outcomes. Co-Authored-By: Claude Code --- AGENTS.md | 35 ++++---- apps/desktop/AGENTS.md | 172 ++------------------------------------- docs/desktop-release.md | 175 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 197 insertions(+), 185 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d3d2d097..f00e1f6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,13 +21,12 @@ Memory, and Voice services. - `make -C apps/teamhub test`: run TeamHub unit tests. - `pnpm contracts:generate && pnpm contracts:verify`: regenerate and verify OpenAPI consumers. - `make check BASE=`: run every app's fast check in parallel. + Fast checks are local and need no Postgres, containers, or full suite. `BASE` is required because the built-in Skill immutability check compares against it; the repository assumes no default branch. - `make check-repository-gate BASE=`: run the repository gate (migration history, commitlint, env inventory, changed-file format and lint, script tests, doc links). -- Before opening a pull request, run the cheap gates listed under Important - Conventions. Those are not the full suite. ## Structure @@ -58,13 +57,11 @@ Memory, and Voice services. - Implement the simplest solution that satisfies the current request. Do not design for requirements outside the current scope, and prefer existing dependencies over new custom infrastructure. -- Before changing shared configuration (root or - shared tool configs, shared scripts, env defaults), find every consumer with - `rg` and check each affected app in the same pull request, not only the one being - fixed. -- After the feature works, perform one explicit deletion pass: identify and - remove code that is no longer needed and abstractions that the current - request does not require. +- A change to shared configuration (root or shared tool configs, shared + scripts, env defaults) is done when every app that consumes it passes its + fast checks in the same pull request, not only the one being fixed. +- A feature is done when no code it made unnecessary and no abstraction the + request does not require is left behind. - When retiring or reshaping a feature, delete its code and drop its tables, columns, rows, and stored files in a forward migration. Data that must survive is converted once inside the migration; do not keep dual-read or @@ -72,15 +69,14 @@ Memory, and Voice services. caches written by the Daemon or CLI on users' machines, but not compatibility with third-party tool versions such as Codex CLI. - During local development, run the focused tests for the changed behavior and - the affected fast boundary checks. Focused Vitest or Go unit tests do not - replace those gates. Do not run complete integration, E2E, container, or + the affected fast checks. Focused Vitest or Go unit tests do not + replace those checks. Do not run complete integration, E2E, container, or reliability suites by default; `make check BASE=` runs only the fast checks. Run a focused heavy scenario when changing that test or harness, reproducing its failure, or when real database, process, concurrency, or recovery semantics cannot be proved at a lower layer. -- Before opening a pull request, run the cheap gates for every touched app. - These are local and do not need Postgres, containers, or the full suite: +- Before opening a pull request, run the fast checks for every touched app: - Web: `make check-web` (typecheck, unit tests, and React Doctor). If the change adds a static import on the Room or app initial graph, also run `pnpm --filter @linger/web run build:assets` to check the initial JS @@ -120,8 +116,12 @@ Memory, and Voice services. or would break a mode (for example `NODE_ENV`, `COMPOSE_PROJECT_NAME`). Every fixed local default for `pnpm dev` lives in `scripts/local-defaults.env`; `scripts/with-local-env.mjs` only merges files. -- The repository is English-only, human-facing docs included. Write new and - changed doc text in English and do not add Chinese to any existing doc. +- The repository is English-only: docs, code, comments, logs, user-facing + strings, prompts, Skills, tests, fixtures, and benchmark data. + `make check-repository-gate` rejects CJK ideographs in changed files. + - NO: a test fixture with a literal Chinese sentence. + - OK: the same text as escaped code points (`"\u4f60\u597d"`), or kana, + accented Latin, or emoji when any unspaced script will do. ## Cross-Module Conventions @@ -147,7 +147,7 @@ to comply. `HTTP`). - TypeScript: export functions as `function` declarations; object types are `interface` (lint-enforced); private class state uses `#field`. -- Comments are English and explain why or state an invariant; never restate +- Comments explain why or state an invariant; never restate the code. No issue or pull request numbers, plan-step labels, or history ("previously", "used to"). Exported Go declarations and packages have doc comments; `make -C apps/teamhub lint-go BASE=` checks this on new @@ -156,8 +156,7 @@ to comply. third-party error prose, model prose, or our own human-readable labels. Carry the decision in structured fields, error codes, or model structured output, and keep human-readable text for display only. -- Code, comments, logs, user-facing strings, prompts, Skills, tests, fixtures, - and benchmark data contain no Chinese. Serve users of any language through +- Serve users of any language through language-agnostic mechanisms: engine language settings, structured fields, model structured output, and prompts that answer in the user's language. Never add per-language keyword lists, regexes, or model-facing examples. diff --git a/apps/desktop/AGENTS.md b/apps/desktop/AGENTS.md index ca87b60d..952ae81e 100644 --- a/apps/desktop/AGENTS.md +++ b/apps/desktop/AGENTS.md @@ -139,7 +139,7 @@ Gateway and runs Agents. The daemon exits with the app. engines pass them to the services whenever they are configured (an edit runs a new job even after a finished one); Native also starts its bundled LiveKit whenever this computer has voice tools, with or without a voice - key (see Release). There is no settings page + key (see Source Package and Release). There is no settings page after setup yet: keys change on the next Install or Try Again. - `src/main/diagnostics.ts`: Diagnostics, "Show Logs" (opens `/logs`) and "Save Diagnostics" (a redacted @@ -244,172 +244,12 @@ Gateway and runs Agents. The daemon exits with the app. the app's own Node (`ELECTRON_RUN_AS_NODE`) in packaged builds and on the `node` in PATH from source. -## Source Package +## Source Package and Release -Release builds set `LINGER_SOURCE_ARCHIVE_URL` (https) and -`LINGER_SOURCE_ARCHIVE_SHA256` before `node build.mjs`. Produce them from the -release tag in the repository root: - -```sh -SOURCE_ARCHIVE_REF=v1.2.3 \ -SOURCE_ARCHIVE_URL_BASE=https://github.com/simplexai-labs/linger/releases/download/v1.2.3 \ - make source-archive -``` - -Upload `.linger-runtime/source-archive/linger-v1.2.3.tar.gz` to that release -and take both values from `source-archive.json` next to it. One commit always -gives the same digest. To ship the package inside the app instead (about -45 MB more, no hosting needed), set `LINGER_SOURCE_ARCHIVE_FILE` to the -archive; the build copies it beside the installer and pins its digest. Without -either (and without `LINGER_CHECKOUT`) the build does not offer a -local install. - -A downloaded source package can have a dependency store for each platform -beside it, so the native install of its packages needs no registry. Build it -from the same tag, on the platform it serves, and set -`LINGER_SOURCE_DEPENDENCIES_URL` and `LINGER_SOURCE_DEPENDENCIES_SHA256` from -`source-dependencies-.json` for that platform's build: - -```sh -SOURCE_ARCHIVE_REF=v1.2.3 \ -SOURCE_ARCHIVE_URL_BASE=https://github.com/simplexai-labs/linger/releases/download/v1.2.3 \ - make source-dependencies -``` - -`SOURCE_DEPENDENCIES_TARGET` builds another platform's store instead (the -`source-dependencies` CI job does this on Linux). Such a store skips install -scripts, so the install still compiles or downloads native modules such as -better-sqlite3. The desktop downloads the store after the source package -(resuming an interrupted download) and unpacks it into -`.linger-runtime/pnpm-store` of the source; when that fails it logs -`local_server.dependencies_failed` and the installer downloads the packages -itself. - -`LINGER_SOURCE_ARCHIVE_MIRRORS` and `LINGER_SOURCE_DEPENDENCIES_MIRRORS` list -other https hosts serving the same files, separated by spaces. The desktop moves -to the next one when a host fails or stalls, and the SHA-256 still decides. - -Native installs also need the publisher's pinned localmodels tool and native -PostgreSQL package for the target. They are pinned in -`distribution/.json`, in the shape `packages/local-installation/src/native-dependencies.ts` reads -(`platform`, `arch`, `localmodelsTool`, `nativeSources`, `voiceTools`, each -source with an optional `mirrors` list of https URLs for the same file). The build copies the -file of its target (`LINGER_DESKTOP_DAEMON_TARGETS`, else this machine) beside -the bundled installer, which reads it at start; `LINGER_DESKTOP_DISTRIBUTION` -names another file instead. A source with `file` (Windows) instead of `url` -names an archive the app ships: the build copies it from -`LINGER_NATIVE_POSTGRES_DIR` (default `~/.cache/linger-desktop`) to -`dist/installer/native/` after checking its size and sha256, and the installer -checks them again before extracting it, as it does for a download. Where the -file has no PostgreSQL for the computer, or the bundled archive is missing, -the setup page does not offer installing on this computer. `src/main/distribution.test.ts` checks that each file holds its -target's PostgreSQL and the localmodels release `scripts/installs/stack.sh` -pins. - -## Release - -Each desktop target (`darwin-arm64`, `linux-x64`) gets its own distribution -file and build. For one release: - -1. Make the source package as above and upload it. On a host of each target, - make its dependency store and upload it beside the package. -2. On a host of each target, build the PostgreSQL package with - `node scripts/prepare-native-postgres.mjs --output DIR --base-url URL`; it - refuses to cross-build. Before uploading, check `initdb`, a connection and - `CREATE EXTENSION vector` against a private data directory. Upload the - `.tar.gz` to `URL` and keep `postgres-.json` from `DIR`. Windows - repackages the pinned EDB binaries, builds only pgvector and adds the - Visual C++ runtime the EDB zip leaves out, so run it there from an x64 - Native Tools Command Prompt for Visual Studio. The installer accepts only - credential-free HTTPS URLs, so `URL` must be the HTTPS CDN. -3. For a new PostgreSQL or localmodels release, update - `distribution/.json`: `localmodelsTool` for the localmodels - release `scripts/installs/stack.sh` pins, and `nativeSources` holding that target's - `postgres-.json` object. -4. Build with `LINGER_DESKTOP_DAEMON_TARGETS` set to the target and the source package - (`LINGER_SOURCE_ARCHIVE_FILE`, or the URL and SHA256, plus the target's - `LINGER_SOURCE_DEPENDENCIES_URL` and SHA256) set, then package (`packaging/mac/package.py ---arch arm64` for macOS, `packaging/linux/package.py` for Linux). -5. Check the package on a clean computer: Quick Install with the default - source ends on "Linger is ready" and Open Linger shows the local server, - signed in, with this computer connected. - -Official releases come from the public simplexai-labs/linger repository: a -`desktop-v` tag pushed there runs steps 4 and the packaging for -`darwin-arm64`, `linux-x64` and `win32-x64` in GitHub Actions and publishes a -linger GitHub Release with `SHA256SUMS.txt`; see -[`docs/desktop-release.md`](../../docs/desktop-release.md). The workflow -fetches the bundled archives from the assets of the `native-deps` prerelease -there, so upload a new pinned archive to it before tagging. - -Windows, Linux x64 and Apple silicon bundle PostgreSQL in the app instead -(`file` in `distribution/.json`; 35 MB on Windows, 10–12 MB -elsewhere), so Native works without the CDN. Put the pinned archives in -`LINGER_NATIVE_POSTGRES_DIR` before `node build.mjs`; the build fails without -them. To download it instead, replace `file` with the HTTPS `url` once it is on -the CDN. Native PostgreSQL refuses an elevated Linger ("Run as -administrator"), as it refuses root elsewhere. - -Every target also bundles the voice tools (`voiceTools`, about 40 MB each): -the official LiveKit server release (Apple silicon: built from the tagged -source with the pinned Go, since LiveKit publishes no macOS binary; without -cgo, so its vendored go-osstat reports an idle CPU, which only matters when -LiveKit spreads rooms over several servers) and a -static LGPL ffmpeg/ffprobe (Linux and Windows: built from the FFmpeg release -in the pinned Alpine image, Windows with MinGW-w64; Apple silicon: the -pinned serversideup LGPL build, which links OpenSSL statically). Make them on -Linux with `node scripts/prepare-native-voice-tools.mjs --target ---output DIR` (Docker; Go for Apple silicon), copy the archive to -`LINGER_NATIVE_POSTGRES_DIR` and `voice-tools-.json` from `DIR` into -`voiceTools`. The build is reproducible except for the Go toolchain download, -and it checks the versions, the ffmpeg components the voice workers use, -static linking and the Mach-O signatures. Licenses ship in `licenses/` of the -archive. On every install and start, whether or not a voice key is saved, -Native extracts the archive into -`tools/native/components`, puts its `bin` first on the services' `PATH` -(TeamHub and the voice workers run `ffmpeg`/`ffprobe` from it) and runs -`livekit-server` as the `livekit` Native service: loopback only, HTTP and -WebSocket on one free port and media on one free UDP port, both kept with -the generated API key and secret in the owner-only `livekit.env` of the -instance, logs in `native/logs/livekit.log`. ICE over TCP stays off because -LiveKit binds it on every interface. Without the tools for the computer, or -when they fail to prepare, Native logs it, keeps voice off, gives TeamHub -`TEAMHUB_VOICE_MEDIA=off` and finishes the install. The macOS tools are ad-hoc signed by their linker and -live in the archive, outside the app's signature, like PostgreSQL. - -A build with a source package for `win32-x64`, `linux-x64` or `darwin-arm64` -also ships TeamHub prebuilt (`scripts/build-teamhub.mjs`): build.mjs -cross-compiles `teamhub`, `migrate` and `account` from that package (`go` in -PATH, `CGO_ENABLED=0`) into `dist/installer/native/teamhub-/` with a -`manifest.json` of their sha256, the package's sha256 and the commit -`git archive` recorded in it; a downloaded package -(`LINGER_SOURCE_ARCHIVE_URL`) is fetched for this. The package scripts -refuse binaries that do not match the manifest or the bundled package -(`packaging/native_bundle.py`). The desktop copies them into the unpacked source -(`.simplexai-runtime/prebuilt/`), the installer checks them again and then -skips the Go download, and `scripts/source-native.mjs` runs them (checking the -sha256 before each start) instead of `go run` / `go build`. Without them, or -when they do not verify, Native downloads Go and compiles as before. - -The same targets ship the services Native runs, built from that package -(`scripts/build-native-runtime.mjs`): Web's production files, Node -Services bundled with esbuild beside the node_modules it loads at runtime, -and the pinned Node.js, in `dist/installer/native/runtime-/` with a -`manifest.json` (schema 1). Windows and Linux ship `runtime.tar.zst` beside -the Node.js release archive. Apple silicon ships the `runtime/` folder and -`node/bin/node` unpacked instead, because the app's signature must cover -every Mach-O file: `packaging/mac/package.py` signs them with the app (Node.js -with `node-entitlements.plist`: JIT, and native modules under the hardened -runtime its release signature keeps), records the signed bytes in the -manifests and seals them with a shallow signature. The desktop unpacks or -copies the services into `.simplexai-runtime/offline/` of the source and -Node.js into its own folder, removing `com.apple.quarantine` on macOS; -`checkNativeRuntime` then verifies every file, and Native installs no -packages and needs no registry. On Linux the services need glibc 2.35 -(Ubuntu 22.04, Debian 12), like the PostgreSQL build; with an older glibc the -desktop leaves them out and Native installs from the network. Model data -(OCR, Voice) is not bundled: Node Services downloads it on first use. -Intel Macs have none of this and install from the network. +Read [Packaging Details](../../docs/desktop-release.md#packaging-details) before +changing `build.mjs`, `packaging/`, `distribution/`, the source package, or the +bundled native PostgreSQL, voice tools, TeamHub, or services, and before +building a package. ## Development Environment diff --git a/docs/desktop-release.md b/docs/desktop-release.md index e74f04b1..16624596 100644 --- a/docs/desktop-release.md +++ b/docs/desktop-release.md @@ -122,7 +122,7 @@ gh release upload native-deps --repo simplexai-labs/linger Each package bundles the source package, PostgreSQL, voice tools, TeamHub and the Native services as described in -[`apps/desktop/AGENTS.md`](../apps/desktop/AGENTS.md). The update feed holds +[Packaging Details](#packaging-details). The update feed holds `manifest.json`, `manifest.json.sig` and the app archive; unpack it into the server's `install/desktop/stable/`. @@ -140,3 +140,176 @@ With the archives already in `LINGER_NATIVE_POSTGRES_DIR` (default `~/.cache/linger-desktop`), `LINGER_NATIVE_DEPS_URL` is not needed. `checksums` refuses a folder without the DMG, the Linux archive and the setup program of that version. + +## Packaging Details + +Paths in this section are relative to `apps/desktop/`, except +`scripts/prepare-native-*.mjs`, `scripts/source-native.mjs`, and +`scripts/installs/`, which are in the repository root. + +### Source Package + +Release builds set `LINGER_SOURCE_ARCHIVE_URL` (https) and +`LINGER_SOURCE_ARCHIVE_SHA256` before `node build.mjs`. Produce them from the +release tag in the repository root: + +```sh +SOURCE_ARCHIVE_REF=v1.2.3 \ +SOURCE_ARCHIVE_URL_BASE=https://github.com/simplexai-labs/linger/releases/download/v1.2.3 \ + make source-archive +``` + +Upload `.linger-runtime/source-archive/linger-v1.2.3.tar.gz` to that release +and take both values from `source-archive.json` next to it. One commit always +gives the same digest. To ship the package inside the app instead (about +45 MB more, no hosting needed), set `LINGER_SOURCE_ARCHIVE_FILE` to the +archive; the build copies it beside the installer and pins its digest. Without +either (and without `LINGER_CHECKOUT`) the build does not offer a +local install. + +A downloaded source package can have a dependency store for each platform +beside it, so the native install of its packages needs no registry. Build it +from the same tag, on the platform it serves, and set +`LINGER_SOURCE_DEPENDENCIES_URL` and `LINGER_SOURCE_DEPENDENCIES_SHA256` from +`source-dependencies-.json` for that platform's build: + +```sh +SOURCE_ARCHIVE_REF=v1.2.3 \ +SOURCE_ARCHIVE_URL_BASE=https://github.com/simplexai-labs/linger/releases/download/v1.2.3 \ + make source-dependencies +``` + +`SOURCE_DEPENDENCIES_TARGET` builds another platform's store instead (the +`source-dependencies` CI job does this on Linux). Such a store skips install +scripts, so the install still compiles or downloads native modules such as +better-sqlite3. The desktop downloads the store after the source package +(resuming an interrupted download) and unpacks it into +`.linger-runtime/pnpm-store` of the source; when that fails it logs +`local_server.dependencies_failed` and the installer downloads the packages +itself. + +`LINGER_SOURCE_ARCHIVE_MIRRORS` and `LINGER_SOURCE_DEPENDENCIES_MIRRORS` list +other https hosts serving the same files, separated by spaces. The desktop moves +to the next one when a host fails or stalls, and the SHA-256 still decides. + +Native installs also need the publisher's pinned localmodels tool and native +PostgreSQL package for the target. They are pinned in +`distribution/.json`, in the shape `packages/local-installation/src/native-dependencies.ts` reads +(`platform`, `arch`, `localmodelsTool`, `nativeSources`, `voiceTools`, each +source with an optional `mirrors` list of https URLs for the same file). The build copies the +file of its target (`LINGER_DESKTOP_DAEMON_TARGETS`, else this machine) beside +the bundled installer, which reads it at start; `LINGER_DESKTOP_DISTRIBUTION` +names another file instead. A source with `file` (Windows) instead of `url` +names an archive the app ships: the build copies it from +`LINGER_NATIVE_POSTGRES_DIR` (default `~/.cache/linger-desktop`) to +`dist/installer/native/` after checking its size and sha256, and the installer +checks them again before extracting it, as it does for a download. Where the +file has no PostgreSQL for the computer, or the bundled archive is missing, +the setup page does not offer installing on this computer. `src/main/distribution.test.ts` checks that each file holds its +target's PostgreSQL and the localmodels release `scripts/installs/stack.sh` +pins. + +### Release + +Each desktop target (`darwin-arm64`, `linux-x64`) gets its own distribution +file and build. For one release: + +1. Make the source package as above and upload it. On a host of each target, + make its dependency store and upload it beside the package. +2. On a host of each target, build the PostgreSQL package with + `node scripts/prepare-native-postgres.mjs --output DIR --base-url URL`; it + refuses to cross-build. Before uploading, check `initdb`, a connection and + `CREATE EXTENSION vector` against a private data directory. Upload the + `.tar.gz` to `URL` and keep `postgres-.json` from `DIR`. Windows + repackages the pinned EDB binaries, builds only pgvector and adds the + Visual C++ runtime the EDB zip leaves out, so run it there from an x64 + Native Tools Command Prompt for Visual Studio. The installer accepts only + credential-free HTTPS URLs, so `URL` must be the HTTPS CDN. +3. For a new PostgreSQL or localmodels release, update + `distribution/.json`: `localmodelsTool` for the localmodels + release `scripts/installs/stack.sh` pins, and `nativeSources` holding that target's + `postgres-.json` object. +4. Build with `LINGER_DESKTOP_DAEMON_TARGETS` set to the target and the source package + (`LINGER_SOURCE_ARCHIVE_FILE`, or the URL and SHA256, plus the target's + `LINGER_SOURCE_DEPENDENCIES_URL` and SHA256) set, then package (`packaging/mac/package.py +--arch arm64` for macOS, `packaging/linux/package.py` for Linux). +5. Check the package on a clean computer: Quick Install with the default + source ends on "Linger is ready" and Open Linger shows the local server, + signed in, with this computer connected. + +Official releases come from the public simplexai-labs/linger repository: a +`desktop-v` tag pushed there runs steps 4 and the packaging for +`darwin-arm64`, `linux-x64` and `win32-x64` in GitHub Actions and publishes a +linger GitHub Release with `SHA256SUMS.txt`; see +[How the workflow runs](#how-the-workflow-runs). The workflow +fetches the bundled archives from the assets of the `native-deps` prerelease +there, so upload a new pinned archive to it before tagging. + +Windows, Linux x64 and Apple silicon bundle PostgreSQL in the app instead +(`file` in `distribution/.json`; 35 MB on Windows, 10–12 MB +elsewhere), so Native works without the CDN. Put the pinned archives in +`LINGER_NATIVE_POSTGRES_DIR` before `node build.mjs`; the build fails without +them. To download it instead, replace `file` with the HTTPS `url` once it is on +the CDN. Native PostgreSQL refuses an elevated Linger ("Run as +administrator"), as it refuses root elsewhere. + +Every target also bundles the voice tools (`voiceTools`, about 40 MB each): +the official LiveKit server release (Apple silicon: built from the tagged +source with the pinned Go, since LiveKit publishes no macOS binary; without +cgo, so its vendored go-osstat reports an idle CPU, which only matters when +LiveKit spreads rooms over several servers) and a +static LGPL ffmpeg/ffprobe (Linux and Windows: built from the FFmpeg release +in the pinned Alpine image, Windows with MinGW-w64; Apple silicon: the +pinned serversideup LGPL build, which links OpenSSL statically). Make them on +Linux with `node scripts/prepare-native-voice-tools.mjs --target +--output DIR` (Docker; Go for Apple silicon), copy the archive to +`LINGER_NATIVE_POSTGRES_DIR` and `voice-tools-.json` from `DIR` into +`voiceTools`. The build is reproducible except for the Go toolchain download, +and it checks the versions, the ffmpeg components the voice workers use, +static linking and the Mach-O signatures. Licenses ship in `licenses/` of the +archive. On every install and start, whether or not a voice key is saved, +Native extracts the archive into +`tools/native/components`, puts its `bin` first on the services' `PATH` +(TeamHub and the voice workers run `ffmpeg`/`ffprobe` from it) and runs +`livekit-server` as the `livekit` Native service: loopback only, HTTP and +WebSocket on one free port and media on one free UDP port, both kept with +the generated API key and secret in the owner-only `livekit.env` of the +instance, logs in `native/logs/livekit.log`. ICE over TCP stays off because +LiveKit binds it on every interface. Without the tools for the computer, or +when they fail to prepare, Native logs it, keeps voice off, gives TeamHub +`TEAMHUB_VOICE_MEDIA=off` and finishes the install. The macOS tools are ad-hoc signed by their linker and +live in the archive, outside the app's signature, like PostgreSQL. + +A build with a source package for `win32-x64`, `linux-x64` or `darwin-arm64` +also ships TeamHub prebuilt (`scripts/build-teamhub.mjs`): build.mjs +cross-compiles `teamhub`, `migrate` and `account` from that package (`go` in +PATH, `CGO_ENABLED=0`) into `dist/installer/native/teamhub-/` with a +`manifest.json` of their sha256, the package's sha256 and the commit +`git archive` recorded in it; a downloaded package +(`LINGER_SOURCE_ARCHIVE_URL`) is fetched for this. The package scripts +refuse binaries that do not match the manifest or the bundled package +(`packaging/native_bundle.py`). The desktop copies them into the unpacked source +(`.simplexai-runtime/prebuilt/`), the installer checks them again and then +skips the Go download, and `scripts/source-native.mjs` runs them (checking the +sha256 before each start) instead of `go run` / `go build`. Without them, or +when they do not verify, Native downloads Go and compiles as before. + +The same targets ship the services Native runs, built from that package +(`scripts/build-native-runtime.mjs`): Web's production files, Node +Services bundled with esbuild beside the node_modules it loads at runtime, +and the pinned Node.js, in `dist/installer/native/runtime-/` with a +`manifest.json` (schema 1). Windows and Linux ship `runtime.tar.zst` beside +the Node.js release archive. Apple silicon ships the `runtime/` folder and +`node/bin/node` unpacked instead, because the app's signature must cover +every Mach-O file: `packaging/mac/package.py` signs them with the app (Node.js +with `node-entitlements.plist`: JIT, and native modules under the hardened +runtime its release signature keeps), records the signed bytes in the +manifests and seals them with a shallow signature. The desktop unpacks or +copies the services into `.simplexai-runtime/offline/` of the source and +Node.js into its own folder, removing `com.apple.quarantine` on macOS; +`checkNativeRuntime` then verifies every file, and Native installs no +packages and needs no registry. On Linux the services need glibc 2.35 +(Ubuntu 22.04, Debian 12), like the PostgreSQL build; with an older glibc the +desktop leaves them out and Native installs from the network. Model data +(OCR, Voice) is not bundled: Node Services downloads it on first use. +Intel Macs have none of this and install from the network. From b2df175396fd5ca17681cd1e108070165821111b Mon Sep 17 00:00:00 2001 From: jeffrey Date: Thu, 12 Nov 2026 22:51:28 +0800 Subject: [PATCH 3/3] fix: fail the English-only check on git grep errors The check treated every non-zero git grep status as "no match", so a git without PCRE let the gate pass. Only status 1 now means clean. The one existing test with literal ideographs uses escaped code points, and the packaging note lists packages/local-installation/ as a root path. Co-Authored-By: Claude Code --- apps/voice-runtime/src/live-worker.test.ts | 7 ++++++- docs/desktop-release.md | 5 +++-- scripts/ci/check-repository-gate.sh | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/voice-runtime/src/live-worker.test.ts b/apps/voice-runtime/src/live-worker.test.ts index 4058a8b8..96822781 100644 --- a/apps/voice-runtime/src/live-worker.test.ts +++ b/apps/voice-runtime/src/live-worker.test.ts @@ -416,7 +416,12 @@ describe("afterCall", () => { expect( afterCall("Switching everything to English.", "All English now"), ).toBe(" All English now"); - expect(afterCall("改好了。", "主题是周五会议")).toBe("主题是周五会议"); + expect( + afterCall( + "\u6539\u597d\u4e86\u3002", + "\u4e3b\u9898\u662f\u5468\u4e94\u4f1a\u8bae", + ), + ).toBe("\u4e3b\u9898\u662f\u5468\u4e94\u4f1a\u8bae"); expect(afterCall("", "Done.")).toBe("Done."); expect(afterCall("Done. ", "Next")).toBe("Next"); }); diff --git a/docs/desktop-release.md b/docs/desktop-release.md index 16624596..20869cde 100644 --- a/docs/desktop-release.md +++ b/docs/desktop-release.md @@ -144,8 +144,9 @@ program of that version. ## Packaging Details Paths in this section are relative to `apps/desktop/`, except -`scripts/prepare-native-*.mjs`, `scripts/source-native.mjs`, and -`scripts/installs/`, which are in the repository root. +`scripts/prepare-native-*.mjs`, `scripts/source-native.mjs`, +`scripts/installs/`, and `packages/local-installation/`, which are in the +repository root. ### Source Package diff --git a/scripts/ci/check-repository-gate.sh b/scripts/ci/check-repository-gate.sh index 811ec189..5b999881 100755 --- a/scripts/ci/check-repository-gate.sh +++ b/scripts/ci/check-repository-gate.sh @@ -63,10 +63,18 @@ fi # The repository is English-only. Only CJK ideographs are rejected, so tests # may still use kana, Hangul, and full-width punctuation as unspaced scripts. -if ((${#changed_files[@]} > 0)) && - LC_ALL=C git grep -nIP '(*UTF)[\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{F900}-\x{FAFF}]' -- "${changed_files[@]}"; then - echo "Changed files contain Chinese characters; the repository is English-only (see AGENTS.md)." >&2 - exit 1 +# git grep exits 1 when nothing matches; any other non-zero status is an error +# (for example a git without PCRE), which must fail the gate rather than pass it. +if ((${#changed_files[@]} > 0)); then + cjk_status=0 + LC_ALL=C git grep -nIP '(*UTF)[\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{F900}-\x{FAFF}]' -- "${changed_files[@]}" || cjk_status=$? + if ((cjk_status == 0)); then + echo "Changed files contain Chinese characters; the repository is English-only (see AGENTS.md)." >&2 + exit 1 + elif ((cjk_status != 1)); then + echo "The English-only check could not run (git grep exited $cjk_status)." >&2 + exit 1 + fi fi make check-scripts-fast check-docs