M3 follow-up: SETUP.md rewritten for single-project approach

Q: Should I just create an iOS target in the current scarf project?
   Would that be easier?
A: Yes — single scarf.xcodeproj with two targets (scarf + scarf-ios)
   is objectively easier than a separate scarf-ios.xcodeproj.

The original conservative recommendation (separate xcodeproj) was
rooted in my not wanting to hand-edit pbxproj. But you're the one
clicking through Xcode's UI to create the target, not me — Xcode
handles multi-target multi-platform projects natively, with zero
risk to the existing Mac target.

Rewrote SETUP.md to describe the single-project flow:

  - `File → New → Target` inside the existing project (not a new
    project file).
  - Both targets share the same SPM package references — ScarfCore
    is already there for Mac, you just add it + ScarfIOS to the
    scarf-ios target via General → Frameworks.
  - One Xcode window, one scheme switcher, unified signing/team
    settings.

Also threaded in M3-specific smoke-test steps (connect to a real
host → see Dashboard load via Citadel SFTP snapshot) and added a
post-M3 troubleshooting entry for the `Cannot find 'Process' in
scope` error — it should never appear now that makeProcess is
`#if !os(iOS)`-guarded, but if it does it's a leaked Mac-only file
in the scarf-ios target membership.

Milestone status table in SETUP.md updated to reflect M3 shipped.

https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
This commit is contained in:
Claude
2026-04-22 23:37:41 +00:00
parent e85a7b170c
commit 92ac102f46
+150 -125
View File
@@ -1,170 +1,195 @@
# scarf-ios — Xcode target setup # scarf-ios — Xcode target setup
This folder contains the source tree for the iOS app (`scarf-ios`), but This folder contains the source tree for the iOS app (`scarf-ios`).
**not** the Xcode project file. Creating the `.xcodeproj` is a one-time The Xcode target itself is added to the **existing `scarf.xcodeproj`**
step you do in Xcode's UI — it's about 5 minutes, and doing it by hand as a second target alongside the Mac `scarf` target — **not** a
produces a project file that's definitely correct for whichever Xcode separate `.xcodeproj`. One project, two targets.
version you're running, rather than a hand-edited pbxproj that might
drift from Xcode's expectations.
Everything the app needs — SSH key generation, Keychain storage, Everything the app needs — SSH key generation, Keychain storage,
onboarding state machine, Citadel-backed connection testing — already onboarding state machine, Citadel-backed SSH client, SQLite-backed
lives in the shared SPM packages (`Packages/ScarfCore`, Dashboard — already lives in the shared SPM packages
`Packages/ScarfIOS`) and is exercised by 88 passing unit tests on (`Packages/ScarfCore`, `Packages/ScarfIOS`) and is exercised by
Linux CI. The Xcode target is mostly just a wrapper that assembles 96 passing unit tests on Linux CI. The iOS target is mostly just a
those packages behind a `@main` SwiftUI app. wrapper that assembles those packages behind a `@main` SwiftUI app.
## One-time: create the Xcode target Creating the target is a one-time ~5-minute step in Xcode's UI.
1. Open **Xcode****File → New → Project…** ## One-time: add the iOS target to the existing project
2. Choose **iOS → App**. Click Next.
3. Fill in: 1. Open `scarf/scarf.xcodeproj` in **Xcode**.
2. Select the project in the Project Navigator.
3. **File → New → Target…** (or use the `+` button at the bottom of
the targets list).
4. Choose **iOS → App**. Click Next.
5. Fill in:
- **Product Name**: `scarf-ios` - **Product Name**: `scarf-ios`
- **Team**: `3Q6X2L86C4` (the same team the Mac app uses for notarization) - **Team**: `3Q6X2L86C4` (the same team the Mac target uses)
- **Organization Identifier**: `com.scarf` - **Organization Identifier**: `com.scarf`
- **Bundle Identifier** will be `com.scarf.scarf-ios`
- **Interface**: **SwiftUI** - **Interface**: **SwiftUI**
- **Language**: **Swift** - **Language**: **Swift**
- **Storage**: **None** (no Core Data, no CloudKit) - **Storage**: **None**
- **Include Tests**: unchecked (SPM covers them) - **Include Tests**: unchecked (SPM covers them)
4. On the save-location sheet, navigate to `<repo>/scarf/` (the same 6. Click **Finish**. Xcode creates the target plus a default source
level as `scarf.xcodeproj`) and hit **Create**. tree under `scarf/scarf-ios/` (a folder you'll immediately throw
5. Xcode produces `<repo>/scarf/scarf-ios/scarf-ios.xcodeproj` and a away, since our real source lives there already).
default source tree you'll immediately throw away. 7. If Xcode asks about "Activate scheme": **yes**.
## One-time: set project settings ## One-time: target settings
In the **scarf-ios** project (target of the same name): With the `scarf-ios` target selected in the project editor:
1. **General → Minimum Deployments → iPhone**: `iOS 18.0`. 1. **General → Minimum Deployments → iPhone**: `iOS 18.0`.
2. **General → Supported Destinations**: keep **iPhone** only. Remove 2. **General → Supported Destinations**: keep **iPhone** only. Remove
iPad + Mac Catalyst + Vision. iPad + Mac Catalyst + Vision.
3. **Info → Bundle Identifier**: `com.scarf.scarf-ios`. 3. **Signing & Capabilities → Team**: `3Q6X2L86C4` (should have
4. **Signing & Capabilities → Team**: `3Q6X2L86C4` (same as Mac). auto-filled from step 5 above).
5. **Build Settings → Swift Language Version**: `Swift 5` (matches 4. **Build Settings → Swift Language Version**: `Swift 5` (matches
the Mac app and both SPM packages). the Mac target + both SPM packages).
## One-time: wire the SPM packages ## One-time: link the SPM packages to the iOS target
1. **File → Add Package Dependencies…** `ScarfCore` and `ScarfIOS` are already in the project's package
2. **Add Local…** button in the lower-left of the dialog. references (the Mac target already uses ScarfCore). You only need to
3. Select `<repo>/scarf/Packages/ScarfCore`. Click **Add Package**. add them to the NEW iOS target.
4. Target to attach it to: **scarf-ios**. Click **Add Package**.
5. Repeat steps 14 for `<repo>/scarf/Packages/ScarfIOS`. 1. Select the project in the navigator → select the `scarf-ios`
- `ScarfIOS` already declares `Citadel` as a dependency — Xcode target → **General → Frameworks, Libraries, and Embedded
will resolve it automatically when you add the local package. Content**.
- On first resolution expect a ~30s wait while Citadel + 2. Click the `+` button.
SwiftNIO-SSH fetch from GitHub. 3. Select **ScarfCore** (library from ScarfCore local package). Add.
4. Click `+` again. Select **ScarfIOS**. Add.
5. Citadel is pulled in transitively by ScarfIOS — no need to add it
explicitly. On first build Xcode resolves it from GitHub
(~30s-1min).
## One-time: replace the default source tree ## One-time: replace the default source tree
1. In Xcode's Project Navigator, delete the auto-generated files 1. In Xcode's Project Navigator, find the `scarf-ios` group Xcode
Xcode created for you: created. Delete:
- `scarf_iosApp.swift` (or `scarf-iosApp.swift`) - `scarf_iosApp.swift` (or `scarf-iosApp.swift`)
- `ContentView.swift` - `ContentView.swift`
- **`Assets.xcassets`** — yes, delete this one too. We ship a - **`Assets.xcassets`** — we ship our own pre-built one.
pre-built `Assets.xcassets/` with the app icon + accent 2. In Finder, open `<repo>/scarf/scarf-ios/`. Drag these four folders
color inside `<repo>/scarf/scarf-ios/` that we'll add in the onto the `scarf-ios` group in Xcode:
next step. - `App/`
2. In Finder, open `<repo>/scarf/scarf-ios/`. Drag **App/**, - `Onboarding/`
**Onboarding/**, **Dashboard/**, and **`Assets.xcassets/`** onto - `Dashboard/`
the `scarf-ios` target in Xcode's navigator. - `Assets.xcassets/`
- In the import sheet: **Create groups**, **Add to target: 3. In the import sheet:
scarf-ios**. - **Destination**: Copy items if needed — **unchecked** (they're
3. Build (`⌘B`). It should compile cleanly against Citadel 0.12.1 already in place).
— every API call in `CitadelSSHService` was cross-checked - **Added folders**: **Create groups**.
against the 0.12.1 tag in April 2026. If you've bumped the pin - **Add to targets**: **scarf-ios** only.
to 0.13+ and something fails here, the likeliest culprit is 4. Build (`⌘B`). Should compile cleanly against Citadel 0.12.1 —
`SSHAuthenticationMethod.ed25519(username:privateKey:)` being every API call in `CitadelSSHService` + `CitadelServerTransport`
renamed or refactored; check the current was cross-checked against the 0.12.1 tag. If you've bumped the
`Sources/Citadel/SSHAuthenticationMethod.swift` in the new pin to 0.13+ and something fails, check
release. `Sources/Citadel/SSHAuthenticationMethod.swift` for the current
`.ed25519(username:privateKey:)` spelling.
## App icon + accent color ## App icon + accent color
The `Assets.xcassets/` inside `<repo>/scarf/scarf-ios/` ships with: Already in `Assets.xcassets/` so you don't configure anything:
- **`AppIcon.appiconset/AppIcon-1024.png`** — the 1024×1024 Scarf - **`AppIcon.appiconset/AppIcon-1024.png`** — the 1024×1024 Scarf
icon from the Mac app's icon set. iOS 14+ renders all smaller icon copied from the Mac app's icon set. iOS 14+ renders all
sizes automatically from the single 1024 image. smaller sizes automatically from the single 1024 image.
- **`AccentColor.colorset`** — a custom Scarf teal (`RGB - **`AccentColor.colorset`** — custom Scarf teal (sRGB `0.227 /
0.227 / 0.525 / 0.722` in light mode; lighter `0.400 / 0.690 0.525 / 0.722` light mode; `0.400 / 0.690 / 0.902` dark). Edit
/ 0.902` in dark mode). If you want a different accent, swap `Contents.json` or Xcode's color picker to change.
the sRGB components in `Contents.json` or edit in Xcode's
color picker.
## Info.plist additions for M2 ## Info.plist for TestFlight
None required. Citadel uses SwiftNIO, which doesn't need the Under the scarf-ios target → **Info → Custom iOS Target Properties**:
network-usage `Info.plist` key unless you hit local-network
discovery — which onboarding doesn't.
If you want to publish to TestFlight in M2, add these under - `LSRequiresIPhoneOS = YES` (usually defaulted)
**Info → Custom iOS Target Properties**:
- `LSRequiresIPhoneOS = YES` (defaults to YES, usually already set)
- `UIApplicationSceneManifest → UIApplicationSupportsMultipleScenes = NO` - `UIApplicationSceneManifest → UIApplicationSupportsMultipleScenes = NO`
(single-window for iPhone) (iPhone single-window)
- `UILaunchScreen` — empty dictionary is fine. - `UILaunchScreen` — empty dictionary is fine
## Smoke test the target Citadel uses SwiftNIO, not Apple's local-network discovery, so no
network-usage-description key is needed.
1. Pick an iPhone simulator (any iPhone running iOS 18+) and hit ⌘R. ## Smoke test
2. You should see the onboarding flow: **Remote host** form → **SSH
key** choice → **Generate** → **Show public key** → …
3. On **Show public key**, the OpenSSH line is selectable and
copy-able. The text renders as `ssh-ed25519 AAAA… scarf-iphone-XXXX`.
4. Tap **I've added this key**. Onboarding calls `CitadelSSHService`
to connect. With no real server, this will fail with
`.hostUnreachable` — that's expected. You should land on the
**Connection failed** screen with a "Retry" button.
5. Real end-to-end test: use a host you actually own, copy the shown
public key into its `~/.ssh/authorized_keys`, tap Retry. You
should reach the **Connected** state and then the placeholder
**Dashboard** with a Disconnect button.
## TestFlight (still M2 — optional) 1. Switch the run destination to an iPhone simulator (any iPhone
running iOS 18+). Xcode's target switcher lets you toggle between
scarf (macOS) and scarf-ios.
2. ⌘R. Expect the onboarding flow:
**Remote host** → **SSH key choice** → **Generate** → **Show
public key** → **I've added this** → **Test connection** → ...
3. On **Show public key**, the OpenSSH line is selectable + copyable
(`ssh-ed25519 AAAA… scarf-iphone-XXXX`).
4. Without a real SSH server, **Test connection** will fail with
`.hostUnreachable` — that's expected. Land on the **Connection
failed** screen with a **Retry** button.
5. Real end-to-end: use a host you own. Copy the shown public key
into `~/.ssh/authorized_keys` on that host. Tap **Retry**. You
should reach **Connected** → **Dashboard**, which then does a
Citadel SFTP snapshot of `~/.hermes/state.db` and renders
session + token stats.
1. **Product → Archive** (select a physical iPhone or "Any iOS ## TestFlight upload
Device (arm64)" as the target).
2. **Window → Organizer → Archives → Distribute App → App Store
Connect → Upload**. Uses your existing team signing setup.
3. First upload will trigger App Store Connect to create the app
record if it doesn't exist. Give it `com.scarf.scarf-ios` and
the same team.
4. After processing, invite testers from App Store Connect.
## What's *not* in M2 1. Scheme selector top-left → **scarf-ios**. Destination: **Any iOS
Device (arm64)** or a physical iPhone.
2. **Product → Archive**.
3. **Window → Organizer → Archives → Distribute App → App Store
Connect → Upload**.
4. First upload creates the App Store Connect app record with
`com.scarf.scarf-ios`. Same team as the Mac app.
5. Invite testers from App Store Connect.
- Dashboard data (sessions, messages, stats) — **M3** adds a ## What's in each milestone
Citadel-backed `ServerTransport` so `HermesDataService` and friends
work over SSH from iOS. - **M2** — Onboarding (SSH key + Keychain), Citadel-based
- Chat — **M4** adds an `SSHExecACPChannel` (the iOS counterpart to "Test Connection", Dashboard placeholder.
`ProcessACPChannel`) so `ACPClient` runs over a Citadel exec - **M3** — Real Dashboard via `CitadelServerTransport` +
session. `HermesDataService` (you're running the M3 PR now).
- Memory editing, Cron, Skills, Settings — **M5**. - **M4** — Chat over an iOS `SSHExecACPChannel`.
- Polish, App Store submission — **M6**. - **M5** — Memory editing, Cron, Skills, Settings.
- **M6** — Polish + App Store submission.
## Troubleshooting ## Troubleshooting
**Citadel fails to resolve.** Delete derived data (`~/Library/Developer/Xcode/DerivedData/scarf-ios-*`) **Citadel fails to resolve.** Delete DerivedData
and `File → Packages → Reset Package Caches`, then rebuild. (`~/Library/Developer/Xcode/DerivedData/scarf-*`) and **File →
Packages → Reset Package Caches**, then rebuild.
**`SSHAuthenticationMethod` has no member `ed25519`.** Shouldn't **`Cannot find 'Process' in scope` when building scarf-ios.** Should
happen against Citadel 0.12.1 (verified), but historically the not happen post-M3 — the `makeProcess` protocol method is now
private-key variant names have changed between minor versions `#if !os(iOS)`-guarded. If you see this: grep the scarf-ios target's
(0.7 → 0.9 → 0.12). See `CitadelSSHService.buildClientSettings(...)` source files for `Process()`, `process.isRunning`, or `terminationHandler`
there's one line to something Mac-only leaked into the iOS target's membership.
update. Keep the protocol conformance intact.
**Keychain reads empty after relaunch.** Check that you haven't **`SSHAuthenticationMethod` has no member `ed25519`.** Shouldn't happen
accidentally set `kSecAttrAccessible` to a value that requires against Citadel 0.12.1 (verified), but historically the private-key
biometric unlock — M2 uses `AfterFirstUnlockThisDeviceOnly` which variant names have changed between minor versions (0.7 → 0.9 → 0.12).
should always be readable. See `CitadelSSHService.buildClientSettings(...)` in ScarfIOS — one
line to update. Keep the protocol conformance intact.
**The shown public-key line doesn't match what OpenSSH generates **Dashboard shows "Couldn't read the Hermes database".** Expected if
from the private key.** It won't — `scarf-ios` uses a compact the host's `~/.hermes/state.db` doesn't exist yet (fresh Hermes
internal PEM shape for the private key (see `Ed25519KeyGenerator` install). Start a Hermes session on the host first, then pull-to-
for the format). The **public** key is standard OpenSSH wire format refresh.
and is interop-safe with `authorized_keys`. If you want to export
the private key for use with `ssh`, that export flow is deferred **Dashboard spins forever on first connect.** Citadel connection
to a future phase. hand-shake + SFTP open can take 5-10s on a cold network. If it's
longer, check that the same SSH key works from a regular `ssh` client
against the same host — sometimes the issue is an authorized_keys
line with a trailing whitespace, or the host uses a restricted shell
that blocks `sqlite3`.
**Keychain reads empty after relaunch.** Check you haven't set
`kSecAttrAccessible` to a biometric-required value. We use
`AfterFirstUnlockThisDeviceOnly`, which is readable any time after
the first post-boot unlock.
**The public-key line doesn't match what `ssh-keygen` would produce
from the same private key.** The **public** key is standard OpenSSH
wire format (interop-safe with `authorized_keys`). The **private**
key uses a compact Scarf-internal PEM (see
`ScarfIOS/Ed25519KeyGenerator.swift`) — it's not directly exportable
to `~/.ssh/id_ed25519`. A future phase adds an export flow that
re-serializes to standard OpenSSH PEM.