native macOS codings agent orchestrator prowl.onev.cat
Prowl AGENTS.md
14 kB

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 #

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=<Team ID> (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=<Team ID> so the test host keeps the same signature.

The Xcode projects are not in Git. Tuist 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):

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.

# XCTest (func testFoo)
-only-testing:ProwlTests/FooTests/testBar
# Swift Testing (@Test func bar)
-only-testing:"ProwlTests/FooTests/bar()"

Requires mise 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<Event>
  • 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:

    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-<slug>/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.