AGENTS.md · git:20261002.a3db074 · 2026-10-02 · sha256 d469c801849de5e8
AGENTS.md git:20261002.a3db074A
Immutable. This exact content never changes and is served at /api/v1/blob/d469c801849de5e8.
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 all tests
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](https://tuist.dev) generates `Prowl.xcworkspace` (the macOS app and the iOS mirror client) from `Workspace.swift`, `Project.swift`, `MirrorClient/iOS/Project.swift`, and the build settings in `Config/Xcode/*.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 supacode -destination "platform=macOS" \
-only-testing:supacodeTests/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:supacodeTests/FooTests/testBar
# Swift Testing (@Test func bar)
-only-testing:"supacodeTests/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.
## 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 `supacodeApp.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 `SupaLogger` for all logging. Never use `print()` or `os.Logger` directly. `SupaLogger` 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 (`ProwlCLI/`, `ProwlCLITests/`, `Package.swift`), 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 `Frameworks/GhosttyKit.xcframework` and terminal resources.
- `Resources/git-wt` (`https://github.com/khoi/git-wt.git`): Bundled `wt` CLI used by Prowl Git worktree flows at runtime.