Files
scarf/scarf/docs/v2.8/WS-9-ios-v0.13-plan.md
T
Alan Wizemann cf8a737ace docs(v2.8): add 8 work-stream plans + coordinator review for Hermes v0.13.0 catch-up
Each plan is the product of one Opus 4.7 1M-context planning agent dispatched
in parallel to investigate a single v0.13 surface. Plans are read-only — they
identify file-by-file edits, type additions, capability gates, tests, and
open questions, but no code is written yet. Implementation agents will be
dispatched per stream once the cross-stream open-questions matrix is resolved.

- WS-2: Persistent Goals + ACP /queue slash commands
- WS-3: Kanban v0.13 diagnostics + recovery UX
- WS-4: Curator archive + prune + list-archived
- WS-5: Messaging Gateway v0.13 expansion (Google Chat, allowlists, behavior toggles)
- WS-6: Provider catalog refresh + image_gen.model + OpenRouter response caching
- WS-7: Settings tab additions (MCP SSE, Cron --no-agent, Web Tools split, Profiles --no-skills)
- WS-8: UX polish (compression count, /new <name>, redaction default, display.language)
- WS-9: ScarfGo iOS catch-up (read-only mirrors of WS-2/3/4/5)

COORDINATOR-REVIEW.md compiles the cross-stream collision matrix, the open-
questions matrix (54 questions clustered into wire-shape unknowns,
architectural decisions, and out-of-scope deferrals), and the recommended
sequencing for implementation + review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:03:41 +02:00

51 KiB

WS-9 Plan: ScarfGo iOS catch-up (read-only mirrors of WS-2 / WS-3 / WS-4 / WS-5)

Workstream: WS-9 of Scarf v2.8.0 Hermes target: v0.13.0 (v2026.5.7) Capability gates consumed (already shipped in WS-1, PR #80):

  • HermesCapabilities.hasGoals (>= 0.13.0) — drives the Goal pill
  • HermesCapabilities.hasACPQueue (>= 0.13.0) — read-only queue indicator stub
  • HermesCapabilities.hasKanbanDiagnostics (>= 0.13.0) — diagnostics on the iOS Kanban detail sheet
  • HermesCapabilities.hasCuratorArchive (>= 0.13.0) — Archived list section in the iOS Curator surface
  • HermesCapabilities.hasGoogleChatPlatform / hasGatewayAllowlists / hasGatewayBusyAckToggle / hasGatewayRestartNotification (>= 0.13.0) — Settings → Platforms additions

Builds on:

  • v2.7.5 iOS Kanban (Scarf iOS/Kanban/ScarfGoKanbanView.swift, ScarfGoKanbanDetailSheet.swift).
  • v2.7.5 iOS Curator (Scarf iOS/Curator/CuratorView.swift).
  • v2.7.5 iOS Settings (Scarf iOS/Settings/SettingsView.swift) including platformsSection.
  • v2.5+ iOS Chat (Scarf iOS/Chat/ChatView.swift) including projectContextBar and transientHint.
  • WS-1 capability flags + the .hermesCapabilities(_:) env injection at ScarfGoTabRoot.swift:153.
  • Phase H precedent: iOS catch-up "parity-match the Mac surfaces but skip mutating CLI verbs."

Owner: TBD Reviewers: Alan (always); whoever owns iOS during v2.8 cycle. Sequencing: WS-9 lands AFTER WS-2 / WS-3 / WS-4 / WS-5 merge to main, since it consumes their model fields, view-model state, and capability flags.


Goals (read-only mirrors of WS-2 / WS-3 / WS-4 / WS-5)

WS-9 is iOS-only and strictly read-only. It mirrors selected Mac surfaces from earlier work-streams without introducing any iOS-side write verb. Per the v2.8.0 release plan, iOS write surfaces (Verify / Reject buttons, iOS create-task, iOS curator-archive button, iOS allowlist editor, etc.) are deferred to v2.8.x.

User-visible additions (all capability-gated, all degrade silently on pre-v0.13 hosts):

  1. Goal pill in iOS chat. When caps.hasGoals == true AND controller.vm.activeGoal != nil, surface a "Goal: " pill at the top of the chat view (mounted next to the existing folder/branch chips in projectContextBar). Read-only — no /goal slash command on iOS in v2.8.0; no clear affordance.
  2. Read-only /queue count chip. When caps.hasACPQueue == true AND controller.vm.queuedPrompts.count > 0, surface a small "N queued" chip in the same projectContextBar. No popover, no mutation. Tap is a no-op (or shows a sheet listing the previews — see Open Question #2).
  3. Kanban v0.13 diagnostics on iOS detail sheet. Extend ScarfGoKanbanDetailSheet to render max_retries, auto_blocked_reason, hallucination_gate_status, and the diagnostics array. NO Verify / Reject buttons; the hallucination state is rendered as a badge with the copy "Worker-created — verify on Mac" (since iOS can't verify in v2.8.0).
  4. iOS Curator Archived section. Append a read-only "Archived" section to the existing Scarf iOS/Curator/CuratorView.swift. Per-row: name, kind, archived-date, optional reason (sized small for thumb scrolling). NO Restore / Prune-this / Prune-all buttons. Empty-state copy points the user to the Mac app for restore.
  5. iOS Settings v0.13 features-active badge. When caps.semver >= 0.13.0, surface a small read-only "v0.13 features active" ScarfBadge at the top of SettingsView with a "Learn more" tap action that opens an action sheet listing the new features.
  6. iOS Platforms read-only mirror (extension to existing platformsSection). Add a Google Chat read-only row, a "Restart notifications" yes/no row, a "Busy ack" yes/no row, and a per-platform allowlist chip-row ("3 allowed channels: …, 4 allowed chats: …"). No editing — that's a Mac-only surface in v2.8.0.

Non-goals (explicitly deferred)

  • iOS write surfaces (Verify / Reject, Create Task, Archive Skill, Prune, Allowlist editor, /goal, /queue send) — deferred to v2.8.x. Per Phase H precedent.
  • iOS Curator surface from scratch — out of scope. iOS already has CuratorView.swift; WS-9 only adds the Archived list. (See Open Question #1 for what the user prompt anticipated.)
  • iOS Gateway/Platforms surface from scratch — out of scope. iOS Settings already has platformsSection (lines 280-288 of SettingsView.swift); WS-9 extends it. There is no separate iOS Gateway feature module today and WS-9 does not add one.
  • iOS goal/queue clear affordance/goal --clear and "Clear all queued" are write verbs; deferred.
  • iOS Kanban verify on tap — iOS Kanban is read-only and stays read-only in v2.8.0.
  • iOS Curator Run Now blocking + progress (synchronous run) — that's a write change in scope of WS-4, not WS-9. iOS keeps fire-and-forget runNow regardless of v0.13.

Existing iOS surface inventory

(Verified by walking Scarf iOS/ at plan time.)

iOS dir Files Mac counterpart
App/ ScarfIOSApp.swift, ScarfGoCoordinator.swift, ScarfGoTabRoot.swift, Theme/ scarfApp.swift, AppCoordinator.swift, SidebarView.swift
Chat/ ChatView.swift, ChatContentFormatter.swift, ProjectPickerSheet.swift, ProjectSlashCommandsBrowser.swift Features/Chat/
Components/ FlowLayout.swift, HermesVersionBanner.swift (cross-feature shared)
Cron/ (read-only views) Features/Cron/
Curator/ CuratorView.swift (read-mostly, runNow/pause/resume/pin/unpin/restore wired) Features/Curator/
Dashboard/ iOS dashboard views Features/Dashboard/
Kanban/ ScarfGoKanbanView.swift, ScarfGoKanbanDetailSheet.swift (5-column horizontal-paged Picker, read-only) Features/Kanban/
Memory/ (read-only views) Features/Memory/
Notifications/ APNSTokenStore.swift, NotificationRouter.swift Core/Services/Notifications*
Onboarding/ (first-run wizard) Features/Onboarding/
Plugins/ PluginsView.swift (Phase H read-only) Features/Plugins/
Profiles/ ProfilesView.swift (Phase H read-only) Features/Profiles/
Projects/ iOS project surfaces (incl. ProjectDetailView.swift) Features/Projects/
Servers/ server-list + connect surfaces Features/Servers/
Settings/ SettingsView.swift, SettingEditorSheet.swift, ScarfMonDiagnosticsView.swift Features/Settings/
Skills/ iOS Skills surface Features/Skills/
Webhooks/ WebhooksView.swift (Phase H read-only) Features/Webhooks/

Surfaces that DO NOT exist on iOS today:

  • No standalone Scarf iOS/Gateway/ or Scarf iOS/Platforms/ directory. iOS surfaces gateway / platform configuration through SettingsView.platformsSection. WS-9 mirror item 6 extends that section; it does NOT spin up a new feature module.
  • No iOS goal / queue surface. WS-2 lays the VM-side scaffolding (activeGoal, queuedPrompts on the shared RichChatViewModel in ScarfCore); WS-9 is what surfaces it on iOS.
  • No iOS dedicated "What's new in v0.13" feature surface. The "v0.13 features active" badge in mirror item 5 is the only entry point WS-9 adds.

Capability injection (verified):

  • ScarfGoTabRoot.swift:52 constructs a HermesCapabilitiesStore per server connection.
  • ScarfGoTabRoot.swift:153 calls .hermesCapabilities(capabilities) on the tab view.
  • All iOS feature views read with @Environment(\.hermesCapabilities) private var capabilitiesStore (see ChatView.swift:30, ProjectDetailView.swift:22, Components/HermesVersionBanner.swift:14).
  • WS-9 reuses the same env injection — no new plumbing required.

1. iOS Goal pill (mirror WS-2)

Source path read. The goal text lives on RichChatViewModel.activeGoal: HermesActiveGoal? (added in WS-2 — see WS-2 plan §3 "Active goal state"). iOS reads the same VM through ChatController.vm (the shared ScarfCore VM). No new ScarfCore field is needed; the WS-2 plumbing flows automatically into iOS.

File: Scarf iOS/Chat/ChatView.swift

1a. Read the capability + goal state in body

iOS already injects @Environment(\.hermesCapabilities) private var capabilitiesStore at line 30. Add a derived flag near the existing supportsImagePrompts computed property (lines 44-46):

private var supportsActiveGoal: Bool {
    capabilitiesStore?.capabilities.hasGoals ?? false
}

private var supportsACPQueue: Bool {
    capabilitiesStore?.capabilities.hasACPQueue ?? false
}

1b. Mount the goal pill alongside the project chip

The projectContextBar (lines 832-892) currently renders only when there's an active project. Adding the goal pill INSIDE that bar would mean a pill-less goal can't render in non-project chats. Solution: split the conditional. Render projectContextBar when projectName != nil OR supportsActiveGoal && controller.vm.activeGoal != nil OR supportsACPQueue && !controller.vm.queuedPrompts.isEmpty. The bar's tinted-strip background works for any of these states.

@ViewBuilder
private var projectContextBar: some View {
    let hasProject = (controller.currentProjectName?.isEmpty == false)
    let hasGoal = supportsActiveGoal && controller.vm.activeGoal != nil
    let hasQueue = supportsACPQueue && !controller.vm.queuedPrompts.isEmpty
    if hasProject || hasGoal || hasQueue {
        HStack(spacing: 8) {
            if hasProject { /* existing project chip */ }
            if hasGoal { goalChip }
            if hasQueue { queueChip }
            Spacer()
            if hasProject && !controller.vm.projectScopedCommands.isEmpty {
                /* existing slash-commands chip */
            }
        }
        .padding(.horizontal, 12)
        .padding(.vertical, 6)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(.tint.opacity(0.1))
    }
}

@ViewBuilder
private var goalChip: some View {
    if let goal = controller.vm.activeGoal {
        Label(truncatedGoalText(goal.text), systemImage: "scope")
            .labelStyle(.titleAndIcon)
            .font(.subheadline)        // semantic — Dynamic Type works
            .foregroundStyle(ScarfColor.info)
            .padding(.horizontal, 8)
            .padding(.vertical, 3)
            .background(ScarfColor.info.opacity(0.16), in: Capsule())
            .lineLimit(1)
            .accessibilityLabel("Goal locked: \(goal.text)")
    }
}

private func truncatedGoalText(_ text: String) -> String {
    text.count <= 28 ? text : String(text.prefix(25)) + "…"
}

Font choice (per CLAUDE.md iOS rules). Use semantic .subheadline because the goal text IS content (the user reads it to recall what they locked the agent on). Per CLAUDE.md "Decision tree per text element: 'is this read for content?' → semantic token. 'Is this chrome / a label / a badge?' → ScarfFont." If the design review pushes back and prefers a fixed-display chip look, switch the inner Text to ScarfFont.captionStrong; the surrounding pill chrome stays the same.

Color choice. ScarfColor.info matches Mac's WS-2 plan (informational state, not warning, not error). Keeps the pill visually distinct from the green "success" branch chip and the orange tinted-strip background of projectContextBar.

Truncation. 25-char prefix matches the iPhone 14 portrait width budget for a chip beside a project name. The full goal text is in the accessibility label (VoiceOver users get the full string).

1c. NO clear affordance

iOS does not get a "Clear goal" gesture in v2.8.0. The pill is purely informational. Tapping is a no-op. Users running /goal --clear from the Mac will see the iOS pill drop on the next polled state refresh (or whenever controller.vm.activeGoal updates — most likely on the next ACP event).


2. iOS Kanban v0.13 diagnostics (mirror WS-3)

Source paths read. All four new fields land on HermesKanbanTask (WS-3 plan §1):

  • task.maxRetries: Int?
  • task.autoBlockedReason: String?
  • task.hallucinationGateStatus: String? → wrap in KanbanHallucinationGate.from(_:)
  • task.diagnostics: [HermesKanbanDiagnostic]

The per-run shape adds run.diagnostics: [HermesKanbanDiagnostic] (WS-3 plan §3). The typed-mirror enums KanbanHallucinationGate and KanbanDiagnosticKind are added in ScarfCore and consumable from iOS by import ScarfCore.

File: Scarf iOS/Kanban/ScarfGoKanbanDetailSheet.swift

2a. Capability gate

Add @Environment(\.hermesCapabilities) private var capabilitiesStore at the top of the struct alongside the existing state (line ~17). Compute once in body:

private var diagnosticsAvailable: Bool {
    capabilitiesStore?.capabilities.hasKanbanDiagnostics ?? false
}

Defensive default to false so a missing capability store (preview, smoke test) renders the v2.7.5 sheet unchanged.

2b. Header chip row — add max_retries chip

Update headerCard(_:) (lines 91-111). Insert between the workspace-kind badge and the tenant badge, gated on diagnosticsAvailable:

if diagnosticsAvailable, let maxRetries = task.maxRetries {
    ScarfBadge("retries: \(maxRetries)", kind: .neutral)
        .accessibilityLabel("Max retries \(maxRetries)")
}

Tooltip on iOS is the accessibility label (no hover). No tap action; this is purely informational.

2c. Header chip row — add hallucination-gate badge

Below the existing badge row, insert a NEW row when KanbanHallucinationGate.from(task.hallucinationGateStatus) == .pending:

if diagnosticsAvailable,
   KanbanHallucinationGate.from(task.hallucinationGateStatus) == .pending {
    HStack(spacing: 6) {
        Image(systemName: "questionmark.diamond.fill")
            .foregroundStyle(ScarfColor.warning)
        Text("Worker-created — verify on Mac")
            .font(.subheadline)        // semantic content text
            .foregroundStyle(ScarfColor.warning)
    }
    .padding(.horizontal, 10)
    .padding(.vertical, 6)
    .background(ScarfColor.warning.opacity(0.10), in: RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous))
    .overlay(
        RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
            .strokeBorder(ScarfColor.warning.opacity(0.4), lineWidth: 1)
    )
    .accessibilityHint("Open this task on the Mac app to verify or reject the worker's claim.")
}

Copy choice. "Worker-created — verify on Mac" is intentional: it surfaces the gate status AND tells the user where the action lives. This is the read-only iOS substitute for Mac's Verify / Reject buttons (which require write CLI verbs deferred to v2.8.x).

Render order. Hallucination badge sits BELOW the chip row but ABOVE the markdown body, so users see the worker-created flag before reading the (potentially hallucinated) body content.

2d. Auto-blocked banner

In headerCard after the priority line, when status is blocked AND task.autoBlockedReason is non-empty:

if diagnosticsAvailable,
   KanbanStatus.from(task.status) == .blocked,
   let reason = task.autoBlockedReason, !reason.isEmpty {
    HStack(alignment: .top, spacing: 8) {
        Image(systemName: "exclamationmark.octagon.fill")
            .foregroundStyle(ScarfColor.danger)
        VStack(alignment: .leading, spacing: 2) {
            Text("Auto-blocked")
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(ScarfColor.danger)
            Text(reason)
                .font(.subheadline)        // semantic — server-supplied verbatim
                .foregroundStyle(.secondary)
        }
    }
    .padding(10)
    .background(ScarfColor.danger.opacity(0.08), in: RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous))
}

2e. Task-level diagnostics block

After the markdown body block (before the Picker tab selector), render the task-level diagnostics list when non-empty:

if diagnosticsAvailable, !detail.task.diagnostics.isEmpty {
    diagnosticsBlock(detail.task.diagnostics, label: "Diagnostics")
}

Helper:

@ViewBuilder
private func diagnosticsBlock(_ diags: [HermesKanbanDiagnostic], label: String) -> some View {
    VStack(alignment: .leading, spacing: 6) {
        Text(label)
            .font(.caption.weight(.semibold))
            .foregroundStyle(.secondary)
        FlowLayout(spacing: 6) {        // existing primitive at Scarf iOS/Components/FlowLayout.swift
            ForEach(diags) { diag in
                let kind = KanbanDiagnosticKind.from(diag.kind)
                ScarfBadge(diag.kind, kind: kind.badgeKind)
                    .accessibilityLabel(diag.message ?? diag.kind)
            }
        }
    }
    .frame(maxWidth: .infinity, alignment: .leading)
}

Tap-on-badge → an expandable detail sheet that shows kind + message + timestamp. iPhone-friendly substitute for the Mac .help() tooltip:

ScarfBadge(diag.kind, kind: kind.badgeKind)
    .onTapGesture { selectedDiagnostic = diag }

Sheet binding: .sheet(item: $selectedDiagnostic) { DiagnosticDetailSheet(diagnostic: $0) }. The detail sheet is a simple NavigationStack with name + message + ISO timestamp + a "Done" toolbar button. Lightweight (~30 lines).

HermesKanbanDiagnostic is Identifiable (per WS-3 plan §2 — synthetic UUID).

2f. Per-run diagnostics in the Runs tab

Update runsSection (lines 167-204). Inside each run row, after the optional error text, append a diagnostics block when present:

if diagnosticsAvailable, !run.diagnostics.isEmpty {
    diagnosticsBlock(run.diagnostics, label: "Run diagnostics")
        .padding(.top, 4)
}

Same diagnosticsBlock helper.

2g. NO write actions

Per WS-9 contract, iOS does not expose Verify / Reject. The hallucination badge in §2c is informational. Mac's KanbanInspectorPane.healthBanner.hallucinationBanner (WS-3 plan §8b) wires Verify/Reject buttons; iOS does not.


3. iOS Curator Archived list (mirror WS-4) — IF iOS Curator exists

Confirmed: iOS Curator surface exists at Scarf iOS/Curator/CuratorView.swift (read-mostly, with runNow / pause / resume / pin / unpin actions). In scope.

Source paths read. WS-4 introduces:

  • HermesCuratorArchivedSkill model (WS-4 plan "New types / fields")
  • CuratorService.listArchived() async throws -> [HermesCuratorArchivedSkill] (WS-4 plan §"New files")
  • CuratorViewModel.archivedSkills: [HermesCuratorArchivedSkill] and loadArchive() async (WS-4 plan §"Edited files / CuratorViewModel")

The shared CuratorViewModel lives in ScarfCore — iOS reuses it directly. The iOS CuratorView already constructs it at line 18. No iOS-side ScarfCore changes required.

File: Scarf iOS/Curator/CuratorView.swift

3a. Capability gate

Add @Environment(\.hermesCapabilities) private var capabilitiesStore at the top of the struct. Compute once in body:

private var archiveAvailable: Bool {
    capabilitiesStore?.capabilities.hasCuratorArchive ?? false
}

3b. Wire loadArchive() into the existing .task

Update the existing .task { await viewModel.load() } (line 92) to also load the archive when capability allows:

.task {
    await viewModel.load()
    if archiveAvailable {
        await viewModel.loadArchive()
    }
}
.refreshable {
    await viewModel.load()
    if archiveAvailable {
        await viewModel.loadArchive()
    }
}

3c. Add the Archived section

After the "Last report" section (lines 74-80) and before the trailing modifiers, render the new section gated on archiveAvailable:

if archiveAvailable {
    archivedSection
}

Helper:

@ViewBuilder
private var archivedSection: some View {
    Section {
        if viewModel.archivedSkills.isEmpty {
            Text("No archived skills — Curator will move stale skills here after the next review cycle.")
                .font(.callout)
                .foregroundStyle(.secondary)
        } else {
            ForEach(viewModel.archivedSkills) { skill in
                VStack(alignment: .leading, spacing: 4) {
                    HStack {
                        Text(skill.name)
                            .font(.body)        // semantic — content
                            .lineLimit(1)
                        Spacer()
                        if let category = skill.category, !category.isEmpty {
                            ScarfBadge(category, kind: .neutral)
                        }
                    }
                    HStack(spacing: 6) {
                        if let reason = skill.reason, !reason.isEmpty {
                            Text(reason)
                                .font(.caption)        // semantic — content
                                .foregroundStyle(.secondary)
                                .lineLimit(2)
                        }
                        Spacer()
                        Text(skill.archivedAtLabel)
                            .font(.caption2)
                            .foregroundStyle(.tertiary)
                    }
                    if let size = skill.sizeBytes, size > 0 {
                        Text(skill.sizeLabel)
                            .font(.caption2)
                            .foregroundStyle(.tertiary)
                    }
                }
            }
        }
    } header: {
        Text("Archived")
    } footer: {
        if !viewModel.archivedSkills.isEmpty {
            Text("Restore or prune archived skills from the Mac app.")
                .font(.caption)
        }
    }
}

Copy. Empty-state mirrors Mac's empty-state copy so the wiki / docs only need one phrasing. The "Restore or prune from the Mac app" footer is the read-only signpost.

Font choice. Skill name + reason → semantic .body / .caption (read for content). Category badge stays ScarfBadge (chrome). Date and size → .caption2 (chrome metadata).

3d. NO write actions

No per-row Restore button (WS-4 Mac surface adds this — iOS does not). No Prune All. The CuratorRestoreSheet Mac fallback for v0.12 hosts does NOT have an iOS counterpart and WS-9 does not introduce one. iOS users wanting to restore an archived skill use the Mac app — that's documented in the section footer.


4. iOS Gateway / Platforms read-only mirror (mirror WS-5) — extending existing iOS Settings → Platforms

Investigation result: iOS does NOT have a separate Gateway/ or Platforms/ directory. Gateway / platform configuration is surfaced through SettingsView.platformsSection (lines 280-288). WS-9 extends this section rather than spinning up a new feature module.

Caveat. WS-5's plan markdown does not yet exist at scarf/docs/v2.8/WS-5-gateway-v0.13-plan.md (verified — the dir contains WS-2/3/4/6/7/8 only). The Mac-side WS-5 plan is forthcoming. WS-9 is forced to make best-inference assumptions about the Mac-side model field names. The capability flags themselves DO exist (hasGoogleChatPlatform, hasGatewayAllowlists, hasGatewayBusyAckToggle, hasGatewayRestartNotification, hasGatewayList) and the surface contract per the user prompt is:

  • Show Google Chat as a new platform entry (read-only)
  • Show allowlists as read-only chip-rows ("3 allowed channels: ..., 4 allowed chats: ...")
  • Show platform-specific toggles as read-only state badges ("Restart notifications: ON", "Busy ack: OFF")

WS-9 mirrors that contract. Concrete model fields are flagged in Open Questions §3 below — the implementer should sync with the WS-5 author before merging.

File: Scarf iOS/Settings/SettingsView.swift

4a. Capability gate

Add the env-injected capability store (it's not currently read in SettingsView):

@Environment(\.hermesCapabilities) private var capabilitiesStore

private var caps: HermesCapabilities {
    capabilitiesStore?.capabilities ?? .empty
}

4b. Extend platformsSection

The current section (lines 280-288) renders five rows: Discord require-mention, Discord auto-thread, Telegram require-mention, Slack reply-to-mode, Matrix require-mention. WS-9 appends:

@ViewBuilder
private var platformsSection: some View {
    Section("Platforms") {
        // Existing rows (lines 282-286) — UNCHANGED.
        yesNoRow("Discord: require mention", vm.config.discord.requireMention)
        yesNoRow("Discord: auto-thread", vm.config.discord.autoThread)
        yesNoRow("Telegram: require mention", vm.config.telegram.requireMention)
        LabeledContent("Slack: reply mode", value: vm.config.slack.replyToMode)
        yesNoRow("Matrix: require mention", vm.config.matrix.requireMention)

        // v0.13 additions (gated).
        if caps.hasGoogleChatPlatform {
            googleChatSubsection
        }
        if caps.hasGatewayBusyAckToggle {
            yesNoRow("Gateway: busy ack", vm.config.gateway.busyAckEnabled)
        }
        if caps.hasGatewayRestartNotification {
            yesNoRow("Gateway: restart notification", vm.config.gateway.restartNotificationEnabled)
        }
        if caps.hasGatewayAllowlists {
            allowlistsSubsection
        }
    }
}

Field-name caveat. The exact field names on HermesConfig.gateway.* and HermesConfig.googleChat.* are TBD by WS-5. Provisional field names used above (busyAckEnabled, restartNotificationEnabled, googleChat.requireMention, etc.) MUST be aligned with the WS-5 model definitions before this code lands. See Open Questions §3.

4c. Google Chat subsection

@ViewBuilder
private var googleChatSubsection: some View {
    yesNoRow("Google Chat: require mention", vm.config.googleChat.requireMention)
    if let space = vm.config.googleChat.defaultSpace, !space.isEmpty {
        LabeledContent("Google Chat: default space", value: space)
    }
}

4d. Allowlists subsection — chip-row summaries

Read-only, summarized counts. Per the user prompt: "3 allowed channels: …, 4 allowed chats: …". On iOS the summary is collapsed (full lists are wide and a SwiftUI List row is narrow). Shape:

@ViewBuilder
private var allowlistsSubsection: some View {
    if let channels = vm.config.gateway.allowedChannels, !channels.isEmpty {
        DisclosureGroup {
            ForEach(channels, id: \.self) { ch in
                Text(ch)
                    .font(.callout.monospaced())
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
            }
        } label: {
            LabeledContent("Allowed channels") {
                Text("\(channels.count)")
                    .font(.callout)
                    .foregroundStyle(.secondary)
            }
        }
    }
    if let chats = vm.config.gateway.allowedChats, !chats.isEmpty {
        DisclosureGroup {
            ForEach(chats, id: \.self) { chat in
                Text(chat)
                    .font(.callout.monospaced())
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
            }
        } label: {
            LabeledContent("Allowed chats") {
                Text("\(chats.count)")
                    .font(.callout)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

UI choice. DisclosureGroup with the count in the label collapses well on iPhone (default-collapsed; the user can tap to expand). Avoids a wall-of-text in a small-screen list. No tap-to-edit (read-only).

4e. NO write actions on iOS Platforms

No editor sheet for Google Chat. No allowlist editor. No toggle switches that send hermes config set. The existing quickEditsSection (lines 84-117) does drive setSetting(key, value) for "v1Editable" specs — WS-9 does NOT add the v0.13 platform fields to SettingSpec.v1Editable. That's a Mac-only concern in v2.8.0.


5. iOS v0.13 features-active badge (Settings)

File: Scarf iOS/Settings/SettingsView.swift

5a. Capability check — semver, not a single flag

Per the prompt: "Capability-gate on caps.semver >= 0.13.0." The HermesCapabilities struct (verified at Packages/ScarfCore/Sources/ScarfCore/Services/HermesCapabilities.swift) exposes atLeastSemver(_:_:_:) — a private helper. The simplest public hook is to use any one of the v0.13-gated flags as the proxy (e.g. caps.hasGoals) since they all resolve to the same >= 0.13.0 threshold; or expose a new public var isV013OrLater: Bool on HermesCapabilities. Recommend the latter for clarity:

Coordination requirement. WS-9 needs HermesCapabilities.isV013OrLater: Bool { atLeastSemver(0, 13, 0) }. If WS-1 didn't ship this, WS-9 adds it as a one-line addition to HermesCapabilities.swift. Cheap and keeps the badge gating honest. Alternative: piggy-back on caps.hasGoals and accept the semantic drift (the badge says "v0.13 features active" but is gated on the goals flag specifically). Recommend the new helper.

5b. Mount the badge above quickEditsSection

var body: some View {
    List {
        if let err = vm.lastError { /* unchanged */ }

        if caps.isV013OrLater {
            v013ActiveBadgeSection
        }

        if !vm.isLoading || vm.config.model != "unknown" {
            quickEditsSection
            // ... rest unchanged
        }
    }
    // ... unchanged modifiers
}

@ViewBuilder
private var v013ActiveBadgeSection: some View {
    Section {
        Button {
            showV013FeaturesSheet = true
        } label: {
            HStack(spacing: 8) {
                ScarfBadge("v0.13 features active", kind: .success)
                Spacer()
                Text("Learn more")
                    .font(.caption)
                    .foregroundStyle(.tint)
                Image(systemName: "chevron.right")
                    .font(.caption)
                    .foregroundStyle(.tertiary)
            }
        }
        .buttonStyle(.plain)
    }
    .listRowBackground(ScarfColor.success.opacity(0.06))
}

State. Add @State private var showV013FeaturesSheet = false near the top.

Color. .success (green) — the host has new capabilities, framing as positive. Distinct from the warning-tinted error banner above it.

5c. "Learn more" sheet

.sheet(isPresented: $showV013FeaturesSheet) {
    V013FeaturesSheet()
}

New file Scarf iOS/Settings/V013FeaturesSheet.swift (~80 lines):

import SwiftUI
import ScarfDesign

struct V013FeaturesSheet: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            List {
                Section {
                    featureRow(
                        icon: "scope",
                        title: "Persistent goals",
                        description: "Type /goal <text> in chat to lock the agent on a target across turns. Mac only in v2.8."
                    )
                    featureRow(
                        icon: "tray.full",
                        title: "ACP /queue",
                        description: "Queue prompts to run after the current turn finishes. Mac only in v2.8."
                    )
                    featureRow(
                        icon: "stethoscope",
                        title: "Kanban diagnostics",
                        description: "Worker distress signals (heartbeat stalls, retry caps, zombies) surface on the task detail."
                    )
                    featureRow(
                        icon: "questionmark.diamond.fill",
                        title: "Hallucination gate",
                        description: "Worker-created cards are flagged for verify/reject. Verify on the Mac app."
                    )
                    featureRow(
                        icon: "archivebox",
                        title: "Curator archive",
                        description: "Stale skills move to an Archived list. Restore or prune from the Mac app."
                    )
                    featureRow(
                        icon: "bubble.left.and.bubble.right",
                        title: "Google Chat platform",
                        description: "New gateway target — configure on the Mac app."
                    )
                } header: {
                    Text("What's new in v0.13")
                } footer: {
                    Text("This iOS release surfaces v0.13 features read-only. Editing lives in the Mac app for v2.8.")
                        .font(.caption)
                }
            }
            .navigationTitle("v0.13 features")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Done") { dismiss() }
                }
            }
        }
    }

    private func featureRow(icon: String, title: String, description: String) -> some View {
        HStack(alignment: .top, spacing: 12) {
            Image(systemName: icon)
                .foregroundStyle(.tint)
                .font(.title3)
                .frame(width: 28)
            VStack(alignment: .leading, spacing: 4) {
                Text(title).font(.body.weight(.semibold))
                Text(description)
                    .font(.callout)
                    .foregroundStyle(.secondary)
            }
        }
        .padding(.vertical, 4)
    }
}

Copy is the load-bearing piece. Each row is one sentence; the read-only-on-iOS framing is in the section footer. No deep links to the relevant tab — that's a v2.8.x polish, not WS-9.


Coordination with WS-2 / WS-3 / WS-4 / WS-5

WS-9 consumes models / fields / capability flags from earlier work-streams. WS-9 must land AFTER all of them merge to main.

Consumed surface Source WS Consumed at
HermesActiveGoal model WS-2 iOS goal pill (§1)
HermesQueuedPrompt model WS-2 iOS queue chip (§1, no popover)
RichChatViewModel.activeGoal observable WS-2 iOS goal pill (§1)
RichChatViewModel.queuedPrompts observable WS-2 iOS queue chip (§1)
HermesCapabilities.hasGoals WS-1 iOS chat (§1)
HermesCapabilities.hasACPQueue WS-1 iOS chat (§1)
HermesKanbanTask.maxRetries WS-3 iOS Kanban detail (§2b)
HermesKanbanTask.autoBlockedReason WS-3 iOS Kanban detail (§2d)
HermesKanbanTask.hallucinationGateStatus + KanbanHallucinationGate WS-3 iOS Kanban detail (§2c)
HermesKanbanTask.diagnostics + HermesKanbanDiagnostic + KanbanDiagnosticKind WS-3 iOS Kanban detail (§2e–§2f)
HermesKanbanRun.diagnostics WS-3 iOS Kanban detail (§2f)
HermesCapabilities.hasKanbanDiagnostics WS-1 iOS Kanban detail (§2a)
HermesCuratorArchivedSkill model WS-4 iOS Curator (§3)
CuratorViewModel.archivedSkills + loadArchive() WS-4 iOS Curator (§3)
CuratorService.listArchived() WS-4 (transitively via VM in §3)
HermesCapabilities.hasCuratorArchive WS-1 iOS Curator (§3)
HermesConfig.gateway.allowedChannels / .allowedChats (TBD field names) WS-5 iOS Settings (§4d)
HermesConfig.gateway.busyAckEnabled / .restartNotificationEnabled (TBD) WS-5 iOS Settings (§4b–§4c)
HermesConfig.googleChat.* (TBD shape) WS-5 iOS Settings (§4c)
HermesCapabilities.hasGoogleChatPlatform / .hasGatewayAllowlists / .hasGatewayBusyAckToggle / .hasGatewayRestartNotification WS-1 iOS Settings (§4)
HermesCapabilities.isV013OrLater (NEW — see §5a) WS-1 (small follow-up) iOS Settings badge (§5)
  1. WS-2 (Goals + queue VM scaffolding) merges → iOS chat goal pill becomes wireable.
  2. WS-3 (Kanban diagnostics models) merges → iOS Kanban detail extension becomes wireable.
  3. WS-4 (Curator archive service + VM state) merges → iOS Curator section becomes wireable.
  4. WS-5 (Gateway / Platforms config models + capability flags consumed) merges → iOS Settings extension becomes wireable.
  5. WS-9 PR opens, builds against the merged baseline, ships all five additions in one PR.

Splitting WS-9 into per-mirror PRs is overkill — each diff is small, all gated, all read-only.

Acceptable to land WS-9 in stages

If WS-5 slips, WS-9 can ship items 1-3-4-5 first (the WS-2/3/4 mirrors plus the badge) and follow up with item 6 (Gateway/Platforms mirror) once WS-5 lands. The badge is independent of any mirror item — it can ship the moment WS-1 capability flags are in (already done).


Files to change / create

File Status Purpose
Scarf iOS/Chat/ChatView.swift EDIT Goal pill + queue chip in projectContextBar (§1)
Scarf iOS/Kanban/ScarfGoKanbanDetailSheet.swift EDIT Diagnostics + max_retries + hallucination badge + auto-blocked banner (§2)
Scarf iOS/Kanban/DiagnosticDetailSheet.swift NEW Tap-target sheet showing one diagnostic's full message + timestamp (§2e)
Scarf iOS/Curator/CuratorView.swift EDIT Archived section + capability gate + extra .task load (§3)
Scarf iOS/Settings/SettingsView.swift EDIT v0.13 badge section + Platforms section extension (§4, §5)
Scarf iOS/Settings/V013FeaturesSheet.swift NEW "Learn more" sheet for the v0.13-features badge (§5c)
Packages/ScarfCore/Sources/ScarfCore/Services/HermesCapabilities.swift EDIT (1 line) public var isV013OrLater: Bool helper if not already present (§5a)

Total: 7 files (2 new), ~350-450 lines. ~80% of the diff is the new sheets and the iOS Kanban detail extension.


Capability gating

Every WS-9 surface is hard-gated. Pre-v0.13 hosts see the v2.7.5 iOS surface unchanged.

Surface Gate Pre-v0.13 behaviour
iOS goal pill caps.hasGoals && vm.activeGoal != nil hidden (transitive impossibility — pill goes nil because Mac doesn't write it)
iOS queue chip caps.hasACPQueue && !vm.queuedPrompts.isEmpty hidden
iOS Kanban max_retries chip caps.hasKanbanDiagnostics && task.maxRetries != nil hidden (if let belt-and-suspenders even if cap leaks)
iOS Kanban hallucination badge caps.hasKanbanDiagnostics && KanbanHallucinationGate.from(...) == .pending hidden
iOS Kanban auto-blocked banner caps.hasKanbanDiagnostics && status == .blocked && reason != nil hidden
iOS Kanban diagnostics blocks (task + run) caps.hasKanbanDiagnostics && !diagnostics.isEmpty hidden
iOS Curator Archived section caps.hasCuratorArchive section absent; loadArchive() not invoked
iOS Settings v0.13 badge caps.isV013OrLater section absent
iOS Settings Google Chat row caps.hasGoogleChatPlatform row absent
iOS Settings Busy ack row caps.hasGatewayBusyAckToggle row absent
iOS Settings Restart notification row caps.hasGatewayRestartNotification row absent
iOS Settings Allowlists rows caps.hasGatewayAllowlists rows absent

Defensive default. Every capabilitiesStore?.capabilities ?? .empty resolves the absent-store case to false for every flag. WS-1's .empty static is the explicit pre-v0.13 sentinel (verified — used elsewhere in iOS already at HermesVersionBanner.swift:14).

No new capability flags. WS-9 adds at most one helper (isV013OrLater) to HermesCapabilities. All other flags are already shipped by WS-1.


How to test

Per CLAUDE.md "remote-servers dogfooding" memory: dogfood against the Mardon Mac Mini at 192.168.0.82 (running the v0.13 binary on the remote-servers branch).

iOS simulator scenarios — v0.13 host

  1. Goal pill

    • Open the iOS chat against a v0.13 host. Switch to the Mac, run /goal finish v2.8 by Friday in the same session. Switch back to iOS — within 2-3 polled state refreshes the pill should appear in projectContextBar with truncated text "finish v2.8 by Friday".
    • VoiceOver: focus the pill, confirm full text reads as "Goal locked: finish v2.8 by Friday".
    • Run /goal --clear from Mac. Confirm pill drops on iOS.
    • Without an active project (chat without projectContextBar triggered today), confirm the bar STILL shows when the goal pill is the only chip — i.e. the bar is no longer project-only. Without a goal AND without a project, confirm the bar stays hidden.
  2. Queue chip

    • Trigger a long-running prompt on Mac, send /queue summarize while it's working. Confirm iOS shows "1 queued" chip in the bar.
    • When the Mac turn finishes and the queued prompt fires, confirm the iOS chip count decrements.
  3. Kanban diagnostics

    • Open the iOS Kanban detail sheet for a task with max_retries: 3. Confirm the "retries: 3" chip shows in the header.
    • Open a task in pending hallucination state. Confirm the yellow "Worker-created — verify on Mac" badge appears below the chip row.
    • Open a blocked task with auto_blocked_reason. Confirm the red "Auto-blocked" banner shows the reason verbatim.
    • Open a task with task-level diagnostics. Confirm the chip-list renders. Tap one — confirm the detail sheet opens with kind + message + timestamp.
    • Open a task whose latest run has darwin_zombie_detected. Confirm the per-run diagnostics chip-list renders inside the Runs tab row.
  4. Curator Archived list

    • On v0.13 host with no archives: confirm Archived section renders with empty-state copy.
    • On v0.13 host with 3 archives: confirm rows show name, category badge, reason, archived-at label, size. No Restore button. Footer hint visible.
    • Pull-to-refresh: confirm loadArchive() re-fires.
  5. iOS Settings v0.13 badge

    • On v0.13 host: confirm the green "v0.13 features active" badge sits above the Quick edits section. Tap "Learn more" — confirm the sheet opens with 6 feature rows.
    • Tap Done — confirm dismissal.
  6. iOS Settings Platforms additions

    • On v0.13 host with Google Chat configured: confirm the Google Chat rows show. Tap is read-only (no nav).
    • With at least 3 allowed channels and 4 allowed chats configured: confirm both DisclosureGroup rows show with the correct counts. Expand each — confirm the entries render in monospaced font.
    • With Busy ack OFF and Restart notifications ON: confirm both rows show the right yes/no labels.

iOS simulator scenarios — pre-v0.13 host (regression smoke)

  1. Connect to a Hermes v0.12 host (Mardon downgrade or local dev install).
  2. Verify:
    • projectContextBar looks unchanged from v2.7.5 (no goal pill, no queue chip).
    • Kanban detail sheet: no max_retries chip, no hallucination badge, no auto-blocked banner, no diagnostics blocks. v2.7.5 layout intact.
    • Curator: no Archived section. Existing runNow / pause / resume / pin actions work.
    • Settings: no v0.13 badge. Platforms section shows the 5 v2.7.5 rows only.
  3. Tap through every existing iOS surface to confirm no regressions.

Dynamic Type accessibility smoke

Per CLAUDE.md: iOS clamps Dynamic Type at the scene root (ScarfIOSApp.swift: .dynamicTypeSize(.xSmall ... .accessibility2)). Verify at both extremes:

  1. Settings → Accessibility → Display & Text Size → set to AX2.
  2. Open chat: confirm goal pill text scales (semantic .subheadline should). Confirm pill chrome doesn't blow out — the truncation kicks in.
  3. Open Kanban detail: confirm body text + diagnostics chip text scale. Badges (ScarfBadge) should NOT scale (they're chrome).
  4. Open Curator Archived list: confirm skill name + reason scale. Archived-at label stays small.
  5. Open Settings v0.13 sheet: confirm description text scales.
  6. Switch to xSmall: confirm nothing collapses in a way that's unreadable.

Build + test gates

  • xcodebuild -project scarf/scarf.xcodeproj -scheme "scarf mobile" -destination 'platform=iOS Simulator,name=iPhone 15' build must succeed.
  • All existing iOS UI smoke tests (if present in the target) stay green.
  • New iOS-side snapshot or UI tests are NOT planned for WS-9 — the surfaces are read-only and visual; manual verification is the right pass for v2.8.0.

Open questions

  1. Does iOS Curator surface exist today? Confirmed yes. Scarf iOS/Curator/CuratorView.swift exists and is read-mostly with runNow / pause / resume / pin / unpin actions. WS-9 mirror item 4 (Curator Archived list) is in scope. (The user prompt anticipated this might be unknown.)

  2. iOS goal/queue chip — is the queue chip tap a no-op or does it open a previews sheet? Recommend tap = no-op for v2.8.0 (read-only badge, mirroring the goal pill's no-op tap). A previews sheet is nice-to-have but doesn't cross the bar for v2.8 — the user can see queued prompts from the Mac app. If review pushes back, a 30-line sheet listing previews + queued-at timestamps is cheap to add.

  3. WS-5 plan does not yet exist (scarf/docs/v2.8/WS-5-gateway-v0.13-plan.md is missing). The exact HermesConfig.gateway.* and HermesConfig.googleChat.* field names are TBD. Action: before WS-9 implementation starts, sync with the WS-5 author to align on:

    • Where do the allowlists live? HermesConfig.gateway.allowedChannels: [String]? or HermesConfig.platforms.<each>.allowedChannels?
    • Are restart-notifications and busy-ack global (one toggle) or per-platform (one per Discord/Slack/Telegram/Matrix/Google-Chat)?
    • Is "busy ack" the right wire name? Hermes might call it busy_acknowledge or busy_indicator.
    • Does Google Chat use the same requireMention shape as Discord/Telegram/Matrix?

    WS-9's Settings extensions (§4) are correct in shape but need the field-name patches once WS-5 confirms. The capability flags are stable.

  4. HermesCapabilities.isV013OrLater helper. WS-1 may or may not have shipped this. If not, WS-9 ships a one-line addition. If caps.hasGoals is acceptable as a proxy (since all v0.13 flags resolve to the same threshold), the helper isn't strictly needed — but the badge copy says "v0.13 features active" so semantic alignment matters. Coordinator should pick one.

  5. projectContextBar re-render frequency. Today it renders only when there's a project. After WS-9, it renders when there's a project OR a goal OR a queued prompt. The added re-render churn during streaming (every diff to vm.activeGoal / vm.queuedPrompts) may matter for ScarfMon's chatRender budget. Action: add a ScarfMon counter to the bar's body to measure during dogfooding. If churn becomes a hot-path issue, extract goalChip and queueChip into separately-scoped subviews so they re-render in isolation.

  6. Animation on pill / chip appearance. Should the goal pill fade in when vm.activeGoal becomes non-nil? Recommend yes — .transition(.opacity.combined(with: .scale(scale: 0.9))) with a .spring(response: 0.3, dampingFraction: 0.7) parent animation. Keeps the bar from feeling like it pops. Apply same to the queue chip and the Kanban hallucination badge.

  7. Tap target for the Kanban hallucination badge. Currently planned as informational-only. Should tapping it open an alert with explanation copy + a "Open in Mac app" placeholder action? Recommend NO for v2.8.0 — the on-screen "verify on Mac" copy is enough; an alert is unnecessary friction for a read-only surface.

  8. iOS deep links from the v0.13 features sheet. Tapping a feature row could deep-link to the relevant tab (e.g. tap "Hallucination gate" → switch to Kanban tab). Recommend defer — the v2.8.0 sheet is text-only. v2.8.x can add the routing.


Out of scope (deferred to v2.8.x or later)

  • iOS write surfaces for everything WS-9 mirrors:
    • /goal and /queue send from iOS chat composer.
    • Verify / Reject buttons on the iOS Kanban detail sheet.
    • Archive / Restore / Prune on the iOS Curator surface.
    • Allowlist editor / platform toggle editor in iOS Settings.
  • Gateway/Platforms iOS feature module from scratch (separate Scarf iOS/Gateway/ or Scarf iOS/Platforms/ dir). v2.8.0 keeps gateway/platform config as an extension to SettingsView.platformsSection.
  • iOS Curator Archive live updates beyond pull-to-refresh + the existing .task invocation. Hermes hasn't shipped a curator-watch surface; iOS won't either.
  • iOS Kanban hallucination badge tap-to-explain alert — recommend not adding (see Open Question #7).
  • iOS Kanban diagnostics history graph — Mac WS-3 also defers this. iOS follows.
  • iOS deep links from v0.13 features sheet — see Open Question #8.
  • Snapshot tests for the new iOS sheets — manual verification is the v2.8.0 pass.
  • Localization — every new copy string is English-only. Existing iOS surfaces aren't localized either; WS-9 stays consistent.
  • iOS Goal pill custom font / pill chrome migration to a ScarfDesign component — keep inline. If Mac WS-2 lands a reusable ScarfGoalPill component in the design package, swap iOS to use it as a follow-up.
  • iOS goal-state persistence across app suspends — relies on the Mac VM state being authoritative. iOS just renders what it polls. If this matters in dogfooding (user perceives a stale pill after a long suspend), revisit.
  • Telemetry counters for new iOS surfaces (e.g. ScarfMon counter on goal-pill appearance). Add if dogfooding surfaces a perf signal; otherwise ship without.
  • Per-platform notification re-routing toggles on iOS (e.g. "send Google Chat alerts to APNS"). Out of scope — APNS routing already lives in Notifications/NotificationRouter.swift and is platform-agnostic.

Estimate

Engineering hours (one engineer, focused), assuming WS-2 / WS-3 / WS-4 / WS-5 are merged to main:

Block Hours
iOS chat goal pill + queue chip in projectContextBar (§1) 2
iOS Kanban detail sheet — chips + banners + diagnostics blocks + tap sheet (§2) 5
iOS Kanban DiagnosticDetailSheet.swift (NEW, ~30 LOC) 1
iOS Curator Archived section (§3) 2
iOS Settings Platforms extension + capability env injection (§4) 3
iOS Settings v0.13 badge + sheet (§5, including new sheet file) 2
HermesCapabilities.isV013OrLater helper (if not present) 0.5
Manual smoke on iPhone simulator (v0.13 + v0.12 hosts) + Dynamic Type pass 3
Code review + revisions 2
Buffer for WS-5 field-name alignment (Open Q #3) 1.5
Total ~22 hours (≈3 working days)

Confidence: medium-high. All five items are mechanical given the existing iOS surface scaffolding (projectContextBar, ScarfGoKanbanDetailSheet, CuratorView, SettingsView.platformsSection). The only real risk is WS-5 field-name drift — captured in Open Question #3 — and it's contained to mirror item 4 (Settings → Platforms extensions). If WS-5 slips, mirror items 1-3-5 ship first; item 6 (Platforms) follows once WS-5 lands.

Critical-path dependency: WS-2, WS-3, WS-4, WS-5 must all be on main before WS-9 PR opens. WS-9 is the final "iOS catch-up" PR of the v2.8.0 release cycle.

Risk register:

  • WS-5 field-name drift. Mitigated by Open Question #3 sync with the WS-5 author before implementation; Settings extensions stub clearly-named provisional field names that fail-fast at compile if WS-5 ships different names.
  • Dynamic Type churn. Goal pill and Kanban diagnostics blocks are content-text — they scale. Verify nothing collapses at AX2; truncation strategies in §1b and the FlowLayout primitive in §2e are the v2.7.5 patterns and known-good.
  • projectContextBar re-render churn. Open Question #5 captures this. Add a ScarfMon counter; revisit if dogfooding shows a hot-path issue.
  • iOS Kanban polling cadence — the existing 5s poll picks up the new fields automatically. No new polling logic required.
  • No iOS test coverage regression. WS-9 doesn't add tests but doesn't remove any either. The shared RichChatViewModel / CuratorViewModel / KanbanService tests in ScarfCore (extended by WS-2/3/4) cover the model + state-machine layer; iOS-specific UI is verified manually in v2.8.0.