Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

35 KiB

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.

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) 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: <visible/active/lifetime reason> 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: <reason> 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:

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