mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
iOS port M6: YAML parser port, Settings view, Cron editing
Ports the Mac app's YAML parser into ScarfCore, unlocking iOS
Settings. Adds Cron editing (add / delete / toggle / edit). Settings
stays read-only this phase (writes need a round-trip-preserving YAML
writer — out of scope). App Store submission deferred to a later
task per the brief.
## ScarfCore — YAML infrastructure
Packages/ScarfCore/Sources/ScarfCore/Parsing/HermesYAML.swift:
- ParsedYAML struct (values / lists / maps)
- HermesYAML.parseNestedYAML(_:) — indent-based block parser
- HermesYAML.stripYAMLQuotes(_:) — single-layer quote stripping
Lifted verbatim from HermesFileService.parseNestedYAML/stripYAMLQuotes
and hoisted into a standalone namespace. Scope unchanged: the subset
Hermes's config.yaml actually uses (block nesting, scalars, bullet
lists, nested maps). NOT full YAML-spec compliance.
Packages/ScarfCore/Sources/ScarfCore/Parsing/HermesConfig+YAML.swift:
- HermesConfig.init(yaml:) — ports HermesFileService.parseConfig
one-for-one. Every default, every key, every legacy fallback
(platforms.slack.* vs slack.*, command_allowlist vs permanent_
allowlist, etc.) matches the Mac implementation.
- Forgiving: malformed YAML produces partial state + defaults
rather than throwing. Callers surface the raw text so users can
diagnose parse failures on their own.
## ScarfCore — Cron editing (write paths)
Packages/ScarfCore/Sources/ScarfCore/ViewModels/IOSCronViewModel.swift:
- toggleEnabled(id:)
- delete(id:)
- upsert(_:)
All funnel through private saveJobs(_:) which encodes the full
CronJobsFile (.prettyPrinted + .sortedKeys), writes atomically via
transport.writeFile (Data.write-atomic from M5). Creates the cron/
directory on fresh installs.
Models/HermesCronJob.swift — both HermesCronJob and CronJobsFile
gained real public memberwise inits (Swift's synthesis was
suppressed by the hand-written Codable; first draft hacked around
this with JSON round-trips). Also HermesCronJob.withEnabled(_:)
does clean field passthrough instead of encode→mutate→decode.
## ScarfCore — iOS Settings VM
Packages/ScarfCore/Sources/ScarfCore/ViewModels/IOSSettingsViewModel.swift:
- Reads ~/.hermes/config.yaml via ServerContext.readText
- Parses with HermesConfig(yaml:)
- Surfaces both parsed config and rawYAML
- M6 read-only by design — config.yaml needs round-trip-preserving
YAML serialization (comments, key order, whitespace) for safe
edits; option (a) hand-write one, (b) YAML library dep, (c)
delegate to `hermes config set` via ACP. Defer.
## iOS app
Scarf iOS/Settings/SettingsView.swift:
- Read-only browser grouped into 10 sections matching the Mac
app's tabs. DisclosureGroup at the bottom reveals raw YAML
source for diagnostics.
Scarf iOS/Cron/CronListView.swift rewritten:
- Toggle-enabled circle (tap to flip, saves atomically)
- Swipe-to-delete
- "+" toolbar for new job → editor sheet
- Row-tap opens editor with existing fields populated
New CronEditorView form:
- Name, Prompt, Enabled toggle
- Schedule: kind picker (cron/interval/once), display, expression
(for cron), run_at (for once)
- Optional model + comma-separated skills + delivery route
- Preserves runtime fields (nextRunAt, lastRunAt,
deliveryFailures, etc.) when editing existing jobs — no reset
Dashboard's Surfaces section gains a 5th row: Settings.
## Test-suite reorganization (real bug caught)
swift-testing's `.serialized` trait serializes WITHIN one @Suite, not
across suites. Shipping M6 revealed a 3-way race on
`ServerContext.sshTransportFactory`:
- M5's `.serialized` suite sets factory, runs, restores.
- M6's `.serialized` suite did the same in parallel — clobbered.
- M0b's non-serialized `serverContextMakeTransportDispatches`
asserted the DEFAULT factory (nil) returned SSHTransport —
saw whichever factory was temporarily installed.
Fix: one serialization domain for everything that touches the
factory. Move cron-editing + settings-load M6 tests into M5's
serialized suite. M0b's factory-dependent assertion (SSHTransport
fallback) also moves to the M5 serialized suite with an explicit
`factory = nil` reset for race-freedom. Pure YAML/config/memberwise
tests stay in the new plain (non-serialized) M6ConfigCronTests
suite — they never touch globals.
## Test results: 108 → 134 passing on Linux
19 new in M6ConfigCronTests:
- YAML parser: scalars, bullets, nested maps, comments, quotes,
inline {} / []
- HermesConfig.init(yaml:): empty → defaults, model + agent,
display, security + blocklist domains, slack legacy fallback,
auxiliary (3 populated + 2 defaulted), permanent_allowlist vs
command_allowlist, quoted strings
- Memberwise inits for HermesCronJob, withEnabled(_:),
CronJobsFile, CronSchedule
7 new in M5FeatureVMTests (.serialized):
- defaultFactoryProducesSSHTransportForRemoteContext (moved +
hardened with explicit factory reset)
- cronUpsertCreatesFileFromScratch, cronToggleEnabledPersists,
cronDeleteRemovesJob, cronUpsertReplacesMatchingId,
cronPreservesRuntimeFieldsAcrossReloads
- settingsLoadsFromConfigYAML, settingsSurfacesMissingFile
## Manual validation needed on Mac
1. Xcode compile clean.
2. Settings: confirm every section populates from your real
~/.hermes/config.yaml. Tap "View source" disclosure, verify raw
text matches the remote file.
3. Cron: toggle-enabled survives refresh + relaunch. Swipe-delete
works. "+" creates jobs; round-trip name/prompt/schedule/skills.
Edit preserves runtime state.
4. Skills: unchanged from M5 (still browse-only, deferred).
Updated scarf/docs/IOS_PORT_PLAN.md with M6's shipped state, the
YAML-parser scope ceiling, the Settings-edit deferral rationale, and
the cross-suite serialization rule for future test authors.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
This commit is contained in:
@@ -696,4 +696,57 @@ Total **98 → 108 tests passing on Linux** via `docker run --rm -v $PWD/Package
|
||||
- **Editing Cron, adding Skills** — both are deferred. Cron editing needs atomic JSON rewrites (doable). Skills install needs git-clone + schema validation (larger).
|
||||
- **Settings tab on iOS is still missing** — requires a YAML parser in ScarfCore or porting `HermesFileService.loadConfig`. Next phase's job.
|
||||
|
||||
### M6 — pending
|
||||
### M6 — shipped (on `claude/ios-m6-settings-polish` branch, separate PR, stacked on M5)
|
||||
|
||||
Ports the Mac app's YAML parser into ScarfCore (unblocking iOS Settings), adds Settings browsing + Cron editing, consolidates the test-serialization story. App Store submission is deferred to a later task after real-device testing.
|
||||
|
||||
**Shipped — ScarfCore:**
|
||||
|
||||
- `Parsing/HermesYAML.swift` — `HermesYAML.parseNestedYAML(_:)` + `stripYAMLQuotes(_:)`. Lifted verbatim from `HermesFileService.parseNestedYAML` / `stripYAMLQuotes` but hoisted into a standalone enum for reuse. Scope unchanged: indent-based block nesting, scalar values, bullet lists, nested maps. Not full YAML-spec compliance; matches exactly what Hermes's `config.yaml` actually uses.
|
||||
- `Parsing/HermesConfig+YAML.swift` — `HermesConfig.init(yaml:)`. Lifted from `HermesFileService.parseConfig` one-for-one. Every default, every key, every legacy fallback (e.g., `platforms.slack.*` vs `slack.*`) tracked to the Mac implementation. Forgiving: malformed YAML produces a partial-state `HermesConfig` rather than throwing.
|
||||
- `ViewModels/IOSSettingsViewModel.swift` — `@Observable @MainActor` VM. Reads `~/.hermes/config.yaml` via transport, parses with the new loader, surfaces both the parsed `HermesConfig` and the raw text (so Settings view can offer a "View source" disclosure). M6 Settings is READ-ONLY — edit-path deferred until a round-trip-preserving YAML writer lands (commits, key order, whitespace would need preservation for a clean edit UX).
|
||||
- `ViewModels/IOSCronViewModel.swift` — added write paths: `toggleEnabled(id:)`, `delete(id:)`, `upsert(_:)`. All funnel through `saveJobs(_:)` which re-encodes the full `CronJobsFile` (`.prettyPrinted + .sortedKeys`) and writes atomically via the transport (Data.write-atomic semantics from M5). Creates the `cron/` directory on fresh installs.
|
||||
- Both `HermesCronJob` and `CronJobsFile` gained real memberwise inits (previously only hand-written `init(from:)` — Swift's synthesis was suppressed). Also `HermesCronJob.withEnabled(_:)` — clean field-passthrough instead of the JSON-roundtrip hack my first draft used.
|
||||
|
||||
**Shipped — iOS app:**
|
||||
|
||||
- `Scarf iOS/Settings/SettingsView.swift` — read-only browser grouped into sections that mirror the Mac app's tabs: Model, Agent, Display, Terminal, Memory, Voice, Security, Compression, Logging, Platforms. `DisclosureGroup` at the bottom reveals the raw YAML source for diagnostics.
|
||||
- `Scarf iOS/Cron/CronListView.swift` rewritten: toggle-enabled circle (tap to flip), swipe-to-delete, "+" toolbar for new-job, row-tap opens the editor sheet. New `CronEditorView` form handles name / prompt / enabled / schedule (kind + display + expression + run_at) / optional model / comma-separated skills / delivery route. Preserves runtime state fields (nextRunAt, lastRunAt, deliveryFailures, etc.) when editing — no resetting the cron's observed history on a field edit.
|
||||
- Dashboard's Surfaces section gets a 5th row: Settings.
|
||||
|
||||
**Test-suite reorganization:**
|
||||
|
||||
Discovered (and fixed) a cross-suite race: swift-testing's `.serialized` trait scopes to one @Suite, not globally. M5's serialized suite installed `ServerContext.sshTransportFactory`, M6's serialized suite did the same, and the M0b non-serialized `serverContextMakeTransportDispatches` test asserted the DEFAULT factory (nil) returned `SSHTransport` — all three raced on the shared static.
|
||||
|
||||
Fix: keep the YAML-parse + memberwise tests in a plain (non-serialized) `M6ConfigCronTests` suite since they're pure. **Move every factory-touching test into the single `.serialized` `M5FeatureVMTests`** — including M6's Cron write-path tests, Settings-load tests, AND the M0b default-factory test (with explicit `factory = nil` reset for race-freedom). Single serialization domain eliminates the race.
|
||||
|
||||
**Test counts:** 108 → **134 passing on Linux**.
|
||||
|
||||
| Suite | New in M6 | Total |
|
||||
|---|--:|--:|
|
||||
| `ScarfCoreSmokeTests` | 0 | 1 |
|
||||
| `M0aPublicInitTests` | 0 | 15 |
|
||||
| `M0bTransportTests` | 0 (1 split out + moved) | 18 |
|
||||
| `M0cServicesTests` | 0 | 8 |
|
||||
| `M0dViewModelsTests` | 0 | 9 |
|
||||
| `M1ACPTests` | 0 | 10 |
|
||||
| `M2OnboardingTests` | 0 | 26 |
|
||||
| `M3TransportTests` | 0 | 5 |
|
||||
| `M4ACPIOSTests` | 0 | 2 |
|
||||
| `M5FeatureVMTests` | **+7** (cron write paths + settings load + default-factory guard) | 21 |
|
||||
| `M6ConfigCronTests` | **+19** (YAML parsing + HermesConfig decode + memberwise inits) | 19 |
|
||||
|
||||
**Manual validation needed on Mac:**
|
||||
1. Xcode compile clean.
|
||||
2. Settings → confirm every section populates from your real `config.yaml`. Tap the "View source" disclosure to verify the raw text matches what's on the remote.
|
||||
3. Cron: toggle a job's enabled flag, verify it survives a full refresh + relaunch. Swipe-to-delete a job. Tap "+" to create a new job; verify prompt + schedule + skills round-trip. Tap an existing job to edit; verify runtime fields (lastRunAt, deliveryFailures) aren't reset.
|
||||
4. Skills: unchanged from M5, still browse-only.
|
||||
|
||||
**Rules next phases can rely on:**
|
||||
- **Any test that touches `ServerContext.sshTransportFactory` or any other global mutable state MUST live in `M5FeatureVMTests`** (the single `.serialized` suite) — or introduce a new cross-suite synchronization primitive. Swift-testing's `.serialized` does NOT serialize across suites.
|
||||
- **YAML parser in ScarfCore is a hard ceiling** — it handles the Hermes config subset, not arbitrary YAML. If a future Hermes version adds constructs the parser doesn't cover (flow-style `[...]`, anchors, `&` references, multi-line `|` blocks), port them on both sides simultaneously.
|
||||
- **Settings writes stay deferred** until a round-trip-preserving YAML writer ships. Options: (a) hand-write one, (b) adopt a YAML lib (adds dependency), (c) delegate to `hermes config set` via ACP.
|
||||
- **Cron editing on iOS is atomic per-save** — full jobs.json rewrites on every change. Fine for current cron sizes (dozens of jobs). If that grows into the thousands, consider partial updates via `hermes cron add/rm/toggle` over ACP.
|
||||
- **Skills install (git-clone + validation over SSH)** remains deferred — it's its own project. The iOS Skills list is read-only; users install from the Mac app or by cloning directly to the remote.
|
||||
|
||||
### M7 — pending (post-testing App Store submission + any polish that surfaces)
|
||||
|
||||
Reference in New Issue
Block a user