# Static Analysis This document defines the repository-managed static analysis baseline. ## Purpose Static analysis should catch: - common correctness issues in TypeScript and Go - repository boundary violations that are cheap to detect mechanically - oversized business files before they become long-term maintenance hotspots It should not: - turn formatting concerns already handled by Oxfmt, Prettier, or `gofmt` into duplicate failures - enforce broad stylistic preferences with weak product value - apply business-file limits to tests, generated code, or type-only surfaces ## Commands Repository entrypoints: - `pnpm setup:dev` - `pnpm check:backdrop-filter-authoring` - `pnpm check:css-has-performance` - `pnpm check:golangci-version` - `pnpm lint` - `pnpm lint:ts` - `pnpm lint:go` - `pnpm typecheck` - `pnpm check:codexproto-generated` - `pnpm check:agent-live-protocol-generated` - `pnpm check:agent-gui-provider-catalog-generated` - `pnpm check:agent-host-boundary` - `pnpm check:agent-provider-strategy-boundaries` - `pnpm check:runtime-image-budgets` `pnpm check:full` remains the full local validation command and includes linting, typechecking, repository checks, and blocking tests. PR CI selects the equivalent affected surfaces instead of running `check:full` as one job. The pull-request workflow and `check:changed` share changed-file classification from `tools/scripts/change-classification.mjs` and repository-check ownership from `tools/scripts/repository-checks.mjs`. Repository checks are grouped by responsibility: policy, tool contracts, generated contracts, and architecture boundaries. TypeScript and Go jobs own language lint/tests only; a check written in TypeScript does not make it a TypeScript check. Package sources and assets select packing independently of language. Documentation-only changes skip code validation while still producing workflow check results through job-level conditions. Do not use workflow-level `paths-ignore` for this gate because missing required checks can leave documentation-only PRs waiting on branch protection. PR workflows execute on a synthetic merge ref so tests cover the result that would land on `main`. Their selectors must still calculate the changed set from the exact pull-request range, `${base.sha}...${head.sha}`, and read manifest comparisons from the pull-request head. Do not diff the merge ref against the original base SHA: if `main` advances while a PR is open, unrelated main-branch changes would select extra TypeScript, package, or repository checks. PR CI keeps the existing `Tooling Consistency` required context as the owner of repository policy, contract, generated, and boundary checks. This preserves branch-protection compatibility while keeping those checks out of language jobs. Windows validation separates Agent process adapters, daemon adapters, and desktop packaging. Agent daemon changes run the process and downstream daemon adapter workflows in parallel; `services/tuttid` changes run only the daemon adapter workflow. The Agent process lane needs only Go, while the daemon lane prepares the builtin Onboarding package before its Go tests. Each lane invokes its selected Go packages together so independent packages can build and test in parallel. The Agent process lane also crosses a native Windows child-process boundary to verify case-insensitive environment-key precedence. It also proves that the account-usage Node boundary reuses a read-locked, content-addressed snapshot across calls and runner recreation, and that snapshot construction observes cancellation. The daemon lane covers durable companion failure backoff and recovery through a real restart. Both adapter workflows also run for matching pushes to `main`. Those trusted runs maintain default-branch Go and pnpm caches that new pull requests can restore; pull-request caches remain isolated to their merge refs. Adapter workflows use shallow checkouts because their tests do not inspect Git history. Desktop and builtin-app changes run the full unsigned Windows package build. Use the desktop workflow's manual dispatch for a full package check when a daemon-only change needs release-package confidence. Workflow definition files are validated by repository tool contracts and do not trigger runtime workflows themselves; this prevents a selector-only PR from starting a Windows runner. Changes to shared Go selector scripts are covered by the repository tool contract suite and continue to select only the affected Go modules. Go workspace, module, or lint configuration changes still select all relevant Go modules because they can change the validity of every target. Agent Session Replay has an additional changed-file lane, `run_agent_session_replay`. Changes to its core, provider transport, daemon recording/replay adapters, Desktop replay surfaces, deterministic fixture, or runner select this classification output. The PR replay job is currently disabled, so the output is advisory and does not execute a blocking gate. Keep the classification aligned with the real composition boundary so it can be enabled without replacing the closed loop with mock-only package selection. Validation runners that spawn nested pnpm commands should read the root `packageManager` field and invoke that pinned version through Corepack. Do not let runner-spawned lanes resolve a bare `pnpm` from `PATH`, because local package-manager shims can differ from the repository pin. Changed-aware lane fingerprints include base, staged, working-tree, and untracked content for each lane. Git subprocess buffers must accommodate large binary or generated diffs; the runner uses a bounded 128 MiB buffer so a normal large pull request does not fail before its validation lanes start. Tests and checks that create temporary Git repositories must also isolate repository-local Git environment variables before invoking Git. In particular, remove inherited `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, index/object overrides, and command-scoped `GIT_CONFIG_*` entries using case-insensitive name matching for Windows compatibility; set `GIT_CEILING_DIRECTORIES` to the fixture root; fail immediately when fixture Git commands fail; and verify the initialized Git directory before staging, committing, fetching, or checking out. A temporary cwd alone is not isolation because `GIT_DIR` takes precedence and can redirect `git init` and later commands into a caller's linked-worktree metadata. Repository validation lane runners must remove the same inherited repository selectors before spawning checks or tests. Git hooks export repository-local environment variables, so a pre-push validation run otherwise gives every child test authority over the caller's linked-worktree metadata. Spawned lanes rediscover the intended repository from their cwd; temporary Git fixtures still apply their own per-command environment and ceiling. ## TypeScript Baseline TypeScript linting uses Oxlint. The current baseline includes: - Oxlint correctness checks - `noUncheckedIndexedAccess` in the shared TypeScript base config Generated TypeScript is not linted by the human-authored TypeScript rule set. Generated output should be controlled through its generator and generation checks instead of hand-edited to satisfy repository lint style. Generated Codex app-server protocol artifacts under `packages/agent/daemon/runtime/codexproto` are checked by `pnpm check:codexproto-generated`. The check fetches the pinned Codex source commit, compares the committed upstream schema snapshot as canonical JSON, reruns the local Go generator, and fails when generated files drift. The schema comparison intentionally ignores JSON formatting differences so vendored upstream artifacts can coexist with repository Prettier formatting. Do not hand-edit generated `*_gen.go` files; update the vendored schema or generator, then regenerate. The codexproto generator runs during `pnpm check:full` alongside full-repository boundary scanners. Generator scratch files must stay outside the repository tree, even when they are removed before the generator exits, so parallel checks cannot observe transient files and fail nondeterministically. The optimistic AgentGUI live fast lane is schema-backed separately from the canonical cloud event. `pnpm check:agent-live-protocol-generated` hashes the live `message_delta` schema, the declarative protobuf-wire/control contract, and the reused canonical `turn_update`, `interaction_update`, and `session_audit` variants. It also generates the Go wire field and delivery-kind constants and checks the committed Go and TypeScript revision outputs. Change either contract and run `pnpm generate:agent-live-protocol`; do not hand-edit the generated revision or wire-constant files. The Agent GUI provider identity catalog under `packages/agent/gui/generated/providerIdentityCatalog.ts` is generated from the daemon provider registry. `pnpm check:agent-gui-provider-catalog-generated` fails when the checked-in TypeScript catalog drifts from the descriptor source of truth, when a generated locale key is absent from any AgentGUI locale, or when a generated icon key has no complete asset set. The same check also keeps the descriptor set equal to closed OpenAPI provider-keyed preference schemas while verifying that `AgentTargetProvider` and `WorkspaceAgentProvider` remain open, bounded identifier contracts that accept extension providers. It runs as part of `pnpm check:full`. Change provider identity, locale keys, icons, and target metadata in the registry, then run `pnpm generate:agent-gui-provider-catalog`; do not hand-edit the generated catalog. Provider strategy is an independent architecture boundary enforced by `pnpm check:agent-provider-strategy-boundaries`. Cross-provider daemon, service, and desktop production code must dispatch behavior through `providerregistry` strategy, capability, and integration descriptors instead of branching on Codex, Claude, Cursor, Hermes, Nexight, OpenClaw, OpenCode, or Tutti Agent identity. The checker reads the complete provider ID set from the daemon registry and rejects identity constants, literal equality comparisons, literal switch cases, and provider-specific Set or array membership dispatch in Go, TypeScript, and TSX production sources. Plain provider catalogs and enum validation remain allowed when they do not select behavior. It keeps an explicit exemption list for registry declarations, generated API enums, and exact provider-owned adapter/parser implementations (including format-specific external-import parsers); additions to that list require an ownership reason and must not hide cross-provider policy. Its fixture suite must exercise every registered provider so a newly migrated provider cannot silently remain outside the boundary rule. Historical or ported-source snapshots that are intentionally kept outside a package's active `tsconfig.json` during migration should also stay out of the type-aware TypeScript lint target. Treat those directories as migration inputs, not as first-class analyzed source, until they are promoted into the active package seam. `exactOptionalPropertyTypes` is intentionally not part of the shared TypeScript baseline yet. The current generated `@hey-api/client-fetch` runtime emits optional properties with explicit `undefined` values that do not typecheck under that option, and the available generator settings do not remove those conflicts. Revisit this after changing the generator version or generated-client strategy. Every TypeScript workspace package that contains source files should provide: - a package-local `tsconfig.json` extending the repository TypeScript base config - a package-local `typecheck` script that runs the repository `tsgo` typecheck wrapper This keeps `pnpm typecheck` authoritative across desktop, shared clients, contracts, and UI packages instead of relying on incidental imports from another package to expose type errors. The wrapper runs native TypeScript with `--noEmit --incremental` and stores package `.tsbuildinfo` files under `.tmp/tsbuildinfo`. This keeps warm local typecheck runs fast without committing cache files. The root `pnpm typecheck` command uses a compact runner that executes package typechecks concurrently, prints only a short summary on success, and stores package logs under `.tmp/typecheck-runs`. TypeScript package `tsconfig.json` files must not use `baseUrl`; use explicit relative `paths` entries when aliases are needed so the configuration stays compatible with native TypeScript. The repository-specific UI boundary policy remains in `pnpm check:ui-boundaries`. Its full-repository walker excludes `apps/mobile/ios/Pods`, because that CocoaPods-generated tree can vendor JavaScript fixtures, SVGs, and icon imports that are not Tutti-authored UI source. Bounded raster UI assets are checked by `pnpm check:runtime-image-budgets`. The policy reads image headers and file sizes without decoding pixels or invoking platform tools. It excludes design masters and window-sized media; the governed paths and resolution rationale are documented in [Runtime Image Assets](runtime-image-assets.md). ## Backdrop Filter Build Contract Production CSS and Tailwind arbitrary-property authoring under `apps/`, `packages/`, and `services/` must use the standard `backdrop-filter` property. Do not handwrite `-webkit-backdrop-filter`: Tailwind/Lightning CSS owns target-aware prefix generation, and duplicate declarations can be folded into a prefix-only result during production optimization. `pnpm check:backdrop-filter-authoring` enforces the source rule. Desktop builds also run `tools/scripts/verify-renderer-css-contracts.mjs` against final renderer assets. The artifact gate permits standard-only output or a generated prefix followed by the standard property; it rejects prefix-only output, reversed ordering, and a launchpad dismiss layer without a non-`none` standard filter. The React `WebkitBackdropFilter` inline-style property is outside this stylesheet optimization boundary and is not rejected by the authoring check. ## CSS Relational Selector Performance `pnpm check:css-has-performance` scans production CSS under `apps/`, `packages/`, and `services/`. It rejects `:has()` when the selector subject is the document root, a Workbench window/surface, an AgentGUI root layout, timeline, transcript row, or another listed large dynamic surface. Those subjects contain editors, streamed transcript content, dock animation, or other frequently mutating descendants. Relational matching there can make the entire subject subtree a style-invalidation candidate. Project the semantic state onto the subject with a class or data attribute instead. The check intentionally permits bounded local subjects such as buttons, icons, dialogs, and individual conversation rows. Add a subject to the policy only when its descendant scope or mutation frequency makes relational invalidation structurally unsafe; do not turn this into a blanket ban on `:has()`. The staged form runs in `pre-commit`. The full form is selected by `check:changed` for production CSS or policy implementation changes and is part of `check:full`. Renderer feature implementation boundaries are checked by `pnpm check:renderer-boundaries`. That check also enforces the Workbench-specific rule that `workspace-workbench/ui/**` imports public service or controller seams instead of `workspace-workbench/services/internal/**`. Workspace Workbench launch coordinators must also route workspace-keyed Shell registrations through the private `WorkspaceScopedRegistrationRegistry` rather than declaring their own `Map`. The rule covers top-level `*LaunchCoordinator.ts` services and the Message Center coordinator, while leaving non-launch coordinator state such as Agent GUI open-session reference counts outside this registration-specific policy. Because the rule is part of `check:renderer-boundaries`, it runs for staged renderer changes, the renderer-boundary lane selected by `check:changed`, and `check:full`. Changes to the renderer-boundary checker or its fixture suite also select that `check:changed` lane, so policy edits validate both their fixtures and the live renderer tree. Connector ownership is checked by `pnpm check:connector-boundaries`. The check keeps Contracts independent from Renderer, keeps Renderer Application free of React and host runtimes, prevents Connector packages from importing product owners, keeps Daemon Core independent from Application/adapters, and prevents Runtime from depending on Daemon Application, SQLite, or Control Plane. It also rejects a Renderer root barrel so Application and UI remain separately importable. Connector package, AgentGUI Connector integration, Desktop Connector adapter, or checker changes select this lane through `repository-checks.mjs`. `pnpm check:ui-boundaries` has a package-scoped temporary migration exception for `packages/agent/gui` while the carried agent activity renderer is being ported into tutti. During that migration the package may keep its existing local SVG and icon-library imports, but new reusable icons and design-system primitives should still move through `@tutti-os/ui-system` before being shared elsewhere. Remove or replace the package-wide exception once the carried renderer no longer duplicates its original UI asset tree. Desktop user-visible copy and locale resources are checked by `pnpm check:i18n`. `pnpm check:agent-activity-runtime-boundaries` scans Agent GUI and desktop renderer production code. Agent activity commands must go through `AgentGUIRuntime`; session-engine consumers must use exported selectors instead of reading `sessionsById`, `turnsById`, `interactionsById`, pending intent maps, or prompt-queue records directly. Entity storage keys and reducer layout are engine implementation details, not consumer contracts. The check also rejects the deleted `workspaceAgentActivityTypes` aggregate, handwritten session/snapshot/presence mirrors, module-global runtime resolver access, and deprecated session lifecycle reads. The desktop reconcile diagnostics module has a narrow serialization-only exception for legacy lifecycle evidence. Direct React external-store subscription enforcement follows the actual `useSyncExternalStore(...)` argument dependency: an argument that directly or indirectly resolves to `getSessionEngine(...)` is rejected, while another store subscription in a file that separately reads the engine is allowed. Keep both the passing unrelated-store fixture and failing direct-engine fixture when this analysis changes; file-level token coexistence is not a valid substitute for the call relationship. `pnpm check:changed` schedules this activity-runtime boundary lane whenever a change touches `packages/agent/gui`, `packages/agent/activity-core`, Desktop's `workspace-agent` or `workspace-workbench` features, or the checker/fixture implementation itself. This keeps the same boundary in the normal changed-file loop instead of discovering violations only in `check:full`. `pnpm check:agent-host-boundary` protects the Agent Host boundary. Agent application-core lifecycle semantics (session/turn/goal/runtime-operation creation, sendability, terminal state, recovery) belong to `packages/agent/host`; `services/tuttid/service/agent` is an adapter that delegates through `ApplicationHost()`. The check scans production (non `_test.go`) Go files under `services/tuttid/service/agent` and rejects new `*Coordinator`, `*Worker`, or `*Actor` orchestration surfaces, detected as a type declaration whose name ends in one of those words or a file whose name ends in `_coordinator.go`, `_worker.go`, or `_actor.go`. It is a ratchet: the `ALLOWLIST` in the checker is the current snapshot (only `composer_live_model_coordinator.go`, a provider-catalog adapter concern), may only shrink, and rejects stale entries whose files no longer exist. A new violation fails until the orchestration moves to `packages/agent/host` or the file is added to `ALLOWLIST` with a reviewed ownership reason. The rule runs in `pnpm check:full`, in the `check:changed` `boundary:agent-host` lane whenever a change touches `services/tuttid/service/agent` or the checker/fixture itself, and in the PR `go-lint` job. The boundary rationale and adapter rules live in the root `AGENTS.md` `Agent Host Boundary` section and `services/tuttid/service/agent/AGENTS.md`. The same check also verifies production reachability: `services/tuttid/wiring.go` must compose Host with `daemon/hostadapter.RuntimeController`, `host.SQLiteWorkspaceStore`, and `NewApplicationHostWithPorts`. This prevents shared lifecycle adapters from existing only in tests while production keeps a parallel service implementation. Production agent service files also reject service-local `serviceHostStore` / `serviceHostRuntime` composition and lazy `NewApplicationHost` factories; isolated in-memory adapters may exist only in `_test.go` fixtures. ## Agent GUI Degradation Ratchet The agent GUI refactor ([docs/architecture/agent-gui-refactor-plan.md](../architecture/agent-gui-refactor-plan.md)) is protected by a degradation ratchet: - `pnpm check:agent-gui-degradation` measures entropy metrics over `packages/agent/gui` and `packages/agent/activity-core` (per-file line counts over the business limit, package-wide effect totals, per-component memoization overages, render-time ref mirrors/caches, provider behavior branches, timers, swallowed catch blocks, view-embedded stores, direct `useSyncExternalStore` calls, module-level mutable globals, presentation schedulers, inline compositor hints, CSS infinite animations/compositor hints, and daemon Go file-length exemptions) and compares them against the committed baseline in `tools/degradation-baseline/agent-gui.json`. Render-mirror counting also covers Desktop's `DesktopAgentGUIWorkbenchBody` host boundary because unstable host callbacks can invalidate the entire Agent GUI subtree. - Effect counts remain package-wide because hooks move with a vertical module during decomposition. Memoization follows the architecture boundary instead: `.tsx` component modules have a five-call budget, while controller/read hooks may own stable projections. Read-layer memoization must stabilize data-owner output; it must not become a second view cache. - Any metric increase fails. Any decrease also fails until the same change updates the baseline with `node tools/scripts/check-agent-gui-degradation.mjs --update-baseline`, so refactor wins stay locked in. - The metric counting rules live in the script; numbers quoted in architecture documents are illustrative only. - `identityExemptFiles` in the baseline lists identity-display files (provider icons, labels, title projections) whose provider branches are tracked in a separate bucket. The list may only shrink, and the checker rejects entries whose files no longer exist so removed seams cannot leave permanent stale exemptions. - CSS `will-change`, `translateZ`/`translate3d`, `backface-visibility: hidden`, and infinite animations are tracked by exact stylesheet selector/property/value fingerprints. Every fingerprint requires a non-empty `presentationHintReasons` entry describing its bounded mounted, visible, active, loading, or interaction lifetime. New hints cannot be accepted by running `--update-baseline` alone; add the reviewed reason first. Stale reasons fail after a hint is removed. - The same CSS policy rejects `transition: all` and requires the root `data-agent-gui-visible="false"` animation pause and `content-visibility: hidden` declarations. It also requires the `data-agent-gui-active="false"` prompt-tip animation and `will-change` release declarations. These are behavior contracts, not baselined debt, so deleting them always fails. - Raw `requestAnimationFrame`, `requestIdleCallback`, and `ResizeObserver` calls, plus inline `willChange` and `translateZ`/`translate3d` hints, are counted per production file. A newly added call or hint must carry a `// presentation-work: ` comment on the same or previous line. Ordinary two-dimensional transforms are not banned. - `pnpm check:agent-gui-degradation:staged` runs in `pre-commit` and blocks new degradation patterns on staged added lines: uncommented timers (a `// timing: ` comment is required outside engine/reducer/selector code, where timers are forbidden entirely), silently swallowed catch blocks, component memoization beyond budget, render-time ref mirrors/caches, store creation in component files, new provider behavior branches, direct `useSyncExternalStore` calls outside the single engine binding file, and new module-level mutable globals, unexplained presentation schedulers/inline compositor hints, unreviewed CSS presentation hints, `transition: all`, and removal of the visibility/active pruning declarations. Ref-mirror and component-cache diagnostics explicitly route business state to the engine/controller and stable projections to selectors/read hooks; refs remain valid for imperative DOM, timer, abort, and external-lifecycle handles. - During a merge commit, staged mode compares the resolved index with `MERGE_HEAD` instead of treating every incoming-parent line as newly added. This keeps the hook focused on branch-authored degradation while the full baseline check still measures the complete merged tree. The business file size limit below also applies to TypeScript under `packages/agent/gui` and `packages/agent/activity-core` through this ratchet: files over the limit that are not in the baseline fail the check, and baselined files may not grow. Render budget tests are the companion mechanism for performance work: the probe utility in `packages/agent/gui/shared/testing/renderBudget.tsx` asserts React commit counts for typical interactions, and budget test cases are delivered with each feature-module slice. Electron `main` and `preload` runtime import graphs are checked by `pnpm check:electron-runtime-boundaries`. That script is intentionally narrow: it ignores type-only imports and test files, then follows reachable runtime imports to catch React/TSX leaks and Electron-externalized workspace packages that still resolve to raw source files. The i18n check enforces: - locale key parity across supported desktop locales - interpolation placeholder parity across locale values - valid references for literal i18n keys used from desktop code - likely hardcoded user-visible copy in desktop renderer and Electron UI surfaces The i18n check discovers locale-resource modules through manifest exports instead of a central script-side registry. Desktop-owned resources use `tuttiI18nModule`; reusable packages may use a package-specific manifest name when that keeps the package vocabulary product-neutral, such as `browserNodeI18nModule` or `agentGuiI18nModule`. Prefer constructing that manifest through the shared helpers exported from `@tutti-os/ui-i18n-runtime`: - `createLocaleObjectI18nModuleManifest(...)` - `createScopedLocaleObjectsI18nModuleManifest(...)` Current manifest modes: - `locale-object` Use this when a module owns top-level locale files such as `locales/en.ts` and `locales/zh-CN.ts`. - `scoped-locale-objects` Use this when a reusable package owns a scoped default dictionary under one namespace and the host merges that package resource into the app-level i18n runtime. Current manifest expectations: - desktop-owned locale resources should expose a manifest from `apps/desktop/src/shared/i18n/*` - reusable package default i18n resources should expose a manifest from the owning package source tree under `packages/*` - reusable package `scoped-locale-objects` manifests should include: `name`, `namespace`, `sourceRoot`, and `localeObjectByLocale` - desktop or package `locale-object` manifests should include: `name` and `fileByLocale`; package-owned `locale-object` manifests should also include `sourceRoot` When adding a new reusable package that owns default i18n resources, do all of the following in the same change: - keep the default resource in the owning package instead of copying it into `apps/desktop` - export an i18n manifest next to that package's i18n resource - merge the package resource into the desktop app-level i18n runtime when the desktop host consumes that package - run `pnpm check:i18n` ## Go Baseline Go linting uses `golangci-lint` across the repository's current Go modules. The current root entrypoint runs the linter from: - `packages/agent/activity-replication` - `packages/agent/host` - `packages/agent/store-sqlite/canonical` - `packages/appcli/core` - `packages/clients/device-authority-go` - `packages/clients/market-go` - `packages/connector/daemon/application` - `packages/connector/daemon/core` - `packages/connector/runtime` - `packages/connector/daemon/adapters/sqlite` - `packages/connector/daemon/adapters/controlplane` - `packages/device-link` - `packages/agent/runtimeprep` - `packages/workspace/files` - `packages/workbench/service` - `services/tuttid` The shared lint configuration currently lives in `services/tuttid/.golangci.yml`. The shared agent daemon runtime under `packages/agent/daemon` is included by changed-aware Go lint when daemon files change, but is not yet part of the root `pnpm lint:go` module list. During the migration, selected historical files carry file-local `revive:disable:file-length-limit` comments. New tutti-owned daemon service/API code should stay outside those exceptions and must continue to satisfy the normal Go lint baseline. Changed-aware Go validation includes the nested `packages/agent/activity-replication`, `packages/agent/daemon`, `packages/agent/host`, `packages/agent/runtimeprep`, `packages/agent/store-sqlite`, and `packages/agent/store-sqlite/canonical`, `packages/clients/device-authority-go`, `packages/clients/market-go`, the five Connector Go modules, and `packages/device-link` modules. Codex app-server protocol changes should also run `pnpm check:codexproto-generated` when schema, generator, or generated protocol files are touched. Every change under `packages/device-link/**`, including Makefiles, Java probe sources, and Android manifests, also selects `pnpm check:device-link-android`. That contract runs the Go suite, Android arm64 cross-compile, and the transport-only Java gomobile binding generation. `pnpm mobile:check` separately generates the Mobile-owned composite binding surface for DeviceLink plus the Agent live Subscriber without requiring an Android SDK. The Mobile package's composite `pnpm check` also runs `check:ios-bindings`, which uses Go's Objective-C binding generator to verify the DeviceLink and live Subscriber headers and expected exported symbols. This binding check needs the repository Go toolchain and macOS Command Line Tools, but not the full iOS SDK; building the XCFramework still requires full Xcode. AAR assembly remains an explicit Android-SDK validation locally. The manually dispatched Mobile Internal Build workflow accepts `android`, `ios`, or `all`. Its Android job installs the pinned SDK/NDK versions, assembles the Mobile composite AAR and internal mobile APK, and uploads a private validation artifact. Android release assembly requires the repository's stable keystore and credentials through the four `ANDROID_RELEASE_*` Actions secrets; the workflow fails closed when they are absent, and verifies both APK alignment and signing certificate before upload. It never generates a temporary signing identity. Each CI build also uses the repository-level workflow run number as its monotonically increasing Android `versionCode`. It validates the DeviceLink consumer build but does not publish Go module tags; the stable package release workflow owns those tags. Its iOS job runs on the pinned macOS 26 runner, assembles the same Mobile binding surface as an XCFramework, archives the React Native app, and uses the repository App Store Connect API key plus the `IOS_DEVELOPMENT_TEAM` repository variable for Xcode-managed cloud signing. It loads the Mobile Podfile's pnpm path compatibility shim before generating the Pods project, combines the GitHub Actions run number and attempt as the unique iOS build number, exports an App Store Connect IPA, verifies that the signed app contains its release `main.jsbundle`, and uploads the archive to TestFlight without registering test device UDIDs. The exported IPA and checksum remain available as a 14-day private validation artifact rather than creating a GitHub Release. Both jobs remain manual so pull request code does not receive mobile signing credentials automatically. Local runs resolve `golangci-lint` from `$(go env GOPATH)/bin` first and fall back to `PATH`. This matches the repository install command without requiring a shell-specific `PATH` edit. The repository pins the CI version through `services/tuttid/.golangci-lint-version`. CI downloads the official installer to a temporary file with transient-network retries, runs it only after the download succeeds, and adds the install directory to `PATH` only after verifying that the pinned binary is executable. If you plan to run `pnpm lint:go` or `pnpm check:full` locally, install `golangci-lint` first. A compatible binary already available on `PATH` remains supported as a fallback. Use `pnpm check:golangci-version` when you only want to verify that the installed binary matches the repository pin without running the broader setup checks. Recommended local install command, using the pinned repository version: ```sh pnpm install:golangci-lint ``` This installs the pinned binary into `$(go env GOPATH)/bin`; repository-managed setup, version, changed-aware, and full lint commands resolve that location automatically. It follows the current official binary-install guidance from golangci-lint and keeps local runs aligned with the version pinned for CI. The current baseline enables a small, high-value set of linters: - `errcheck` - `govet` - `ineffassign` - `nolintlint` - `staticcheck` - `unused` - `revive` In golangci-lint v2, `staticcheck` also covers checks that were previously exposed as separate `gosimple` and `stylecheck` linters. `nolintlint` keeps lint suppressions explicit and valid when an exception is necessary. ## Business File Size Limit Business-code files must stay at or below `800` lines. This limit is a refactoring trigger: - when a business file exceeds the limit, prefer splitting responsibilities or extracting focused helpers before adding more logic - do not treat the limit as a suggestion to bypass with casual exceptions The limit does not apply to: - test files - generated files - pure type declarations - contracts packages - bootstrap or helper surfaces outside the configured business paths Current first-pass scope: - TypeScript business paths under `apps/desktop/src/main/*`, `apps/desktop/src/preload/*`, and `packages/clients/*` - Go business paths under `packages/workspace/files/*` and `services/tuttid/app/*`, `api/*`, `biz/*`, `data/*`, `server/*`, and `service/*` ## Workflow Rules - keep `pre-commit` focused on staged formatting and cheap boundary checks - keep linting in `pre-push` and pull-request CI through the shared root scripts - prefer extending the existing lint configs before adding new one-off repository scripts - add repository-specific scripts only when standard lint tooling cannot express the rule cleanly