docs(wiki-audit): full v2.5 cross-section sync against active codebase

Alan Wizemann
2026-04-27 13:00:18 +02:00
parent 2f4744b797
commit 60233169b5
32 changed files with 457 additions and 168 deletions
+20 -5
@@ -1,18 +1,33 @@
# ACP Subprocess # ACP Subprocess
ACP — Agent Client Protocol — is Hermes's chat protocol: JSON-RPC 2.0 over stdio. Scarf's Rich Chat surface speaks ACP end-to-end. There is no SQLite polling involved in chat; tokens, thoughts, tool calls, and permission prompts all stream live from the subprocess. ACP — Agent Client Protocol — is Hermes's chat protocol: JSON-RPC 2.0 over stdio (or its bidirectional equivalent). Scarf's Rich Chat surface speaks ACP end-to-end. There is no SQLite polling involved in chat; tokens, thoughts, tool calls, and permission prompts all stream live from the channel.
## Subprocess construction ## ACPClient + ACPChannel
[`ACPClient`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/ACPClient.swift) is an `actor` that owns the subprocess and exposes an `AsyncStream<ACPEvent>`. [`ACPClient`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/ACP/ACPClient.swift) is an `actor` that owns whatever-it-is that ferries JSON-RPC bytes back and forth, and exposes an `AsyncStream<ACPEvent>`. The "whatever-it-is" is abstracted behind the [`ACPChannel`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/ACP/ACPChannel.swift) protocol so Mac and iOS share the client without `#if os(...)`:
The `Process` is created via `transport.makeProcess(executable: "hermes", args: ["acp"])`: | Channel | Where it lives | What it wraps |
|---|---|---|
| [`ProcessACPChannel`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/ACP/ProcessACPChannel.swift) | ScarfCore | A Foundation `Process` running `hermes acp` (local) or `ssh -T host -- hermes acp` (Mac remote). |
| [`SSHExecACPChannel`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfIOS/Sources/ScarfIOS/SSHExecACPChannel.swift) | ScarfIOS | A Citadel SSH exec channel (no Foundation `Process` available on iOS) — bidirectional stdin/stdout streams over the SSH transport. |
ACPClient consumes whichever channel the host provides at init; from there the protocol handling is identical.
## Process channel construction (Mac)
`ProcessACPChannel` is created via `transport.makeProcess(executable: "hermes", args: ["acp"])`:
- **Local:** spawns `hermes acp` with `HermesFileService.enrichedEnvironment()` so MCP servers and shell tools find brew/nvm/asdf binaries on `PATH`. `TERM` is removed so terminal escapes don't pollute JSON-RPC. - **Local:** spawns `hermes acp` with `HermesFileService.enrichedEnvironment()` so MCP servers and shell tools find brew/nvm/asdf binaries on `PATH`. `TERM` is removed so terminal escapes don't pollute JSON-RPC.
- **Remote:** the transport returns `/usr/bin/ssh -T <opts> host -- hermes acp`. `SSH_AUTH_SOCK` is inherited so the GUI-launched Scarf reaches the user's ssh-agent. `TERM` is removed. - **Remote:** the transport returns `/usr/bin/ssh -T <opts> host -- hermes acp`. `SSH_AUTH_SOCK` is inherited so the GUI-launched Scarf reaches the user's ssh-agent. `TERM` is removed.
`-T` (no PTY) is critical — without it stdin/stdout would be PTY-cooked and the JSON-RPC framing would break. `-T` (no PTY) is critical — without it stdin/stdout would be PTY-cooked and the JSON-RPC framing would break.
## SSH exec channel construction (iOS)
`SSHExecACPChannel` opens a Citadel exec channel against the user's configured Hermes host, runs `hermes acp` over it, and surfaces the channel's `inbound` / `outbound` byte streams as the same stdin/stdout abstraction `ACPClient` consumes from `Process`. There's no PTY allocation — Citadel's exec is binary-clean by default.
Because iOS's PATH is stripped on non-interactive SSH (Citadel doesn't source rc files — see [Transport Layer § CitadelServerTransport](Transport-Layer)), the channel inlines `PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"` in front of `hermes acp` exactly the way `runProcess` does. Same fix, same reason.
## Lifecycle ## Lifecycle
| Phase | What happens | | Phase | What happens |
@@ -96,4 +111,4 @@ Raw error messages are noisy. `ACPErrorHint` (in the same file) pattern-matches
| "Rate limit" / 429 | "AI provider rate-limited; try again later" | | "Rate limit" / 429 | "AI provider rate-limited; try again later" |
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (ScarfCore extraction + ACPChannel abstraction + iOS SSHExecACPChannel section)_
+43 -16
@@ -1,19 +1,33 @@
# Adding a Feature Module # Adding a Feature Module
The MVVM-F recipe. Adding a feature touches 4 existing files and creates 2 new ones. The MVVM-F recipe. Adding a Mac feature touches 4 existing files and creates 2 new ones; iOS additions are simpler since each tab is its own module.
## Directory shape > **Should the ViewModel live in ScarfCore or the Mac/iOS target?** Per [ScarfCore Package](ScarfCore-Package): if the VM does I/O against Hermes (transport reads, parsing, attribution lookups, formatting), it belongs in [`Packages/ScarfCore/Sources/ScarfCore/ViewModels/`](https://github.com/awizemann/scarf/tree/main/scarf/Packages/ScarfCore/Sources/ScarfCore/ViewModels) so iOS can reuse it. If the VM owns Mac- or iOS-specific UI state (toolbar items, keyboard shortcut bindings), keep it in the target.
## Directory shape (Mac)
``` ```
scarf/scarf/scarf/Features/MyFeature/ scarf/scarf/scarf/Features/MyFeature/
Views/ Views/
MyFeatureView.swift MyFeatureView.swift
ViewModels/ ViewModels/
MyFeatureViewModel.swift MyFeatureViewModel.swift ← target-local, only if Mac-specific UI state
``` ```
Both subdirectories are conventional — even features that only have one view of each follow this shape so file discovery is consistent. Both subdirectories are conventional — even features that only have one view of each follow this shape so file discovery is consistent.
If the ViewModel is shared, the Mac feature module only contains the View(s) and reaches into `ScarfCore.MyFeatureViewModel`.
## Directory shape (iOS)
```
scarf/Scarf iOS/MyFeature/
MyFeatureView.swift
MyFeatureView+Components.swift (optional split for compositional sub-views)
```
iOS feature modules are flatter — no Views/ViewModels/ subdirectories. Each tab in [`ScarfGoTabRoot`](https://github.com/awizemann/scarf/blob/main/scarf/Scarf%20iOS/App/ScarfGoTabRoot.swift) wires the feature view directly. Adding a *primary* tab is rare in v2.5; secondary screens just push onto the parent tab's `NavigationStack`.
## Step 1: Create the ViewModel ## Step 1: Create the ViewModel
```swift ```swift
@@ -101,17 +115,13 @@ enum SidebarSection: String, CaseIterable, Identifiable {
## Step 4: Register in SidebarView ## Step 4: Register in SidebarView
In [`Navigation/SidebarView.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/SidebarView.swift), add the case to the right `Section`: In [`Navigation/SidebarView.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/SidebarView.swift), add the case to the right `Section` array. The current shape uses a `[Section]` declaration; add your case to the matching items list:
```swift ```swift
Section("Interact") { Section(title: "Interact", items: [.chat, .memory, .skills, .myFeature]),
ForEach([SidebarSection.chat, .memory, .skills, .myFeature]) { section in
Label(section.rawValue, systemImage: section.icon).tag(section)
}
}
``` ```
Pick the section thematically — Monitor for views, Interact for talking-to-Hermes, Configure for setup, Manage for operational. Pick the section thematically — Monitor for views, Projects for project-scoped surfaces, Interact for talking-to-Hermes, Configure for setup, Manage for operational. See [Sidebar & Navigation](Sidebar-and-Navigation) for the canonical 5-group structure (22 cases as of v2.5).
## Step 5: Wire routing ## Step 5: Wire routing
@@ -126,7 +136,16 @@ case .myFeature: MyFeatureView(context: serverContext)
## Step 6: (If your feature uses a new service) ## Step 6: (If your feature uses a new service)
If you needed a new service to back this feature, add it under `Core/Services/` and inject any shared instance in `ContextBoundRoot` via `.environment(...)`. See [Adding a Service](Adding-a-Service). If you needed a new service to back this feature, decide between [`ScarfCore`](ScarfCore-Package) (shared with iOS) and Mac-only `Core/Services/`, then inject any shared instance in `ContextBoundRoot` via `.environment(...)`. See [Adding a Service](Adding-a-Service).
## Step 7: (If the feature should also be on iOS)
Mac and iOS share data + view-models via ScarfCore but have separate views. To bring `MyFeature` to ScarfGo:
1. Add `Scarf iOS/MyFeature/MyFeatureView.swift` consuming the same ScarfCore ViewModel.
2. Add a row, sub-tab, or tab to the appropriate parent in [`ScarfGoTabRoot.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Scarf%20iOS/App/ScarfGoTabRoot.swift). Most additions push onto an existing tab's `NavigationStack` — not a new tab. New tabs in v2.5+ require Coordinator + product-design review (5-tab cap on iPhone today).
3. Apply ScarfDesign tokens — see [Design System](Design-System). Heads-up: iOS uses semantic Dynamic Type tokens (`.font(.body)` etc.) for body copy and `ScarfFont` only for chrome/badges/intentional fixed-size; Mac uses `ScarfFont` everywhere.
4. Re-test against the iOS simulator. Verify multi-server switching doesn't leak feature state.
## Cross-feature rules ## Cross-feature rules
@@ -135,16 +154,24 @@ The hard rules ([CLAUDE.md](https://github.com/awizemann/scarf/blob/main/CLAUDE.
- **Features never import sibling features.** If `MyFeature` needs data another feature also uses, the data lives in a service, not in that other feature. - **Features never import sibling features.** If `MyFeature` needs data another feature also uses, the data lives in a service, not in that other feature.
- **Cross-feature navigation goes through `AppCoordinator`.** Set `coordinator.selectedSection = .otherFeature` and (if needed) `coordinator.selectedSessionId = ...`. - **Cross-feature navigation goes through `AppCoordinator`.** Set `coordinator.selectedSection = .otherFeature` and (if needed) `coordinator.selectedSessionId = ...`.
## Files touched ## Files touched (Mac)
- ✏️ `Navigation/AppCoordinator.swift` — 1 enum case, 1 icon line. - ✏️ `Navigation/AppCoordinator.swift` — 1 enum case, 1 icon line.
- ✏️ `Navigation/SidebarView.swift` — add to the right `Section`. - ✏️ `Navigation/SidebarView.swift` — add to the right `Section` items array.
- ✏️ `ContentView.swift` — 1 switch case. - ✏️ `ContentView.swift` — 1 switch case.
- ✏️ `scarfApp.swift` — only if you needed to inject a new shared service. - ✏️ `scarfApp.swift` — only if you needed to inject a new shared service.
-`Features/MyFeature/Views/MyFeatureView.swift` — new. -`Features/MyFeature/Views/MyFeatureView.swift` — new.
-`Features/MyFeature/ViewModels/MyFeatureViewModel.swift` — new. -`Features/MyFeature/ViewModels/MyFeatureViewModel.swift` OR `Packages/ScarfCore/.../ViewModels/MyFeatureViewModel.swift` — new (location depends on whether iOS will share it).
Total: ~5-10 lines across 4 existing files, plus 2 new files. Total: ~510 lines across 4 existing files, plus 12 new files.
## Files touched (iOS, optional)
- ✏️ `Scarf iOS/App/ScarfGoTabRoot.swift` — only if adding a primary tab.
- ✏️ Whichever existing tab pushes the new view onto its `NavigationStack`.
-`Scarf iOS/MyFeature/MyFeatureView.swift` — new.
iOS feature additions usually skip the AppCoordinator step — the iOS coordinator (`ScarfGoCoordinator`) handles cross-tab signalling, not per-view dispatch.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added ScarfCore VM placement guidance + iOS step + 5-group sidebar reference)_
+15 -5
@@ -1,6 +1,16 @@
# Adding a Service # Adding a Service
Services live under `scarf/scarf/scarf/Core/Services/`. They mediate between Hermes (filesystem, SQLite, subprocess) and feature ViewModels. The recipe is short. Services mediate between Hermes (filesystem, SQLite, subprocess) and feature ViewModels. As of v2.5 there are **two homes** for a service depending on whether iOS needs it:
| Home | When to use | Path |
|---|---|---|
| **`ScarfCore` package** | The service does pure I/O against Hermes (transport reads, parsing, attribution, formatting) — **default choice**, since iOS likely needs it too. | [`scarf/Packages/ScarfCore/Sources/ScarfCore/Services/`](https://github.com/awizemann/scarf/tree/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services) |
| **Mac target** (`scarf/Core/Services/`) | The service depends on AppKit, Sparkle, NSWorkspace, AppleScript, or anything else iOS can't link. | [`scarf/scarf/scarf/Core/Services/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/scarf/Core/Services) |
| **`ScarfIOS` package** | iOS-only — needs Citadel, iOS Keychain, UIKit. | [`scarf/Packages/ScarfIOS/Sources/ScarfIOS/`](https://github.com/awizemann/scarf/tree/main/scarf/Packages/ScarfIOS/Sources/ScarfIOS) |
When in doubt, **start in ScarfCore.** Promote to a target only when you hit a framework restriction. See [ScarfCore Package](ScarfCore-Package) for the package boundary rationale.
The recipe below applies regardless of home.
## Pick the isolation ## Pick the isolation
@@ -64,9 +74,9 @@ actor MyStatefulService {
## Conventions ## Conventions
- **Take `ServerContext` in `init`.** Never hardcode `ServerContext.local` — services must work against any window's bound server. - **Take `ServerContext` in `init`.** Never hardcode `ServerContext.local` — services must work against any window's bound server.
- **Route I/O through `context.transport` or the `context.read*/write*/runHermes` helpers.** Never use `FileManager`, `Process`, or `NSWorkspace.open` directly for Hermes paths — those break on remote (and break the rule from the project's [feedback memory](Wiki-Maintenance)). - **Route I/O through `context.transport` or the `context.read*/write*/runHermes` helpers.** Never use `FileManager`, `Process`, or `NSWorkspace.open` directly for Hermes paths — those break on remote (and break the rule from the project's [feedback memory](Wiki-Maintenance)). On iOS specifically, `Process` doesn't exist at all — services in ScarfCore must stay platform-agnostic.
- **Surface errors as `throws` or `Result`.** Don't swallow them; the UI knows what to do with them. - **Surface errors as `throws` or `Result`.** Don't swallow them; the UI knows what to do with them.
- **Don't log to `print`** — use `os.Logger` (`logger.error()` for unexpected, `logger.warning()` for expected). - **Don't log to `print`** — use `os.Logger` (`logger.error()` for unexpected, `logger.warning()` for expected). v2.5's logger conversion sweep removed the last `print("[Scarf] …")` calls; new code should follow.
- **Don't do synchronous file I/O on `@MainActor`.** Either dispatch via `Task.detached { }.value`, or expose async methods. - **Don't do synchronous file I/O on `@MainActor`.** Either dispatch via `Task.detached { }.value`, or expose async methods.
## Wiring the service into a feature ## Wiring the service into a feature
@@ -106,7 +116,7 @@ Use Environment for things every window has exactly one of — file watcher, ser
## Tests ## Tests
Service code that uses transport is testable with a mock transport. See [Testing](Testing) — what exists today is minimal, but the `ServerTransport` protocol is the obvious extension point for fakes. Service code that uses transport is testable with a mock transport. ScarfCore ships a `MockTransport` and the test suite exercises the contract — see [Testing](Testing). Add tests in `scarf/Packages/ScarfCore/Tests/ScarfCoreTests/` for ScarfCore services; in `scarf/scarfTests/` for Mac-only ones.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (ScarfCore / Mac / ScarfIOS placement matrix; logger sweep note)_
+77 -31
@@ -1,12 +1,13 @@
# Build and Run # Build and Run
Scarf is a single Xcode target with one SPM dependency (Sparkle). No CocoaPods, no Carthage, no submodules. Scarf is one Xcode project with **two app targets**: `scarf` (the macOS app) and `scarf mobile` (ScarfGo, iOS). Both targets share three local SwiftPM packages (`ScarfCore`, `ScarfIOS`, `ScarfDesign`) and pull in Sparkle (Mac auto-update) + Citadel (iOS pure-Swift SSH) as remote dependencies. No CocoaPods, no Carthage, no submodules.
## Prerequisites ## Prerequisites
- **macOS 14.6+ (Sonoma)** on the dev machine. - **macOS 14.6+ (Sonoma)** on the dev machine.
- **Xcode 16.0+**. - **Xcode 16.0+**.
- **Hermes** at `~/.hermes/` (so the local server window has something to point at — see [First Run](First-Run)). - **Hermes** at `~/.hermes/` (so the local Mac window has something to point at — see [First Run](First-Run)).
- For iOS: **Xcode iOS 18.0 simulator** (or a real iPhone running iOS 18+) and a Hermes-running host you can SSH to from the simulator.
## Open in Xcode ## Open in Xcode
@@ -22,6 +23,8 @@ Build with ⌘B; run with ⌘R.
## Build from the command line ## Build from the command line
### Mac scheme (`scarf`)
Debug build: Debug build:
```bash ```bash
@@ -36,6 +39,18 @@ xcodebuild -project scarf/scarf.xcodeproj -scheme scarf \
-arch arm64 -arch x86_64 ONLY_ACTIVE_ARCH=NO build -arch arm64 -arch x86_64 ONLY_ACTIVE_ARCH=NO build
``` ```
### iOS scheme (`scarf mobile`)
Debug build for the simulator (no physical device needed):
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme "scarf mobile" \
-configuration Debug \
-destination "generic/platform=iOS Simulator" build
```
The iOS target is iPhone-only as of v2.5: `TARGETED_DEVICE_FAMILY = 1`, `SUPPORTS_MACCATALYST = NO`, `SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO`.
## Project layout ## Project layout
``` ```
@@ -43,31 +58,53 @@ scarf/ repo root
CLAUDE.md project instructions for Claude Code CLAUDE.md project instructions for Claude Code
CONTRIBUTING.md CONTRIBUTING.md
README.md README.md
releases/v<ver>/ per-version notes + appcast entry icon-v2.5.png README hero icon
assets/screenshots/ ScarfGo screenshot gallery (in README)
design/ Reference UI kit (JSX mockups + token bundle source)
releases/v<ver>/ per-version notes (RELEASE_NOTES.md, TESTFLIGHT_CHECKLIST.md, APP_STORE_METADATA.md)
templates/ Community .scarftemplate catalog source
tools/ build-catalog.py + tests
scripts/ scripts/
release.sh full release pipeline release.sh Mac release pipeline (archive + notarize + appcast + tag)
wiki.sh this wiki helper wiki.sh GitHub wiki helper
ExportOptions.plist catalog.sh .scarftemplate catalog publishing
scarf/ Xcode project root scarf/ Xcode project root
scarf.xcodeproj scarf.xcodeproj Two targets: scarf, scarf mobile
docs/ internal dev notes (PRD, Discovery, ARCH) docs/ Internal dev notes + PRIVACY_POLICY.md
standards/ read-only reference standards Packages/ Local SwiftPM packages
scarf/ APP TARGET — start here ScarfCore/ Shared models, services, view-models, transport,
scarfApp.swift @main App ACP, parsing, security — Mac + iOS both link this
ContentView.swift window root ScarfIOS/ iOS-only: CitadelServerTransport, KeychainSSHKeyStore,
SSHExecACPChannel, CitadelSSHService
ScarfDesign/ Rust palette + tokens + components — Mac + iOS
scarf/ MAC TARGET — start here
scarfApp.swift @main App, multi-window, ServerLiveStatus polling
ContentView.swift window root, detail view switch
Core/ Core/
Services/ 9 services Services/ Mac-only services (Sparkle wrapper, etc.)
Models/ 13+ models Persistence/ ServerRegistry (plist)
Transport/ 4 transport files Features/ Mac feature modules
Persistence/ ServerRegistry
Utilities/ markdown helpers
Features/ 25 feature modules
Navigation/ AppCoordinator + SidebarView Navigation/ AppCoordinator + SidebarView
Assets.xcassets Assets.xcassets Mac app icon (rust set), AccentColor
Info.plist Info.plist + scarf.entitlements
scarf.entitlements Scarf iOS/ iOS TARGET — ScarfGo
scarfTests/ App/ ScarfGoTabRoot, ScarfGoCoordinator, theme glue
scarfUITests/ Onboarding/ SSH key generation + connection test
Servers/ Multi-server list + Forget flow
Dashboard/ Stats grid + recent sessions + Switch server button
Chat/ Single-column chat + composer + project picker
Projects/ Project detail tabs (Dashboard / Site / Sessions)
Skills/ Hub / Installed / Updates sub-tabs
Memory/ MEMORY.md + USER.md editor
Cron/ Read-only list with human-readable schedules
Settings/ Read view + Quick Edits sheet (7 keys)
Components/ ScarfDesign-wrapped iOS controls
Notifications/ APNs skeleton (gated apnsEnabled = false)
Assets.xcassets iOS rust app icon set + accent color
scarfTests/ Mac target unit tests (Swift Testing)
scarfUITests/ Mac UI tests
Scarf iOSTests/ iOS unit test placeholder
Scarf iOSUITests/ iOS UI test placeholder
``` ```
## Swift 6 concurrency ## Swift 6 concurrency
@@ -79,23 +116,32 @@ The project compiles with strict concurrency on. Two non-negotiables ([detail in
## What to look at first ## What to look at first
1. [`scarfApp.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/scarfApp.swift) — App entry point, multi-window setup, `ContextBoundRoot` injecting the `ServerContext` and friends. 1. [`scarfApp.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/scarfApp.swift) — Mac app entry point, multi-window setup, `ContextBoundRoot` injecting the `ServerContext` and friends.
2. [`Navigation/AppCoordinator.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/AppCoordinator.swift) — single source of truth for navigation. 2. [`Navigation/AppCoordinator.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/AppCoordinator.swift) — single source of truth for Mac navigation.
3. [`Core/Models/ServerContext.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ServerContext.swift) — the unified handle to a Hermes install. 3. [`Models/ServerContext.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ServerContext.swift) — the unified handle to a Hermes install (used by both targets).
4. [`Core/Transport/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Transport) — local vs. SSH abstraction. 4. [`Transport/`](https://github.com/awizemann/scarf/tree/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport) (LocalTransport + SSHTransport) and [`ScarfIOS/CitadelServerTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfIOS/Sources/ScarfIOS/CitadelServerTransport.swift) — the three transport implementations.
5. Any feature module under [`Features/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Features) for the MVVM-F shape. 5. Any feature module under [`scarf/Features/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Features) for the Mac MVVM-F shape, or under [`Scarf iOS/`](https://github.com/awizemann/scarf/tree/main/scarf/Scarf%20iOS) for the iOS equivalents.
6. [`ScarfGoTabRoot.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Scarf%20iOS/App/ScarfGoTabRoot.swift) — iOS root, 5-tab navigation, ServerContext wiring.
## Running tests ## Running tests
Mac target tests:
```bash ```bash
xcodebuild test -project scarf/scarf.xcodeproj -scheme scarf xcodebuild test -project scarf/scarf.xcodeproj -scheme scarf
``` ```
See [Testing](Testing) for what is and isn't tested. ScarfCore SwiftPM tests (the bulk of v2.5 coverage — 14 test suites):
```bash
swift test --package-path scarf/Packages/ScarfCore
```
See [Testing](Testing) for the full inventory.
## Releasing ## Releasing
Don't run `xcodebuild archive` / `notarytool` / `gh release create` by hand — use the release script. See [Release Process](Release-Process). The Mac release runs through `./scripts/release.sh <version>` — don't invoke `xcodebuild archive` / `notarytool` / `gh release create` by hand. The iOS release is a separate Xcode Archive + App Store Connect upload (see [`releases/v<ver>/TESTFLIGHT_CHECKLIST.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/TESTFLIGHT_CHECKLIST.md) and [`APP_STORE_METADATA.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/APP_STORE_METADATA.md)). See [Release Process](Release-Process).
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (two targets, ScarfCore + ScarfIOS + ScarfDesign packages, iPhone-only iOS)_
+13 -7
@@ -5,9 +5,9 @@ Thanks for your interest in contributing to Scarf. The canonical contributor gui
## Quick start ## Quick start
1. Fork and clone <https://github.com/awizemann/scarf>. 1. Fork and clone <https://github.com/awizemann/scarf>.
2. Open `scarf/scarf.xcodeproj` in Xcode 16+. 2. Open `scarf/scarf.xcodeproj` in Xcode 16+. Two app targets: `scarf` (macOS) and `scarf mobile` (ScarfGo, iOS). Both share the local SwiftPM packages [ScarfCore](ScarfCore-Package), `ScarfIOS`, and [ScarfDesign](Design-System).
3. Build and run (requires macOS 14.6+ and Hermes installed at `~/.hermes/`). 3. Build and run (requires macOS 14.6+ and Hermes installed at `~/.hermes/` for the local Mac window; iOS sim or device + a SSH-reachable Hermes host for ScarfGo).
4. Read [Build & Run](Build-and-Run) for the codebase tour and [Architecture Overview](Architecture-Overview) for the layering. 4. Read [Build & Run](Build-and-Run) for the codebase tour, [Architecture Overview](Architecture-Overview) for the layering, and [Design System](Design-System) for the rust palette + token usage.
## Code conventions ## Code conventions
@@ -22,8 +22,11 @@ The full list is in [`CONTRIBUTING.md`](https://github.com/awizemann/scarf/blob/
## What's good to work on ## What's good to work on
- Anything in the [Roadmap](Roadmap). - Anything in the [Roadmap](Roadmap) or [ScarfGo Roadmap](ScarfGo-Roadmap).
- Test coverage — see [Testing](Testing) for the highest-value gaps. - iOS-specific gaps — Cron editor, Settings full-YAML editor, Insights / Activity views, iPad layout polish, push notifications wiring once Hermes ships a sender. See [Platform Differences](Platform-Differences).
- Test coverage — see [Testing](Testing) for the highest-value remaining gaps (UI tests, log streaming).
- iOS localization — strings are extracted; translations welcome. See [Localization](Localization).
- Templates for the public catalog — see [Project Templates](Project-Templates) and the catalog at [awizemann.github.io/scarf/templates/](https://awizemann.github.io/scarf/templates/).
- Wiki content — every stub on the wiki is a pull request opportunity. See [Wiki Maintenance](Wiki-Maintenance) for the workflow. - Wiki content — every stub on the wiki is a pull request opportunity. See [Wiki Maintenance](Wiki-Maintenance) for the workflow.
- Bug reports with reproducible steps. - Bug reports with reproducible steps.
@@ -32,7 +35,10 @@ The full list is in [`CONTRIBUTING.md`](https://github.com/awizemann/scarf/blob/
1. Open an issue first describing the change. This avoids rework if the maintainer has constraints in mind. 1. Open an issue first describing the change. This avoids rework if the maintainer has constraints in mind.
2. One feature or fix per PR — keeps reviews tight. 2. One feature or fix per PR — keeps reviews tight.
3. Include a clear description of what changed and why. 3. Include a clear description of what changed and why.
4. Ensure `xcodebuild -project scarf/scarf.xcodeproj -scheme scarf build` succeeds with zero warnings. 4. Ensure both schemes build clean:
- `xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build`
- `xcodebuild -project scarf/scarf.xcodeproj -scheme "scarf mobile" -configuration Debug -destination "generic/platform=iOS Simulator" build`
5. Run the ScarfCore test suite if you touched anything in `Packages/ScarfCore`: `swift test --package-path scarf/Packages/ScarfCore`.
## Reporting issues ## Reporting issues
@@ -57,4 +63,4 @@ Two paths:
Be kind, be specific, assume good faith. Disagreements about technical direction are welcome; personal attacks aren't. Be kind, be specific, assume good faith. Disagreements about technical direction are welcome; personal attacks aren't.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (two targets, ScarfCore tests, iOS contribution areas)_
+8 -1
@@ -35,11 +35,18 @@ Every Scarf window has a connection pill in the toolbar showing the bound server
- **Token/cost are zero but you've used Hermes** — the schema may predate v0.7. Update Hermes; Scarf detects the new columns automatically. - **Token/cost are zero but you've used Hermes** — the schema may predate v0.7. Update Hermes; Scarf detects the new columns automatically.
- **Yellow "Can't read Hermes state" pill on remote** — open Manage Servers → Run Diagnostics. Each failed check explains why and how to fix. - **Yellow "Can't read Hermes state" pill on remote** — open Manage Servers → Run Diagnostics. Each failed check explains why and how to fix.
## ScarfGo Dashboard (iOS, v2.5+)
ScarfGo's Dashboard is a slimmer take: a stats grid (Sessions / Messages / Tool Calls / Tokens) over a recent-sessions card, plus a Sessions sub-tab with a project filter Menu. Swipe the row to resume a session. The Hermes Process / Gateway / Active Platforms cards are Mac-only — they read state Hermes doesn't yet expose remotely in a phone-friendly way. See [Platform Differences](Platform-Differences) for the full Mac vs iOS feature matrix.
A **Switch server** button in the iOS Dashboard's top-right corner (added v2.5) returns you to the Servers list without first navigating to the System tab — handy when you have multiple Hermes hosts configured.
## Related pages ## Related pages
- [Insights & Activity](Insights-and-Activity) for deeper analytics. - [Insights & Activity](Insights-and-Activity) for deeper analytics.
- [Chat](Chat) for talking to the running Hermes. - [Chat](Chat) for talking to the running Hermes.
- [Servers & Remote](Servers-and-Remote) for adding remote hosts and the diagnostics flow. - [Servers & Remote](Servers-and-Remote) for adding remote hosts and the diagnostics flow.
- [ScarfGo](ScarfGo) for the iOS Dashboard tour.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added iOS Dashboard cross-reference + Switch server button)_
+16 -14
@@ -1,29 +1,31 @@
# Data Model # Data Model
Plain Codable structs under [`scarf/scarf/scarf/Core/Models/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Models). These are the in-memory representation of Hermes state — sessions, messages, config, cron jobs, MCP servers, project dashboards. None of them own I/O; services read from `~/.hermes/` (via [transport](Transport-Layer)) and decode into these types. Plain Codable structs under [`scarf/Packages/ScarfCore/Sources/ScarfCore/Models/`](https://github.com/awizemann/scarf/tree/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models). These are the in-memory representation of Hermes state — sessions, messages, config, cron jobs, MCP servers, project dashboards. None of them own I/O; services read from `~/.hermes/` (via [transport](Transport-Layer)) and decode into these types.
In v2.5 every model moved out of the Mac target into the shared [ScarfCore](ScarfCore-Package) SwiftPM package, so iOS reuses them byte-for-byte. The path references below all point at the package locations.
## Sessions and messages ## Sessions and messages
| Type | Conformances | Notable fields | Notes | | Type | Conformances | Notable fields | Notes |
|---|---|---|---| |---|---|---|---|
| `HermesSession` | `Identifiable, Sendable` | `id, source, userId, model, title, parentSessionId, startedAt, endedAt, endReason, messageCount, toolCallCount, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, estimatedCostUSD, actualCostUSD, costStatus, billingProvider` | `isSubagent` = `parentSessionId != nil`; `displayCostUSD` prefers `actualCostUSD`; `sourceIcon` calls `KnownPlatforms.icon(for:source)`. | | `HermesSession` | `Identifiable, Sendable` | `id, source, userId, model, title, parentSessionId, startedAt, endedAt, endReason, messageCount, toolCallCount, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, estimatedCostUSD, actualCostUSD, costStatus, billingProvider, apiCallCount` _(v0.11+)_ | `isSubagent` = `parentSessionId != nil`; `displayCostUSD` prefers `actualCostUSD`; `sourceIcon` calls `KnownPlatforms.icon(for:source)`. `apiCallCount` (v0.11+) counts per-turn API round-trips, distinct from `toolCallCount`. |
| `HermesMessage` | `Identifiable, Sendable` | `id, sessionId, role, content, toolCallId, timestamp, tokenCount, finishReason, toolCalls, toolName, reasoning` | `isUser/isAssistant/isToolResult` helpers; `hasReasoning` for v0.7+ reasoning support. | | `HermesMessage` | `Identifiable, Sendable` | `id, sessionId, role, content, toolCallId, timestamp, tokenCount, finishReason, toolCalls, toolName, reasoning, reasoningContent` _(v0.11+)_ | `isUser/isAssistant/isToolResult` helpers; `hasReasoning` for v0.7+ reasoning support. `preferredReasoning` returns `reasoningContent` when both columns are populated (v0.11 path) and falls back to legacy `reasoning`. |
| `HermesToolCall` (nested in `HermesMessage`) | custom Codable | `callId, functionName, arguments` | `toolKind` categorizes to `.read/.edit/.execute/.fetch/.browser/.other`; `argumentsSummary` extracts command/path/query/url or truncates to 120 chars. | | `HermesToolCall` (nested in `HermesMessage`) | custom Codable | `callId, functionName, arguments` | `toolKind` categorizes to `.read/.edit/.execute/.fetch/.browser/.other`; `argumentsSummary` extracts command/path/query/url or truncates to 120 chars. |
## Server context and paths ## Server context and paths
| Type | Purpose | | Type | Purpose |
|---|---| |---|---|
| [`ServerContext`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ServerContext.swift) | The unifying handle. Holds `id, displayName, kind` (`.local` or `.ssh(SSHConfig)`); exposes `paths`, `makeTransport()`, and high-level helpers (`readText`, `writeText`, `runHermes`, `openInLocalEditor`). The `local` static is the well-known UUID `00000000-0000-0000-0000-000000000001`. | | [`ServerContext`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ServerContext.swift) | The unifying handle. Holds `id, displayName, kind` (`.local` or `.ssh(SSHConfig)`); exposes `paths`, `makeTransport()`, and high-level helpers (`readText`, `writeText`, `runHermes`, `openInLocalEditor`). The `local` static is the well-known UUID `00000000-0000-0000-0000-000000000001`. |
| `SSHConfig` (nested) | `host, user?, port?, identityFile?, remoteHome?, hermesBinaryHint?` | | `SSHConfig` (nested) | `host, user?, port?, identityFile?, remoteHome?, hermesBinaryHint?` |
| `UserHomeCache` (nested) | Process-wide cache of remote `$HOME` per `ServerID`; probed once via `ssh host echo $HOME`, memoized. | | `UserHomeCache` (nested) | Process-wide cache of remote `$HOME` per `ServerID`; probed once via `ssh host echo $HOME`, memoized. |
| [`HermesPathSet`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesPathSet.swift) | All `~/.hermes/*` paths as nonisolated computed properties. Parameterized by `home` and `isRemote`. Includes `hermesBinaryCandidates` (`~/.local/bin/hermes`, `/opt/homebrew/bin/hermes`, `/usr/local/bin/hermes`, `~/.hermes/bin/hermes`) and a `binaryHint` override for remote. | | [`HermesPathSet`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesPathSet.swift) | All `~/.hermes/*` paths as nonisolated computed properties. Parameterized by `home` and `isRemote`. Includes `hermesBinaryCandidates` (`~/.local/bin/hermes`, `/opt/homebrew/bin/hermes`, `/usr/local/bin/hermes`, `~/.hermes/bin/hermes`) and a `binaryHint` override for remote. |
See [Hermes Paths](Hermes-Paths) for the full path table. See [Hermes Paths](Hermes-Paths) for the full path table.
## Config ## Config
[`HermesConfig`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesConfig.swift) parses `config.yaml` into a typed tree. About 100+ properties grouped into nested setting structs: [`HermesConfig`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConfig.swift) parses `config.yaml` into a typed tree. About 100+ properties grouped into nested setting structs:
- `display`, `terminal`, `browser`, `voice`, `auxiliary`, `security`, `humanDelay`, `compression`, `checkpoints`, `logging`, `delegation` - `display`, `terminal`, `browser`, `voice`, `auxiliary`, `security`, `humanDelay`, `compression`, `checkpoints`, `logging`, `delegation`
- Per-platform: `discord, telegram, slack, matrix, mattermost, whatsapp, homeAssistant` - Per-platform: `discord, telegram, slack, matrix, mattermost, whatsapp, homeAssistant`
@@ -35,15 +37,15 @@ See [Hermes Paths](Hermes-Paths) for the full path table.
| Type | Purpose | | Type | Purpose |
|---|---| |---|---|
| [`HermesCronJob`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesCronJob.swift) | Job definition with `schedule, prompt, skills?, model?, enabled, state, deliver, nextRunAt?, lastRunAt?, lastError?, preRunScript?, deliveryFailures?, timeoutType?, timeoutSeconds?, silent?`. Nested `CronSchedule` and `CronJobsFile` container. | | [`HermesCronJob`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesCronJob.swift) | Job definition with `schedule, prompt, skills?, model?, enabled, state, deliver, nextRunAt?, lastRunAt?, lastError?, preRunScript?, deliveryFailures?, timeoutType?, timeoutSeconds?, silent?`. Nested `CronSchedule` and `CronJobsFile` container. |
| [`HermesMCPServer`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesMCPServer.swift) | MCP server config: `name, transport (.stdio/.http), command?, args, url?, auth?, env, headers, timeouts, enabled, toolsInclude/Exclude, resourcesEnabled, promptsEnabled, hasOAuthToken`. | | [`HermesMCPServer`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesMCPServer.swift) | MCP server config: `name, transport (.stdio/.http), command?, args, url?, auth?, env, headers, timeouts, enabled, toolsInclude/Exclude, resourcesEnabled, promptsEnabled, hasOAuthToken`. |
| [`MCPServerPreset`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/MCPServerPreset.swift) | Curated presets gallery (Filesystem, GitHub, Postgres, Slack, Linear, Sentry, Puppeteer, Memory, Fetch). | | [`MCPServerPreset`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/MCPServerPreset.swift) | Curated presets gallery (Filesystem, GitHub, Postgres, Slack, Linear, Sentry, Puppeteer, Memory, Fetch). |
| `HermesTool`, `HermesToolset`, `KnownPlatforms` | Tool/toolset definitions and a switch for platform icons (cli, telegram, discord, slack, whatsapp, signal, email, homeassistant, webhook, matrix, feishu, mattermost, imessage). | | `HermesTool`, `HermesToolset`, `KnownPlatforms` | Tool/toolset definitions and a switch for platform icons (cli, telegram, discord, slack, whatsapp, signal, email, homeassistant, webhook, matrix, feishu, mattermost, imessage). |
| `HermesSkill`, `HermesSkillCategory` | Installed skills with `id, name, category, path, files, requiredConfig`. | | `HermesSkill`, `HermesSkillCategory` | Installed skills with `id, name, category, path, files, requiredConfig`, plus v0.11 frontmatter fields `allowedTools?`, `relatedSkills?`, `dependencies?` (parsed from SKILL.md YAML by `SkillsScanner`). Old skills without these fields stay nil and the chip rows hide themselves in the UI. |
## Project dashboards ## Project dashboards
The dashboard schema lives in [`ProjectDashboard.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ProjectDashboard.swift): The dashboard schema lives in [`ProjectDashboard.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ProjectDashboard.swift):
- `ProjectRegistry``ProjectEntry` (name, path) - `ProjectRegistry``ProjectEntry` (name, path)
- `ProjectDashboard``DashboardSection``DashboardWidget` (`.stat / .progress / .text / .table / .chart / .list / .webview`) - `ProjectDashboard``DashboardSection``DashboardWidget` (`.stat / .progress / .text / .table / .chart / .list / .webview`)
@@ -54,11 +56,11 @@ The full schema is also documented in `scarf/docs/DASHBOARD_SCHEMA.md` in the ma
## ACP messages ## ACP messages
[`ACPMessages.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/ACPMessages.swift) defines the JSON-RPC envelopes (`ACPRequest`, `ACPRawMessage`, `ACPError`) and the typed events (`ACPEvent.messageChunk / thoughtChunk / toolCallStart / toolCallUpdate / permissionRequest / promptComplete / availableCommands / connectionLost / unknown`). See [ACP Subprocess](ACP-Subprocess) for the protocol details. [`ACPMessages.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/ACPMessages.swift) defines the JSON-RPC envelopes (`ACPRequest`, `ACPRawMessage`, `ACPError`) and the typed events (`ACPEvent.messageChunk / thoughtChunk / toolCallStart / toolCallUpdate / permissionRequest / promptComplete / availableCommands / connectionLost / unknown`). See [ACP Subprocess](ACP-Subprocess) for the protocol details.
## Constants ## Constants
[`HermesConstants.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Models/HermesConstants.swift) holds query defaults — `sessionLimit = 100`, `messageSearchLimit = 50`, `toolCallLimit = 50`, `previewContentLength = 100`, `logLineLimit = 200` — plus SQLite C macro replacements and file-size unit constants for display formatting. [`HermesConstants.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConstants.swift) holds query defaults — `sessionLimit = 100`, `messageSearchLimit = 50`, `toolCallLimit = 50`, `previewContentLength = 100`, `logLineLimit = 200` — plus SQLite C macro replacements and file-size unit constants for display formatting.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (ScarfCore extraction + v0.11 fields + SKILL.md frontmatter)_
+5 -1
@@ -1,5 +1,7 @@
# First Run # First Run
> **Looking for ScarfGo's first run?** The iOS app has a multi-step onboarding flow (host details → SSH key generation → paste public key → connection test). See [ScarfGo Onboarding](ScarfGo-Onboarding) for the walkthrough. This page covers the macOS app.
The first time you launch Scarf, it opens a single window bound to your local Hermes install at `~/.hermes/`. That window is automatic — there is no setup screen. The first time you launch Scarf, it opens a single window bound to your local Hermes install at `~/.hermes/`. That window is automatic — there is no setup screen.
## What Scarf expects ## What Scarf expects
@@ -36,5 +38,7 @@ See [Servers & Remote](Servers-and-Remote) for prerequisites on the remote host
Each Scarf window is bound to exactly one server. Open as many windows as you want — they run with independent state and can be tiled side-by-side. Each Scarf window is bound to exactly one server. Open as many windows as you want — they run with independent state and can be tiled side-by-side.
ScarfGo on iOS uses a single-window 5-tab layout instead — see [Sidebar & Navigation](Sidebar-and-Navigation) for the cross-platform navigation comparison.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (cross-linked ScarfGo onboarding + iOS nav)_
+6 -4
@@ -19,7 +19,7 @@ View and edit Hermes scheduled jobs (`~/.hermes/cron/jobs.json`):
| Column | What it shows | | Column | What it shows |
|---|---| |---|---|
| Name | The job's display name. | | Name | The job's display name. |
| Schedule | Cron expression or run-at timestamp. | | Schedule | **Human-readable phrase** _(v2.5+)_ — "Every 6 hours", "Weekdays at 09:00", "@hourly", etc. — falling back to the raw cron expression for anything the formatter doesn't recognize. Backed by [`CronScheduleFormatter`](Core-Services); ScarfGo renders the same text. |
| State | `enabled` / `paused` / `failed` / `running` with an icon. | | State | `enabled` / `paused` / `failed` / `running` with an icon. |
| Last run / next run | Timestamps. | | Last run / next run | Timestamps. |
| Delivery | Channel format like `discord:chat:thread`. | | Delivery | Channel format like `discord:chat:thread`. |
@@ -84,17 +84,19 @@ Restructured in 1.6 into a 10-tab layout exposing ~60 previously hidden config f
|---|---| |---|---|
| **General** | Updates (Sparkle toggle + manual check), basic preferences. | | **General** | Updates (Sparkle toggle + manual check), basic preferences. |
| **Display** | Streaming, reasoning visibility, cost display, verbose mode. | | **Display** | Streaming, reasoning visibility, cost display, verbose mode. |
| **Agent** | Model picker (backed by models.dev catalog — 111 providers), max turns, approval mode, reasoning effort. | | **Agent** | Model picker (backed by models.dev catalog — 111 providers + 6 overlay-only providers from `HERMES_OVERLAYS`), max turns, approval mode, reasoning effort. |
| **Terminal** | Terminal backend, Docker / container settings, modal options. | | **Terminal** | Terminal backend, Docker / container settings, modal options. |
| **Browser** | Browser backend selection. | | **Browser** | Browser backend selection. |
| **Voice** | TTS / STT providers, PTT, silence threshold (default 200ms). | | **Voice** | TTS / STT providers, PTT, silence threshold (default 200ms). |
| **Memory** | `memory_enabled`, `memory_char_limit`, `user_char_limit`, `memory_provider`. | | **Memory** | `memory_enabled`, `memory_char_limit`, `user_char_limit`, `memory_provider`. **Reset memory** _(v2.5+)_ — a toolbar button on Memory views (Mac + iOS) runs `hermes memory reset --yes` with a destructive-confirmation dialog. |
| **Aux Models** | All 8 auxiliary model tasks (vision, web extract, compression, delegation, etc.). | | **Aux Models** | All 8 auxiliary model tasks (vision, web extract, compression, delegation, etc.). |
| **Security** | Tirith sandbox, command allowlist, website blocklist, redaction. | | **Security** | Tirith sandbox, command allowlist, website blocklist, redaction. |
| **Advanced** | Logging level / rotation, checkpoints, human-delay simulation, compression thresholds. | | **Advanced** | Logging level / rotation, checkpoints, human-delay simulation, compression thresholds. |
**Backup & Restore** lives at the bottom — wraps `hermes backup` (zips the current profile) and `hermes import` (unzips into the active profile). One-click via `context.runHermes`. **Backup & Restore** lives at the bottom — wraps `hermes backup` (zips the current profile) and `hermes import` (unzips into the active profile). One-click via `context.runHermes`.
ScarfGo's Settings tab is **read view + Quick Edits** — see [ScarfGo](ScarfGo) and [Platform Differences](Platform-Differences). The 7 quick-edit keys (`model.default` / `provider`, `agent.approval_mode` / `max_turns`, `display.streaming` / `show_cost` / `show_reasoning`) shell out to `hermes config set`. Other keys remain read-only on iOS.
## Related pages ## Related pages
- [Core Services](Core-Services) — the services backing each of these views. - [Core Services](Core-Services) — the services backing each of these views.
@@ -102,4 +104,4 @@ Restructured in 1.6 into a 10-tab layout exposing ~60 previously hidden config f
- [Updating](Updating) — Sparkle, the appcast, and how the auto-update flow works. - [Updating](Updating) — Sparkle, the appcast, and how the auto-update flow works.
--- ---
_Last updated: 2026-04-24 — Scarf v2.3 (Web Dashboard launcher in Health; ships in v2.4)_ _Last updated: 2026-04-25 — Scarf v2.5.0 (human-readable cron schedules, hermes memory reset, ScarfGo Quick Edits cross-link)_
+39 -4
@@ -1,24 +1,46 @@
# Hermes Paths # Hermes Paths
Canonical layout of `~/.hermes/`. Scarf reads these paths through `HermesPathSet` (`Core/Models/HermesPathSet.swift`). Updates here should also be mirrored to the **Key Paths** block in `CLAUDE.md` in the main repo. Canonical layout of `~/.hermes/`. Scarf reads these paths through [`HermesPathSet`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesPathSet.swift) in the ScarfCore package — same path resolution on Mac and iOS. Updates here should mirror to the **Key Paths** block in `scarf/CLAUDE.md`.
## Hermes-owned paths
| Path | What lives here | Scarf access | | Path | What lives here | Scarf access |
|---|---|---| |---|---|---|
| `~/.hermes/` | Hermes home | read-only base | | `~/.hermes/` | Hermes home | read-only base |
| `~/.hermes/state.db` | SQLite (WAL mode) — sessions, messages, activity, costs | **read-only**, never written | | `~/.hermes/state.db` | SQLite (WAL mode) — sessions, messages, activity, costs | **read-only**, never written |
| `~/.hermes/config.yaml` | Hermes runtime config (platforms, models, LLM settings, ~60 fields) | read + write (via Settings) | | `~/.hermes/config.yaml` | Hermes runtime config (platforms, models, LLM settings, ~60 fields) | read + write (Mac Settings; iOS Quick Edits via `hermes config set`) |
| `~/.hermes/.env` | Secrets and env vars referenced by `config.yaml` | read + write (preserves comments + blanks) | | `~/.hermes/.env` | Secrets and env vars referenced by `config.yaml` | read + write (preserves comments + blanks) |
| `~/.hermes/auth.json` | Auth tokens (Nous Portal subscription, Spotify OAuth, other provider tokens) | read-only — Hermes owns the write path |
| `~/.hermes/SOUL.md` | Top-level personality | read + write |
| `~/.hermes/memories/MEMORY.md` | Hermes's project/topic memory | read + write | | `~/.hermes/memories/MEMORY.md` | Hermes's project/topic memory | read + write |
| `~/.hermes/memories/USER.md` | Hermes's user memory | read + write | | `~/.hermes/memories/USER.md` | Hermes's user memory | read + write |
| `~/.hermes/sessions/session_*.json` | Session metadata files | read-only | | `~/.hermes/sessions/session_*.json` | Session metadata files | read-only |
| `~/.hermes/cron/jobs.json` | Scheduled job definitions | read + write | | `~/.hermes/cron/jobs.json` | Scheduled job definitions | read + write |
| `~/.hermes/cron/output/` | Cron job output captures | read-only |
| `~/.hermes/logs/agent.log` | Agent log (per-session tagged) | read-only, tail | | `~/.hermes/logs/agent.log` | Agent log (per-session tagged) | read-only, tail |
| `~/.hermes/logs/errors.log` | Error log | read-only, tail | | `~/.hermes/logs/errors.log` | Error log | read-only, tail |
| `~/.hermes/logs/gateway.log` | Messaging-gateway log | read-only, tail | | `~/.hermes/logs/gateway.log` | Messaging-gateway log | read-only, tail |
| `~/.hermes/skills/` | Installed skills | read + write (install/update/uninstall) | | `~/.hermes/gateway_state.json` | Gateway live state (PID, connected platforms) | read-only |
| `~/.hermes/skills/` | Installed skills (with v0.11 SKILL.md frontmatter) | read + write (install/update/uninstall) |
| `~/.hermes/plugins/` | Installed plugins (cloned from Git URLs) | read + write | | `~/.hermes/plugins/` | Installed plugins (cloned from Git URLs) | read + write |
| `~/.hermes/personalities/` | Personalities + their `SOUL.md` | read + write | | `~/.hermes/personalities/` | Personalities + their `SOUL.md` | read + write |
| `~/.hermes/profiles/` | Isolated Hermes instances | read + write | | `~/.hermes/profiles/` | Isolated Hermes instances | read + write |
| `~/.hermes/mcp-tokens/*.json` | Per-server MCP OAuth tokens | read (detect) + delete (clear) |
## Scarf-owned paths under `~/.hermes/scarf/`
These are written by Scarf, never by Hermes. Both clients (Mac + iOS) read and write the same files so attribution and project context survive across platforms.
| Path | What lives here | Notes |
|---|---|---|
| `~/.hermes/scarf/projects.json` | Project registry — every directory you've registered as a Scarf project | Mac authors via Projects sidebar; iOS reads via SFTP. |
| `~/.hermes/scarf/session_project_map.json` | Attribution sidecar — maps Hermes session IDs to project paths | Written when project-scoped chat starts. Drives the project-filter UI on both Mac global Sessions and ScarfGo Dashboard. |
| `<project>/.scarf/dashboard.json` | Per-project dashboard JSON | Lives inside the project, not under `~/.hermes/`. |
| `<project>/.scarf/template.lock.json` | `.scarftemplate` install manifest (when a project was installed from a template) | Drives clean uninstall. |
| `<project>/.scarf/manifest.json` | Cached `template.json` for templates with a config schema | Drives the post-install Configuration sheet. |
| `<project>/.scarf/config.json` | Non-secret configuration values | Secrets are `keychain://...` URIs; resolved at use time. |
| `<project>/.scarf/slash-commands/<name>.md` _(v2.5+)_ | Project-scoped slash commands | See [Slash Commands](Slash-Commands). |
| `<project>/AGENTS.md` (between `<!-- scarf-project:begin -->` markers) | Auto-managed project context block | Idempotent, secret-safe. See [Projects & Profiles](Projects-and-Profiles). |
## ACP ## ACP
@@ -29,12 +51,25 @@ Chat does not go through the filesystem. It is a subprocess: `hermes acp` (local
| Path | What lives here | | Path | What lives here |
|---|---| |---|---|
| `~/Library/Caches/scarf/snapshots/<server-id>/` | Atomic `state.db` snapshots pulled from remote servers via `sqlite3 .backup` | | `~/Library/Caches/scarf/snapshots/<server-id>/` | Atomic `state.db` snapshots pulled from remote servers via `sqlite3 .backup` |
| `~/Library/Application Support/com.scarf/skill-snapshots/<serverID>.json` | Per-server skill snapshot for the v2.5 "What's New" pill |
| `/tmp/scarf-ssh-<uid>/` | SSH ControlMaster sockets (per-host `%C` hash). Mode 0700; per-uid suffix isolates between local users. Lives under `/tmp` to stay within macOS' 104-byte Unix domain socket path limit. | | `/tmp/scarf-ssh-<uid>/` | SSH ControlMaster sockets (per-host `%C` hash). Mode 0700; per-uid suffix isolates between local users. Lives under `/tmp` to stay within macOS' 104-byte Unix domain socket path limit. |
| `~/Library/Preferences/com.scarf.app.plist` | App preferences + the server registry | | `~/Library/Preferences/com.scarf.app.plist` | App preferences + the server registry |
## iOS-side (ScarfGo) state
ScarfGo can't write to `~/Library/Caches/scarf/...` — it lives in its own iOS app sandbox. Equivalent paths:
| What | Where |
|---|---|
| SQLite snapshots | iOS Caches dir → `<sandbox>/Library/Caches/scarf/snapshots/<server-id>/state.db` |
| Skill snapshots ("What's New" pill) | `UserDefaults` (the iOS sandbox doesn't have a clean Application Support equivalent for tiny per-server JSON) |
| Server registry (multi-server) | `UserDefaults` key `com.scarf.ios.servers.v2` (auto-migrated from legacy `ScarfGo.servers.v1`) |
| SSH private keys | iOS Keychain — service `com.scarf.ssh-key`, account `server-key:<UUID>`, accessibility `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. Never iCloud-synced. |
| Template config secrets | iOS Keychain — service `com.scarf.template.<slug>`, account `<fieldKey>:<project-path-hash>` |
## Log line format ## Log line format
Hermes log lines may carry an optional `[session_id]` tag between the level and the logger name. `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse correctly. Hermes log lines may carry an optional `[session_id]` tag between the level and the logger name. `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse correctly.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.2_ _Last updated: 2026-04-25 — Scarf v2.5.0 (Scarf-owned paths section, slash-commands/template lock-files, iOS sandbox + Keychain layout)_
+5 -3
@@ -24,10 +24,12 @@ All queries are aggregations over the same `sessions`, `messages`, `tool_calls`
The full conversation history browser: The full conversation history browser:
- **List** — every session, ordered by start date DESC. Subagent sessions (those with a `parent_session_id`) are filtered from the main list and accessible by drilling into the parent. - **List** — every session, ordered by start date DESC. Subagent sessions (those with a `parent_session_id`) are filtered from the main list and accessible by drilling into the parent.
- **Detail panel** — full message stream: user → assistant → tool calls → tool results, with markdown rendering. **Reasoning blocks** (v0.7+) render in a collapsed section. - **Project filter** _(v2.5+)_ — Menu above the list picks **All projects / Unattributed / one entry per registered project**. Each row carries a tinted folder chip when the session is attributed to a project. The filter and the badges share the same `SessionAttributionService` ScarfGo's Sessions tab uses, so cross-platform parity is by construction. See [Projects & Profiles](Projects-and-Profiles).
- **Detail panel** — full message stream: user → assistant → tool calls → tool results, with markdown rendering. **Reasoning blocks** (v0.7+) render in a collapsed section. v0.11+ `messages.reasoning_content` (when present) is preferred over the legacy `reasoning` blob.
- **API call counter** _(v2.5+)_ — each row carries a network-icon chip showing `sessions.api_call_count` (v0.11+). Distinct from `tool_call_count`; counts per-turn API round-trips.
- **Tool call inspector** — pretty-printed arguments, function name, result. Categorized by `toolKind` (read / edit / execute / fetch / browser / other). - **Tool call inspector** — pretty-printed arguments, function name, result. Categorized by `toolKind` (read / edit / execute / fetch / browser / other).
- **Search** — full-text via SQLite's `messages_fts` FTS5 virtual table. Limit defaults to 50 hits. - **Search** — full-text via SQLite's `messages_fts` FTS5 virtual table. Limit defaults to 50 hits.
- **Actions** — rename (`hermes sessions rename`), delete (`hermes sessions delete`), JSONL export (`hermes sessions export`). - **Actions** — rename (`hermes sessions rename`), delete (`hermes sessions delete`), JSONL export (`hermes sessions export`). Right-click any row in the v2.5 chat sessions sidebar exposes the same Rename / Delete actions inline.
Click a session in the Dashboard's "Recent" card to land here with that session pre-selected. Click a session in the Dashboard's "Recent" card to land here with that session pre-selected.
@@ -44,4 +46,4 @@ The per-tool execution feed — what Hermes did, when, and with what arguments:
All three views observe the file watcher and re-query when `state.db` changes. Remote windows pull a fresh atomic snapshot via `sqlite3 .backup` (deduped by `SnapshotCoordinator` so Dashboard + Insights + Sessions don't each spawn parallel backups). See [Transport Layer](Transport-Layer) for the snapshot mechanics. All three views observe the file watcher and re-query when `state.db` changes. Remote windows pull a fresh atomic snapshot via `sqlite3 .backup` (deduped by `SnapshotCoordinator` so Dashboard + Insights + Sessions don't each spawn parallel backups). See [Transport Layer](Transport-Layer) for the snapshot mechanics.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (project filter + badges, API call chip, reasoning_content preference)_
+3 -1
@@ -1,5 +1,7 @@
# Installation # Installation
> **Looking for the iPhone?** This page covers the macOS desktop app. ScarfGo, the iOS companion, ships via TestFlight — see [ScarfGo](ScarfGo) and [ScarfGo Onboarding](ScarfGo-Onboarding) for that flow.
## System requirements ## System requirements
- **macOS 14.6+ (Sonoma)** or newer. - **macOS 14.6+ (Sonoma)** or newer.
@@ -43,4 +45,4 @@ Scarf uses [Sparkle](https://sparkle-project.org/) for automatic updates from a
- [Uninstalling](Uninstalling) — removing the app and its files. - [Uninstalling](Uninstalling) — removing the app and its files.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added ScarfGo cross-link)_
+8 -1
@@ -19,6 +19,7 @@ You can have a local window and up to 8 remote windows reachable by ⌘ + number
|---|---|---| |---|---|---|
| ⌃B | Chat | Toggle the sessions sidebar inside Chat | | ⌃B | Chat | Toggle the sessions sidebar inside Chat |
| ⌘S | Personalities (SOUL.md editor) | Save the current SOUL.md | | ⌘S | Personalities (SOUL.md editor) | Save the current SOUL.md |
| **1**…**9** _(v2.5+)_ | Chat permission sheet | Approve / deny by option number — Hermes's request-permission sheet binds 19 to the option buttons. Visible "1. " / "2. " prefixes hint the binding. Power users approve / deny without reaching for the mouse. |
## Dialog defaults ## Dialog defaults
@@ -51,5 +52,11 @@ There's no command palette and no global shortcuts beyond ⌘ + number for windo
If you'd like to see additional shortcuts, file an issue — keyboard accessibility is welcome contribution territory. If you'd like to see additional shortcuts, file an issue — keyboard accessibility is welcome contribution territory.
## ScarfGo (iOS)
iPhone has no Mac keyboard, so the binding tables above don't apply. The numbered permission-sheet hints (1. / 2. / …) still render on iOS but as hierarchy cues — the row tap target is the action.
If you connect a Bluetooth keyboard or an iPad with a Magic Keyboard, you get system iOS shortcuts (⌘C copy, ⌘W close sheet, ⌘N new chat in some flows). No app-specific bindings yet — on the [ScarfGo Roadmap](ScarfGo-Roadmap).
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (numbered approval shortcuts + iOS note)_
+3 -1
@@ -2,6 +2,8 @@
Scarf 2.1 added full UI translations on top of English. Seven languages ship in the box and more can be contributed via a plain GitHub PR — no translation-management tool, no account to create. Scarf 2.1 added full UI translations on top of English. Seven languages ship in the box and more can be contributed via a plain GitHub PR — no translation-management tool, no account to create.
> **ScarfGo (iOS) is English-only in v2.5.** The iOS strings are extracted but no translations are contributed yet. Localizing the iOS app is on the [ScarfGo Roadmap](ScarfGo-Roadmap) — most of the strings already exist in `Localizable.xcstrings`, so contributing iOS translations would lean on the same workflow described below.
## Supported languages ## Supported languages
| Locale | Name | Status | | Locale | Name | Status |
@@ -70,4 +72,4 @@ Per-locale JSON under `tools/translations/` is the canonical source of truth for
Deeper dev-facing notes on which SwiftUI patterns silently bypass localization (and how to avoid them when adding new UI) are in [`scarf/docs/I18N.md`](https://github.com/awizemann/scarf/blob/main/scarf/docs/I18N.md). Deeper dev-facing notes on which SwiftUI patterns silently bypass localization (and how to avoid them when adding new UI) are in [`scarf/docs/I18N.md`](https://github.com/awizemann/scarf/blob/main/scarf/docs/I18N.md).
--- ---
_Last updated: 2026-04-21 — Scarf v2.1.0_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added iOS English-only note + ScarfGo Roadmap link)_
+1 -1
@@ -66,4 +66,4 @@ Enable / disable Hermes toolsets per platform.
- [Hermes Paths](Hermes-Paths) — `~/.hermes/plugins/`, `config.yaml` `mcp_servers` key. - [Hermes Paths](Hermes-Paths) — `~/.hermes/plugins/`, `config.yaml` `mcp_servers` key.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (no MCP/Plugins/Webhooks/Tools changes since v2.0.1; refresh footer)_
+4 -4
@@ -17,7 +17,7 @@ Both clients talk to the same Hermes host with the same paths and the same data.
| **Cron editor (write)** | Yes | No | Mac builds a richer editor sheet; iOS read-only in v1. Coming. | | **Cron editor (write)** | Yes | No | Mac builds a richer editor sheet; iOS read-only in v1. Coming. |
| **Skills tree** | Yes | Yes (read-only) | iOS won't grow a skill editor — too cramped. | | **Skills tree** | Yes | Yes (read-only) | iOS won't grow a skill editor — too cramped. |
| **Settings (read)** | Yes (full YAML) | Yes (full YAML) | — | | **Settings (read)** | Yes (full YAML) | Yes (full YAML) | — |
| **Settings (write)** | Yes (full YAML editor) | **No** | iOS deferred. The plan was a curated `hermes config set <key> <value>` shell-out for 7 keys; that landed in the codebase but isn't surfaced in v1 because changing settings remotely without local validation is too easy to break. Mac stays the canonical editor. | | **Settings (write)** | Yes (full YAML editor) | **Quick Edits (7 keys)** | iOS exposes a curated `hermes config set <key> <value>` shell-out for `model.default`, `model.provider`, `agent.approval_mode`, `agent.max_turns`, `display.show_cost`, `display.show_reasoning`, `display.streaming`. Other keys stay read-only — Mac is the canonical full-YAML editor. |
| **Slash commands — author** _(v2.5)_ | Yes (per-project tab + live preview) | **No** | Multi-line markdown editing on a phone keyboard is its own UX problem; iOS gets a read-only browser instead. | | **Slash commands — author** _(v2.5)_ | Yes (per-project tab + live preview) | **No** | Multi-line markdown editing on a phone keyboard is its own UX problem; iOS gets a read-only browser instead. |
| **Slash commands — invoke / browse** _(v2.5)_ | Yes (slash menu) | Yes (read-only browser sheet) | — | | **Slash commands — invoke / browse** _(v2.5)_ | Yes (slash menu) | Yes (read-only browser sheet) | — |
| **Templates — install** | Yes | No | Templates are a content-creation surface; iOS won't get a UI for it in v1. Use Mac. | | **Templates — install** | Yes | No | Templates are a content-creation surface; iOS won't get a UI for it in v1. Use Mac. |
@@ -29,7 +29,7 @@ Both clients talk to the same Hermes host with the same paths and the same data.
| **Tool Gateway (Nous Portal)** | Yes (provider picker + Auxiliary tab + Health surface) | Provider exists in catalog | iOS Settings is read-only, so tool-gateway routing toggles aren't editable. Use Mac. | | **Tool Gateway (Nous Portal)** | Yes (provider picker + Auxiliary tab + Health surface) | Provider exists in catalog | iOS Settings is read-only, so tool-gateway routing toggles aren't editable. Use Mac. |
| **Insights / activity charts** | Yes | No | Charts are richer on bigger screens; deferred. | | **Insights / activity charts** | Yes | No | Charts are richer on bigger screens; deferred. |
| **Terminal (SwiftTerm)** | Mac doesn't have it either | No | Hermes shell mode is CLI-only; neither client wraps it. | | **Terminal (SwiftTerm)** | Mac doesn't have it either | No | Hermes shell mode is CLI-only; neither client wraps it. |
| **Localization** | 7 languages (de, es, fr, it, ja, pt-BR, zh-Hans) | English only | iOS strings are extracted but no translations contributed in v1. | | **Localization** | 7 languages (en, de, es, fr, ja, pt-BR, zh-Hans — verified in `Localizable.xcstrings`) | English only | iOS strings are extracted but no translations contributed in v1. |
| **Multi-server** | Yes (one window per server) | Yes (sidebar-adaptable Tab root) | — | | **Multi-server** | Yes (one window per server) | Yes (sidebar-adaptable Tab root) | — |
| **Push notifications** | n/a (Mac uses local notifications + the menu bar) | Skeleton present, gated `apnsEnabled = false` | Push sender doesn't exist on Hermes side yet. Capability disabled in target. Flips on simultaneously when both lights are green. | | **Push notifications** | n/a (Mac uses local notifications + the menu bar) | Skeleton present, gated `apnsEnabled = false` | Push sender doesn't exist on Hermes side yet. Capability disabled in target. Flips on simultaneously when both lights are green. |
| **Sparkle auto-update** | Yes | n/a | iOS uses TestFlight / App Store. | | **Sparkle auto-update** | Yes | n/a | iOS uses TestFlight / App Store. |
@@ -63,7 +63,7 @@ Those belong on Mac, where you have a keyboard, real screen, and the full Hermes
These are gaps without a fundamental reason; they just need engineering time: These are gaps without a fundamental reason; they just need engineering time:
- **Cron editor.** Add / remove jobs from the phone. The data model is shared; only the editor sheet is missing. - **Cron editor.** Add / remove jobs from the phone. The data model is shared; only the editor sheet is missing.
- **Scoped Settings editor.** A whitelisted "Quick edits" sheet for the 7 keys most users actually change (`model.default`, `model.provider`, `agent.approval_mode`, `agent.max_turns`, `display.show_cost`, `display.show_reasoning`, `display.streaming`). - ~~**Scoped Settings editor.**~~ ✅ Shipped in v2.5 — the **Quick Edits** sheet covers the 7 keys most users actually change (`model.default`, `model.provider`, `agent.approval_mode`, `agent.max_turns`, `display.show_cost`, `display.show_reasoning`, `display.streaming`). Listed here for history; arbitrary-key editing remains future work.
- **Health summary card.** A reduced version of the Mac Health view — just enough to answer "is the gateway running, is the DB reachable, is the agent crashy?" - **Health summary card.** A reduced version of the Mac Health view — just enough to answer "is the gateway running, is the DB reachable, is the agent crashy?"
- **Localization.** Translate the strings the Mac app already has; reuse the `.strings` files. - **Localization.** Translate the strings the Mac app already has; reuse the `.strings` files.
- **iPad layout pass.** Probably one afternoon's verification. - **iPad layout pass.** Probably one afternoon's verification.
@@ -80,4 +80,4 @@ These are gaps without a fundamental reason; they just need engineering time:
--- ---
_Last updated: 2026-04-25 — v2.5._ _Last updated: 2026-04-25 — v2.5 (audit pass: Settings Quick Edits ship; locale list corrected)._
+1 -1
@@ -49,4 +49,4 @@ Quick Commands live in `config.yaml` under the `quick_commands` key.
- [Hermes Paths](Hermes-Paths) — where `.env`, `config.yaml`, and `personalities/` live. - [Hermes Paths](Hermes-Paths) — where `.env`, `config.yaml`, and `personalities/` live.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (no behavior changes since v2.0.1; refresh footer)_
+3 -2
@@ -58,7 +58,8 @@ If you join the ScarfGo beta via TestFlight, Apple shares anonymized crash repor
- iOS Keychain storage uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` so credentials are unreachable while the device is locked and never synced to iCloud. - iOS Keychain storage uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` so credentials are unreachable while the device is locked and never synced to iCloud.
- SSH connections use the same protocol stack as `ssh(1)` — strict host-key verification on first connect, key-based auth (no passwords are sent over the wire), and Citadel's pure-Swift implementation on iOS. - SSH connections use the same protocol stack as `ssh(1)` — strict host-key verification on first connect, key-based auth (no passwords are sent over the wire), and Citadel's pure-Swift implementation on iOS.
- The macOS app is sandboxed where possible and notarized via Apple's standard Developer ID flow. - The macOS app is **notarized via Apple's standard Developer ID flow** (signed + stapled by `xcrun notarytool` on every release). It is **not App-Sandboxed** — Scarf needs direct read access to `~/.hermes/` and the ability to spawn the `hermes` CLI, both of which the App Sandbox forbids. This is why Scarf is distributed via GitHub Releases + Sparkle rather than the Mac App Store.
- ScarfGo on iOS runs inside the standard iOS app sandbox — no special entitlements beyond Keychain access for the SSH key.
## Children's privacy ## Children's privacy
@@ -69,7 +70,7 @@ Neither app is directed at children under 13 and we do not knowingly collect any
Because we don't collect any data on developer-controlled servers, there is nothing for you to opt out of, request deletion of, or export. To remove all app-stored data from your device: Because we don't collect any data on developer-controlled servers, there is nothing for you to opt out of, request deletion of, or export. To remove all app-stored data from your device:
- **ScarfGo**: delete the app. iOS purges the Keychain group + app container. - **ScarfGo**: delete the app. iOS purges the Keychain group + app container.
- **Scarf**: delete the app and the `~/Library/Containers/com.scarf` directory (the app is sandboxed; this is the only on-disk data). - **Scarf**: delete `Scarf.app` from `/Applications`, then optionally remove `~/Library/Caches/scarf/` (remote SQLite snapshots), `~/Library/Preferences/com.scarf.app.plist` (server registry + preferences), and `~/Library/Application Support/com.scarf/` (skill snapshots). See [Uninstalling](Uninstalling) for the full cleanup.
Your Hermes host's data (`~/.hermes/`) stays untouched — that's yours to manage. Your Hermes host's data (`~/.hermes/`) stays untouched — that's yours to manage.
+6 -3
@@ -24,7 +24,9 @@ The full schema is documented in [`scarf/docs/DASHBOARD_SCHEMA.md`](https://gith
**Adding a project:** click + in the Projects sidebar, pick a directory. The project is registered in `~/.hermes/scarf/projects.json`; the dashboard JSON lives in `<project>/.scarf/dashboard.json` (which you should add to your project's `.gitignore` if it's user-specific). **Adding a project:** click + in the Projects sidebar, pick a directory. The project is registered in `~/.hermes/scarf/projects.json`; the dashboard JSON lives in `<project>/.scarf/dashboard.json` (which you should add to your project's `.gitignore` if it's user-specific).
**Sharing a project:** as of v2.2.0, projects can be packaged into `.scarftemplate` bundles and shared with anyone — see [Project Templates](Project-Templates). Export turns a live project into a redistributable bundle; install unpacks one and sets up the dashboard, skills, cron jobs, and configuration schema in a single preview-and-confirm step. The public catalog lives at [awizemann.github.io/scarf/templates/](https://awizemann.github.io/scarf/templates/). **Per-project tabs** _(v2.3+, v2.5)_: clicking a project row reveals a tabbed detail view — **Dashboard**, **Sessions**, **Site** (when the dashboard has a webview widget), and **Slash Commands** (v2.5). The Sessions tab lists chats attributed to the project; **New Chat** spawns `hermes acp` with the project's directory as the session cwd and writes a Scarf-managed block into `<project>/AGENTS.md` so the agent boots with project context. Attribution survives across Mac and ScarfGo via the shared `SessionAttributionService`. See [Slash Commands](Slash-Commands) for the per-project authoring tab.
**Sharing a project:** as of v2.2.0, projects can be packaged into `.scarftemplate` bundles and shared with anyone — see [Project Templates](Project-Templates). Export turns a live project into a redistributable bundle; install unpacks one and sets up the dashboard, skills, cron jobs, configuration schema, and (in v2.5+) project-scoped slash commands in a single preview-and-confirm step. The public catalog lives at [awizemann.github.io/scarf/templates/](https://awizemann.github.io/scarf/templates/).
## Profiles ## Profiles
@@ -41,10 +43,11 @@ Profiles live under `~/.hermes/profiles/`. The currently active profile is whate
## Related pages ## Related pages
- [Project Templates](Project-Templates) — `.scarftemplate` bundles, the install / export / author flows, the public catalog. - [Project Templates](Project-Templates) — `.scarftemplate` bundles (schemaVersion 3 in v2.5), the install / export / author flows, the public catalog.
- [Slash Commands](Slash-Commands) — project-scoped slash commands authored in the per-project Slash Commands tab.
- [Hermes Paths](Hermes-Paths) — `~/.hermes/profiles/` and the projects registry. - [Hermes Paths](Hermes-Paths) — `~/.hermes/profiles/` and the projects registry.
- [Memory & Skills](Memory-and-Skills) — memory is profile-scoped. - [Memory & Skills](Memory-and-Skills) — memory is profile-scoped.
- [Settings](Gateway-Cron-Health-Logs) — exposes "Backup & Restore" buttons (`hermes backup` / `hermes import`) at the profile level. - [Settings](Gateway-Cron-Health-Logs) — exposes "Backup & Restore" buttons (`hermes backup` / `hermes import`) at the profile level.
--- ---
_Last updated: 2026-04-23 — Scarf v2.2.0_ _Last updated: 2026-04-25 — Scarf v2.5.0 (per-project Sessions / Site / Slash Commands tabs + AGENTS.md context handoff)_
+17 -2
@@ -1,6 +1,8 @@
# Release Process # Release Process
Scarf releases are produced by a single local script: [`scripts/release.sh`](https://github.com/awizemann/scarf/blob/main/scripts/release.sh) in the main repo. **The script is the source of truth** — this page is a public-facing summary; do not duplicate prerequisites or step-by-step internals here. > **Two release tracks as of v2.5.** The Mac app ships through GitHub Releases + Sparkle (this page). ScarfGo (iOS) ships through TestFlight / App Store Connect — see [`releases/v<version>/TESTFLIGHT_CHECKLIST.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/TESTFLIGHT_CHECKLIST.md) and [`APP_STORE_METADATA.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/APP_STORE_METADATA.md). The two tracks are independent and don't share a single command — they share the version number by convention.
Mac releases are produced by a single local script: [`scripts/release.sh`](https://github.com/awizemann/scarf/blob/main/scripts/release.sh) in the main repo. **The script is the source of truth** — this page is a public-facing summary; do not duplicate prerequisites or step-by-step internals here.
## Modes ## Modes
@@ -41,6 +43,19 @@ Releases are EdDSA-signed by Sparkle. The private key lives in the user's macOS
- Bump the **Latest release** line on [Home](Home). - Bump the **Latest release** line on [Home](Home).
- Append the new version to [Release Notes Index](Release-Notes-Index). - Append the new version to [Release Notes Index](Release-Notes-Index).
- If the version includes ScarfGo changes: separately archive + upload via Xcode Organizer per [`TESTFLIGHT_CHECKLIST.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/TESTFLIGHT_CHECKLIST.md). The Mac release script doesn't touch iOS.
## iOS release flow (separate from `release.sh`)
ScarfGo ships through:
1. **Xcode → Product → Archive** for the `scarf mobile` scheme (Any iOS Device destination).
2. **Organizer → Distribute App → App Store Connect → Upload** — automatic re-sign.
3. App Store Connect processes the binary (~515 min). Once ready, add it to a TestFlight group + submit for **Beta App Review** (2448h queue).
4. After Beta Review approval, the public TestFlight URL ([testflight.apple.com/join/qCrRpcTz](https://testflight.apple.com/join/qCrRpcTz)) accepts new joiners. Until then it shows "not accepting new testers."
5. Public App Store submission is a separate review (2472h) using the same processed build — see [`APP_STORE_METADATA.md`](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/APP_STORE_METADATA.md) for the description, keywords, support URL, and privacy URL fields.
The iOS `MARKETING_VERSION` should match the Mac `MARKETING_VERSION` for the same release; the iOS `CURRENT_PROJECT_VERSION` (build number) increments independently per Apple's monotonic-build-number rule. There's no automation for iOS bumping yet — manual edit in the Xcode target before archiving.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added iOS release flow + cross-link to TestFlight + App Store metadata files)_
+7 -6
@@ -11,12 +11,12 @@ What's next for Scarf. Public, opinionated, subject to change. The internal vers
## Near-term (2.6 candidates) ## Near-term (2.6 candidates)
- **iOS cron editor.** Add / remove / toggle cron jobs from the phone. Data model is already shared; just needs the editor sheet. - **iOS cron editor.** Add / remove / toggle cron jobs from the phone. Data model is already shared; just needs the editor sheet.
- **iOS scoped Settings editor.** The codebase already has the `hermes config set` shell-out wired for 7 keys; needs UX work + the curated whitelist before exposing. - ~~**iOS scoped Settings editor.**~~ ✅ Shipped in v2.5 — Quick Edits sheet covers 7 commonly-changed keys via `hermes config set`. Arbitrary-key editing is the v2.6+ stretch.
- **iOS push notifications, lit up.** Three things need to happen together: enable the Push Notifications capability in the Xcode target, ship a Hermes-side push sender, flip `apnsEnabled = true`. Skeleton + lock-screen action category are already in place. - **iOS push notifications, lit up.** Three things need to happen together: enable the Push Notifications capability in the Xcode target, ship a Hermes-side push sender, flip `NotificationRouter.apnsEnabled = true`. Skeleton + lock-screen "Approve / Deny" action category are already in place.
- **iPad layout pass.** `.tabViewStyle(.sidebarAdaptable)` is wired; needs verification + a target-flag flip. - **iPad layout pass.** v2.5 ships iPhone-only (`TARGETED_DEVICE_FAMILY = 1`, Catalyst + Designed-for-iPad disabled). `.tabViewStyle(.sidebarAdaptable)` is wired in the view layer; flipping the target flag and verifying is the bulk of the work.
- **More MCP presets.** The curated list grows as MCP ecosystem matures. - **More MCP presets.** The curated list grows as MCP ecosystem matures.
- **Mermaid diagrams in the wiki.** Architecture pages get a lot of value from one good diagram. - **Mermaid diagrams in the wiki.** Architecture pages get a lot of value from one good diagram.
- **Per-project FSEvents on remote.** Remote currently has one global mtime-poll loop ([HermesFileWatcher](Core-Services) line 84 has a TODO); per-project paths would reduce remote chattiness. - **Per-project FSEvents on remote.** Remote currently has one global mtime-poll loop ([HermesFileWatcher](Core-Services) has a TODO); per-project paths would reduce remote chattiness.
## Medium-term ## Medium-term
@@ -35,10 +35,11 @@ What's next for Scarf. Public, opinionated, subject to change. The internal vers
## What we're NOT doing ## What we're NOT doing
- **A web version of Scarf.** The whole point is being a native macOS app close to the metal. - **A web version of Scarf.** The whole point is being native macOS app on the desktop, iPhone app on mobile, both close to the metal.
- **Background sync / push notifications.** Scarf is a viewer; Hermes runs the agent. - **Background sync.** Scarf is a viewer; Hermes runs the agent. Pull happens when you open a tab, not in the background. (Push notifications, when Hermes ships a sender, are an *event* surface — they alert; they don't sync.)
- **Bundled Hermes installer.** Hermes installation belongs in Hermes-land. - **Bundled Hermes installer.** Hermes installation belongs in Hermes-land.
- **Closed-source / paid tier.** MIT-licensed, free, will stay that way. - **Closed-source / paid tier.** MIT-licensed, free, will stay that way.
- **Local Hermes runtime on iOS.** Hermes is Python; iOS doesn't sandbox Python runtimes practically. ScarfGo will always be a thin client over SSH.
## Suggesting features ## Suggesting features
+3 -3
@@ -31,7 +31,7 @@ Tap **Add Server**. Fill in:
- **User** — the SSH user. Often the same login you `ssh user@host` with from your terminal. - **User** — the SSH user. Often the same login you `ssh user@host` with from your terminal.
- **Port** — defaults to 22. Override if your host uses a non-standard SSH port. - **Port** — defaults to 22. Override if your host uses a non-standard SSH port.
- **Nickname (optional)** — display name in ScarfGo. Defaults to `user@host`. - **Nickname (optional)** — display name in ScarfGo. Defaults to `user@host`.
- **Hermes binary hint (optional)** — leave empty unless you know `hermes` is at an unusual path. ScarfGo prepends `~/.local/bin`, `/opt/homebrew/bin`, and `/usr/local/bin` to PATH automatically — those four cover ~95% of pipx + Homebrew installs. - **Hermes binary hint (optional)** — leave empty unless you know `hermes` is at an unusual path. ScarfGo prepends `~/.local/bin`, `/opt/homebrew/bin`, and `/usr/local/bin` to PATH automatically — those three cover ~95% of pipx + Homebrew installs. If your `hermes` lives elsewhere (custom virtualenv, system-managed install dir like `~/.hermes/bin`), set the hint to its absolute path.
Tap **Next**. Tap **Next**.
@@ -100,7 +100,7 @@ Citadel's raw exec channel doesn't source the user's shell rc files (`.bashrc`,
2. In ScarfGo: **Servers → tap the server → Edit → Hermes binary hint** → paste the absolute path (e.g. `/opt/scarf-tools/bin/hermes`). 2. In ScarfGo: **Servers → tap the server → Edit → Hermes binary hint** → paste the absolute path (e.g. `/opt/scarf-tools/bin/hermes`).
3. Re-test the connection. 3. Re-test the connection.
The `~/.hermes/bin/hermes` location is also auto-probed (some self-install layouts put it there), so set the hint only if your install is somewhere none of the four candidates cover. The inline PATH covers `~/.local/bin`, `/opt/homebrew/bin`, and `/usr/local/bin`. Anything else — including `~/.hermes/bin` self-install layouts — needs the binary-hint override.
### "Connection refused" / "Connection timed out" ### "Connection refused" / "Connection timed out"
@@ -156,4 +156,4 @@ Full policy: [Privacy Policy](Privacy-Policy).
- [Support](Support) — bug reports, feature requests, security disclosures. - [Support](Support) — bug reports, feature requests, security disclosures.
--- ---
_Last updated: 2026-04-25 — Scarf v2.5.0_ _Last updated: 2026-04-25 — Scarf v2.5.0 (audit pass: corrected PATH-prefix candidate count + `~/.hermes/bin` claim)_
+15 -6
@@ -11,7 +11,7 @@ ScarfGo is the on-the-go iPhone companion to [Scarf](Home). Its scope is deliber
- **Transport:** pure-Swift SSH via [Citadel](https://github.com/orlandos-nl/Citadel) 0.12.x — no OpenSSH client subprocess (iOS sandbox). - **Transport:** pure-Swift SSH via [Citadel](https://github.com/orlandos-nl/Citadel) 0.12.x — no OpenSSH client subprocess (iOS sandbox).
- **Shared core:** `ScarfCore` SPM package — Models / Transport / Services / ViewModels portable across macOS and iOS, unit-tested on Linux in CI. - **Shared core:** `ScarfCore` SPM package — Models / Transport / Services / ViewModels portable across macOS and iOS, unit-tested on Linux in CI.
- **iOS-only code:** `ScarfIOS` package (Citadel glue, Keychain key storage) + `Scarf iOS/` SwiftUI views. - **iOS-only code:** `ScarfIOS` package (Citadel glue, Keychain key storage) + `Scarf iOS/` SwiftUI views.
- **Target:** iPhone, iOS 18+. iPad / macCatalyst deferred. - **Target:** iPhone-only, iOS 18+. v2.5 ships with `TARGETED_DEVICE_FAMILY = 1`, `SUPPORTS_MACCATALYST = NO`, `SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO`, `SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO`. iPad and Catalyst flags are explicitly OFF until layout polish lands (M10+).
## What's shipped today (M6) ## What's shipped today (M6)
@@ -62,9 +62,18 @@ Features that only make sense on mobile, in priority order:
5. **Lock-screen quick-approve.** Notification action button for "Approve" / "Deny" on pending permissions so the agent keeps running while you're away from the app. 5. **Lock-screen quick-approve.** Notification action button for "Approve" / "Deny" on pending permissions so the agent keeps running while you're away from the app.
6. **Scoped Settings editor.** Not a generic YAML round-trip editor — a curated set of high-value fields (model / provider / approval mode / max turns / display toggles) that save via `hermes config set <key> <value>` over SSH exec. Hermes owns the YAML round-trip; Scarf just picks values. 6. **Scoped Settings editor.** Not a generic YAML round-trip editor — a curated set of high-value fields (model / provider / approval mode / max turns / display toggles) that save via `hermes config set <key> <value>` over SSH exec. Hermes owns the YAML round-trip; Scarf just picks values.
### M10 — TestFlight ### M10 — TestFlight ✅ Shipped (v2.5)
App Store Connect + provisioning profile + internal TestFlight group of 1 (Alan). Public TestFlight after 23 real sessions on a real iPhone without crashes. App Store Connect + Apple Distribution cert + Apple Developer Program enrollment + privacy policy live at `awizemann.github.io/scarf/privacy/`. Public TestFlight live at <https://testflight.apple.com/join/qCrRpcTz> — accepts new joiners after Apple's Beta Review approves the first build (2448h queue). See [TESTFLIGHT_CHECKLIST.md](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/TESTFLIGHT_CHECKLIST.md) for the submission flow + [APP_STORE_METADATA.md](https://github.com/awizemann/scarf/blob/main/releases/v2.5.0/APP_STORE_METADATA.md) for the public App Store metadata bundle (description, keywords, support URL, etc.) staged for the eventual public release.
### M11+ — Post-TestFlight feedback loop
- Iterate on TestFlight feedback over v2.5.x patches.
- iPad layout polish (flip device family flag + verify).
- Cron editor on iOS — adds the editor sheet that's missing in v2.5.
- iOS localization — translate the strings the Mac already has.
- Push notifications — flip the capability + deploy Hermes-side push sender.
- Deeper Insights / Activity views, scaled to phone screen sizes.
## Known Issues ## Known Issues
@@ -78,7 +87,7 @@ All tracked from the 2026-04-24 pass-1 smoke test. This list is the truth about
| 2 | **Non-retryable provider errors → perpetual spinner.** ACP error triplet (`acpError`, `acpErrorHint`, `acpErrorDetails`) promoted to ScarfCore so Mac + ScarfGo share state; ChatView renders an inline banner with Copy Details / Expand. `handlePromptComplete` now calls `recordPromptStopFailureUsingProvider(stopReason:)` on non-`end_turn` stops with the stderr tail appended. | Cross-platform | ✅ Fixed | | 2 | **Non-retryable provider errors → perpetual spinner.** ACP error triplet (`acpError`, `acpErrorHint`, `acpErrorDetails`) promoted to ScarfCore so Mac + ScarfGo share state; ChatView renders an inline banner with Copy Details / Expand. `handlePromptComplete` now calls `recordPromptStopFailureUsingProvider(stopReason:)` on non-`end_turn` stops with the stderr tail appended. | Cross-platform | ✅ Fixed |
| 3 | **No connecting feedback when entering Chat.** ChatController's existing `.connecting` state now drives a `.regularMaterial` overlay with "Connecting to <nickname>…" + ProgressView. | ScarfGo | ✅ Fixed | | 3 | **No connecting feedback when entering Chat.** ChatController's existing `.connecting` state now drives a `.regularMaterial` overlay with "Connecting to <nickname>…" + ProgressView. | ScarfGo | ✅ Fixed |
| 4 | **`isAgentWorking` doesn't clear after primary response.** Split into computed `isGenerating` (agent still producing text) + `isPostProcessing` (agent done producing; ACP `promptComplete` not yet fired). Prominent spinner drops as soon as the reply is visible; subtle "Finishing up…" pill covers auxiliary post-work. Applied cross-platform. | Cross-platform | ✅ Fixed | | 4 | **`isAgentWorking` doesn't clear after primary response.** Split into computed `isGenerating` (agent still producing text) + `isPostProcessing` (agent done producing; ACP `promptComplete` not yet fired). Prominent spinner drops as soon as the reply is visible; subtle "Finishing up…" pill covers auxiliary post-work. Applied cross-platform. | Cross-platform | ✅ Fixed |
| 5 | **ACP command missing PATH prefix.** SSH exec runs a non-interactive shell whose PATH is `/usr/bin:/bin:/usr/sbin:/sbin`. Fixed by prepending common install locations (`~/.local/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `~/.hermes/bin`) to PATH inline in the exec command. Mirrors `HermesPathSet.hermesBinaryCandidates`. | ScarfGo | ✅ Fixed | | 5 | **ACP command missing PATH prefix.** SSH exec runs a non-interactive shell whose PATH is `/usr/bin:/bin:/usr/sbin:/sbin`. Fixed by prepending the three most common pipx + Homebrew install locations (`~/.local/bin`, `/opt/homebrew/bin`, `/usr/local/bin`) to PATH inline on every Citadel `runProcess` and `SSHExecACPChannel` invocation. Self-install layouts at `~/.hermes/bin` need the per-server **Hermes binary hint** override. | ScarfGo | ✅ Fixed |
| 6 | **SFTP `~` tilde not expanded.** Per-connection cached `resolveHome()` on `ConnectionHolder` + `resolveSFTPPath()` helper applied to every SFTP entry point (`readFile` / `writeFile` / `fileExists` / `stat` / `listDirectory` / `createDirectory` / `removeFile`). | ScarfGo | ✅ Fixed | | 6 | **SFTP `~` tilde not expanded.** Per-connection cached `resolveHome()` on `ConnectionHolder` + `resolveSFTPPath()` helper applied to every SFTP entry point (`readFile` / `writeFile` / `fileExists` / `stat` / `listDirectory` / `createDirectory` / `removeFile`). | ScarfGo | ✅ Fixed |
| 7 | **No loading state on Memory editor.** Switched to throwing read (#8) so `lastError` populates on real failures instead of silently showing "empty" — the existing error banner now renders. | ScarfGo | ✅ Fixed | | 7 | **No loading state on Memory editor.** Switched to throwing read (#8) so `lastError` populates on real failures instead of silently showing "empty" — the existing error banner now renders. | ScarfGo | ✅ Fixed |
| 8 | **`ServerContext.readText` swallows errors.** New `readTextThrowing(_:)` distinguishes "file absent" from "transport error"; old nil-returning `readText` stays as a `try?` shim for callers that really don't care. Memory editor uses the throwing variant. | Cross-platform | ✅ Fixed | | 8 | **`ServerContext.readText` swallows errors.** New `readTextThrowing(_:)` distinguishes "file absent" from "transport error"; old nil-returning `readText` stays as a `try?` shim for callers that really don't care. Memory editor uses the throwing variant. | Cross-platform | ✅ Fixed |
@@ -123,7 +132,7 @@ The pass-1 session also surfaced the user-facing roadmap we delivered through M8
## For contributors ## For contributors
See [Architecture Overview](Architecture-Overview) for how ScarfCore + ScarfIOS fit together. The `scarf-mobile-development` branch is the working branch; the M7/M8/M9 changes above live there pending a pass-2 smoke test before merging to `main`. See [Architecture Overview](Architecture-Overview) for how ScarfCore + ScarfIOS fit together, [ScarfCore Package](ScarfCore-Package) for the package boundaries, and [Transport Layer](Transport-Layer) for the Citadel transport details. M6 → M10 are all merged to `main` and shipped as part of Scarf v2.5.
--- ---
_Last updated: 2026-04-24post M7/M8/M9 implementation, pass-2 pending_ _Last updated: 2026-04-25Scarf v2.5.0 (M10 TestFlight shipped; iPhone-only target settings; PATH-prefix correction)_
+3 -3
@@ -41,7 +41,7 @@ Onboarding details:
| **Memory** | Read + edit `MEMORY.md` and `USER.md`. The "Saved" pill survives keyboard dismissal; Revert undoes unsaved edits. | | **Memory** | Read + edit `MEMORY.md` and `USER.md`. The "Saved" pill survives keyboard dismissal; Revert undoes unsaved edits. |
| **Cron** | List view of `~/.hermes/cron/jobs.json` with **human-readable schedules** ("Every 6 hours", "Weekdays at 09:00") and a relative next-run ("in 4 hours"). Read-only in v1 — editing comes later. | | **Cron** | List view of `~/.hermes/cron/jobs.json` with **human-readable schedules** ("Every 6 hours", "Weekdays at 09:00") and a relative next-run ("in 4 hours"). Read-only in v1 — editing comes later. |
| **Skills** | Browse the skills tree from `~/.hermes/skills/`. Read-only. | | **Skills** | Browse the skills tree from `~/.hermes/skills/`. Read-only. |
| **Settings** | Read-only view of `config.yaml`. An in-app editor is planned but not in v1 — see [Platform Differences](Platform-Differences). | | **Settings** | Read view of full `config.yaml` plus a **Quick Edits** section that flips 7 commonly-changed keys (`model.default`, `model.provider`, `agent.approval_mode`, `agent.max_turns`, `display.show_cost`, `display.show_reasoning`, `display.streaming`) via `hermes config set` on the remote. Other keys remain read-only — edit from the Mac app or a remote shell. |
| **Slash commands** _(v2.5)_ | Read-only browser of project-scoped slash commands shipped via `<project>/.scarf/slash-commands/`. Tap a row to see the expanded prompt with a sample-argument field. Authoring is Mac-only in v1. See [Slash Commands](Slash-Commands). | | **Slash commands** _(v2.5)_ | Read-only browser of project-scoped slash commands shipped via `<project>/.scarf/slash-commands/`. Tap a row to see the expanded prompt with a sample-argument field. Authoring is Mac-only in v1. See [Slash Commands](Slash-Commands). |
## Project-scoped chat ## Project-scoped chat
@@ -61,7 +61,7 @@ If the SFTP write fails (permissions, disk full, network drop), ScarfGo surfaces
- **No local mode.** ScarfGo only operates against an SSH-reachable Hermes host. There's no local Hermes runtime on iOS. - **No local mode.** ScarfGo only operates against an SSH-reachable Hermes host. There's no local Hermes runtime on iOS.
- **No push notifications yet.** The skeleton (UNNotificationCenter delegate, "Approve / Deny" action category) ships in the binary but is gated behind an internal feature flag because: (a) the Push Notifications capability is not yet enabled in the Xcode target, (b) Hermes doesn't yet have a push sender. When both land, push lights up on the iOS side without an app update — well, with one update to flip the flag. Watch this page. - **No push notifications yet.** The skeleton (UNNotificationCenter delegate, "Approve / Deny" action category) ships in the binary but is gated behind an internal feature flag because: (a) the Push Notifications capability is not yet enabled in the Xcode target, (b) Hermes doesn't yet have a push sender. When both land, push lights up on the iOS side without an app update — well, with one update to flip the flag. Watch this page.
- **No in-app config editor.** Settings is read-only in v1. Use the Mac app or a remote shell to change values. - **Limited config editor.** Settings on iOS surfaces a 7-key **Quick Edits** sheet that shells out to `hermes config set`; the rest of `config.yaml` stays read-only. Editing arbitrary keys still belongs on the Mac app or a remote shell.
- **No template install UI.** `.scarftemplate` install + uninstall is Mac-only in v1. - **No template install UI.** `.scarftemplate` install + uninstall is Mac-only in v1.
- **No terminal mode.** Rich-chat (ACP) only. - **No terminal mode.** Rich-chat (ACP) only.
- **English only.** The Mac app ships in 7 languages; ScarfGo is English-only for v1. - **English only.** The Mac app ships in 7 languages; ScarfGo is English-only for v1.
@@ -113,4 +113,4 @@ A: [ScarfGo Roadmap](ScarfGo-Roadmap) tracks shipped milestones (M6 / M7 / M8 /
--- ---
_Last updated: 2026-04-25 — v2.5 public TestFlight._ _Last updated: 2026-04-25 — v2.5 public TestFlight (corrected Settings — Quick Edits ship in v1)._
+4 -2
@@ -1,6 +1,8 @@
# Servers & Remote # Servers & Remote
Scarf 2.0 is multi-server. Each window binds to one Hermes install — your local `~/.hermes/` (synthesized automatically) or any number of remote SSH hosts. Server state lives in `~/Library/Preferences/com.scarf.app.plist` via the `ServerRegistry`. > **Adding a server on iOS?** ScarfGo's Servers list works the same idea but with on-device key generation. See [ScarfGo Onboarding](ScarfGo-Onboarding) for the iPhone walkthrough. The rest of this page is the macOS Mac-app flow.
Scarf 2.0 is multi-server. Each Mac window binds to one Hermes install — your local `~/.hermes/` (synthesized automatically) or any number of remote SSH hosts. Server state lives in `~/Library/Preferences/com.scarf.app.plist` via the `ServerRegistry`. ScarfGo (iOS) uses a single-window TabView; switching servers from its Servers list rebuilds the tab root against the new context.
## Adding a remote server ## Adding a remote server
@@ -54,4 +56,4 @@ See [Keyboard Shortcuts](Keyboard-Shortcuts).
- [Hermes Paths](Hermes-Paths) for what each remote file is. - [Hermes Paths](Hermes-Paths) for what each remote file is.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added ScarfGo cross-references)_
+18 -6
@@ -16,7 +16,7 @@ Each Scarf window has its **own** `AppCoordinator` — selection in one window d
## SidebarSection ## SidebarSection
`SidebarSection` is the source of truth for every sidebar item. Each case has a `rawValue` (display name) and an `icon` (SF Symbol name). The 23 cases are grouped into four sidebar headers: `SidebarSection` ([`AppCoordinator.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/AppCoordinator.swift)) is the source of truth for every sidebar item. Each case has a `rawValue` (display name) and an `icon` (SF Symbol name). **22 cases** grouped into **5 sidebar headers** (the order is hardcoded in `SidebarView.swift`):
### Monitor (4) ### Monitor (4)
@@ -27,6 +27,14 @@ Each Scarf window has its **own** `AppCoordinator` — selection in one window d
| Sessions | `bubble.left.and.bubble.right` | | Sessions | `bubble.left.and.bubble.right` |
| Activity | `bolt.horizontal` | | Activity | `bolt.horizontal` |
### Projects (1)
| Section | Icon |
|---|---|
| Projects | `square.grid.2x2` |
Projects has its own header — not a Manage sub-item — because per-project work (Dashboard / Sessions / Site / Slash Commands tabs, template install) is a top-level workflow surface in v2.5.
### Interact (3) ### Interact (3)
| Section | Icon | | Section | Icon |
@@ -47,19 +55,20 @@ Each Scarf window has its **own** `AppCoordinator` — selection in one window d
| Webhooks | `arrow.up.right.square` | | Webhooks | `arrow.up.right.square` |
| Profiles | `person.2.crop.square.stack` | | Profiles | `person.2.crop.square.stack` |
### Manage (8) ### Manage (7)
| Section | Icon | | Section | Icon |
|---|---| |---|---|
| Projects | `square.grid.2x2` |
| Tools | `wrench.and.screwdriver` | | Tools | `wrench.and.screwdriver` |
| MCP Servers | `puzzlepiece.extension` | | MCP Servers | `puzzlepiece.extension` |
| Gateway | `antenna.radiowaves.left.and.right` | | Messaging Gateway | `antenna.radiowaves.left.and.right` |
| Cron | `clock.arrow.2.circlepath` | | Cron | `clock.arrow.2.circlepath` |
| Health | `stethoscope` | | Health | `stethoscope` |
| Logs | `doc.text` | | Logs | `doc.text` |
| Settings | `gearshape` | | Settings | `gearshape` |
The Gateway item's `displayName` is **"Messaging Gateway"** — disambiguates from the v2.3 Tool Gateway (Nous Portal subscription routing) which is a Health-tab surface, not its own sidebar item. The enum case is still `.gateway` and the persisted state file path (`~/.hermes/gateway_state.json`) is unchanged.
## SidebarView ## SidebarView
[`SidebarView.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/SidebarView.swift) is a `List` with hardcoded `Section` headers. Selection is two-way bound to `coordinator.selectedSection`: [`SidebarView.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Navigation/SidebarView.swift) is a `List` with hardcoded `Section` headers. Selection is two-way bound to `coordinator.selectedSection`:
@@ -67,6 +76,7 @@ Each Scarf window has its **own** `AppCoordinator` — selection in one window d
```swift ```swift
List(selection: $coordinator.selectedSection) { List(selection: $coordinator.selectedSection) {
Section("Monitor") { … } Section("Monitor") { … }
Section("Projects") { … }
Section("Interact") { … } Section("Interact") { … }
Section("Configure") { … } Section("Configure") { … }
Section("Manage") { … } Section("Manage") { … }
@@ -86,9 +96,11 @@ Each window is bound to one `ServerContext` and one `AppCoordinator`. The window
## ScarfGo (iOS) navigation ## ScarfGo (iOS) navigation
ScarfGo uses a different model — a 5-tab `TabView` rather than a sidebar. The tabs (Dashboard | Projects | Chat | Skills | System) are wrapped in their own `NavigationStack`s so push navigation (Cron editor, Memory detail, Project detail, Settings) stays scoped to the tab. Cross-tab signalling (Dashboard row → Chat tab resume, Project Detail → in-project chat handoff, notification deep-link → Chat) flows through `ScarfGoCoordinator`. `.tabViewStyle(.sidebarAdaptable)` automatically switches to a sidebar layout on iPad / Mac Catalyst — no separate code path. ScarfGo uses a different model — a 5-tab `TabView` rather than a sidebar. The tabs (Dashboard | Projects | Chat | Skills | System) are wrapped in their own `NavigationStack`s so push navigation (Cron editor, Memory detail, Project detail, Settings) stays scoped to the tab. Cross-tab signalling (Dashboard row → Chat tab resume, Project Detail → in-project chat handoff, notification deep-link → Chat) flows through `ScarfGoCoordinator`.
`.tabViewStyle(.sidebarAdaptable)` is wired so the system can switch to a sidebar layout on larger devices — but as of v2.5 the iOS target ships **iPhone-only** (`TARGETED_DEVICE_FAMILY = 1`, `SUPPORTS_MACCATALYST = NO`, `SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO`). iPad polish is on the [ScarfGo Roadmap](ScarfGo-Roadmap) but not in scope for v2.5.
The Mac sidebar's "System" / advanced sections collapse into the iOS **System** tab (server identity, Memory link, Cron link, Settings link, Disconnect / Forget). See [Platform Differences](Platform-Differences) for the full Mac↔iOS feature matrix. The Mac sidebar's "System" / advanced sections collapse into the iOS **System** tab (server identity, Memory link, Cron link, Settings link, Disconnect / Forget). See [Platform Differences](Platform-Differences) for the full Mac↔iOS feature matrix.
--- ---
_Last updated: 2026-04-25 — Scarf v2.5.0 (ScarfGo 5-tab nav added)_ _Last updated: 2026-04-25 — Scarf v2.5.0 (audit pass: 5 sidebar groups not 4, 22 cases not 23, Projects own group, Messaging Gateway display name, iPhone-only iOS target)_
+61 -25
@@ -1,52 +1,88 @@
# Testing # Testing
**Honest disclosure:** Scarf's test coverage is minimal. The `scarfTests/` and `scarfUITests/` targets exist but contain placeholder tests only. The project has historically relied on dogfooding — the maintainer runs Scarf against their own daily Hermes install and against a remote dogfooding host. The bulk of Scarf's automated coverage lives in the **ScarfCore** SwiftPM package — 14 test suites covering the iOS-port milestones (M0M9) plus dedicated parsers, services, and view-models. The Mac target has 10 additional test suites for Mac-specific surfaces (Tool Gateway, ProjectsViewModel, NousAuthFlow, template config, etc.). UI tests are placeholder-only on both targets; dogfooding fills the gap.
This page documents what's in place and where contributions would help most.
## Frameworks ## Frameworks
When tests are added, the standard is **Swift Testing** (`@Suite` / `@Test` macros), not XCTest. Per the project conventions ([CLAUDE.md](https://github.com/awizemann/scarf/blob/main/CLAUDE.md)): **Swift Testing** (`@Suite` / `@Test` macros) for all new tests, not XCTest. Per [CLAUDE.md](https://github.com/awizemann/scarf/blob/main/CLAUDE.md):
- Use `@Suite` and `@Test` macros for all new tests. - Use `@Suite` and `@Test` macros.
- Protocol-oriented services for testability — the `ServerTransport` protocol is the obvious mocking seam. - Protocol-oriented services for testability — `ServerTransport` is the mocking seam, with `MockTransport` already in the ScarfCore test target.
- No timing-dependent tests: use polling with early exit, not `Task.sleep` + assertion. - No timing-dependent tests: use polling with early exit, not `Task.sleep` + assertion.
- Singleton state isolation: call cleanup methods + `await Task.yield()` before assertions. - Singleton state isolation: call cleanup methods + `await Task.yield()` before assertions.
- No `print()` in production code — use `os.Logger`. `print()` is fine in `#Preview` and test helpers. - Cross-suite state contention (e.g. `ServerContext.sshTransportFactory`) goes into a single `.serialized` suite — see `M3TransportTests.swift`. v2.5's test-reliability pass consolidated every factory-touching test there to fix flakes.
## Running ## Running
ScarfCore (the main coverage):
```bash
swift test --package-path scarf/Packages/ScarfCore
```
Mac target:
```bash ```bash
xcodebuild test -project scarf/scarf.xcodeproj -scheme scarf xcodebuild test -project scarf/scarf.xcodeproj -scheme scarf
``` ```
Or in Xcode: ⌘U. Or in Xcode: ⌘U with the matching scheme selected.
## What would be high-value to add ## ScarfCore test inventory ([`scarf/Packages/ScarfCore/Tests/ScarfCoreTests/`](https://github.com/awizemann/scarf/tree/main/scarf/Packages/ScarfCore/Tests/ScarfCoreTests))
If you're looking for a contribution, these are the gaps that would matter most: | Suite | Covers |
|---|---|
| `ScarfCoreSmokeTests` | Module imports, public surface visibility. |
| `M0bTransportTests` | `LocalTransport` + `SSHTransport` protocol contract, atomic-write semantics, error classification. |
| `M0cServicesTests` | `HermesFileService`, `HermesEnvService`, `HermesDataService` (against fixture DBs), `ProjectDashboardService`. |
| `M0dViewModelsTests` | `IOSDashboardViewModel`, `IOSSettingsViewModel`, etc. — view-model state machines without UI. |
| `M1ACPTests` | `ACPClient` JSON-RPC framing, event extraction, error-hint pattern matching. |
| `M2OnboardingTests` | iOS onboarding state machine, key generation, paste-import flow. |
| `M3TransportTests` | `.serialized` suite for cross-suite state contention; transport factory swaps. |
| `M4ACPIOSTests` | `SSHExecACPChannel` over a Citadel test fixture. |
| `M5FeatureVMTests` | iOS feature view-models — Memory, Cron, Skills. |
| `M6ConfigCronTests` | YAML config parsing, cron job round-trip, `HermesConfig` field mapping. |
| `M9SlashCommandTests` | Project-scoped slash command parsing, `{{argument}}` substitution, default fallback, AGENTS.md block extension. |
| `SkillsHubParserTests` | `hermes skills browse`/`search` Rich-table parser — every column shape, continuation rows, header skip. |
| `SkillFrontmatterParserTests` | SKILL.md YAML frontmatter parser (`allowed_tools`, `related_skills`, `dependencies`). |
| `CronScheduleFormatterTests` | Cron string → English translator across every recognized shape. |
1. **`ServerTransport` mock + `LocalTransport` smoke tests** — every service depends on transport, so a `MockTransport` unlocks unit-testing all of them. ## Mac target test inventory ([`scarf/scarfTests/`](https://github.com/awizemann/scarf/tree/main/scarf/scarfTests))
2. **`HermesEnvService` round-trip tests** — non-destructive .env editing has tricky comment / blank-line preservation; would benefit from regression coverage.
3. **`ACPClient` JSON-RPC framing tests** — feed canned JSON-RPC byte streams and assert events emitted. | Suite | Covers |
4. **`HermesPathSet` path-resolution tests** — local vs. remote home, binary hint precedence. |---|---|
5. **`HermesConfig` decoding tests** — load representative `config.yaml` fixtures and check field mapping. | `ToolGatewayTests` | Tool Gateway routing, `HERMES_OVERLAYS` mirror, Auxiliary tab behavior. |
6. **`SSHTransport` shell-quoting tests** — `shellQuote` and `remotePathArg` are correctness-critical and pure functions. | `ProjectAgentContextServiceTests` | AGENTS.md `<!-- scarf-project -->` block — idempotency, secret-safe field-name surfacing, preserve-around-block invariant. |
| `SessionAttributionServiceTests` | Sidecar JSON read/write, attribution lookup, refresh cadence. |
| `NousAuthFlowTests` | Device-code flow parser, subscription-required detection, billing URL extraction. |
| `ProjectsViewModelTests` | Mac project sidebar — folder grouping, archive, ⌘19 jumps, search. |
| `ProjectRegistryMigrationTests` | v2.2 → v2.3 registry migration. |
| `CredentialPoolsGatingTests` | Per-provider strategy + last-4 preview. |
| `TemplateConfigTests` | Schema validation (string/text/number/bool/enum/list/secret). |
| `ProjectTemplateTests` | `.scarftemplate` install, lock-file accounting, uninstall preserve-user-files invariant. |
| `scarfTests` | Misc. legacy. |
163 tests across 12 suites pre-2.5; v2.5 brought that to **179 tests across 13 suites** (per the release notes), with the slash-command test additions and the cross-suite race fixes.
## Manual verification flows ## Manual verification flows
For any change that touches behavior, here's the manual checklist the maintainer runs before tagging a release: For changes that touch UI or remote-host behavior, the maintainer runs:
- Open a local window — Dashboard loads, Sessions browser populates, Memory editor opens. - Open a local Mac window — Dashboard loads, Sessions browser populates, Memory editor opens.
- Open a remote window — same Dashboard / Sessions / Memory, but against the dogfooding host. - Open a remote Mac window — same Dashboard / Sessions / Memory, against the dogfooding host (`Mardon` Mac mini).
- Send a Rich Chat message — receives a streamed response, reasoning shows if the model emits it. - Open ScarfGo against the same host — Dashboard / Chat / Skills / System all populate; Browse Hub returns results (regression check on the Citadel `executeCommandStream` v2.5 fix).
- Edit and save a memory file — change appears in Hermes on next agent turn. - Send a Rich Chat message — streamed response, reasoning shows if the model emits it.
- Run a Cron job — appears in the Cron view, has correct delivery channel. - Edit and save a memory file — change appears in Hermes on next agent turn (Mac + iOS).
- Trigger `hermes memory reset` from the Memory toolbar — destructive confirm, reset runs, view refreshes.
- Run a Cron job — appears in Cron view, schedule renders as human-readable phrase.
- Toggle a tool in Tools — `hermes tools enable/disable` runs and the dot color updates. - Toggle a tool in Tools — `hermes tools enable/disable` runs and the dot color updates.
- iOS-specific: forget a server, re-onboard, verify Keychain wipe + re-creation; Settings → Quick Edits flips a value via `hermes config set`.
## Why so little automated coverage? ## What's still under-covered
The app is a thin GUI over Hermes — most behavior depends on (a) the OS file system, (b) SQLite, (c) SSH, (d) a long-running subprocess speaking JSON-RPC. Mocking these well is non-trivial and historically the cost has been higher than the bug rate justified. The transport protocol now makes it cheaper; the gap is finally worth filling. - **UI tests.** `scarfUITests/` and `Scarf iOSUITests/` are placeholder-only. Worth contributing if you have UI-test experience.
- **`HermesLogService.streamLines` remote tail.** Hard to mock SSH stream behavior cleanly; relies on the dogfooding flow today.
- **Sparkle update flow.** The auto-update path is exercised manually before each release (see [Release Process](Release-Process)).
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (179 tests, 13 suites; full inventory)_
+39 -6
@@ -1,10 +1,16 @@
# Transport Layer # Transport Layer
The `ServerTransport` protocol unifies local and SSH I/O. Services consume `transport.readFile(path)`, `transport.runProcess(...)`, `transport.snapshotSQLite(path)`, etc., without caring whether the bytes come from disk or the wire. Two implementations live under [`scarf/scarf/scarf/Core/Transport/`](https://github.com/awizemann/scarf/tree/main/scarf/scarf/Core/Transport): `LocalTransport` and `SSHTransport`. The `ServerTransport` protocol unifies local and SSH I/O. Services consume `transport.readFile(path)`, `transport.runProcess(...)`, `transport.snapshotSQLite(path)`, etc., without caring whether the bytes come from disk or the wire. Three implementations exist as of v2.5:
- **`LocalTransport`** — direct `FileManager` + `Process` against the local disk (`scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/LocalTransport.swift`).
- **`SSHTransport`** — OpenSSH-driven, multiplexed via ControlMaster. **Mac only**; iOS doesn't ship the `/usr/bin/ssh` binary (`scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/SSHTransport.swift`).
- **`CitadelServerTransport`** — pure-Swift SSH via Citadel + NIO. **iOS only**, used by ScarfGo for every remote primitive (`scarf/Packages/ScarfIOS/Sources/ScarfIOS/CitadelServerTransport.swift`).
All three implement the same protocol, so services in [ScarfCore](ScarfCore-Package) can consume any of them without `#if os(...)` shims.
## Protocol surface ## Protocol surface
[`ServerTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/ServerTransport.swift) exposes: [`ServerTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/ServerTransport.swift) exposes:
**Identity** **Identity**
- `contextID: ServerID` — UUID; namespaces caches under `~/Library/Caches/scarf/snapshots/<id>/`. - `contextID: ServerID` — UUID; namespaces caches under `~/Library/Caches/scarf/snapshots/<id>/`.
@@ -31,7 +37,7 @@ The `ServerTransport` protocol unifies local and SSH I/O. Services consume `tran
## Errors ## Errors
[`TransportErrors.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/TransportErrors.swift) defines `TransportError`: [`TransportErrors.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/TransportErrors.swift) defines `TransportError`:
| Case | Cause | | Case | Cause |
|---|---| |---|---|
@@ -47,7 +53,7 @@ Stderr-pattern classification turns raw `ssh` errors into the right case so the
## LocalTransport ## LocalTransport
[`LocalTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/LocalTransport.swift) — a thin wrapper around `FileManager`, `Process`, and `DispatchSourceFileSystemObject`. [`LocalTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/LocalTransport.swift) — a thin wrapper around `FileManager`, `Process`, and `DispatchSourceFileSystemObject`.
- **Atomic writes:** writes to `<path>.scarf.tmp`, sets `0600` if the filename suggests a secret, then `replaceItemAt` (existing) or `moveItem` (new). - **Atomic writes:** writes to `<path>.scarf.tmp`, sets `0600` if the filename suggests a secret, then `replaceItemAt` (existing) or `moveItem` (new).
- **Process timeout:** polls every 100ms until deadline; `terminate()` if exceeded. - **Process timeout:** polls every 100ms until deadline; `terminate()` if exceeded.
@@ -56,7 +62,7 @@ Stderr-pattern classification turns raw `ssh` errors into the right case so the
## SSHTransport ## SSHTransport
[`SSHTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Transport/SSHTransport.swift) — every primitive becomes an `ssh`/`scp`/`sftp` invocation, multiplexed over a single ControlMaster connection. [`SSHTransport.swift`](https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/SSHTransport.swift) — every primitive becomes an `ssh`/`scp`/`sftp` invocation, multiplexed over a single ControlMaster connection.
### ControlMaster pooling ### ControlMaster pooling
@@ -122,5 +128,32 @@ sqlite3 '/tmp/scarf-snapshot-XYZ.db' "PRAGMA journal_mode=DELETE;"
See [Servers & Remote](Servers-and-Remote) for setup and troubleshooting. See [Servers & Remote](Servers-and-Remote) for setup and troubleshooting.
## CitadelServerTransport (iOS, v2.5+)
The iOS app can't shell out to `/usr/bin/ssh` — there's no such binary in the iOS sandbox. Instead, ScarfGo drives [Citadel](https://github.com/orlandos-nl/Citadel), a pure-Swift SSH/SFTP/exec implementation built on SwiftNIO. `CitadelServerTransport` wraps it behind the same `ServerTransport` protocol so all of ScarfCore consumes one shape.
### What's shared with the Mac transports
- Same `readFile` / `writeFile` / `stat` / `listDirectory` / `runProcess` / `snapshotSQLite` / `watchPaths` API.
- Same `TransportError` classification (host unreachable, auth failed, command failed, etc.).
- Same atomic-write convention (`<path>.scarf.tmp` → SFTP `rename`).
- Same SQLite snapshot mechanics — `sqlite3 .backup` on the remote, SFTP-pull the snapshot, `PRAGMA journal_mode=DELETE` to strip WAL.
### What's iOS-specific
- **Pure-Swift exec channel.** Citadel's exec channel does the SSH wire protocol (RFC 4254) directly; there is no shelled-out `ssh -T host -- cmd`. One long-lived `SSHClient` per host, kept warm by `CitadelConnectionHolder`.
- **Pure-Swift SFTP.** All `readFile` / `writeFile` / `stat` / `listDirectory` go over SFTP via Citadel's `SFTPClient`. Path resolution rewrites `~/...` to the probed `$HOME` (SFTP doesn't expand tildes per RFC 4254).
- **Inline PATH prefix on every `runProcess`.** Citadel's raw exec channel doesn't source the user's shell rc files, so non-interactive sessions land with `PATH=/usr/bin:/bin`. v2.5 inlines `PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"` on every command so pipx-installed `hermes` resolves and any subprocess hermes spawns can find git/curl/python. Mac's OpenSSH sshd handles this transparently via login-shell init; Citadel does not.
- **Output preservation on non-zero exit.** Citadel's high-level `executeCommand` API throws `CommandFailed` and discards captured stdout when the remote exits non-zero. v2.5 drives `executeCommandStream` directly — drains stdout + stderr regardless of outcome, recovers the actual exit code from the `CommandFailed` catch. This was the bug behind "Skills Browse failed" on iOS while Mac worked.
- **Keychain-backed SSH key.** Each configured server holds its own Ed25519 keypair in the iOS Keychain (`com.scarf.ssh-key` service, `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, account `server-key:<UUID>`). Mac uses the system ssh-agent + `~/.ssh/config`; iOS keys never leave the device.
- **Watch via mtime polling.** Same 3-second cadence as `SSHTransport` — Citadel doesn't have an equivalent of inotify-over-SSH.
- **No streamed exec yet.** `streamLines` is a stub on iOS; log tailing in ScarfGo uses periodic refreshes instead. Future work — Citadel exposes the raw exec channel, just hasn't been wired up.
### Connection holder + reuse
Citadel's `SSHClient.connect(...)` handshake costs ~500ms on a warm network. ScarfGo keeps a long-lived per-server `CitadelConnectionHolder` so subsequent calls reuse the same TCP+crypto session — same idea as Mac ControlMaster, different mechanism. The holder is cached per-`ServerID` so two configured remotes don't contend on a single channel pool.
See [ScarfGo Onboarding](ScarfGo-Onboarding) for user-side setup and [ScarfCore Package](ScarfCore-Package) for why `KeychainSSHKeyStore` lives in `ScarfIOS` and not `ScarfCore`.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.2_ _Last updated: 2026-04-25 — Scarf v2.5.0 (ScarfCore path refresh + CitadelServerTransport section)_
+4
@@ -80,3 +80,7 @@ If something else is still slow, it's likely Python startup + a working but heav
- [MCP, Plugins, Webhooks, Tools](MCP-Servers-Plugins-Webhooks-Tools) — the in-app editor for `mcp_servers`. - [MCP, Plugins, Webhooks, Tools](MCP-Servers-Plugins-Webhooks-Tools) — the in-app editor for `mcp_servers`.
- [ACP Subprocess](ACP-Subprocess) — how Scarf talks to `hermes acp`. - [ACP Subprocess](ACP-Subprocess) — how Scarf talks to `hermes acp`.
- [Servers & Remote](Servers-and-Remote) — connectivity issues that aren't MCP-related. - [Servers & Remote](Servers-and-Remote) — connectivity issues that aren't MCP-related.
- [ScarfGo Onboarding](ScarfGo-Onboarding) — iOS-specific PATH workarounds (Citadel inlines `$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin` on every command in v2.5+ so pipx-installed tools resolve, but `hermes` sub-tools may still be missing if their PATH isn't covered).
---
_Last updated: 2026-04-25 — Scarf v2.5.0_
+4 -2
@@ -1,6 +1,8 @@
# Uninstalling # Uninstalling
Scarf is a self-contained `.app` bundle with no installers, launch agents, or kernel extensions. Removing it is two steps; cleaning up its caches is one more. > **Removing ScarfGo from your iPhone?** Standard iOS app delete — long-press the icon → Remove App → Delete App. iOS purges the Keychain group (your SSH keys) and app container along with the binary, so nothing lingers. The Hermes host's `~/.ssh/authorized_keys` line you added during onboarding stays — clean it up manually if you want.
This page covers the macOS app. Scarf is a self-contained `.app` bundle with no installers, launch agents, or kernel extensions. Removing it is two steps; cleaning up its caches is one more.
## Quit and remove the app ## Quit and remove the app
@@ -36,4 +38,4 @@ To uninstall Hermes itself, follow the Hermes documentation — that's a separat
Just for completeness: the GitHub wiki at <https://github.com/awizemann/scarf/wiki> is a separate git repo from the main one. Uninstalling the app has no effect on the wiki. Just for completeness: the GitHub wiki at <https://github.com/awizemann/scarf/wiki> is a separate git repo from the main one. Uninstalling the app has no effect on the wiki.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.2_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added ScarfGo iOS uninstall note)_
+3 -1
@@ -1,5 +1,7 @@
# Updating # Updating
> **ScarfGo updates differently.** The iOS app updates through TestFlight (and, in future, the App Store) — Apple's standard update channel. The Sparkle flow described below is **macOS only**.
Scarf uses [Sparkle](https://sparkle-project.org/) to deliver automatic updates from a GitHub-Pages-hosted appcast at `https://awizemann.github.io/scarf/appcast.xml`. Each release is EdDSA-signed by the maintainer's private key; Scarf refuses any update whose signature doesn't verify against the embedded `SUPublicEDKey`. Scarf uses [Sparkle](https://sparkle-project.org/) to deliver automatic updates from a GitHub-Pages-hosted appcast at `https://awizemann.github.io/scarf/appcast.xml`. Each release is EdDSA-signed by the maintainer's private key; Scarf refuses any update whose signature doesn't verify against the embedded `SUPublicEDKey`.
## Automatic updates ## Automatic updates
@@ -35,4 +37,4 @@ App preferences live in `~/Library/Preferences/com.scarf.app.plist` and are forw
If Sparkle reports a signature mismatch or download error, see the [Health](Gateway-Cron-Health-Logs) view for details. Re-trigger the check; if it persists, file an issue with the Sparkle log content. If Sparkle reports a signature mismatch or download error, see the [Health](Gateway-Cron-Health-Logs) view for details. Re-trigger the check; if it persists, file an issue with the Sparkle log content.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (clarified macOS-only Sparkle vs ScarfGo TestFlight)_
+3 -1
@@ -82,6 +82,8 @@ _Last updated: YYYY-MM-DD — stub_
- **Release Process** on the wiki is a pointer; the canonical instructions live in `CLAUDE.md` and the header of `scripts/release.sh`. - **Release Process** on the wiki is a pointer; the canonical instructions live in `CLAUDE.md` and the header of `scripts/release.sh`.
- **Hermes Paths** mirrors the Key Paths block in `CLAUDE.md` — update both when paths change. - **Hermes Paths** mirrors the Key Paths block in `CLAUDE.md` — update both when paths change.
- **Release notes** stay in `releases/v<ver>/RELEASE_NOTES.md` on `main`. The wiki's [Release Notes Index](Release-Notes-Index) only links out. - **Release notes** stay in `releases/v<ver>/RELEASE_NOTES.md` on `main`. The wiki's [Release Notes Index](Release-Notes-Index) only links out.
- **TestFlight + App Store metadata** stay in `releases/v<ver>/TESTFLIGHT_CHECKLIST.md` and `APP_STORE_METADATA.md` on `main`. The wiki's [ScarfGo](ScarfGo) page links to the live TestFlight URL but doesn't duplicate Apple-side metadata.
- **Privacy Policy** has two copies on purpose: the canonical one at `awizemann.github.io/scarf/privacy/` (linked from the iOS Info.plist + App Store Connect), plus a wiki mirror at [Privacy Policy](Privacy-Policy) for in-wiki readability. The wiki copy is updated alongside major releases.
- **Internal dev notes** (PRD, Hermes API discovery, raw architecture) live in `scarf/docs/` in the main repo. The wiki carries the public-relevant parts in distilled form, not full duplicates. - **Internal dev notes** (PRD, Hermes API discovery, raw architecture) live in `scarf/docs/` in the main repo. The wiki carries the public-relevant parts in distilled form, not full duplicates.
## For external contributors ## For external contributors
@@ -92,4 +94,4 @@ The wiki is **not forked** when someone forks the main repo — it is a separate
- **Larger changes:** clone `git@github.com:awizemann/scarf.wiki.git` directly, or open an issue describing the proposed change and we'll work it in. - **Larger changes:** clone `git@github.com:awizemann/scarf.wiki.git` directly, or open an issue describing the proposed change and we'll work it in.
--- ---
_Last updated: 2026-04-20 — Scarf v2.0.1_ _Last updated: 2026-04-25 — Scarf v2.5.0 (added TestFlight + App Store metadata + privacy-mirror source-of-truth rules)_