Cmux Architecture
cmux package architecture, refactor layering, dependency inversion, file organization, DocC documentation, package design discipline, testability, and Swift 6 concurrency rules. Use before adding or meaningfully rewriting Swift files, Swift packages, coordinators, services, repositories, or public package APIs.
- Skill ID
- manaflow-ai/cmux/cmux-architecture
- Publisher
- manaflow-ai
- Repository
- cmux
- Installs
- 0
- Files
- 5
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
manaflow-ai/cmux/cmux-architectureInstalls these files- SKILL.md
- agents/openai.yaml
- references/concurrency-carveouts.md
- references/file-api-discipline.md
- references/package-boundaries.md
What this skill tells the agent
cmux Architecture
Package architecture
cmux is migrating from a single app target into Swift Packages under Packages/. Every new package must be:
- Ergonomic. Default to internal access;
publiconly what downstream consumers actually use. - Acyclic. Packages form a strict DAG. Share a type by lifting it to a lower package or defining a protocol seam in the consumer. Every new dependency edge requires re-checking that the graph stays acyclic.
- Whole-domain. One package owns a full domain (settings, appearance, workspace, terminal, browser, command palette).
CmuxAppearanceMath+CmuxAppearanceTheme+CmuxAppearanceSettingsis folder structure insideCmuxAppearance, not module structure. A boundary exists because more than one consumer needs the contents, or a build/test seam must exist.
When in doubt, extract leaf-first: the package with no internal dependencies. Existing packages under Packages/ predate this policy; do not use them as design references.
Wiring a new package into cmux.xcodeproj needs explicit pbxproj entries in both the cmux and cmux-unit targets. See references/package-boundaries.md.
Group folders. Every package lives physically under exactly one group directory: Packages/Shared/<pkg> (both apps), Packages/iOS/<pkg> (iOS only), or Packages/macOS/<pkg> (macOS only). cmux.xcworkspace/contents.xcworkspacedata mirrors that folder shape, with three groups whose container locations are those folders and every package directory as a FileRef under its folder's group. The folder is the source of truth: to move a package, git mv the directory then run python3 scripts/check-workspace-package-groups.py --write. Cross-group .package(path:) deps use ../../<Group>/<Name>. Never hand-edit workspace group membership. CI runs python3 scripts/check-workspace-package-groups.py --check and fails on drift.
Lockfiles. Do not gitignore cmux-owned Package.resolved files; SwiftPM resolution changes must be visible in PR diffs. Track the root Xcode lockfile and every cmux-owned package-local Package.resolved produced by standalone swift package resolve / swift build / swift test. A package-local lockfile is the source of truth for that package's standalone resolution and is not replaced by cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved. Vendored third-party directories may keep their upstream ignore policy. CI runs python3 scripts/check-package-resolved-policy.py.
Feature flags mean remote PostHog runtime flags. Unless the user explicitly asks for a compile-time flag, local setting, or environment variable, implement a feature flag through CmuxFeatureFlags with a PostHog key, an explicit unavailable fallback, registry metadata, live update behavior, and focused tests. A local override may support dogfood but must not be the production control plane.
Layers
Five layers, dependencies point only downward:
- Core (
CmuxCore): pureSendablevalues, IDs, DTOs, errors, shared protocol seams. No AppKit/SwiftUI/I/O. The lift target when two domains need the same type. - Services / infrastructure:
actors implementing core protocols against the outside world (process/PTY, filesystem, sockets, web API, notifications, auth). One package per cohesive capability. - Domain / state:
@MainActor @Observablemodels plus Coordinators, one package per feature domain, owning that domain's mutable state. ExemplarCmuxSettings. - UI: SwiftUI/AppKit views, one UI package per domain package, depending only on its domain package plus Core, never a Service directly. Exemplar
CmuxSettingsUI. - Executable (
cmuxApp/AppDelegate): thin composition shim, no business logic.
Classify every extracted entity by intent:
- Coordinator:
@MainActor @Observableorchestrator that sequences a user flow and owns navigation/selection/lifecycle state, calling Services and child models. Does no I/O itself. - Service:
actor(or@MainActoronly when an AppKit main-thread API forces it) performing one outside-world capability; exposesasync/awaitplusAsyncStream; holds only its own resource handles and no UI state. - Repository:
actormediating one persistence source of truth (file, defaults, web API) behind CRUD-shaped async methods returning value types. Precedents:JSONConfigStore,UserDefaultsSettingsStore.
Dependency inversion. Lower packages publish protocols; concrete Services/Repositories conform; higher layers depend on any Protocol, never the concrete type, and never a stored property reaching across modules. Constructor (init) injection only: no global container, no singleton, no static let shared. The executable app target is the single composition root, the one place concretes are named and the object graph is assembled. SwiftUI Environment may carry already-constructed @Observable models down a view tree (as SettingsRuntime does), never service wiring.
State and SwiftUI. Domain state lives in @MainActor @Observable models, never ObservableObject/@Published. A god model decomposes into cohesive child @Observable sub-models owned by their domain packages and composed by held reference; cross-domain reads go behind read-only protocols. In views use @State (owned), @Bindable or plain let (passed in), or @Environment(M.self) plus .environment(...) (injected). Never @StateObject / @ObservedObject / @EnvironmentObject / .environmentObject(_:).
Executable-target boundary (invert, never work around):
@maincmuxAppandAppDelegatestay in the executable target as the thin composition shim. That residual is the intended end state, not debt.- A type is declared in exactly one module and a lower package cannot extend a higher-owned type, so
AppDelegate+*/cmuxApp+*/Workspace+*extensions do not move down. Extract the behavior into a Coordinator/Service/Repository, have the god object own an instance, and reduce the extension to a one-line forward. - Stored properties cannot cross module boundaries. Decompose god-model state into child
@Observablesub-models owned by domain packages, composed by held reference, with cross-cutting reads behind read-only protocols.
File organization
One major type per file, named after the type (Control.swift, LabeledChoice.swift, ListControl.swift, not one shared SettingControl.swift). Applies to all new code in Packages/ and all new app-target files.
- Trivial private helpers, nested types, and single-line extensions used only inside the file may stay with the parent type. Anything with a meaningful body gets its own file, including a
private final classnested in another file's type. - Conformance-adding extensions for a type defined elsewhere go in
TypeName+Conformance.swiftorTypeName+Feature.swift, not bundled into the consuming feature file. - Type-erased wrappers live next to what they erase:
Foo.swiftandAnyFoo.swift. - The god files (
ContentView.swift,Workspace.swift,TabManager.swift,cmuxApp.swift) are what this rule exists to stop. Splitting one file per type is correct even if it triples the file count. File count is cheap; "find this type" being unanswerable is expensive.
Documentation
Every public symbol in a new package under Packages/ gets a Swift-DocC /// comment at the time it is written. Docs are part of the API surface, not follow-up work.
- First line is a one-sentence summary that fits on one line and ends with a period. Blank
///line before any discussion paragraph. Use- Parameter name:/- Returns:/- Throws:oninitandfuncsymbols that take parameters or throw. Markdown is fine. - Reference symbols with double backticks (`
CmuxSetting); plain backticks for non-symbol code (UserDefaults.standard`).
More from manaflow-ai
Use this skill in a project
Open any RiverX project, search the Skills panel for manaflow-ai/cmux/cmux-architecture, and install it in one click.
