
Cmux Architecture
- 2.6k installs
- 25.6k repo stars
- Updated August 5, 2026
- manaflow-ai/cmux
cmux-architecture is a Claude Code skill that helps developers design cmux terminal-multiplexer layout, session models, and agent-facing interfaces before implementing backend services or terminal UX.
About
cmux-architecture is a Claude Code skill for designing cmux, a terminal multiplexer aimed at agent-driven development workflows. The skill guides layout of pane orchestration, session state models, and interfaces that AI agents invoke before writing backend services or terminal user experience. Developers reach for cmux-architecture when planning how multiple terminal panes, sessions, and agent commands interact in cmux. It standardizes architectural decisions so implementation skills like cmux-dev-workflow and cmux-testing have a consistent foundation.
- Defines cmux session and pane orchestration model
- Specifies agent-to-terminal control boundaries
- Documents multiplexer topology for parallel agent work
- Aligns CLI UX with backend session lifecycle
- Captures extension points for integrations and plugins
Cmux Architecture by the numbers
- 2,607 all-time installs (skills.sh)
- +301 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #326 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manaflow-ai/cmux --skill cmux-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 25.6k |
| Last updated | August 5, 2026 |
| Repository | manaflow-ai/cmux ↗ |
How do you design cmux terminal multiplexer architecture for agents?
Design cmux terminal-multiplexer layout, session model, and agent-facing interfaces before implementing backend services or terminal UX.
Who is it for?
cmux contributors or agent-tooling developers defining terminal multiplexer structure before coding session APIs.
Skip if: Developers only using tmux or iTerm without cmux, or teams needing generic microservice architecture unrelated to terminal multiplexing.
When should I use this skill?
A developer plans cmux pane orchestration, session APIs, or agent terminal interfaces before implementation.
What you get
Architecture spec covering pane layout, session model, and agent interface contracts.
- Pane layout architecture
- Session model specification
- Agent interface contract
Files
cmux Architecture
Package architecture
We are migrating cmux from a single app target into Swift Packages under Packages/. Every new package must satisfy three rules:
- Ergonomic. Public API surface matches what callers naturally want to write. Default to internal access; expose
publiconly for types and functions that downstream consumers actually use. Avoid friction such as forcing every call site through a builder or wrapper when a direct API is fine. - No dependency cycles. Packages form a strict DAG. A package may only depend on packages strictly lower in the graph. When two packages need to share a type, lift it to a common lower-level package or define a protocol seam in the consumer. Every new dependency edge requires re-checking that the graph stays acyclic.
- Clear but not overly narrow responsibilities. A package owns one full domain (e.g. _settings_, _appearance_, _workspace_, _terminal_, _browser_, _command palette_), not a slice of one. A package called "appearance math" or "workspace model" is too narrow — it forces every consumer that touches the surrounding domain to also depend on the sibling slices. Prefer a single
CmuxAppearancethat owns settings, theming, colors, glass, and snapshots together, overCmuxAppearanceMath+CmuxAppearanceTheme+CmuxAppearanceSettings. Don't fragment a domain intoCmuxFooFormatting+CmuxFooLogic+CmuxFooState— that's folder structure inside a single package, not module structure. A package boundary exists because more than one consumer needs the contents, or a build/test seam needs to exist.
When in doubt, extract leaf-first: pull out the package that has no internal dependencies. Consumers in the app target stay put and only update imports. Each leaf shrinks the app target without requiring downstream packages to exist yet.
The existing packages under Packages/ predate this policy and should not be used as design references.
Wiring a new local package into the project. cmux.xcodeproj lists package dependencies explicitly (it is not a synchronized-folder project). Adding Packages/CmuxFoo means mirroring an existing package's project.pbxproj entries — one XCLocalSwiftPackageReference (in the project's packageReferences), one XCSwiftPackageProductDependency, and a PBXBuildFile linked in the Frameworks phase of every target that imports it. The app-target packages link into both cmux and cmux-unit (so tests can import and inject them); copy a recent leaf like CmuxSocketControl for the exact shape, then run scripts/normalize-pbxproj.py and scripts/check-pbxproj.sh. A package the app builds against but cmux-unit does not link will compile the app yet fail the test target.
Refactor architecture: layers, Coordinator/Service/Repository, dependency inversion
These higher-level patterns are binding on every new or moved/meaningfully-rewritten file. (The full blueprint, with worked examples and the per-god decomposition, lives in the cmuxterm-hq control repo under docs/cmux-refactor-audit/blueprint/; the enforceable core is below.)
Layered, downward-only DAG. Packages form a strict acyclic graph in five layers; dependencies point only downward:
1. Core (e.g. CmuxCore) — pure Sendable values, IDs, DTOs, errors, and the protocol seams shared across domains. No AppKit/SwiftUI/I/O. The lift target when two domains need the same type. 2. Services / infrastructure — actors implementing core protocols against the outside world (process/PTY, filesystem, sockets, web API, notifications, auth). One package per cohesive capability. 3. Domain / state — @MainActor @Observable models + Coordinators, one package per feature domain; owns that domain's mutable state. CmuxSettings is the exemplar. 4. UI — SwiftUI/AppKit views, one UI package per domain package, depending only on its domain package + Core, never on a Service directly. CmuxSettingsUI is the exemplar. 5. Executable (cmuxApp / AppDelegate) — a thin composition shim, no business logic.
Classify every extracted entity by intent:
- Coordinator — a
@MainActor @Observableorchestrator that sequences a user flow and owns navigation/selection/lifecycle state, calling Services and child models. Does no I/O itself. - Service — an
actor(or@MainActoronly when an AppKit main-thread API forces it) performing one outside-world capability; exposesasync/await+AsyncStream, holds only its own resource handles, holds no UI state. - Repository — an
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. Share a type by lifting it to Core or defining a protocol seam in the consumer — never a stored property reaching across modules. Injection is 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), but is never the source of truth for service wiring.
State + SwiftUI wiring. 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 the home object via held references; cross-domain reads go behind read-only protocols. In views use @State (owned), @Bindable / plain let (passed-in), or @Environment(M.self) + .environment(...) (injected) — never @StateObject / @ObservedObject / @EnvironmentObject / .environmentObject(_:).
Executable-target boundary (three hard constraints — invert, never work around):
1. @main cmuxApp and AppDelegate stay in the executable target as the thin composition shim; that residual is the intended end state, not debt. 2. 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. 3. Stored properties cannot cross module boundaries: decompose god-model state into child @Observable sub-models owned by domain packages, composed by held reference, with cross-cutting reads behind read-only protocols.
File organization
One major type per file. Each struct, class, enum, actor, or protocol that is part of a public API (or has any meaningful body) lives in its own file named after the type (Control.swift, LabeledChoice.swift, ListControl.swift — not one shared SettingControl.swift). This rule applies to all new code in Packages/ and to any new files added to the app target.
- Small, closely-bound helpers (
private struct, nested types, single-line extensions used only inside the file) can stay with the parent type. Anything bigger or independently meaningful gets its own file. - 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 (
AnyFoo) live next to the type they erase (Foo.swiftandAnyFoo.swift), each in its own file. - Existing god files (
ContentView.swift,Workspace.swift,TabManager.swift,cmuxApp.swift) are the pattern this rule exists to stop. When migrating code out of them, split into one file per type even if it triples the file count. File count is cheap; "find this type" being unanswerable is expensive.
Documentation
Every public symbol in any new Swift package under Packages/ is documented with a Swift-DocC triple-slash comment at the time of writing. Treat docs as part of the API surface, not as follow-up work.
- Format. Use
///doc comments above the symbol. First line is a one-sentence summary that fits on a single line and ends with a period. If more context is needed, leave a blank///line, then add a discussion paragraph. Use- Parameter name:/- Returns:/- Throws:callouts oninitandfuncsymbols that take parameters or throw. Use Markdown freely (bold, fenced code blocks for examples, backticks for inline code). - Cross-references. Refer to other symbols using double-backticks: `
CmuxSetting. Plain backticks are for non-symbol code (UserDefaults.standard,@AppStorage`). - What to document on each symbol. Types: what they represent and when to use them. Enums: meaning of each case. Init parameters: especially defaults and the reason for them. Properties: what value they hold and any invariants. Methods: what they do, plus parameters/returns/throws. Generic constraints: which
Value/Elementshapes the type accepts and why (e.g.,Sendable & Codable). - Examples. Non-trivial APIs get at least one example in a fenced
`swiftblock, ideally a real declaration from this codebase. Keep examples short and idiomatic. - Internal vs public.
internalandprivatesymbols get a one-line///when the intent is non-obvious; verbosity is not required at that scope. The public boundary is the one that needs full coverage. - No stale docs. When you change a symbol's behavior or signature, update its doc comment in the same edit. Docs that describe last week's behavior are worse than no docs.
- Don't comment-narrate the body. Doc comments describe the contract from the outside. Inline
//comments inside method bodies are reserved for non-obvious why, not what (the existing rule from the top-level guidance still applies).
This rule applies to all packages under Packages/. Code in the main app target is not retroactively required to be documented, but new public symbols added to packages must be.
Package design discipline
These are the recurring design mistakes that have to be caught at the design step, not at code review:
- No shared-singleton accessors.
static let standard/shared/defaulton a package type that holds runtime state is a singleton-by-another-name. Construct the package type at the app's startup site and inject it.static letis fine for declarations — identifiers, schema entries, enum cases — but not for behavior. - No namespace-enums.
enum Foo { static func bar() }(a no-case enum used as a namespace) is a fake namespace that fights the rest of the design (no instances, no DI, no test seam). Prefer a value-typed struct passed via constructor when the helper might gain configuration, or a file-scopeprivate funcfor pure helpers internal to one file. - No parallel hand-maintained registries. When a list mirrors a set of declared items (e.g.
catalog.allmirroring the catalog's stored properties), derive the list viaMirrorreflection or a macro. Two sources of truth drift silently; the IDE doesn't tell you. - Prefer compile-time invariants to runtime traps. If the pattern is
guard ... else { assertionFailure(...); return default }for a "programmer error" case, encode it in the type system (phantom types, separate concrete flavors). Runtime traps become silent fallbacks in release builds. - No free functions. Functionality is always scoped to an entity that owns the responsibility: a method on a value type, an extension on the type the operation belongs to, or a member of the Coordinator/Service/Repository that uses it. Top-level
funcdeclarations (any visibility, including file-scopeprivate func) are banned. The only sanctioned exception is a@convention(c)trampoline a C API forces on us, marked with a one-line justification. - Nested types still count for the one-major-type-per-file rule. A
private final class WatcherAttachmentinsideJSONConfigFileWatcher.swiftis a major type. Move it to its own file the moment it has a meaningful body.
Testability
Every public type added to Packages/ must be testable from a test target without launching the app target, without booting AppKit, and without depending on the user's filesystem or UserDefaults.standard. Production-grade designs surface a test seam at every boundary:
- No global state in package code. Every public type that needs
UserDefaults,FileManager, an on-disk path, an environment variable, or a clock takes it via initializer parameter. Tests pass aUserDefaults(suiteName:)scoped to the test, a temp directory URL, a fixedDate, etc. - No reliance on `.shared` / `.standard`. A public type that hardcodes
UserDefaults.standardorFileManager.defaultinside its implementation cannot be tested without polluting the developer's actual settings. Inject these at the seam. - Test through injected seams, never a static test hook. A
nonisolated(unsafe) static var fooForTesting(or any global mutable "override" a test swaps in) is global state by another name: it leaks across tests, forcesnonisolated(unsafe), and usually needs a lock. Replace it with a protocol seam injected throughinit(e.g.init(commandRunner: any CommandRunning = CommandRunner())); the test passes a conforming fake. When you extract such a type into a package, deleting the static hook (and the lock it required) is part of the extraction, not a follow-up. - Public APIs return values, not side effects, where possible. A function that mutates global UserDefaults and returns
Voidis harder to test than one that returns the changed value and lets the caller persist. Prefer pure transformations + thin imperative layers. - Asynchronous APIs surface their observation as `AsyncStream`. Tests can iterate
AsyncStreamdeterministically and assert the sequence of yielded values. AvoidNotificationCenter-only patterns where the test has to spin a runloop. - Document the test pattern alongside any non-trivial public surface. The package's
README.mdand any DocC catalog should show how to instantiate the type with test-friendly dependencies.
If a design is hard to test, it is wrong. Reach for the constructor parameter list, not the test bench.
Modern Swift concurrency
All new code in Packages/ and any new files added to the app target use Swift 6 concurrency primitives: actor, async/await, AsyncStream/AsyncSequence, @Observable, @MainActor. Old primitives — locks, manual KVO, @Published, completion handlers, DispatchQueue used as a serial lock — are not allowed.
If you find yourself reaching for a lock to protect ongoing mutable shared state, the type is almost always the wrong shape — promote it to an actor. The exception is the narrow lock carve-out below.
Do not introduce a single-method `actor` purely as a mutex. An actor Guard { func claim() -> Bool } whose only job is to guard a flag is a lock with extra ceremony: it forces synchronous callers — a Process termination handler, a DispatchSource event handler, a withCheckedContinuation resume race — through Task { await guard.claim() }, which adds suspension points, ordering hops, and reentrancy surface to what is fundamentally a synchronous compare-and-set. That makes the code worse, not safer. A tiny synchronous guard like that belongs in the lock carve-out, not an actor.
When extracting existing code that uses a forbidden primitive into a package, reconsider the shape at the seam rather than copying it blindly — usually it wants an actor. But a one-shot single-resume guard (a Process termination handler vs. a timeout vs. a spawn failure racing to resume one withCheckedContinuation) is exactly a case the lock carve-out covers: keep a synchronous primitive, hidden behind the type. Drain Process pipes concurrently on detached tasks keyed by the raw fd (an Int32 is Sendable; a FileHandle is not).
Forbidden in new code (no exceptions without a written justification in the PR description):
- Locks.
NSLock,NSRecursiveLock,os_unfair_lock,OSAllocatedUnfairLock,pthread_mutex_t,Synchronization.Mutex,DispatchSemaphoreused as a lock. Useactorisolation. Mutable shared state belongs in an actor; reads and writes areasync. (Narrow carve-out below: a lock is allowed where theactor/asyncalternative would genuinely worsen the code, with justification.) - KVO via `NSObject` subclassing. Any
class Foo: NSObjectwhose purpose is to overrideobserveValue(forKeyPath:...)or calladdObserver(_:forKeyPath:...). Replace withNotificationCenter.default.notifications(named:)AsyncSequence, or theNSKeyValueObservationtoken API at the seam only. - `DispatchQueue` used as a synchronization primitive. A
DispatchQueue(label:)accessed viaqueue.sync { ... }to serialize mutable state is a lock with different syntax. Use anactor. Queues are fine for event delivery (e.g. aDispatchSourcehandler), not for protecting state. - Combine for change propagation. No
@Published, noObservableObject, noPassthroughSubject/CurrentValueSubject, noAnyCancellablefor change observation. Use@Observable(Observation framework, Swift 5.9+) for SwiftUI state, orAsyncStream/AsyncSequencefor cross-actor change propagation. - Completion-handler APIs. Authoring a new public API with a
(Result<T, Error>) -> Voidor(T?, Error?) -> Voidcallback is forbidden. Useasync throws -> T. When wrapping a legacy callback at the boundary, usewithCheckedContinuation/withCheckedThrowingContinuationand keep it confined to that one seam. - `DispatchQueue.main.async { ... }`. Annotate the destination with
@MainActor. Call sites eitherawaitthe main-isolated function or are themselves@MainActor. - Sleeping as a synchronization substitute.
Task.sleep/Clock.sleep(or any sleep) used to poll for a condition, to let state "settle" before reading it, or to race a callback/animation is forbidden — use a real signal (AsyncStream,NSKeyValueObservation, a completion, a state change).DispatchQueue.asyncAfteris banned outright (it is neither cancellable-by-structure nor testable). A bounded, cancellable, intended delay or deadline is allowed under theClock.sleepcarve-out below.
Required shape:
- Mutable shared state →
actor. Reads/writes/reset areasync. Observers receiveAsyncStreamreturned by the actor. - SwiftUI view-render-friendly state →
@Observable @MainActorview-model that subscribes to the actor'sAsyncStreamand projects snapshots. Don't read actor state synchronously from view code. - Cross-process / cross-thread invariants → expressed via actor isolation, not via locks or queues.
- New public observable surfaces →
AsyncStreamorAsyncSequence. Not callbacks, not@Published, not rawNotificationCentersubscription.
Acceptable with a one-line justification comment on the declaration:
These low-level primitives have no async-native replacement. They must be hidden behind an AsyncStream or actor surface; callers never see them.
DispatchSource.makeFileSystemObjectSourcefor file watching (no Foundation async equivalent).DispatchSource.makeReadSource/makeWriteSourcefor low-level socket I/O.- A bounded, cancellable `Clock.sleep` (preferred) or `Task.sleep` for a genuine delay/deadline that is itself the intended behavior — a minimum display duration, an auto-dismiss, a check timeout. Drive it from an injected
Clock(or a duration) so tests advance virtual time with no real waiting, and wire the sleepingTask's cancellation to the relevant lifecycle so a state transition cancels the pending delay (store theTask, cancel it on transition; or usewithTaskCancellationHandler). For true delays/deadlines only, never to poll, settle, or race — those still require a real signal. One-line justification on the call site. DispatchSource.makeTimerSource(one-shot) only when a genuine deadline must fire outside any async context — a non-asynctype with noTaskto host the sleep. Prefer theClock.sleepcarve-out above whenever the code is already on an actor or in async code (it is cancellation-integrated and testable; a rawDispatchSourcetimer is not, and has suspend/resume/cancel footguns). Hide the timer behind the type, cancel it on the non-timeout path, and never use it to poll or fake a sleep.- A lock for a synchronous compare-and-set called from non-async callbacks, where promoting to an
actorwould only addTask/awaithops and reentrancy. The canonical case is a one-shot resume guard: several synchronousProcess/DispatchSourcecallbacks race to resume onewithCheckedContinuationexactly once.OSAllocatedUnfairLock(initialState:)guarding aBool(claimed once, checked synchronously in each callback) is correct, deterministic, and lets the callback resume the continuation inline. This carve-out is for short, non-blocking critical sections over a tiny flag/counter — not for guarding ongoing domain state (that is still anactor). Keep it private to the type, with a one-line justification. NSKeyValueObservationtoken (the closure-based API) when wrapping a Foundation/AppKit type that exposes change only via KVO.
`@unchecked Sendable` and `nonisolated(unsafe)`:
Both require a comment on the declaration explaining the safety argument. Examples that pass review:
// Wraps DispatchSourceFileSystemObject; every mutation happens on `queue`.
private final class WatcherAttachment: @unchecked Sendable { ... }
// UserDefaults is Apple-documented thread-safe; OK to read nonisolated.
private nonisolated(unsafe) let defaults: UserDefaultsWithout a justification comment, the diff is rejected. @unchecked Sendable on an entire actor or struct is almost always wrong; prefer nonisolated(unsafe) let on the single non-Sendable property.
Scope and enforcement:
- Applies to: every new file in
Packages/, every new file in the app target, every meaningful rewrite of an existing Swift file. - Existing app target code may continue to use the old primitives until rewritten. Do not retrofit blindly.
- Code review checklist (Codex, CodeRabbit, Greptile, and human reviewers): reject diffs that introduce
@Published/ObservableObject/DispatchQueue.main.async/addObserver(_:forKeyPath:...)/DispatchQueue.asyncAfterin new code, orTask.sleep/Clock.sleepused to poll, settle, or race rather than as a bounded, cancellable, injected-clock delay with justification. Reject a lock (NSLock/OSAllocatedUnfairLock/etc.) or@unchecked Sendable/nonisolated(unsafe)unless it falls under a documented carve-out and carries a one-line justification — and reject a single-methodactorthat exists only to guard a flag (use the lock carve-out instead).
Detailed references
- Read references/package-boundaries.md for detailed package extraction, dependency graph, composition-root, and pbxproj wiring guidance.
- Read references/concurrency-carveouts.md for detailed examples and review guidance around actors, locks, DispatchSource, sleep,
@unchecked Sendable, andnonisolated(unsafe). - Read references/file-api-discipline.md for one-type-per-file, DocC, public API, and design-smell details.
interface:
display_name: "cmux Architecture"
short_description: "Apply cmux package, refactor, file organization, DocC, testability, and concurrency rules."
default_prompt: "Use this skill before adding or meaningfully rewriting Swift files, Swift packages, coordinators, services, repositories, package APIs, or Swift concurrency code in cmux."
Concurrency Carve-outs
This reference expands the Swift 6 concurrency rules.
Default shape
Use modern Swift primitives:
actorfor mutable shared stateasync/awaitfor asynchronous APIsAsyncStreamorAsyncSequencefor observation@Observable @MainActorfor SwiftUI-facing state@MainActorinstead ofDispatchQueue.main.async
Do not add new @Published, ObservableObject, completion-handler APIs, KVO observer overrides, or queue-as-lock patterns in new package code or meaningful rewrites.
Actor, not lock
Ongoing mutable shared state belongs in an actor. If the state has a lifecycle, multiple operations, or can be observed, an actor is almost always the right shape.
Example actor-owned responsibilities:
- process registry
- file watcher state
- socket session table
- retry/idempotency state
- provider lifecycle state
Single-method actor smell
Do not introduce an actor whose only job is to guard a boolean flag:
actor ResumeGuard {
func claim() -> Bool { ... }
}That pattern is usually a lock with extra suspension and reentrancy surface. In synchronous callback races, the callback often needs an immediate compare-and-set, not a Task { await ... } hop.
Lock carve-out
A private lock is acceptable for a short, synchronous compare-and-set called from non-async callbacks where an actor would worsen ordering and reentrancy.
Canonical case:
- process termination handler
- timeout callback
- spawn failure callback
- all race to resume exactly one
withCheckedContinuation
Use a tiny private guard, document the reason on the declaration, and keep the critical section non-blocking.
This carve-out does not allow locking ongoing domain state.
DispatchSource carve-outs
These low-level primitives have no async-native replacement and are acceptable behind an async or actor surface:
DispatchSource.makeFileSystemObjectSourceDispatchSource.makeReadSourceDispatchSource.makeWriteSource
Hide the source behind the type. Callers should see an AsyncStream, AsyncSequence, or actor API, not raw DispatchSource lifecycle.
Sleep carve-out
Clock.sleep or Task.sleep is acceptable only for a genuine bounded delay or deadline that is the intended behavior:
- minimum display duration
- auto-dismiss
- check timeout
- deadline for a provider operation
It is not acceptable for polling, settling UI state, or racing an animation/callback.
Prefer an injected Clock or duration so tests can advance virtual time. Store and cancel sleeping tasks on lifecycle transitions.
Timer source carve-out
Use DispatchSource.makeTimerSource only when a genuine deadline must fire outside any async context and there is no task to host Clock.sleep.
Prefer Clock.sleep whenever the code is already async or actor-isolated.
Sendability escape hatches
@unchecked Sendable and nonisolated(unsafe) require comments on the declaration explaining why the usage is sound.
Good examples:
// Wraps DispatchSourceFileSystemObject; every mutation happens on `queue`.
private final class WatcherAttachment: @unchecked Sendable { ... }
// UserDefaults is Apple-documented thread-safe; OK to read nonisolated.
private nonisolated(unsafe) let defaults: UserDefaultsPrefer narrowing the escape hatch to one property rather than marking an entire actor or value type unchecked.
Review checklist
Reject diffs that introduce any of these in new code without a documented carve-out:
@PublishedObservableObjectDispatchQueue.main.asyncDispatchQueue.asyncAfteraddObserver(_:forKeyPath:...)- queue-as-lock synchronization
- lock for ongoing mutable state
Task.sleeporClock.sleepused to poll/settle/race@unchecked Sendablewithout a safety commentnonisolated(unsafe)without a safety comment
File and API Discipline
This reference expands file organization, documentation, and design-smell rules.
One major type per file
Each meaningful struct, class, enum, actor, or protocol lives in its own file named after the type.
This applies to:
- public API types
- internal types with meaningful bodies
- private nested types that have grown beyond a tiny helper
- type-erased wrappers
- conformance extensions for externally owned types
File count is cheap. Not knowing where a type lives is expensive.
Allowed small helpers
Small, closely-bound helpers can stay with the parent type when they are private and trivial:
- a tiny nested enum used only for local branching
- a one-line private extension
- a local helper that does not have independent behavior
Move the helper once it has meaningful lifecycle, state, protocol conformance, or enough logic to test independently.
Extension files
Conformance-adding extensions for a type defined elsewhere go in files such as:
TypeName+Conformance.swiftTypeName+Feature.swift
Do not hide important conformances inside unrelated feature files.
DocC for public package APIs
Every public symbol in new Swift packages under Packages/ needs a /// DocC comment at the time it is written.
Document:
- what a type represents
- when to use it
- enum case meaning
- property invariants
- init parameters and defaults
- method parameters, returns, and throws
- generic constraints
Use double-backtick symbol references for symbols:
/// Stores a typed ``CmuxSetting`` value.Use plain backticks for non-symbol code:
/// Reads from `UserDefaults.standard` only when injected by the caller.Design smells
Avoid runtime state singletons:
static let sharedstatic let standardstatic let default
Static declarations are fine for identifiers, schema entries, and enum cases. Runtime behavior should be constructed at app startup and injected.
Avoid namespace enums:
enum Foo {
static func bar() { ... }
}If behavior may need configuration or a test seam, use a value type or service. If it is a pure local helper, keep it private near its caller.
Avoid parallel hand-maintained registries. If a list mirrors declared items, derive it via reflection or a macro where practical.
Prefer compile-time invariants to runtime traps. A guard plus assertionFailure plus fallback often means the type model is too weak.
Package Boundaries
This reference expands cmux package extraction and refactor architecture rules.
Why package boundaries exist
A package boundary should exist because more than one consumer needs the domain, because a build/test seam is useful, or because the package isolates a cohesive external capability. It should not exist just to make a file list look smaller.
Good package names describe a domain:
CmuxSettingsCmuxSettingsUICmuxAppearanceCmuxWorkspaceCmuxBrowserCmuxSocketControl
Weak package names describe a slice:
CmuxAppearanceMathCmuxWorkspaceModelCmuxFooFormattingCmuxFooLogicCmuxFooState
Slices force callers to depend on several sibling packages any time they touch the real domain.
Dependency graph
Packages form a strict downward-only DAG:
1. Core: pure Sendable values, IDs, DTOs, errors, and protocol seams. No AppKit, SwiftUI, or I/O. 2. Services/infrastructure: actors implementing core protocols against external systems. 3. Domain/state: @MainActor @Observable models and Coordinators. 4. UI: SwiftUI/AppKit views that depend on domain packages and Core, not services directly. 5. Executable: cmuxApp and AppDelegate as the composition root.
If two domains need a shared type, lift the type to a lower package or define a protocol seam. Do not make sibling packages reach sideways.
Extract leaf-first
When uncertain, extract the package that has no internal dependencies first. This keeps the migration incremental and avoids needing several downstream packages to exist before one package can compile.
Leaf-first extraction also makes review easier:
- fewer dependency edges
- fewer project-file entries
- simpler tests
- clearer rollback path
Composition root
The executable app target is the single composition root. Concrete services and repositories are named there and injected into coordinators/models.
Do not introduce:
- global containers
- runtime state singletons
static let shared- service lookups from package internals
SwiftUI Environment may carry already-constructed observable models down a view tree. It should not become the source of truth for service wiring.
Executable target boundary
@main cmuxApp and AppDelegate stay in the executable target. Do not move extensions of executable-owned types down into lower packages. A lower package cannot extend a higher-owned type without creating the wrong dependency direction.
Instead:
1. Extract behavior into a Coordinator, Service, or Repository in the appropriate package. 2. Inject it into the god object or app composition root. 3. Reduce the original extension to a one-line forward if it must remain.
pbxproj wiring
cmux.xcodeproj lists package dependencies explicitly. Adding Packages/CmuxFoo means mirroring existing package entries:
- one
XCLocalSwiftPackageReference - one
XCSwiftPackageProductDependency - one
PBXBuildFilelinked in the Frameworks phase of every target that imports it
App-target packages link into both cmux and cmux-unit, so tests can import and inject them. A package linked by the app but not cmux-unit can make the app build pass while the test target fails.
After editing the project file, run:
scripts/normalize-pbxproj.py
scripts/check-pbxproj.shRelated skills
FAQ
What does cmux-architecture design before coding?
cmux-architecture covers cmux pane layout, session state models, and agent-facing terminal interfaces so backend services and terminal UX implement against a shared multiplexer design.
Who should use cmux-architecture?
cmux-architecture targets contributors building cmux or developers designing agent-driven terminal workflows who need session and pane structure defined before writing orchestration code.
How does cmux-architecture relate to other cmux skills?
cmux-architecture precedes cmux-dev-workflow and cmux-testing by establishing layout and session contracts that implementation and test skills follow during cmux development.