[codex] Add more cmux agent skills (#5614)

* Add more cmux agent skills

* Trim cmux CLAUDE skill index

* Keep setup and local dev in CLAUDE

* Expose repo skills to Claude and Codex

* Keep core testing policies in CLAUDE

* Keep pitfalls and release rules in CLAUDE
This commit is contained in:
Abdulaziz Albahar
2026-06-09 12:06:54 -05:00
committed by GitHub
parent 136c7c4b4f
commit 075410eef1
54 changed files with 1608 additions and 125 deletions
+1
View File
@@ -0,0 +1 @@
../skills
+1
View File
@@ -0,0 +1 @@
../../skills/cmux
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-architecture
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-backend
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-browser
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-customization
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-debugging
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-dev-workflow
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-diagnostics
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-ghostty
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-keyboard-shortcuts
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-localization
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-markdown
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-release
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-settings
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-shared-behavior
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-socket-policy
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-testing
+1
View File
@@ -0,0 +1 @@
../../skills/cmux-workspace
+25 -125
View File
@@ -2,6 +2,7 @@
## Initial setup
Run the setup script to initialize submodules, build GhosttyKit, and install the pbxproj normalization pre-commit hook:
```bash
./scripts/setup.sh
@@ -28,12 +29,14 @@ By default, `reload.sh` builds but does **not** launch the app. The script print
3. Format it as a markdown link using the template for your agent type.
Example. If `reload.sh` output contains:
```
```text
App path:
/Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux DEV my-tag.app
```
**Claude Code** outputs:
```markdown
=======================================================
[cmux DEV my-tag.app](file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app)
@@ -41,7 +44,8 @@ App path:
```
**Codex** outputs:
```
```markdown
=======================================================
[my-tag: file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app](file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app)
=======================================================
@@ -50,19 +54,14 @@ App path:
Never use `/tmp/cmux-<tag>/...` app links in chat output.
For CLI or socket dogfood against a tagged Debug app, use the tag-bound helper and set `CMUX_TAG`.
Do not use `/tmp/cmux-cli` for tagged dogfood, since that symlink points at the most recently
reloaded build and can target the user's main app socket.
Do not use `/tmp/cmux-cli` for tagged dogfood, since that symlink points at the most recently reloaded build and can target the user's main app socket.
```bash
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh list-workspaces
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh send --workspace workspace:1 --surface surface:1 "echo ok"
```
The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.sock`, and uses the
matching tagged CLI from `~/Library/Developer/Xcode/DerivedData/cmux-<tag>/...`. It also scrubs
ambient cmux terminal context (`CMUX_SOCKET`, `CMUX_SOCKET_PASSWORD`, workspace/surface/tab/panel
IDs, cmuxd socket, and debug log), then sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and
`CMUX_BUNDLED_CLI_PATH` for the selected tag.
The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.sock`, and uses the matching tagged CLI from `~/Library/Developer/Xcode/DerivedData/cmux-<tag>/...`. It also scrubs ambient cmux terminal context (`CMUX_SOCKET`, `CMUX_SOCKET_PASSWORD`, workspace/surface/tab/panel IDs, cmuxd socket, and debug log), then sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and `CMUX_BUNDLED_CLI_PATH` for the selected tag.
After making code changes, always use `reload.sh --tag` to build. **Never run bare `xcodebuild` or `open` an untagged `cmux DEV.app`.** Untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
@@ -123,80 +122,6 @@ This creates an isolated app with its own name, bundle ID, socket, and derived d
Before launching a new tagged run, clean up any older tags you started in this session (quit old tagged app + remove its `/tmp` socket/derived data).
## Cloud VM secrets
Cloud VM build, test, and local dev scripts use provider secrets from `~/.secrets/cmux.env`.
- `E2B_API_KEY`
- `FREESTYLE_API_KEY`
- R2 upload vars used by `web/scripts/build-cloud-vm-images.ts` when creating Freestyle snapshots
Load them with:
```bash
set -a
source ~/.secrets/cmux.env
set +a
```
`~/.secrets/cmuxterm-dev.env` is for local Stack/web env and does not contain the provider build keys.
`bun dev` sources `~/.secrets/cmux.env` first when present, then `~/.secrets/cmuxterm-dev.env` so
cmuxterm-specific Stack settings override broader cmux secrets. The web dev loader still accepts
the legacy `~/.secret/cmuxterm.env` and `~/.secrets/cmuxterm.env` paths while machines migrate.
## Backend TypeScript
Default backend TypeScript to Effect. For code under `web/app/api/**`, `web/services/**`, and
backend scripts that touch providers, databases, auth, rate limits, retries, timeouts, or telemetry,
model workflows as `Effect.Effect` values with typed domain errors and explicit service
dependencies. Keep Next route handlers thin: parse the request, run one Effect program at the
boundary, map typed errors to HTTP responses, and treat unexpected defects separately.
Use plain TypeScript only for trivial data shapes, constants, config files, frontend React code, or
small glue where Effect would add ceremony without improving failure handling.
Cloud VM backend logic must stay in Vercel route handlers and Effect services backed by Postgres.
Do not reintroduce Rivet or a raw actor protocol for this feature unless a later architecture doc
explicitly changes the control plane.
Production and staging Cloud VM Postgres should use the Vercel Marketplace AWS Aurora PostgreSQL
OIDC/RDS IAM path. Runtime env names are `CMUX_DB_DRIVER=aws-rds-iam`, `AWS_ROLE_ARN`,
`AWS_REGION`, `PGHOST`, `PGPORT`, `PGUSER`, and `PGDATABASE`. Run production/staging migrations
with `bun db:migrate:aws-rds-iam`; never run Drizzle migrations from Vercel build or route startup.
Local development keeps using the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
Cloud VM create pricing gates should use Stack Auth team payment items when enabled. Postgres remains
the source of truth for VM lifecycle, active VM limits, idempotency, and usage events.
## Debug event log
When adding debug event instrumentation, put events (keys, mouse, focus, splits, tabs)
in the unified DEBUG build log:
This section describes the required destination and shape for debug logs when they
are added. It is not a blanket requirement to add debug logs to every new code path.
Most temporary probes should be added only during the dogfood debug loop and removed
before merge.
```bash
tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug.log)"
```
- Untagged Debug app: `/tmp/cmux-debug.log`
- Tagged Debug app (`./scripts/reload.sh --tag <tag>`): `/tmp/cmux-debug-<tag>.log`
- `reload.sh` writes the current path to `/tmp/cmux-last-debug-log-path`
- `reload.sh` writes the selected dev CLI path to `/tmp/cmux-last-cli-path`
- `reload.sh` updates `/tmp/cmux-cli` and `$HOME/.local/bin/cmux-dev` to that CLI
- Implementation: `Packages/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift`
- App shim: `Sources/App/DebugLogging.swift`
- Free function `cmuxDebugLog("message")` — logs with timestamp and appends to file in real time from cmux code
- The package implementation and app shim are `#if DEBUG`; all call sites must be wrapped in `#if DEBUG` / `#endif`
- 500-entry ring buffer; `CMUXDebugLog.DebugEventLog.shared.dump()` writes full buffer to file
- Key events logged in `AppDelegate.swift` (monitor, performKeyEquivalent)
- Mouse/UI events logged inline in views (ContentView, BrowserPanelView, etc.)
- Focus events: `focus.panel`, `focus.bonsplit`, `focus.firstResponder`, `focus.moveFocus`
- Bonsplit events: `tab.select`, `tab.close`, `tab.dragStart`, `tab.drop`, `pane.focus`, `pane.drop`, `divider.dragStart`
## Regression test commit policy
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
@@ -212,14 +137,6 @@ This makes it visible in the GitHub PR UI (Commits tab, check statuses) that the
- For optimistic UI or CLI updates, keep one mutation path, record pending state with a request id or previous snapshot, reconcile from the authoritative result, and handle failure with an explicit rollback or error state. Do not let each entrypoint maintain its own optimistic copy.
- When a user says tests missed a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete.
## Debug menu
The app has a **Debug** menu in the macOS menu bar (only in DEBUG builds). Use it for visual iteration:
- **Debug > Debug Windows** contains panels for tuning layout, colors, and behavior. Entries are alphabetical with no dividers.
- To add a debug toggle or visual option: create an `NSWindowController` subclass with a `shared` singleton, add it to the "Debug Windows" menu in `Sources/cmuxApp.swift`, and add a SwiftUI view with `@AppStorage` bindings for live changes.
- When the user says "debug menu" or "debug window", they mean this menu, not `defaults write`.
## Pitfalls
- **Custom UTTypes** for drag-and-drop must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations` (e.g. `com.splittabbar.tabtransfer`, `com.cmux.sidebar-tab-reorder`).
@@ -238,40 +155,6 @@ The app has a **Debug** menu in the macOS menu bar (only in DEBUG builds). Use i
- **Foundation, SwiftUI, AttributeGraph, and WebKit semantics change silently between macOS major versions.** A function that "obviously" returns the same value on every macOS is not a reliable assumption. Concrete case from https://github.com/manaflow-ai/cmux/issues/4529: `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returns `"/.."` on macOS 14 and 15 but `"/"` on macOS 26 — Apple silently fixed the underlying CFURL normalization. The repo's `macos-26` CI and every maintainer's dev machine were on the fixed-behavior side; every reporter on the issue was on the broken side. Always test on the reporter's macOS before declaring a user-reported repro disproven. AWS M4 Pro builders (`cmux-aws-mac`, `cmux-aws-m4pro`, `aws-m4pro-1..6`) are pre-provisioned on macOS 15.7.4 and the preferred empirical-repro path; see the `regression-hunt` skill in the cmuxterm-hq sibling repo for the full playbook.
- **Test files in `cmuxTests/` must be wired into `cmux.xcodeproj/project.pbxproj`.** A `.swift` file added to the worktree without a matching `PBXFileReference` + `PBXSourcesBuildPhase` entry is silently ignored by Xcode and never compiles or runs on CI. Both `xcodebuild test -only-testing:cmuxTests/<TestClass>` and bot reviews pass with "Executed 0 tests" — so the missing wiring is indistinguishable from a clean two-commit red/green regression test until a real user hits the bug. The `workflow-guard-tests` job runs `./scripts/lint-pbxproj-test-wiring.sh` to catch this at PR time; surfaced during the https://github.com/manaflow-ai/cmux/issues/4529 investigation against https://github.com/manaflow-ai/cmux/pull/4536. Add via Xcode (drag the file into the cmuxTests target) or hand-edit the four pbxproj entries; reference any wired sibling like `TabManagerUnitTests.swift` as a template.
## Test quality policy
- Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns.
- Do not add tests that read checked-in metadata or project files such as `Resources/Info.plist`, `project.pbxproj`, `.xcconfig`, or source files only to assert that a key, string, plist entry, or snippet exists.
- Tests must verify observable runtime behavior through executable paths (unit/integration/e2e/CLI), not implementation shape.
- For metadata changes, prefer verifying the built app bundle or the runtime behavior that depends on that metadata, not the checked-in source file.
- If a behavior cannot be exercised end-to-end yet, add a small runtime seam or harness first, then test through that seam.
- If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and state that explicitly.
## Socket command threading policy
- Do not use `DispatchQueue.main.sync` for high-frequency socket telemetry commands (`report_*`, `ports_kick`, status/progress/log metadata updates).
- For telemetry hot paths:
- Parse and validate arguments off-main.
- Dedupe/coalesce off-main first.
- Schedule minimal UI/model mutation with `DispatchQueue.main.async` only when needed.
- Commands that directly manipulate AppKit/Ghostty UI state (focus/select/open/close/send key/input, list/current queries requiring exact synchronous snapshot) are allowed to run on main actor.
- If adding a new socket command, default to off-main handling; require an explicit reason in code comments when main-thread execution is necessary.
## Socket focus policy
- Socket/CLI commands must not steal macOS app focus (no app activation/window raising side effects).
- Only explicit focus-intent commands may mutate in-app focus/selection (`window.focus`, `workspace.select/next/previous/last`, `surface.focus`, `pane.focus/last`, browser focus commands, and v1 focus equivalents).
- All non-focus commands should preserve current user focus context while still applying data/model changes.
## Testing policy
**Never run tests locally.** All tests (E2E, UI, python socket tests) run via GitHub Actions or on the VM.
- **E2E / UI tests:** trigger via `gh workflow run test-e2e.yml` (see cmuxterm-hq CLAUDE.md for details)
- **Unit tests:** `xcodebuild -scheme cmux-unit` is safe (no app launch), but prefer CI
- **Python socket tests (tests_v2/):** these connect to a running cmux instance's socket. Never launch an untagged `cmux DEV.app` to run them. If you must test locally, use a tagged build's socket (`/tmp/cmux-debug-<tag>.sock`) with `CMUX_SOCKET_PATH=/tmp/cmux-debug-<tag>.sock`
- **Never `open` an untagged `cmux DEV.app`** from DerivedData. It conflicts with the user's running debug instance.
## Ghostty submodule workflow
Ghostty changes must be committed in the `ghostty` submodule and pushed to the `manaflow-ai/ghostty` fork.
@@ -348,3 +231,20 @@ Notes:
- README download button points to `releases/latest/download/cmux-macos.dmg`.
- Versioning: bump the minor version for updates unless explicitly asked otherwise.
- Changelog: update `CHANGELOG.md`; docs changelog is rendered from it.
## Skills
Detailed cmux contributor rules live in repo skills under `skills/`; use the task-specific skill before changing that area.
Core skill map:
- `cmux-dev-workflow`: setup, tagged reloads, Xcode project normalization, sidebar extension tagging, local dev build isolation.
- `cmux-architecture`: package boundaries, refactor architecture, file/API discipline, testability, Swift concurrency rules.
- `cmux-backend`: backend TypeScript, Effect, Cloud VM control plane, provider secrets, Postgres and migrations.
- `cmux-debugging`: debug event log, Debug menu, runtime pitfalls, typing-sensitive paths, SwiftUI list boundaries.
- `cmux-localization`: user-facing strings, localization files, shortcut text, and localization audit.
- `cmux-testing`: regression policy, Swift Testing, test quality, test wiring, local vs CI validation.
- `cmux-socket-policy`: socket command threading and focus preservation.
- `cmux-shared-behavior`: shared action paths for multi-entrypoint behavior and optimistic updates.
- `cmux-ghostty`: Ghostty submodule and GhosttyKit workflow.
- `cmux-release`: release, version bump, changelog, pretag guard, and release asset workflow.
+6
View File
@@ -151,3 +151,9 @@ Without a justification comment, the diff is rejected. `@unchecked Sendable` on
- 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.asyncAfter` in new code, or `Task.sleep`/`Clock.sleep` used 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-method `actor` that exists only to guard a flag (use the lock carve-out instead).
## Detailed references
- Read [references/package-boundaries.md](references/package-boundaries.md) for detailed package extraction, dependency graph, composition-root, and pbxproj wiring guidance.
- Read [references/concurrency-carveouts.md](references/concurrency-carveouts.md) for detailed examples and review guidance around actors, locks, DispatchSource, sleep, `@unchecked Sendable`, and `nonisolated(unsafe)`.
- Read [references/file-api-discipline.md](references/file-api-discipline.md) for one-type-per-file, DocC, public API, and design-smell details.
@@ -0,0 +1,114 @@
# Concurrency Carve-outs
This reference expands the Swift 6 concurrency rules.
## Default shape
Use modern Swift primitives:
- `actor` for mutable shared state
- `async`/`await` for asynchronous APIs
- `AsyncStream` or `AsyncSequence` for observation
- `@Observable @MainActor` for SwiftUI-facing state
- `@MainActor` instead of `DispatchQueue.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:
```swift
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.makeFileSystemObjectSource`
- `DispatchSource.makeReadSource`
- `DispatchSource.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:
```swift
// 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: UserDefaults
```
Prefer 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:
- `@Published`
- `ObservableObject`
- `DispatchQueue.main.async`
- `DispatchQueue.asyncAfter`
- `addObserver(_:forKeyPath:...)`
- queue-as-lock synchronization
- lock for ongoing mutable state
- `Task.sleep` or `Clock.sleep` used to poll/settle/race
- `@unchecked Sendable` without a safety comment
- `nonisolated(unsafe)` without a safety comment
@@ -0,0 +1,86 @@
# 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.swift`
- `TypeName+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:
```swift
/// Stores a typed ``CmuxSetting`` value.
```
Use plain backticks for non-symbol code:
```swift
/// Reads from `UserDefaults.standard` only when injected by the caller.
```
## Design smells
Avoid runtime state singletons:
- `static let shared`
- `static let standard`
- `static 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:
```swift
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.
@@ -0,0 +1,89 @@
# 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:
- `CmuxSettings`
- `CmuxSettingsUI`
- `CmuxAppearance`
- `CmuxWorkspace`
- `CmuxBrowser`
- `CmuxSocketControl`
Weak package names describe a slice:
- `CmuxAppearanceMath`
- `CmuxWorkspaceModel`
- `CmuxFooFormatting`
- `CmuxFooLogic`
- `CmuxFooState`
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 `PBXBuildFile` linked 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:
```bash
scripts/normalize-pbxproj.py
scripts/check-pbxproj.sh
```
+45
View File
@@ -0,0 +1,45 @@
---
name: cmux-backend
description: "Backend TypeScript and Cloud VM development rules for cmux. Use when editing web/app/api, web/services, backend scripts, Cloud VM lifecycle, provider integrations, Postgres, Stack Auth pricing gates, migrations, or provider image build scripts."
---
# cmux Backend
Use this skill for backend TypeScript, Cloud VM, provider, database, auth, rate-limit, retry, timeout, or telemetry work.
## Core rules
- Default backend TypeScript to Effect under `web/app/api/**`, `web/services/**`, and backend scripts that touch providers, databases, auth, rate limits, retries, timeouts, or telemetry.
- Keep Next route handlers thin: parse the request, run one Effect program at the boundary, map typed errors to HTTP responses, and treat unexpected defects separately.
- Use plain TypeScript only for trivial data shapes, constants, config files, frontend React code, or small glue where Effect would add ceremony without improving failure handling.
- Cloud VM backend logic must stay in Vercel route handlers and Effect services backed by Postgres.
- Do not reintroduce Rivet or a raw actor protocol for Cloud VM unless a later architecture doc explicitly changes the control plane.
- Production and staging Cloud VM Postgres use the Vercel Marketplace AWS Aurora PostgreSQL OIDC/RDS IAM path.
- Runtime env names are `CMUX_DB_DRIVER=aws-rds-iam`, `AWS_ROLE_ARN`, `AWS_REGION`, `PGHOST`, `PGPORT`, `PGUSER`, and `PGDATABASE`.
- Run production/staging migrations with `bun db:migrate:aws-rds-iam`; never run Drizzle migrations from Vercel build or route startup.
- Local development keeps using the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
- Cloud VM create pricing gates should use Stack Auth team payment items when enabled.
- Postgres remains the source of truth for VM lifecycle, active VM limits, idempotency, and usage events.
## Secrets
Cloud VM build, test, and local dev scripts use provider secrets from `~/.secrets/cmux.env`.
- `E2B_API_KEY`
- `FREESTYLE_API_KEY`
- R2 upload vars used by `web/scripts/build-cloud-vm-images.ts` when creating Freestyle snapshots
Load them with:
```bash
set -a
source ~/.secrets/cmux.env
set +a
```
`~/.secrets/cmuxterm-dev.env` is for local Stack/web env and does not contain the provider build keys. `bun dev` sources `~/.secrets/cmux.env` first when present, then `~/.secrets/cmuxterm-dev.env` so cmuxterm-specific Stack settings override broader cmux secrets. The web dev loader still accepts the legacy `~/.secret/cmuxterm.env` and `~/.secrets/cmuxterm.env` paths while machines migrate.
## Detailed references
- Read [references/effect-boundaries.md](references/effect-boundaries.md) when shaping route handlers, services, typed errors, retries, or dependency injection.
- Read [references/cloud-vm-control-plane.md](references/cloud-vm-control-plane.md) when touching VM lifecycle, migrations, Postgres, provider idempotency, or pricing gates.
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Backend"
short_description: "Use Effect and Postgres-backed Cloud VM rules for backend work."
default_prompt: "Use this skill when editing cmux backend TypeScript, Vercel route handlers, Effect services, Cloud VM lifecycle code, provider integrations, database migrations, or pricing gates."
@@ -0,0 +1,60 @@
# Cloud VM Control Plane
This reference expands the Cloud VM rules for lifecycle, persistence, migrations, and provider coordination.
## Source of truth
Postgres is the source of truth for:
- VM lifecycle state
- active VM limits
- idempotency records
- usage events
- provider identifiers
- team/account ownership
Provider state is observed and reconciled, not treated as the canonical application state. If provider state and database state disagree, write code that makes the reconciliation explicit.
## Vercel and Effect boundary
Cloud VM backend logic lives in Vercel route handlers and Effect services. Route handlers should not become a raw actor protocol or long-running in-memory control plane. The durable state belongs in Postgres, and request-time workflows should be idempotent.
Do not reintroduce Rivet or a raw actor protocol unless a later architecture document explicitly changes this control plane.
## Migrations
Production and staging migrations use:
```bash
bun db:migrate:aws-rds-iam
```
Never run Drizzle migrations from Vercel build or route startup. Build/startup migrations make deploy behavior non-deterministic and couple app availability to schema mutation.
Local development keeps using the `CMUX_PORT`-derived Docker Postgres path from `bun dev`.
## AWS RDS IAM runtime
Production and staging Cloud VM Postgres should use the Vercel Marketplace AWS Aurora PostgreSQL OIDC/RDS IAM path with these runtime env names:
- `CMUX_DB_DRIVER=aws-rds-iam`
- `AWS_ROLE_ARN`
- `AWS_REGION`
- `PGHOST`
- `PGPORT`
- `PGUSER`
- `PGDATABASE`
Avoid inventing parallel env names for the same settings. Every new name creates another migration and deploy surface.
## Pricing and active limits
Cloud VM create pricing gates should use Stack Auth team payment items when enabled. Active limits and usage events should be persisted, not inferred from transient process memory.
When changing create/start flows, verify:
- idempotency prevents duplicate provider creates
- team ownership is checked before provider allocation
- active VM limits are enforced before expensive provider work
- usage events are written exactly once for the lifecycle moment they represent
- failed provider calls leave a recoverable database state
@@ -0,0 +1,72 @@
# Effect Boundaries
This reference expands the backend TypeScript rules for route handlers, services, and scripts.
## Route handler shape
Route handlers should be shallow adapters. They should parse request input, construct or select the required Effect program, run it once at the boundary, and translate domain errors to HTTP responses. Keep workflow sequencing, retries, provider calls, and database updates outside the handler body.
A good handler answers these questions quickly:
- What input does the route accept?
- Which Effect program performs the workflow?
- Which typed errors map to expected HTTP statuses?
- Which failures are unexpected defects?
Avoid route handlers that interleave parsing, database writes, provider calls, and response construction. That shape makes retries and idempotency hard to audit.
## Service shape
Use Effect services when a workflow crosses an external boundary or has meaningful failure semantics:
- provider APIs
- database reads or writes
- auth and team lookup
- payment or quota checks
- retries and timeout policy
- telemetry and usage recording
- idempotency claims
Model expected failures as typed domain errors. Prefer names that describe the business failure, not the transport layer. For example, `VmLimitExceeded`, `ProviderCapacityUnavailable`, or `IdempotencyConflict` is more useful to callers than a raw `FetchError`.
## Dependency shape
Make service dependencies explicit. Do not hide important runtime dependencies behind globals when an Effect service can receive them as layer requirements.
Good dependencies are concrete capabilities:
- database client
- provider client
- auth/team service
- clock or timeout policy
- telemetry sink
- idempotency repository
Bad dependencies are broad ambient containers or untyped option bags that force every workflow to rediscover what it actually needs.
## Plain TypeScript carve-out
Plain TypeScript is fine for data-only code:
- constants
- schema declarations
- config objects
- frontend components
- pure formatting helpers
- tiny route glue with no external effects
The point is not to use Effect everywhere. The point is to use it where explicit failure, dependency, retry, and cancellation semantics reduce real ambiguity.
## Error mapping
Expected domain errors should become clear HTTP responses. Unexpected defects should not be disguised as expected user errors.
When adding a new route, check that:
- invalid input maps to 400 or the existing validation status
- auth and entitlement failures map to the existing auth/payment statuses
- active-limit or quota failures are explicit
- provider unavailability is distinguishable from a defect
- idempotency conflicts return a deterministic response
If a caller needs to retry, the response should make that practical.
+54
View File
@@ -0,0 +1,54 @@
---
name: cmux-debugging
description: "Debug logging, Debug menu, runtime pitfalls, typing-latency-sensitive paths, SwiftUI list snapshot boundaries, OS-version repros, and local visual iteration for cmux. Use when adding debug probes, diagnosing UI/runtime issues, touching terminal rendering, tab/sidebar list views, drag/drop UTTypes, or using the Debug menu."
---
# cmux Debugging
## Debug event log
When adding debug event instrumentation, put events (keys, mouse, focus, splits, tabs) in the unified DEBUG build log. This is not a blanket requirement to add logs to every new code path. Most temporary probes should be added only during the dogfood debug loop and removed before merge.
```bash
tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug.log)"
```
- Untagged Debug app: `/tmp/cmux-debug.log`
- Tagged Debug app (`./scripts/reload.sh --tag <tag>`): `/tmp/cmux-debug-<tag>.log`
- `reload.sh` writes the current path to `/tmp/cmux-last-debug-log-path`
- `reload.sh` writes the selected dev CLI path to `/tmp/cmux-last-cli-path`
- `reload.sh` updates `/tmp/cmux-cli` and `$HOME/.local/bin/cmux-dev` to that CLI
- Implementation: `Packages/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift`
- App shim: `Sources/App/DebugLogging.swift`
- Free function `cmuxDebugLog("message")` logs with timestamp and appends to file in real time from cmux code
- The package implementation and app shim are `#if DEBUG`; all call sites must be wrapped in `#if DEBUG` / `#endif`
- 500-entry ring buffer; `CMUXDebugLog.DebugEventLog.shared.dump()` writes full buffer to file
- Key events logged in `AppDelegate.swift` (monitor, performKeyEquivalent)
- Mouse/UI events logged inline in views (ContentView, BrowserPanelView, etc.)
- Focus events: `focus.panel`, `focus.bonsplit`, `focus.firstResponder`, `focus.moveFocus`
- Bonsplit events: `tab.select`, `tab.close`, `tab.dragStart`, `tab.drop`, `pane.focus`, `pane.drop`, `divider.dragStart`
## Debug menu
The app has a **Debug** menu in the macOS menu bar only in DEBUG builds. Use it for visual iteration.
- **Debug > Debug Windows** contains panels for tuning layout, colors, and behavior. Entries are alphabetical with no dividers.
- To add a debug toggle or visual option: create an `NSWindowController` subclass with a `shared` singleton, add it to the "Debug Windows" menu in `Sources/cmuxApp.swift`, and add a SwiftUI view with `@AppStorage` bindings for live changes.
- When the user says "debug menu" or "debug window", they mean this menu, not `defaults write`.
## Runtime pitfalls
- Custom UTTypes for drag-and-drop must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations`.
- Do not add an app-level display link or manual `ghostty_surface_draw` loop; rely on Ghostty wakeups/renderer to avoid typing lag.
- `WindowTerminalHostView.hitTest()` is typing-latency-sensitive. All divider/sidebar/drag routing is gated to pointer events only. Do not add work outside the `isPointerEvent` guard.
- `TabItemView` uses `Equatable` conformance plus `.equatable()` to skip body re-evaluation during typing. Do not add environment/store/binding reads without updating equality and the call site.
- `TerminalSurface.forceRefresh()` is called on every keystroke. Do not add allocations, file I/O, or formatting there.
- `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift`, not from SwiftUI panel containers.
- List subtrees with `LazyVStack`, `LazyHStack`, `List`, or `ForEach` must pass immutable row snapshots plus closures below the boundary. Do not pass observable stores into row views.
- Functions called from SwiftUI `body` must not mutate state or schedule store writes.
- Foundation, SwiftUI, AttributeGraph, and WebKit semantics can change between macOS major versions. Test on the reporter's macOS before declaring a user repro disproven.
## Detailed references
- Read [references/debug-event-log.md](references/debug-event-log.md) when adding or interpreting debug log probes.
- Read [references/runtime-pitfalls.md](references/runtime-pitfalls.md) before touching terminal rendering, hit testing, tab rows, list virtualization, search overlay layering, or OS-version-sensitive code.
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Debugging"
short_description: "Use cmux debug logs, Debug menu, and runtime pitfall rules safely."
default_prompt: "Use this skill when adding debug probes, diagnosing cmux UI/runtime issues, touching typing-sensitive terminal paths, SwiftUI list rows, drag/drop UTTypes, search overlay layering, or Debug menu windows."
@@ -0,0 +1,66 @@
# Debug Event Log
The debug event log is the preferred shared destination for temporary and durable DEBUG-only probes.
## Destination
Tagged builds write tag-specific logs:
- untagged Debug app: `/tmp/cmux-debug.log`
- tagged Debug app: `/tmp/cmux-debug-<tag>.log`
`reload.sh` writes the current path to `/tmp/cmux-last-debug-log-path`, so the most robust tail command is:
```bash
tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug.log)"
```
Use this instead of guessing whether the current run is tagged.
## Shape
The package implementation lives in `Packages/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift`, and the app shim lives in `Sources/App/DebugLogging.swift`.
Call sites use:
```swift
#if DEBUG
cmuxDebugLog("focus.panel ...")
#endif
```
Every call site must be guarded by `#if DEBUG` / `#endif`. The implementation and shim are DEBUG-only, so unguarded call sites break non-Debug builds.
## When to add probes
Add probes during a dogfood debug loop when they help answer a concrete question:
- Which event path fired?
- Which panel or pane had focus?
- Which split/tab/drop transition occurred?
- Did a stale view or responder receive an event?
- Did a path fire on every keypress?
Do not add broad instrumentation just because a file is nearby. Remove temporary probes before merge unless they are low-volume and clearly useful for future debugging.
## Naming
Prefer stable event prefixes:
- `focus.panel`
- `focus.bonsplit`
- `focus.firstResponder`
- `focus.moveFocus`
- `tab.select`
- `tab.close`
- `tab.dragStart`
- `tab.drop`
- `pane.focus`
- `pane.drop`
- `divider.dragStart`
Put dynamic details after the prefix. This makes `rg`, `tail`, and log filtering practical.
## Ring buffer
The debug logger has a 500-entry ring buffer. `CMUXDebugLog.DebugEventLog.shared.dump()` writes the full buffer to file. Use this when the interesting event occurred before you started tailing.
@@ -0,0 +1,84 @@
# Runtime Pitfalls
This reference expands the high-risk cmux runtime rules.
## Drag-and-drop UTTypes
Custom UTTypes must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations`. Examples include:
- `com.splittabbar.tabtransfer`
- `com.cmux.sidebar-tab-reorder`
If drag/drop works only inside a narrow local test but fails across process or extension boundaries, check Info.plist before rewriting the drag model.
## Terminal rendering and typing latency
Do not add an app-level display link or manual `ghostty_surface_draw` loop. cmux relies on Ghostty wakeups and renderer scheduling. A second draw loop can make typing lag worse and hide the real invalidation source.
`TerminalSurface.forceRefresh()` in `Sources/GhosttyTerminalView.swift` is called on every keystroke. Do not add:
- allocation-heavy formatting
- file I/O
- logging to disk
- string interpolation in hot loops
- layout work
If you need to observe this path, use the smallest possible DEBUG-only probe and remove it before merge unless it is intentionally durable.
## Hit testing
`WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift` is called on every event, including keyboard events. Divider/sidebar/drag routing is intentionally gated to pointer events.
Do not add work outside the `isPointerEvent` guard. Even "small" checks compound on typing paths.
## Tab rows
`TabItemView` in `ContentView.swift` uses `Equatable` conformance plus `.equatable()` to skip body re-evaluation during typing.
Before adding any of these to the view:
- `@EnvironmentObject`
- `@ObservedObject`
- `@Binding`
- a plain store read in `body`
- a new parameter derived from mutable global state
Update the `==` function and verify the `ForEach` call site still uses `.equatable()`. Prefer passing precomputed immutable values.
## Terminal find layering
`SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift`, the AppKit portal layer. Do not mount it from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`.
Portal-hosted terminal views can sit above SwiftUI during split/workspace churn. Mounting the search UI at the wrong layer creates intermittently hidden or detached search controls.
## Snapshot boundary for list subtrees
In any SwiftUI panel whose `body` contains a `LazyVStack`, `LazyHStack`, `List`, or `ForEach` of rows, no view below that boundary may hold a reference to an `ObservableObject` or `@Observable` store. That includes:
- `@ObservedObject`
- `@EnvironmentObject`
- `@StateObject`
- `@Bindable`
- a plain `let store: SomeStore`
Rows and drop gaps receive immutable value snapshots plus closure action bundles only.
This avoids the class of bugs where an orthogonal published change invalidates every row and thrashes `LazyLayoutViewCache`, causing a main-thread spin loop. Reference patterns include `IndexSectionActions`, `SectionGapActions`, and `SessionSearchFn` in `Sources/SessionIndexView.swift`.
## No body-time mutation
A function called from SwiftUI `body`, directly or through a helper, must not:
- write observable state
- schedule `Task { @MainActor in store.x = ... }`
- call `DispatchQueue.main.async` to write store state
State-changing work triggered by "new data appeared" belongs in a reload completion, a `didSet`, or a property observer. It does not belong in the projection that feeds `ForEach`.
## OS-version repros
Foundation, SwiftUI, AttributeGraph, and WebKit behavior can change silently between macOS versions. A function that seems deterministic on macOS 26 may behave differently on macOS 14 or 15.
Concrete example: `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returned `"/.."` on macOS 14 and 15 but `"/"` on macOS 26.
When a user reports a repro on an older macOS, test on that macOS before declaring the repro disproven. AWS M4 Pro builders such as `cmux-aws-mac`, `cmux-aws-m4pro`, and `aws-m4pro-1..6` are pre-provisioned on macOS 15.7.4 and are the preferred empirical repro path.
+32
View File
@@ -5,6 +5,32 @@ description: "Contributor workflow rules for cmux setup, Xcode project normaliza
# cmux Dev Workflow
## Tagged local dev
After making code changes, always run the reload script with a tag to build the Debug app:
```bash
./scripts/reload.sh --tag <short-tag>
```
By default, `reload.sh` builds but does not launch the app. Pass `--launch` only when you need to open it automatically.
Never run bare `xcodebuild` or open an untagged `cmux DEV.app`. Untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
For CLI or socket dogfood against a tagged Debug app, use the tag-bound helper and set `CMUX_TAG`:
```bash
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh list-workspaces
```
Do not use `/tmp/cmux-cli` for tagged dogfood. That symlink points at the most recently reloaded build.
When rebuilding cmuxd for release/bundling, always use ReleaseFast:
```bash
cd cmuxd && zig build -Doptimize=ReleaseFast
```
## Initial setup
Run the setup script to initialize submodules, build GhosttyKit, and install the pbxproj normalization pre-commit hook:
@@ -38,3 +64,9 @@ To author a NEW sample extension that is tag-ready:
- `PRODUCT_BUNDLE_IDENTIFIER` = `<appBase>$(CMUX_BUNDLE_ID_SUFFIX)` for the app target and `<appBase>$(CMUX_BUNDLE_ID_SUFFIX).<leaf>` for the appex (suffix before the appex leaf so the appex id stays prefixed by the app id).
- appex `INFOPLIST_KEY_CFBundleDisplayName` (or the `CFBundleDisplayName` Info.plist value) = `<Name>$(CMUX_DISPLAY_NAME_SUFFIX)`.
- it must be ad-hoc signed by xcodebuild (Info.plist bound, entitlements intact) for pkd to ingest the tagged copy; do not re-sign post-build.
## Detailed references
- Read [references/tagged-builds.md](references/tagged-builds.md) for detailed tagged reload, app link, socket, and cleanup behavior.
- Read [references/xcode-project-normalization.md](references/xcode-project-normalization.md) before touching `.xcode-version` or `cmux.xcodeproj/project.pbxproj`.
- Read [references/sidebar-extension-tagging.md](references/sidebar-extension-tagging.md) when changing ExtensionKit sidebar extension identifiers, tagged sample extensions, or `pluginkit` verification.
@@ -0,0 +1,60 @@
# Sidebar Extension Tagging
Tagged dev builds need distinct ExtensionKit sidebar extension points so concurrent dev builds do not collide.
## Build settings
Three build settings drive the tagging model:
- `CMUX_SIDEBAR_EXTENSION_POINT_ID`
- `CMUX_BUNDLE_ID_SUFFIX`
- `CMUX_DISPLAY_NAME_SUFFIX`
The default extension point is:
```text
com.cmuxterm.app.cmux.sidebar
```
Tagged host builds scope it to:
```text
com.cmuxterm.app.debug.<tag>.cmux.sidebar
```
## Why display name matters
`AppExtensionIdentity` exposes stable fields such as bundle identifier, localized name, extension point identifier, and id. cmux keys its identity off the stable bundle identifier, but OS-level enable/disable and availability grouping uses display name.
Two same-named appexes installed side by side can be treated as one logical extension. Per-tag display names keep tagged sample extensions distinct.
## Tagged sample extensions
`./scripts/reload-extension.sh --tag <tag> [--host-bundle-id <id>] [--example sample|tabs|both]` builds a matching tag-scoped sample extension.
It passes:
- `CMUX_SIDEBAR_EXTENSION_POINT_ID=<host-bundle-id>.cmux.sidebar`
- `CMUX_BUNDLE_ID_SUFFIX=.<tag>`
- `CMUX_DISPLAY_NAME_SUFFIX=" <tag>"`
It installs exactly what xcodebuild produced. It does not re-sign. A bare `codesign --force --sign -` strips appex entitlements and the extension drops its host XPC connection.
## New sample extension checklist
For a new tag-ready sample extension:
- appex Info.plist has `EXAppExtensionAttributes:EXExtensionPointIdentifier = $(CMUX_SIDEBAR_EXTENSION_POINT_ID)`
- app and appex targets define `CMUX_SIDEBAR_EXTENSION_POINT_ID`
- app and appex targets define `CMUX_BUNDLE_ID_SUFFIX`
- app and appex targets define `CMUX_DISPLAY_NAME_SUFFIX`
- app `PRODUCT_BUNDLE_IDENTIFIER` uses `<appBase>$(CMUX_BUNDLE_ID_SUFFIX)`
- appex `PRODUCT_BUNDLE_IDENTIFIER` uses `<appBase>$(CMUX_BUNDLE_ID_SUFFIX).<leaf>`
- appex display name appends `$(CMUX_DISPLAY_NAME_SUFFIX)`
- xcodebuild ad-hoc signs the appex with entitlements intact
Verify with:
```bash
pluginkit -m -p <host-bundle-id>.cmux.sidebar
```
@@ -0,0 +1,61 @@
# Tagged Builds
Tagged builds isolate app name, bundle ID, socket, and DerivedData path so multiple agents and the user's normal app do not collide.
## Reload
Use:
```bash
./scripts/reload.sh --tag <tag>
```
`reload.sh` builds but does not launch by default. It terminates any running app with the same tag after a successful build, so opening the printed app path launches the fresh binary.
Use:
```bash
./scripts/reload.sh --tag <tag> --launch
```
only when the task requires launching.
## App path links
`reload.sh` prints:
```text
App path:
/absolute/path/to/cmux DEV <tag>.app
```
Build chat links from that exact path. Prepend `file://` and URL-encode spaces as `%20`. Do not hardcode DerivedData paths and never use `/tmp/cmux-<tag>/...` app links in chat output.
## Tagged CLI and socket
For CLI or socket dogfood against a tagged Debug app, use:
```bash
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh list-workspaces
CMUX_TAG=<tag> scripts/cmux-debug-cli.sh send --workspace workspace:1 --surface surface:1 "echo ok"
```
Do not use `/tmp/cmux-cli` for tagged dogfood. That symlink points at the most recently reloaded build and can target the user's main app socket.
The helper:
- refuses to run without `CMUX_TAG`
- targets `/tmp/cmux-debug-<tag>.sock`
- uses the matching tagged CLI from DerivedData
- scrubs ambient cmux terminal context
- sets `CMUX_SOCKET_PATH`, `CMUX_BUNDLE_ID`, and `CMUX_BUNDLED_CLI_PATH`
## Cleanup
Before launching a new tagged run, clean up older tags started in the same session:
- quit old tagged app
- remove its `/tmp` socket if stale
- remove derived data only when you are sure no active task needs it
Do not open an untagged `cmux DEV.app` from DerivedData. It shares the default debug socket and bundle ID with other agents.
@@ -0,0 +1,46 @@
# Xcode Project Normalization
cmux is pinned to Xcode 26.x. `.xcode-version` records the major version. `cmux.xcodeproj/project.pbxproj` carries `objectVersion = 60`, which is what Xcode 26 writes by default.
`objectVersion = 77` is reserved for projects that adopt synchronized folder groups. cmux does not use synchronized folder groups yet.
## Pre-commit hook
`scripts/setup.sh` installs:
```text
scripts/git-hooks/pre-commit
```
The hook runs:
```bash
scripts/normalize-pbxproj.py
```
on staged `cmux.xcodeproj/project.pbxproj` changes. This sorts high-churn sections so Xcode's nondeterministic reordering does not reach commits.
## CI guard
CI runs:
```bash
scripts/check-pbxproj.sh
```
It enforces both:
- the `.xcode-version` / `objectVersion` pin
- pbxproj normalization
## Bumping Xcode
To bump the pin:
1. Edit `.xcode-version`.
2. Open `cmux.xcodeproj` in the new Xcode so it rewrites `objectVersion`.
3. Add a case in `scripts/check-pbxproj.sh` mapping the new Xcode major to the objectVersion that Xcode writes.
4. Normalize the project file.
5. Treat the bump as a deliberate team decision.
Do not change `objectVersion` opportunistically as part of unrelated project edits.
+59
View File
@@ -0,0 +1,59 @@
---
name: cmux-ghostty
description: "Ghostty submodule and GhosttyKit workflow rules for cmux. Use when modifying the ghostty submodule, rebuilding GhosttyKit.xcframework, updating the parent submodule pointer, or documenting fork conflict notes."
---
# cmux Ghostty
## GhosttyKit builds
When rebuilding GhosttyKit.xcframework, always use Release optimizations:
```bash
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
```
## Submodule workflow
Ghostty changes must be committed in the `ghostty` submodule and pushed to the `manaflow-ai/ghostty` fork. Keep `docs/ghostty-fork.md` up to date with any fork changes and conflict notes.
```bash
cd ghostty
git remote -v # origin = upstream, manaflow = fork
git checkout -b <branch>
git add <files>
git commit -m "..."
git push manaflow <branch>
```
To keep the fork up to date with upstream:
```bash
cd ghostty
git fetch origin
git checkout main
git merge origin/main
git push manaflow main
```
Then update the parent repo with the new submodule SHA:
```bash
cd ..
git add ghostty
git commit -m "Update ghostty submodule"
```
## Submodule safety
When modifying a submodule, always push the submodule commit to its remote `main` branch before committing the updated pointer in the parent repo. Never commit on a detached HEAD or temporary branch; the commit can be orphaned and lost.
Verify with:
```bash
cd <submodule> && git merge-base --is-ancestor HEAD origin/main
```
## Detailed reference
- Read [references/submodule-safety.md](references/submodule-safety.md) before committing submodule pointer updates or resolving Ghostty fork conflicts.
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Ghostty"
short_description: "Handle Ghostty submodule, fork, and GhosttyKit build workflow safely."
default_prompt: "Use this skill when modifying the ghostty submodule, rebuilding GhosttyKit.xcframework, pushing Ghostty fork changes, updating the parent submodule pointer, or editing docs/ghostty-fork.md."
@@ -0,0 +1,51 @@
# Submodule Safety
Submodule commits can be easy to lose. The parent repository records only a commit SHA, not the branch that made the SHA reachable.
## Safe sequence
1. Enter the submodule.
2. Create or select the intended branch.
3. Commit the submodule changes.
4. Push the submodule commit to the correct remote.
5. Verify the pushed branch contains the commit.
6. Return to the parent repository.
7. Commit the updated submodule pointer.
For Ghostty:
```bash
cd ghostty
git remote -v
git checkout -b <branch>
git add <files>
git commit -m "..."
git push manaflow <branch>
```
If the parent pointer is supposed to track fork `main`, make sure the commit is an ancestor of that remote branch:
```bash
git fetch manaflow main
git merge-base --is-ancestor HEAD manaflow/main
```
The top-level CLAUDE note uses `origin/main` as the generic verification form for submodules. In the Ghostty submodule, check the actual remote names first because `origin` may be upstream and `manaflow` may be the fork.
## Detached HEAD hazard
Do not commit submodule changes on a detached HEAD and then update the parent pointer. That creates a parent commit pointing at a SHA that may not be reachable from any remote branch. A future checkout or CI job can fail to fetch it.
## Fork documentation
Keep `docs/ghostty-fork.md` updated when fork changes or conflict notes matter for future upstream merges. The point is to preserve why the fork diverged, not just that it diverged.
## GhosttyKit optimization
Rebuild GhosttyKit.xcframework with ReleaseFast:
```bash
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
```
Debug or default optimization builds can hide performance characteristics and should not be used for the checked-in framework refresh path.
+37
View File
@@ -0,0 +1,37 @@
---
name: cmux-localization
description: "Localization rules and audit workflow for cmux UI strings, settings rows, menus, shortcuts, schema/config text, docs, command/help text, alerts, tooltips, and web messages. Use whenever changing user-facing text."
---
# cmux Localization
Use this skill for any user-facing string change.
## Hard rules
- All user-facing strings must be localized.
- Use `String(localized: "key.name", defaultValue: "English text")` for Swift/AppKit/SwiftUI strings.
- Keys go in `Resources/Localizable.xcstrings` with translations for all supported languages, currently English and Japanese.
- Never use bare string literals in SwiftUI `Text()`, `Button()`, alert titles, tooltips, menus, or dialogs.
- Localization audit is required for every user-facing change.
- `defaultValue`, English fallback text, schema descriptions, or copied English strings do not count as localization.
- For localized web/docs content, update every supported message catalog, currently `web/messages/en.json` and `web/messages/ja.json`, plus any localized data structures carrying inline translations.
## Audit checklist
Before finishing a task that changes UI, Settings rows, menus, shortcut metadata, schema/config text, docs, command/help text, alerts, or tooltips:
1. Enumerate the changed user-facing surfaces.
2. Verify each surface has entries for every supported locale.
3. Parse touched localization files.
4. Compare changed message keys across locales.
5. Use `rg` over changed Swift/TS/TSX/docs files for newly introduced bare English.
6. State the localization audit in the final handoff, or explicitly say what could not be verified.
## Related shortcut rule
Every new cmux-owned keyboard shortcut must be added to `KeyboardShortcutSettings`, visible/editable in Settings, supported in `~/.config/cmux/cmux.json`, and documented in the keyboard shortcut and configuration docs.
## Detailed reference
- Read [references/audit-workflow.md](references/audit-workflow.md) for a deeper audit process, common false positives, and examples of surfaces that count as user-facing.
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Localization"
short_description: "Localize and audit every cmux user-facing string change."
default_prompt: "Use this skill whenever changing cmux UI text, settings labels, menu items, shortcut metadata, schema/config text, docs, command/help output, alerts, tooltips, or web messages."
@@ -0,0 +1,68 @@
# Localization Audit Workflow
This reference expands the localization rules for cmux.
## What counts as user-facing
Treat text as user-facing if it can appear in:
- SwiftUI views
- AppKit menus and dialogs
- alerts and confirmation sheets
- tooltips and accessibility labels
- Settings rows and descriptions
- command palette entries
- keyboard shortcut metadata
- CLI help or command output
- JSON schema descriptions shown in docs or editors
- docs pages
- web UI
- generated configuration examples shown to users
Internal debug-only labels may still deserve localization if they are visible in the Debug menu or a debug window used by contributors.
## Swift and AppKit
Use:
```swift
String(localized: "key.name", defaultValue: "English text")
```
Update `Resources/Localizable.xcstrings` for all supported languages. Currently that means English and Japanese.
Do not rely on `defaultValue` as the English localization. It is a fallback and development convenience, not a completed localization entry.
## Web and docs
For localized web/docs content, update:
- `web/messages/en.json`
- `web/messages/ja.json`
- any localized data structures with inline translations
Keep keys aligned across locales. A key added only to English is incomplete even if the UI falls back at runtime.
## Bare English search
After changing Swift, TS, TSX, or docs files, search the changed files for newly introduced user-facing English. Useful patterns include:
```bash
git diff --name-only -- '*.swift' '*.ts' '*.tsx' '*.md'
rg 'Text\\("[A-Z][^"]+"' -- '*.swift'
rg 'Button\\("[A-Z][^"]+"' -- '*.swift'
rg 'tooltip|alert|title|description|label' -- '*.swift' '*.ts' '*.tsx'
```
These searches are not proof by themselves. They are prompts to inspect likely user-facing strings.
## Final handoff
Every UI/text-affecting final handoff should state:
- which surfaces changed
- which localization files were updated
- which audit commands or manual checks were run
- anything that could not be verified
If no user-facing strings changed, say that clearly.
+54
View File
@@ -0,0 +1,54 @@
---
name: cmux-release
description: "cmux release workflow, version bumping, changelog updates, pretag guard, release tags, and release asset expectations. Use when preparing or troubleshooting a cmux release."
---
# cmux Release
Use the `/release` command to prepare a new release. This will:
1. Determine the new version (bumps minor by default)
2. Gather commits since the last tag and update the changelog
3. Update `CHANGELOG.md` (the docs changelog page at `web/app/docs/changelog/page.tsx` reads from it)
4. Run `./scripts/bump-version.sh` to update both versions
5. Commit, run `./scripts/release-pretag-guard.sh`, tag, and push
## Version bumping
```bash
./scripts/bump-version.sh
./scripts/bump-version.sh patch
./scripts/bump-version.sh major
./scripts/bump-version.sh 1.0.0
```
This updates both `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`. The build number is auto-incremented and is required for Sparkle auto-update to work.
Before creating a release tag, run:
```bash
./scripts/release-pretag-guard.sh
```
If it fails, run `./scripts/bump-version.sh`, commit the build-number bump, then retry tagging.
Manual release steps if not using the command:
```bash
./scripts/release-pretag-guard.sh
git tag vX.Y.Z
git push origin vX.Y.Z
gh run watch --repo manaflow-ai/cmux
```
## Notes
- Requires GitHub secrets: `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`.
- The release asset is `cmux-macos.dmg` attached to the tag.
- README download button points to `releases/latest/download/cmux-macos.dmg`.
- Bump the minor version for updates unless explicitly asked otherwise.
- Update `CHANGELOG.md`; docs changelog is rendered from it.
## Detailed reference
- Read [references/release-checklist.md](references/release-checklist.md) for a more detailed release checklist and common failure handling.
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Release"
short_description: "Prepare cmux releases with version, changelog, guard, tag, and asset rules."
default_prompt: "Use this skill when preparing a cmux release, bumping versions, updating CHANGELOG.md, running the release pretag guard, tagging, or troubleshooting release assets."
@@ -0,0 +1,77 @@
# Release Checklist
This reference expands the cmux release workflow.
## Default path
Prefer the `/release` command. It should handle:
- choosing the version
- gathering commits since the last tag
- updating `CHANGELOG.md`
- running `./scripts/bump-version.sh`
- committing release metadata
- running `./scripts/release-pretag-guard.sh`
- tagging and pushing
## Version policy
Use a minor bump by default. Use patch or major only when explicitly requested or clearly justified by the release scope.
The version bump script updates both:
- `MARKETING_VERSION`
- `CURRENT_PROJECT_VERSION`
The build number must increase for Sparkle auto-update. If `release-pretag-guard.sh` fails because the build number is not monotonic, run the bump script, commit the build-number bump, and retry the guard.
## Changelog
Update `CHANGELOG.md`. The docs changelog page at `web/app/docs/changelog/page.tsx` renders from it, so do not update a separate docs changelog source.
Keep the changelog user-facing. Mention user-visible fixes, behavior changes, and compatibility notes more prominently than internal refactors.
## Tagging
Run before tagging:
```bash
./scripts/release-pretag-guard.sh
```
Manual tag flow:
```bash
git tag vX.Y.Z
git push origin vX.Y.Z
gh run watch --repo manaflow-ai/cmux
```
## Release asset
The expected release asset is:
```text
cmux-macos.dmg
```
The README download button points to:
```text
releases/latest/download/cmux-macos.dmg
```
If the asset name changes, update every surface that assumes this path.
## Required secrets
Release signing/notarization depends on:
- `APPLE_CERTIFICATE_BASE64`
- `APPLE_CERTIFICATE_PASSWORD`
- `APPLE_SIGNING_IDENTITY`
- `APPLE_ID`
- `APPLE_APP_SPECIFIC_PASSWORD`
- `APPLE_TEAM_ID`
If release automation fails before signing, inspect workflow configuration and version metadata first. If it fails during signing/notarization, inspect the secret availability and Apple account status.
+38
View File
@@ -0,0 +1,38 @@
---
name: cmux-shared-behavior
description: "Shared behavior and mutation-path rules for cmux. Use when a behavior is exposed through multiple entrypoints such as keyboard shortcuts, command palette, context menu, CLI, settings, debug menu, optimistic UI, or tests that previously missed a bug."
---
# cmux Shared Behavior
Use one shared action/model path when behavior is exposed through multiple entrypoints.
## Shared entrypoints
When a behavior is exposed through multiple surfaces, implement one shared action/model path and verify every entrypoint that should invoke it.
Common entrypoints include:
- keyboard shortcut
- command palette
- context menu
- CLI/socket command
- settings UI
- debug menu
Do not patch one surface while leaving the others with duplicated logic.
## Optimistic updates
For optimistic UI or CLI updates:
- keep one mutation path
- record pending state with a request id or previous snapshot
- reconcile from the authoritative result
- handle failure with an explicit rollback or error state
Do not let each entrypoint maintain its own optimistic copy.
## Missed-bug coverage
When a user says tests missed a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete.
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Shared Behavior"
short_description: "Keep multi-entrypoint behavior and optimistic updates on one shared path."
default_prompt: "Use this skill when changing behavior exposed through multiple cmux entrypoints, optimistic UI or CLI mutation flows, or tests that previously missed a user-visible bug."
+27
View File
@@ -0,0 +1,27 @@
---
name: cmux-socket-policy
description: "Socket command threading and focus policy for cmux CLI/socket work. Use when adding or changing socket commands, CLI commands, telemetry commands, focus/select/open/close/send-key behavior, or automation that could steal app focus."
---
# cmux Socket Policy
## Threading policy
- Do not use `DispatchQueue.main.sync` for high-frequency socket telemetry commands such as `report_*`, `ports_kick`, status/progress updates, or log metadata updates.
- For telemetry hot paths, parse and validate arguments off-main.
- Dedupe and coalesce off-main first.
- Schedule minimal UI/model mutation with `DispatchQueue.main.async` only when needed.
- Commands that directly manipulate AppKit/Ghostty UI state are allowed to run on the main actor.
- If adding a new socket command, default to off-main handling and require an explicit reason in code comments when main-thread execution is necessary.
## Focus policy
- Socket/CLI commands must not steal macOS app focus.
- Do not activate the app or raise windows unless the command has explicit focus intent.
- Only explicit focus-intent commands may mutate in-app focus/selection.
- Explicit focus-intent commands include `window.focus`, `workspace.select/next/previous/last`, `surface.focus`, `pane.focus/last`, browser focus commands, and v1 focus equivalents.
- All non-focus commands should preserve the current user focus context while still applying data/model changes.
## Detailed reference
- Read [references/threading-and-focus.md](references/threading-and-focus.md) when adding a command, changing command execution context, or deciding whether focus changes are allowed.
@@ -0,0 +1,4 @@
interface:
display_name: "cmux Socket Policy"
short_description: "Keep cmux socket commands off hot main-thread paths and focus-safe."
default_prompt: "Use this skill when adding or changing cmux socket or CLI commands, telemetry handlers, focus/select/open/close/send-key behavior, or automation that could activate the app or steal focus."
@@ -0,0 +1,60 @@
# Socket Threading and Focus
Socket commands are a control plane. They often run because an agent, script, or background tool is reporting state, not because a user asked the app to become active.
## Telemetry hot paths
High-frequency telemetry commands include:
- `report_*`
- `ports_kick`
- status updates
- progress updates
- log metadata updates
These should avoid synchronous main-thread work. Parse and validate arguments off-main, dedupe/coalesce before crossing to UI state, and schedule only the smallest required mutation.
`DispatchQueue.main.sync` is especially risky because it can block the socket handling path behind UI work and can deadlock if the command path is already main-adjacent.
## Commands allowed on main actor
Commands that directly manipulate AppKit or Ghostty UI state may need main actor execution:
- focus
- select
- open/close UI surfaces
- send key/input
- list/current queries requiring an exact synchronous UI snapshot
The command should document why main-thread execution is necessary. Do not cargo-cult main actor isolation onto telemetry commands.
## Focus preservation
Most socket commands should not change the user's macOS focus. A background agent may be running in one workspace while the user is actively using another app or cmux workspace.
Non-focus commands should apply model/data changes without:
- activating the app
- raising a window
- selecting another workspace
- focusing a pane
- focusing a surface
If a command needs focus behavior, name and document it as focus-intent.
## Explicit focus-intent commands
Only explicit focus-intent commands may mutate in-app focus/selection. Examples:
- `window.focus`
- `workspace.select`
- `workspace.next`
- `workspace.previous`
- `workspace.last`
- `surface.focus`
- `pane.focus`
- `pane.last`
- browser focus commands
- v1 focus equivalents
When adding a new command, decide whether it is focus-intent as part of the API contract, not as an implementation accident.
+24
View File
@@ -5,6 +5,24 @@ description: "cmux testing rules for Swift Testing, test target compilation, and
# cmux Testing
## Regression test commit policy
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
1. **Commit 1:** Add the failing test only (no fix). CI should go red.
2. **Commit 2:** Add the fix. CI should go green.
This makes it visible in the GitHub PR UI that the test genuinely fails without the fix.
## Test quality policy
- Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns.
- Do not add tests that read checked-in metadata or project files such as `Resources/Info.plist`, `project.pbxproj`, `.xcconfig`, or source files only to assert that a key, string, plist entry, or snippet exists.
- Tests must verify observable runtime behavior through executable paths (unit/integration/e2e/CLI), not implementation shape.
- For metadata changes, prefer verifying the built app bundle or the runtime behavior that depends on that metadata, not the checked-in source file.
- If a behavior cannot be exercised end-to-end yet, add a small runtime seam or harness first, then test through that seam.
- If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and state that explicitly.
## Test framework
Swift Testing is the current Apple-supported primitive for tests on this codebase (shipped with Swift 6 / Xcode 16, supported on the macOS versions we target). Use it for everything that is not a UI test.
@@ -20,3 +38,9 @@ Swift Testing is the current Apple-supported primitive for tests on this codebas
## Test target validation
`reload.sh` does not compile the test target. It builds only the `cmux` scheme, so a green `reload.sh` says nothing about whether `cmuxTests`/`cmuxUITests` still compile. A symbol that is moved or renamed can keep the `cmux` app building while breaking the test target (real case: a `write(to:atomically:)` typo and a removed `TabManager.CommandResult` only surfaced in the `tests` job). Before pushing package/refactor changes, build the `cmux-unit` scheme (with `-derivedDataPath /tmp/cmux-<tag>` and, for `cmuxApp`/`AppDelegate` churn, the GlobalISel workaround flag) or let the `tests` CI job gate it — never treat `reload.sh` alone as proof the tests build.
## Detailed references
- Read [references/swift-testing-migration.md](references/swift-testing-migration.md) when converting XCTest unit tests to Swift Testing or adding new package tests.
- Read [references/regression-and-quality.md](references/regression-and-quality.md) when adding a regression test, deciding whether a test is behavioral enough, or checking Xcode project test wiring.
- Read [references/local-vs-ci-validation.md](references/local-vs-ci-validation.md) when choosing between `reload.sh`, `cmux-unit`, GitHub Actions, E2E/UI tests, and Python socket tests.
@@ -0,0 +1,46 @@
# Local vs CI Validation
## `reload.sh`
`reload.sh` builds the Debug app for a tag. It does not compile the test target.
A successful reload proves the app target built. It does not prove:
- `cmuxTests` compile
- `cmuxUITests` compile
- package test targets compile
- test-only imports still resolve
For package/refactor work, treat reload as insufficient by itself.
## Unit test target
`xcodebuild -scheme cmux-unit` is safe because it does not launch the app. Prefer CI when practical, but use `cmux-unit` when package/refactor changes can break tests while the app target still builds.
Use a tagged derived data path:
```bash
xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-<tag> build
```
For `cmuxApp` or `AppDelegate` churn, include the repo's known GlobalISel workaround flag if required by current project instructions.
## E2E and UI tests
E2E and UI tests run via GitHub Actions or on the VM. Trigger E2E/UI through:
```bash
gh workflow run test-e2e.yml
```
Do not launch an untagged app locally to satisfy socket/UI tests.
## Python socket tests
Python socket tests under `tests_v2/` connect to a running cmux instance socket. If they must be run locally, use a tagged build socket:
```bash
CMUX_SOCKET_PATH=/tmp/cmux-debug-<tag>.sock
```
Never launch or target an untagged `cmux DEV.app` for these tests. It can conflict with the user's running debug instance.
@@ -0,0 +1,50 @@
# Regression and Test Quality
## Regression commit policy
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
1. Add the failing test only.
2. Add the fix.
This makes it visible in GitHub that the test fails without the fix and passes with it.
## Behavioral tests
Tests should verify observable runtime behavior through executable paths:
- unit
- integration
- E2E
- CLI
- artifact-level behavior of a built product
Avoid tests that only verify:
- source code text
- method signatures
- AST fragments
- grep-style patterns
- checked-in plist/project/config snippets
For metadata changes, prefer testing the built app bundle or the runtime behavior that depends on the metadata. If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and say so.
## Test wiring
Test files in `cmuxTests/` must be wired into `cmux.xcodeproj/project.pbxproj`.
A `.swift` file added to `cmuxTests/` without matching project entries can be silently ignored by Xcode. Both targeted `xcodebuild test -only-testing:cmuxTests/<TestClass>` and bot reviews can pass with "Executed 0 tests".
The `workflow-guard-tests` job runs:
```bash
./scripts/lint-pbxproj-test-wiring.sh
```
When hand-editing wiring, use a wired sibling like `TabManagerUnitTests.swift` as the template.
## When tests missed a bug
When the user says tests missed a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete.
Do not add a broad implementation-shape test that would have passed while the user-visible bug remained.
@@ -0,0 +1,70 @@
# Swift Testing Migration
Use Swift Testing for unit and integration tests. XCTest remains for UI tests.
## New tests
New unit and integration tests should:
```swift
import Testing
@Suite
struct ExampleTests {
@Test
func computesValue() {
#expect(1 + 1 == 2)
}
}
```
Use `try #require(...)` when a value must be unwrapped before continuing.
## XCTest conversion
When touching an existing XCTest unit test, convert in place if the edit naturally crosses that code.
Mapping:
- `XCTestCase` subclass -> `@Suite struct` or `@Suite final class`
- `func testFoo()` -> `@Test func foo()`
- `XCTAssertEqual(a, b)` -> `#expect(a == b)`
- `XCTAssertTrue(condition)` -> `#expect(condition)`
- `XCTUnwrap(value)` -> `try #require(value)`
- `XCTFail("message")` -> `Issue.record("message")`
- `setUp()` -> `init()`
- `tearDown()` -> `deinit`
- async setup -> `async init()`
Do not bulk-rewrite untouched tests just to migrate them.
## Parameterized tests
Prefer:
```swift
@Test(arguments: [
("input-a", "output-a"),
("input-b", "output-b"),
])
func formats(input: String, expected: String) {
#expect(format(input) == expected)
}
```
This is clearer than duplicating test methods with copy/paste assertions.
## Parallel execution
Swift Testing runs tests in parallel by default, including across suites. If a suite genuinely needs ordering or guards shared mutable state, use `.serialized`:
```swift
@Suite(.serialized)
struct FileBackedTests { ... }
```
Prefer isolated temp directories and injected dependencies over serialization when practical.
## UI tests
Files under `cmuxUITests/` stay on XCTest/XCUITest. Swift Testing does not support `XCUIApplication` UI testing.