This fork is primarily for onevcat-specific customizations; before doing any release work, read `docs-ai/001-fork-bootstrap-and-release-pipeline/release-runbook.md` (and the upstream ledger `docs-ai/017-upstream-sync-process/upstream-ledger.md`) for fork publishing guidance. Fork history and design decisions are recorded under `docs-ai/` (see `docs-ai/README.md`). ## Build Commands ```bash make build-ghostty-xcframework # Rebuild GhosttyKit from Zig source (requires mise) make generate # Generate Prowl.xcworkspace and the Xcode projects with Tuist (to open them in Xcode) make build-app # Build macOS app (Debug) via xcodebuild make run-app # Build and launch Debug app make install-dev-build # Build and copy to /Applications (Debug) make install-release # Build Release, sign locally, install to /Applications make format-changed # Run swift-format on changed Swift files only make format # Run full-tree swift-format cleanup make lint # Run swiftlint only make check # Run changed-file format, swift-format lint, swiftlint, and the string catalog check make audit-localization # Release check of the string catalog and unlocalized UI copy (see the sync-l10n skill) make test # Run the Mac app tests make test-all # Run every test suite: scripts, CLI, Mac app, iOS mirror (unit and UI), Android mirror make benchmark-build # Benchmark CI-like clean/warm-CAS build and test time make bench # Run performance benchmarks with -O; append absolute medians to ~/Library/Logs/Prowl/measurements/bench/ make measure-cpu # Steady-state CPU + per-symbol attribution of the running Prowl Debug app make capture-spike # Sample the running Prowl Debug app when CPU crosses a threshold make measure-titles # Black-box check that animated tab titles stay coalesced (~1 change/s) make agent-versions # Compare installed tier-A agent CLI versions with the managed-hook attestation (docs-ai 064) make test-agent-contracts # Zero-inference runtime inventory; AGENT_CONTRACT_ARGS="--mode preflight --runtime codex" make log-stream # Stream app logs (subsystem: com.onevcat.prowl) make build-cli # Build CLI (prowl) via SwiftPM make test-cli-smoke # Run CLI executable smoke tests make test-cli-unit # Run CLI unit tests make test-cli-integration # Run CLI integration tests (socket round-trip) make bump-version # Bump version (date-based YYYY.M.DD) and create git tag; used by release.sh ``` Debug builds are ad-hoc signed by default, so building needs no certificate. An ad-hoc signature's designated requirement is its cdhash, which changes on every rebuild, so macOS re-asks for Desktop/Documents/Downloads access from Prowl Debug — and from the commands running in its panes — after each build. If your worktrees live in those folders, set `PROWL_DEVELOPMENT_TEAM=` (environment or `Config/Secrets.env`) and `make build-app` / `make test` sign the Debug app and test host with your Apple Development identity instead; the Team ID is the certificate's OU, not the ID in parentheses after your name. With it set, replace the `CODE_SIGNING_*` settings in ad-hoc `xcodebuild test` invocations like the one below with `DEVELOPMENT_TEAM=` so the test host keeps the same signature. The Xcode projects are not in Git. [Tuist](https://tuist.dev) generates `Prowl.xcworkspace` (the macOS app and the iOS mirror client) from `Workspace.swift`, `App/Project.swift`, `Mirror/iOS/Project.swift`, and the build settings in `App/Config/*.xcconfig`. The `make` targets generate it when it is absent or older than these files. To change a target, a dependency, a build setting, or a scheme, edit the manifest or the xcconfig file, never the generated project. Source folders are synchronized folders, so a new source file needs no manifest change. The package lockfile is `.package.resolved`; always build with `-workspace Prowl.xcworkspace`. Run a single test class or method (after `make generate`): ```bash xcodebuild test -workspace Prowl.xcworkspace -scheme Prowl -destination "platform=macOS" \ -only-testing:ProwlTests/TerminalTabManagerTests \ CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" -skipMacroValidation ``` **Swift Testing vs XCTest `-only-testing` format**: Swift Testing (`@Test`) requires trailing `()` in the test identifier. Without it, `xcodebuild` silently matches nothing and reports `TEST SUCCEEDED` with zero tests run. ```bash # XCTest (func testFoo) -only-testing:ProwlTests/FooTests/testBar # Swift Testing (@Test func bar) -only-testing:"ProwlTests/FooTests/bar()" ``` Requires [mise](https://mise.jdx.dev/) for zig, tuist, swiftlint, and xcsift tooling. `make log-stream` shows no `TCA` action lines by default: per-action logging — the action label plus a full app-state snapshot and diff — is gated off because it runs on every action and shows up as steady main-thread cost. Launch with `PROWL_LOG_TCA_ACTIONS=1` (scheme env var, or exported before `open`) to trace the action stream through the unified log. ## Repository Layout | Folder | Content | | --- | --- | | `App/` | The macOS app: `Sources/` (module `Prowl`), `Tests/` (`ProwlTests`), `Resources/` (bundled files), `Config/` (`Info.plist`, entitlements, xcconfig), `Project.swift` | | `CLI/` | SwiftPM package of the `prowl` CLI, its contracts, and its tests | | `Shared/` | SwiftPM package `ProwlShared` (library `ProwlCLIShared`): code that the app and the CLI share | | `Mirror/` | Remote Mirror parts: `iOS/`, `Android/`, `Relay/` (SwiftPM package of the relay process), `Shared/` (sources that the app and the iOS client compile) | | `ThirdParty/` | Submodules: `ghostty`, `git-wt` | | `docs/`, `skills/` | Agent-facing manual and skills; the app bundles both | | `docs-ai/` | Design records. Numbered files before entry 074 use the old paths; `docs-ai/README.md` has the path table | | `scripts/`, `Config/` | Tooling (`scripts/bin/` holds binaries and helper scripts) and local configuration templates | The project name is Prowl everywhere. The old name `supacode` stays only for stored data, the legacy settings readers, the keychain profile `supacode-notary`, and the upstream repository; `make check` rejects other uses. ## Architecture Prowl is a macOS orchestrator for running multiple coding agents in parallel, using GhosttyKit as the underlying terminal. ### Core Data Flow ``` AppFeature (root TCA store) ├─ RepositoriesFeature (repos + worktrees) ├─ CommandPaletteFeature ├─ SettingsFeature (appearance, updates, repo settings) └─ UpdatesFeature (Sparkle auto-updates) WorktreeTerminalManager (global @Observable terminal state) ├─ selectedWorktreeID (tracks current selection for bell logic) └─ WorktreeTerminalState (per worktree) └─ TerminalTabManager (tab/split management) └─ GhosttySurfaceState[] (one per terminal surface) GhosttyRuntime (shared singleton) └─ ghostty_app_t (single C instance) └─ ghostty_surface_t[] (independent terminal sessions) ``` ### TCA ↔ Terminal Communication The terminal layer (`WorktreeTerminalManager`) is `@Observable` but outside TCA. Communication uses `TerminalClient`: ``` Reducer → terminalClient.send(Command) → WorktreeTerminalManager ↓ Reducer ← .terminalEvent(Event) ← AsyncStream ``` - **Commands**: `createTab`, `closeFocusedTab`, `prune`, `setSelectedWorktreeID`, etc. - **Events**: `notificationReceived`, `tabCreated`, `tabClosed`, `focusChanged`, `taskStatusChanged` - Wired in `ProwlApp.swift`, subscribed in `AppFeature.task` ### Key Dependencies - **TCA (swift-composable-architecture)**: App state, reducers, side effects - **GhosttyKit**: Terminal emulator (built from Zig source in ThirdParty/ghostty) - **Sparkle**: Auto-update framework - **swift-dependencies**: Dependency injection for TCA clients - **PostHog**: Analytics - **Sentry**: Error tracking ## Ghostty Keybindings Handling - Ghostty keybindings are handled via runtime action callbacks in `GhosttySurfaceBridge`, not by app menu shortcuts. - App-level tab actions should be triggered by Ghostty actions (`GHOSTTY_ACTION_NEW_TAB` / `GHOSTTY_ACTION_CLOSE_TAB`) to honor user custom bindings. - `GhosttySurfaceView.performKeyEquivalent` routes bound keys to Ghostty first; only unbound keys fall through to the app. ## Code Guidelines - Target macOS 26.0+, Swift 6.2+ - Before doing a big feature or when planning, consult with pfw (pointfree) skills on TCA, Observable best practices first. - Use `@ObservableState` for TCA feature state; use `@Observable` for non-TCA shared stores; never `ObservableObject` - Always mark `@Observable` classes with `@MainActor` - Modern SwiftUI only: `foregroundStyle()`, `NavigationStack`, `Button` over `onTapGesture()` - Before changing a window toolbar control, its grouping, or Liquid Glass, read `docs-ai/061-native-toolbar-controls/toolbar-controls.md` and perform a Debug visual verification. - When a new logic changes in the Reducer, always add tests - In unit tests, never use `Task.sleep`; use `TestClock` (or an injected clock) and drive time with `advance`. - Prefer Swift-native APIs over Foundation where they exist (e.g., `replacing()` not `replacingOccurrences()`) - Avoid `GeometryReader` when `containerRelativeFrame()` or `visualEffect()` would work - Do not use NSNotification to communicate between reducers. - Prefer `@Shared` directly in reducers for app storage and shared settings; do not introduce new dependency clients solely to wrap `@Shared`. - Use `ProwlLogger` for all logging. Never use `print()` or `os.Logger` directly. `ProwlLogger` prints in DEBUG and uses `os.Logger` in release. - UI copy is localized (`docs-ai/070-app-localization/000-plan.md`). Write it so the compiler can extract it: a literal passed straight to SwiftUI (`Text("…")`, `.help("…")`), `String(localized: "…")` when you need a `String`, and a `LocalizedStringKey` or `LocalizedStringResource` parameter for a helper that takes copy. Write long copy as one multi-line literal, not a `+` chain. Do not translate during feature work: the `sync-l10n` skill adds translations and removes unused entries at release time. Tests run in English (pinned in the scheme), so assert on literal English copy. ### Formatting & Linting - 2-space indentation, 120 character line length (enforced by `.swift-format.json`) - `swift-format` is the source of truth for trailing commas: multi-element collection literals keep trailing commas, while single-element collection literals may have them removed. - SwiftLint runs in strict mode; never disable lint rules without permission - Custom SwiftLint rule: `store_state_mutation_in_views` — do not mutate `store.*` directly in view files; send actions instead - Before creating a PR, run `make check`. Use `make format` only for intentional full-tree formatting cleanup. - If `make check` fails with `swift-format: command not found`, the Xcode toolchain is not on `PATH`. The Makefile invokes `swift-format` unqualified, and the binary ships inside Xcode rather than in a standard bin directory. Prepend it for the invocation: ```bash export PATH="$(dirname "$(xcrun --find swift-format)"):$PATH" make check ``` `make lint` is unaffected — it already runs SwiftLint through `mise exec`. ## UX Standards - Buttons must have tooltips explaining the action and associated hotkey - Use Dynamic Type, avoid hardcoded font sizes - Components should be layout-agnostic (parents control layout, children control appearance) - Never use custom colors, always use system provided ones. - We use `.monospaced()` modifier on fonts when appropriate ## Rules - After a task, ensure the app builds: `make build-app` - When working on CLI code (`CLI/`, `Shared/`, `Mirror/Relay/`), run `make build-cli`, `make test-cli-smoke`, `make test-cli-unit`, and `make test-cli-integration` before committing. - When you change user-facing behavior (keyboard shortcuts, settings, the `prowl` CLI, or a feature's UX), update the matching file under `docs/` in the same change. For a full audit, run the `sync-docs` skill. - `docs-ai/` is curated, durable product/design documentation — never a working-note archive. Use the `write-ai-doc` skill only for a substantial feature or a non-trivial fix whose design and result must guide future implementation. Do not create entries for reviews, audits, routine research or investigations, status reports, test runs, or docs-only work unless onevcat explicitly asks for a `docs-ai/` record. When uncertain, do not create an entry. For qualifying work, create `docs-ai/NNN-/000-plan.md` before coding and complete `001-action.md` after implementation. Follow-up work on the same topic amends the existing entry (see `docs-ai/README.md`). - When implementing a new feature or fixing a bug that is unrelated to the current branch's active work, first create a dedicated branch from the latest `origin/main`; then work, commit, push, and open a PR from that branch. - Automatically commit your changes and your changes only. Do not use `git add .` - Before you go on your task, check the current git branch name, if it's something generic like an animal name, name it accordingly. Do not do this for main branch - After implementing an execplan, always submit a PR if you're not in the main branch - PRs must target `onevcat/Prowl` (this fork), never the upstream `supabitapp/supacode`, unless explicitly requested. - Fork releases must be notarized. Never publish non-notarized releases (`ENABLE_NOTARIZATION=0` is forbidden). ## Submodules - `ThirdParty/ghostty` (`https://github.com/ghostty-org/ghostty`): Source dependency used to build `App/Frameworks/GhosttyKit.xcframework` and terminal resources. - `ThirdParty/git-wt` (`https://github.com/khoi/git-wt.git`): Bundled `wt` CLI used by Prowl Git worktree flows at runtime.