feat(hermes-v12): version-aware capability detection (Phase A)

Introduces `HermesCapabilities` (parsed from `hermes --version`) and a
per-server `HermesCapabilitiesStore` injected into Mac `ContextBoundRoot`
and iOS `ScarfGoTabRoot` via `.environment(_:)` and `.hermesCapabilities`.
Subsequent v0.12-targeted UI (Curator, Kanban, ACP image input,
auxiliary.curator, prompt cache TTL, etc.) can branch on these flags so
older Hermes installs degrade silently instead of throwing on unknown CLI
subcommands.

Adds `curatorReportJSON` / `curatorReportMD` paths to `HermesPathSet`.

Bumps the Hermes version target in CLAUDE.md from v2026.4.23 (v0.11.0) to
v2026.4.30 (v0.12.0) and lists the v0.12 surfaces Scarf will consume.

Side fixes:

- `M5FeatureVMTests.ScriptedTransport` was missing
  `cachedSnapshotPath` after that property was added in 7b864d7;
  added `URL? { nil }` stub.
- `M0dViewModelsTests` referenced `.degraded(reason:)` after the case
  gained `hint` + `cause`; updated.
- `RemoteBackupService.zipDirectory` and `RemoteRestoreService.unzipArchive`
  used `Foundation.Process` unconditionally, breaking the iOS build
  (Process is unavailable on iOS). Wrapped in `#if !os(iOS)` with iOS
  stubs that throw — the backup/restore flow is Mac-only by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-01 12:10:06 +02:00
parent 421e6030df
commit a90a29add8
10 changed files with 530 additions and 4 deletions
+25
View File
@@ -36,6 +36,29 @@ struct ScarfGoTabRoot: View {
/// through here.
@State private var coordinator = ScarfGoCoordinator()
/// Hermes version + capability flags for this remote. Drives the
/// iOS version banner (v0.11 hosts get a yellow "update for new
/// features" banner) and capability-gated affordances like ACP
/// image attachments. Constructed once per server connection so
/// the detection runs over the active SSH transport.
@State private var capabilities: HermesCapabilitiesStore
init(
serverID: ServerID,
config: IOSServerConfig,
key: SSHKeyBundle,
onSoftDisconnect: @escaping @MainActor () async -> Void,
onForget: @escaping @MainActor () async -> Void
) {
self.serverID = serverID
self.config = config
self.key = key
self.onSoftDisconnect = onSoftDisconnect
self.onForget = onForget
let ctx = config.toServerContext(id: serverID)
_capabilities = State(initialValue: HermesCapabilitiesStore(context: ctx))
}
/// SwiftUI's `.onChange(of: ScenePhase)` modifier on a non-active
/// tab doesn't fire while the tab is unmounted the coordinator
/// is the single source of truth for scene-phase transitions
@@ -118,6 +141,8 @@ struct ScarfGoTabRoot: View {
.tabViewStyle(.sidebarAdaptable)
.environment(\.serverContext, ctx)
.environment(\.scarfGoCoordinator, coordinator)
.environment(capabilities)
.hermesCapabilities(capabilities)
.onAppear {
// Give the notification router a handle to this session's
// coordinator so notification-taps can route across tabs.