Rules for coding agents (and humans) working in Lilo. Bias toward small, KISS changes - ROADMAP.md lists the non-goals.
- The Makefile is the only command surface. Don't invent build steps;
make helplists everything. make validateis the gate - UI lint/tests, SwiftLint, the Xcode build, Swift tests, and the extension bridge. Run it before claiming a change outsidelilo-ui/works. It should stay around 30 s warm: write tests that keep it there (CONTRIBUTING.md). (CI additionally runsmake e2eandmake swift-test-slow, checks thatLilo.xcodeprojand the generated extension bridge bundles are in sync, and builds Release.)- Scope the checks to what changed. A change confined to
lilo-ui/(Svelte, TS, CSS, locales) needs onlymake ui-lint ui-test; keepmake validatefor changes that touch Swift, the bridge or the build. swift build/swift testare fine for fast local checks oncemake vt-buildandmake ui-buildhave producedVendor/and the generated sources;xcodebuild(viamake) is the official path. Run directly, it takes-derivedDataPath .derived.noindex/xcodeasmakedoes, so a worktree's build goes with the worktree.- After adding or renaming a Swift file, run
make project-syncand commit the regeneratedLilo.xcodeproj-xcodebuildand CI fail until it is synced.
- KISS. Minimum code that solves the problem. No speculative abstraction, no single-use protocols, no config knobs with one caller. If it could be half the size, rewrite it.
- Generic, never per site. Fix a site's problem with the web platform or a mechanism every site gets: no hostname check, no reliance on a site's markup, class names or internals, no user agent for one site. Such a quirk breaks silently on the site's next deploy and helps no other site. The existing Meet modules are no precedent. When no generic fix exists, say so and document the limit in COMPATIBILITY.md.
- Surgical diffs. Touch only what the task needs and match the surrounding style. Don't refactor code that isn't part of the task.
- Placement. A new file goes where the placement rule says; tests mirror source paths.
- Design bar: Apple and Arc. Every screen and interaction must feel like a first-party macOS app with Arc's polish: native conventions (HIG spacing, SF Symbols, system fonts, standard shortcuts and menus), calm motion that respects Reduce motion, no layout jumps, nothing clipped or misaligned, AA contrast, every control reachable by keyboard. If it would look out of place in an Apple app, it is not done. docs/DESIGN.md holds the consistency rules every screen follows.
- Efficiency first. Do the least work possible: no polling, no redundant renders or recomputation, no resending unchanged data, nothing at idle. A feature that costs CPU, memory or battery when unused is a bug.
- Comments only for what the code cannot say: a constraint, a non-obvious why, a deliberate oddity. Never restate the next line or record history. Code, comments and commit messages are in English; user-facing copy is translated.
- No real user data in tests, fixtures, docs, commits or release notes: a URL, page title, folder name or account seen while debugging is replaced by a made-up equivalent (
example.comhosts, generic names) that reproduces the same case. - Commits follow Conventional Commits (
fix(sidebar): ...), one logical change each. - The WebKit fork is optional. Lilo is built for the WebKit that ships with macOS, and every feature works on it alone: the fork only brings engine bug and performance fixes early, and runs only while it holds every security fix of the installed macOS. Never build on an API or behavior only the fork has. See docs/WEBKIT_FORK.md.
- One web engine (WebKit), direct dependencies only. Terminal tabs use libghostty-vt from a pinned Ghostty commit:
make vt-builddownloads it prebuilt and checksum-pinned,make vt-build-sourcecompiles it with Zig.
- Localization - user-facing strings live in
lilo-ui/src/lib/i18n/locales/*.json; add every key to all catalogs (themake ui-testparity check fails otherwise). Native AppKit strings use.localizedunder thenativenamespace. See docs/I18N.md. - Extension Gap APIs - add one only when WebKit lacks the Chrome API and a real extension flow needs it. A Swift handler must be paired with JS augmentation, and COMPATIBILITY.md must be updated: it describes only what still differs from Chrome. See CONTRIBUTING.md.
- Page-world APIs - anything a web page can see goes through
PageWorld(handlers in its bridge world, modules patching WebKit's prototypes): a global, a page-world handler or a function printing its source flags Lilo as a bot.PageWorldTestsfails on any such trace. See docs/ARCHITECTURE.md. - Private WebKit API - guard every private selector with
responds(to:)and degrade gracefully when it is missing; register new private delegate hooks inPrivateDelegateSelectorTests. See docs/ARCHITECTURE.md. - Release notes - a change people using Lilo will notice gets a note from
make change, committed with the code under.changes/unreleased/; without a prompt, run the Makefile's$(CHANGIE)asnew --interactive=false -k <Kind> -b <text>. Never editCHANGELOG.md:make release-versiongenerates it from the notes. A feature release (x.y.0) also needs its What's New highlights and its website story, ormake release-versionrefuses it. See docs/RELEASING.md. - CSS
:has()is fine, but not always the best tool: a broad one makes WebKit recheck its whole container on each change inside. When Svelte state already knows the condition, a class is simpler and cheaper. - Generated files (
ChromeAPIAugmentation.js,ChromeAPIBackgroundPrelude.js,ChromeAPIGapModules.json,ChromeWebStoreShim.js,ChromeContentCompat.js,ChromeStaticRules.js,*.gen.swift,tabProtocol.gen.ts, plus the git-ignoredLilo/Generated/NativeStrings.swiftandLilo/Generated/ChromeUIAssets.swift) are build outputs - never hand-edit them; regenerate viamake ext-bridge-build/make bridge-gen/make ui-build.
For website changes, read site/AGENTS.md for Astro-specific rules.
Architecture deep-dive: docs/ARCHITECTURE.md. Setup, tests and the pull request flow: CONTRIBUTING.md.