[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:
Symlink
+1
@@ -0,0 +1 @@
|
||||
../skills
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-architecture
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-backend
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-browser
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-customization
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-debugging
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-dev-workflow
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-diagnostics
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-ghostty
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-keyboard-shortcuts
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-localization
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-markdown
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-release
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-settings
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-shared-behavior
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-socket-policy
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-testing
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../skills/cmux-workspace
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user