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

638 lines
35 KiB
Markdown

# 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: <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:
```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