Scarf Updates https://awizemann.github.io/scarf/appcast.xml Scarf macOS app updates en Version 2.7.5 34 2.7.5 14.6 Fri, 08 May 2026 10:56:09 +0000

What's in 2.7.5

A feature release that lifts Scarf's Kanban surface from a read-only list (the v2.6 placeholder shipped while upstream Kanban was still mid-rework) to a full drag-and-drop board with the complete Hermes v0.12 mutation surface wired up — plus per-project boards bound to a Scarf-minted tenant slug, and a read-only board on iOS for at-a-glance status from your phone. No data migrations, no schema changes; pre-v0.12 hosts gracefully hide the surface.

New features

Mac

  • Drag-and-drop Kanban board (scarf/Features/Kanban/Views/KanbanBoardView.swift). Five visible columns — Triage / Up Next (todo + ready) / Running / Blocked / Done — collapsing Hermes's seven status values into a layout that doesn't waste space on ready, which the dispatcher only ever holds for a few seconds. Triage hides itself when empty; archived hides behind a header toggle. Drop a card onto a column and Scarf maps the gesture to the right Hermes verbs through a pure transition planner: drop-on-Running fires kanban dispatch (the dispatcher then spawns a worker), drop-on-Blocked opens a sheet asking for a reason and calls kanban block, drop-on-Done opens a result sheet and calls kanban complete, blocked → running chains unblock + dispatch. Forbidden transitions (anything dropped on Done; anything dragged out of Triage) reject with a red drop-target stroke and a tooltip explaining why — Done is terminal, Triage is promoted by a specifier worker, neither has a CLI verb that maps cleanly. Optimistic local updates apply on drop and revert on CLI failure with a toast, so the UI feels instant.
  • Side-pane inspector (KanbanInspectorPane.swift). Click a card and a 420 px pane slides in from the trailing edge. Not a modal sheet — modal would block triaging the next card after closing. Header carries the status, an inline assignee menu (more on that below), workspace kind, and tenant; below that, four tabs render hermes kanban show <id> data: Comments (with an inline composer that calls kanban comment), Events (the task_events log with per-kind glyphs), Runs (one row per attempt with outcome badge + summary + error), and Log — the worker's captured stdout/stderr from hermes kanban log <id>, polled every 2 s while the task is running with a "● streaming" indicator and auto-scroll to the latest line, snapshot-only with a refresh button when the task is in a terminal state. The action bar at the bottom has all the per-status verbs — Start (which is claim rebranded as a user-visible action), Complete, Block, Unblock, Archive — every one with a help tooltip explaining what it does and what Hermes verb it invokes. The "Archive" tooltip explicitly notes Hermes has no hard-delete: archived tasks remain in ~/.hermes/kanban.db and are recoverable via the "Show archived" toggle until hermes kanban gc runs.
  • Inspector auto-refresh. While the inspector is open, the detail (header, action buttons, comments, events, runs) re-fetches every 5 s on the same cadence as the board itself, so a worker transition (e.g. running → done elsewhere) is reflected without the user having to close + reopen. The Log tab's 2 s poll runs separately and self-cancels the moment the task transitions out of running.
  • Inline assignee picker on the inspector header. The assignee badge is a clickable menu — set means a .brand (rust) chip, unassigned means a .warning (yellow) chip so the eye catches it instantly. Tapping opens a menu of every known profile (union of ~/.hermes/profiles/, current task assignees, and the active local profile from HermesProfileResolver) plus an "Unassigned" option. Selection routes through kanban assign and immediately follows with kanban dispatch so the task gets picked up promptly. Solves the "I assigned a profile but nothing happened" gap end-to-end without the user touching a terminal.
  • Health banner in the inspector. Surfaces two conditions that previously left users staring at a stuck task with no explanation. Yellow when the task is unassigned in ready / todo: "Won't run automatically — Hermes's dispatcher silently skips tasks with no assignee." The dispatcher's own --json output literally lists these under skipped_unassigned; we now surface that to the human. Red when the most-recently-completed run ended in a non-success outcome (stale_lock / crashed / gave_up / timed_out / spawn_failed / reclaimed / failed): banner displays the outcome label + the raw error field from the run record, so you don't have to dig into the Runs tab to discover it. The red banner is suppressed while a fresh attempt is running — once status flips back to running, the previous outcome is stale signal and the Log tab's live stream is the right thing to look at.
  • Card-level signals. Cards in running get a 2 px ScarfColor.info left edge + a subtle title shimmer so live work is obvious at a glance. Blocked cards get a 2 px ScarfColor.warning left edge + a ⚠ glyph next to the title. Done cards dim to 0.7 opacity in light mode, 0.55 in dark, with a green ✓ in the title row. Cards in ready / todo with no assignee get a yellow ⚠ glyph in the title row with a tooltip explaining the dispatcher won't pick them up — same signal as the inspector banner, just at the board level so triage is one keypress away.
  • Board | List toggle at the top of the route. The v2.6 read-only list view is preserved in KanbanListView.swift and surfaced via a segmented picker, so users on narrow windows or anyone who prefers a flat sortable list can opt in. Choice persists across launches via @AppStorage.
  • New Task sheet (KanbanCreateSheet.swift). Title, body (markdown supported), assignee (defaults to HermesProfileResolver.activeProfileName() so newly-created tasks actually run), workspace kind (segmented Scratch / Worktree / Project Dir; locked to Project Dir on per-project boards), priority slider, comma-separated skills with autocomplete from ~/.hermes/skills/, optional tenant (hidden on per-project boards — the slug is implicit), and a "Send to triage" toggle. Submit fires kanban create --json and immediately follows with kanban dispatch so an assigned task transitions readyrunning within seconds rather than waiting for the gateway dispatcher's internal cycle.
  • Kanban moved from Manage → Monitor in the sidebar. It's runtime work-in-progress, not configuration. Sits between Activity and the rest of Manage so users see "what's happening right now" at a glance.

Per-project Kanban

  • DashboardTab.kanban on every project, capability-gated on HermesCapabilities.hasKanban. Renders a project-scoped KanbanBoardView filtered to the project's tenant slug. Workspace defaults in the New Task sheet are pre-pinned to dir:<project.path>. Empty state explains the project doesn't have any tasks yet and offers a "New Task" CTA — the empty board IS the discovery surface.
  • Tenant minting via KanbanTenantResolver. Each Scarf project gets a stable scarf:<slug> tenant minted on first kanban interaction and persisted to <project>/.scarf/manifest.json (new optional kanbanTenant field on ProjectTemplateManifest). Slug rules: lowercased, hyphenated, ≤ 48 chars, scarf: prefix to avoid collision with hand-typed tenants. Once minted, the tenant is immutable across rename — tasks already on the board carry the original slug, so renaming the project doesn't orphan them. Bare projects (no manifest) get a sentinel manifest written with id: scarf/<project-id> + version: 0.0.0 + just the kanbanTenant set; the ProjectAgentContextService reader recognizes the sentinel and refuses to surface it as a "Template" line in the AGENTS.md block, so the project doesn't suddenly start advertising a fake template to the agent.
  • Agent-side tenant injection. ProjectAgentContextService.renderBlock emits a "Kanban tenant" line inside the <!-- scarf-project --> markers in <project>/AGENTS.md whenever a tenant exists, instructing the agent to pass --tenant scarf:<slug> on hermes kanban create. ChatViewModel.startACPSession already calls refresh(for:) before opening every project chat, so the agent reads a fresh tenant on every session start with no extra wiring. Agents are imperfect at flag discipline; a forgotten --tenant lands the task in the global "Untagged" group rather than failing — acceptable v2.7.5 behavior.
  • kanban_summary dashboard widget (KanbanSummaryWidgetView.swift). New widget kind for project dashboards: shows the top three running / blocked / todo tasks for the project's tenant by priority, plus a glance footer ("12 todo · 3 running · 5 blocked") sourced from kanban stats. Polls every 10 s while the dashboard is foregrounded. Widget vocabulary registered in tools/widget-schema.json and rendered on the catalog site via site/widgets.js; template authors can drop a { kind: kanban_summary, max_rows: 3 } block into dashboard.json.

iOS / iPadOS

  • Read-only Kanban tab on ProjectDetailView (Scarf iOS/Kanban/ScarfGoKanbanView.swift). Same five-column collapse rendered as a horizontally-paged segmented Picker of single-column lists — HIG-friendly on iPhone where a 5-column grid forces unreadable card widths. Pulls live status, assignee, workspace, skills, priority chips. Tap a card → modal NavigationStack detail sheet (ScarfGoKanbanDetailSheet.swift) with the same Comments / Events / Runs tabs the Mac inspector has. Read-only in v2.7.5 — mutations + drag-drop on iPad land in v2.8 once the Mac flow is fully shaken out. Card titles use semantic .headline (not ScarfFont) so Dynamic Type works; chrome (badges) stays on ScarfBadge for fixed visual weight per the project's iOS conventions.

ScarfCore

  • KanbanService actor (Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift) — pure-I/O Sendable actor wrapping every Hermes v0.12 verb (list / show / runs / stats / assignees / create / assign / claim / comment / complete / block / unblock / archive / dispatch / link / unlink / log). Dispatches each CLI invocation through Task.detached(priority: .utility) matching the existing concurrency conventions. Errors land in KanbanError and surface as inline banners (not modal alerts) since the board is high-frequency. The "no matching tasks" stdout sentinel is normalized to [] rather than thrown.
  • Pure transition planner. KanbanService.plan(for: KanbanTransition) is a synchronous function that maps a (from, to) column pair to the right verb sequence — (.upNext, .running) → [.dispatch], (.blocked, .running) → [.unblock, .dispatch], etc. Disallowed transitions throw KanbanError.forbiddenTransition with a user-actionable reason. The planner is fully tested in KanbanModelsTests.swift. Critically: dispatch (not claim) is the verb used for Up-Next → Running. Hermes's claim is documented as "manual alternative to the dispatcher" and assumes the caller spawns the worker themselves — Scarf doesn't, so calling claim from drag-drop reserved tasks but never spawned work, and the dispatcher reclaimed them ~15 minutes later (stale_lock). dispatch is the right primitive for a GUI client.
  • Cross-platform KanbanTenantReader. Read-only projection over <project>/.scarf/manifest.json's kanbanTenant field. The full ProjectTemplateManifest type lives in the Mac target; this lightweight reader gives iOS a way to filter the per-project board by tenant without linking the full manifest model.
  • Timestamp decoding tolerates both shapes. Hermes emits created_at / started_at / completed_at / last_heartbeat_at etc. as Unix integer seconds (its SQLite columns are INTEGER), but earlier wire docs implied ISO-8601 strings. The decoder now accepts either an integer or a string and normalizes to ISO-8601 so downstream code only handles one type. Locked in by decodeUnixIntegerTimestamps in KanbanModelsTests.
  • KanbanBoardViewModel optimistic merge. Holds optimisticOverrides: [taskId: status] for in-flight drags; the polled response merges with optimistic state until the server confirms the new status, so a stale poll arriving milliseconds after a drop can't snap the card back to its old column. On CLI failure the override is removed and the message lands in the inline banner.

Dispatch + assignee fixes

A diagnostic round driving real tasks end-to-end exposed a connected bug pattern that the polish pass closed:

  • Hermes's dispatcher silently skips unassigned tasks — its kanban dispatch --json output literally lists them under a skipped_unassigned key and moves on. Tasks created without an assignee sat in ready indefinitely and the user had no signal anything was wrong. The New Task sheet now defaults to the active Hermes profile, the inspector header shows a yellow "Unassigned" chip + warning banner, every ready / todo card without an assignee gets a ⚠ glyph + tooltip, and the inspector's inline assignee picker fixes it in one click.
  • Drag-to-Running used to call claim, which is a manual alternative to the dispatcher. Status flipped to running, but no worker spawned (Scarf doesn't host workers), and 15 minutes later the dispatcher reclaimed the task with a stale_lock outcome. Replaced with dispatch end-to-end so the gateway-running dispatcher actually does the spawning.
  • hermes kanban assignees empty-state was leaking into the picker. The CLI prints a literal sentinel (no assignees — create a profile with hermes -p <name> setup) when the table is empty; the parser was tokenizing it on whitespace and offering (no as a profile in the menu. Parser now skips the sentinel, validates each candidate against ^[a-zA-Z0-9_-]+$, and falls back cleanly to the active local profile when the table is empty.
  • spawn_failed from "executable not found on PATH" — most subtle of the lot. macOS GUI apps inherit a launch-services PATH (/usr/bin:/bin:/usr/sbin:/sbin) that doesn't include ~/.local/bin (where pipx installs hermes) or /opt/homebrew/bin. Scarf was finding hermes for its own invocation via the absolute-path resolver in HermesPathSet.hermesBinaryCandidates, but when the dispatcher then spawned a worker process, that worker inherited Scarf's GUI PATH and couldn't find hermes by name — recording an outcome=spawn_failed run with the exact "executable not found on PATH" message. LocalTransport now grows an environmentEnricher static (mirroring SSHTransport.environmentEnricher) wired by scarfApp.swift to the same HermesFileService.enrichedEnvironment() login-shell probe the SSH transport uses. Every local subprocess Scarf spawns now sees the user's full PATH and credential env, so a spawned-from-Scarf hermes can spawn its children by name without reaching for absolute paths. Defense-in-depth: subprocessEnvironment(forExecutable:) also unconditionally prepends the executable's parent directory to PATH, so the fix works even if the enricher hasn't been wired (early startup, tests).

Migrating from 2.7.1

Sparkle will offer the update automatically. No config migration, no schema changes — ~/.hermes/kanban.db is shared across all Hermes clients and Scarf only reads/writes through the documented CLI surface. Existing Scarf projects pick up the new project Kanban tab on first open; the tenant slug is minted lazily on first kanban interaction inside the project, so projects with no kanban activity stay byte-identical until the user opens the tab.

If you have an existing project with a Scarf-managed manifest.json, the new optional kanbanTenant field is added on next mint and lives alongside any template-author config schema without touching it. Templates do not ship kanbanTenant (it's user-machine-scoped state); the export pipeline strips it.

If you've been running tasks via the v2.6 read-only list and your Hermes host already runs the gateway dispatcher, your existing kanban tasks should appear on the board automatically — there's no migration step. Tasks created without an assignee in v2.6 will now show the yellow "Unassigned" warning until you fix them through the inline picker.

Known limitations

  • Within-column reorder is not supported. Hermes has no update verb and no position column on the tasks table — priority is write-once at create time. Sort order inside each column is priority DESC, created_at DESC, matching the dispatcher's actual run order. We considered a client-side ordering sidecar; rejected because the on-screen order would diverge from what runs next, which is worse than no manual order. Will revisit if Hermes ships an update --priority verb.
  • No live watch streaming yet. The board polls every 5 s; the inspector polls detail on the same cadence and the Log tab on a 2 s cadence while running. hermes kanban watch --json event streaming + reconnect-with-backoff lands in v2.8 along with iOS write surfaces.
  • No bulk re-tag for legacy NULL-tenant tasks. Tasks created before this release (assignee or no assignee) appear in the global "Untagged" group on the global board. Hermes has no tenant mutation verb post-create, so retagging would be archive + recreate — too destructive to ship in this release.

Acknowledgements

  • Driven end-to-end against a fresh local Hermes v0.12.0 install with the gateway dispatcher running. Real bug surface mostly came from doing instead of speculating: the claim vs dispatch distinction, the silent skipped_unassigned behavior, the (no parse leak, the integer-vs-ISO timestamp shape, and the stale "Last run" banner during a fresh attempt all surfaced from driving real tasks and watching what actually happened.
]]>
Version 2.7.1 33 2.7.1 14.6 Thu, 07 May 2026 10:51:54 +0000

What's in 2.7.1

A patch release covering three bug reports filed against 2.7.0, plus follow-up cleanups in the same neighborhood. No data migrations, no UI surface changes — drop-in replacement for 2.7.0 on Mac.

Bug fixes

Mac

  • #77 — Sessions screen renders empty even when Dashboard reports sessions exist. v2.7.0 folded the Sessions tab's two SQL queries (sessions list + previews) into a single batched SSH round-trip for perf. The combined wire payload for any user with ~150+ sessions crossed macOS's 16–64 KB pipe-buffer threshold; without a concurrent reader draining the pipe, the remote sqlite3 -json blocked, the script never finished, our 30-second timeout fired, and the call returned an empty result. SSHScriptRunner now drains stdout/stderr concurrently with the running process via FileHandle.readabilityHandler, so the kernel pipe never fills. Same fix applied to the local-execution path. New regression test pushes 256 KB of synthetic output through the runner and asserts full delivery — would have wedged pre-fix.
  • #78 — Skills "What's New" pill contradicts the Updates sub-tab. The pill at the top of the Skills page was rendering on every sub-tab, including Updates. It counts local file deltas since the user last clicked "Mark as seen" (e.g. "18 new" = 18 skills landed on disk that you haven't acknowledged), while the Updates body runs hermes skills check to find skills with newer upstream versions available — a different concept. Two surfaces using the word "update" for two different things made the screen contradict itself. Two changes: the pill now renders only on the Installed sub-tab (Mac and ScarfGo), and its label says "X changed since you last looked" instead of "X updated" so the local-file vocabulary doesn't collide with upstream-update vocabulary anywhere on the page.
  • #79 — Skills hub search returns nothing for terms visible in Browse. With the source picker on "All Sources", hermes skills search <query> (no --source flag) routes through Hermes's centralized index and skips external API sources (skills-sh, github, clawhub, lobehub, well-known) — but Browse still aggregates from those sources, so a skill like honcho would show up in Browse and disappear in search. Same picker, same query, contradictory results. Rather than chase Hermes's index gaps, "All Sources" search now means "filter what you can already see": Scarf caches the most recent Browse payload and runs a client-side substring filter (case-insensitive against name, description, and identifier) against it, instantly. Source-specific searches still shell out to hermes skills search --source <s> for full upstream search semantics. Five new tests cover the filter behavior.
  • hermesPIDResult() — narrow the Hermes "is it running?" probe to the gateway. Previously pgrep -f hermes, which matched any process with "hermes" in its argv: chat sessions Scarf itself spawns, hermes -z one-shots, log tails, even the README in an editor. The Dashboard "Hermes is running" badge could read true even when the gateway daemon was down. Tightened to a regex that matches only the gateway shape — python -m hermes_cli.main gateway run … and /path/to/hermes gateway run …. All callers (DashboardViewModel, HealthViewModel, SettingsViewModel, scarfApp, stopHermes) want the gateway PID specifically. Cherry-picked from #76 — thanks to @unixwzrd for the diagnosis and regex.
  • HealthViewModel.stopDashboard() — stop the dashboard by port, not pkill -f. External-instance fallback used to be pkill -f "hermes dashboard", broad enough to match shell history, log tails, README readers — anything with the substring in its argv. Now lsof -tiTCP:<port> -sTCP:LISTEN resolves the PID actually bound to the dashboard port and only that one process gets SIGTERM. Trusting the port is correct here: Scarf owns the configured port and the user-visible intent is "stop the thing on this port." Direction cherry-picked from #76; the -c hermes filter from the original was dropped because Hermes installs as a Python shebang script and the kernel COMM is python, not hermes-c hermes would silently miss every standard install.

Documentation + tooling

  • scripts/local-build.sh + BUILDING.md for contributor builds. New unsigned single-arch Debug build script for contributors without an Apple Developer account. Detects arm64 / x86_64, verifies xcode-select / xcrun / xcodebuild, probes the Metal toolchain (offers an interactive install on TTY, errors cleanly on CI), resolves Swift packages, builds Debug with signing disabled. Optional one-touch ditto to /Applications/scarf.app on explicit y/N. The canonical Release universal CLI in README.md is unchanged — local-build.sh is an alternative for contributors, not a replacement for the shipping build. Cherry-picked from #76.
  • BUILDING.md + CONTRIBUTING.md — restored Sonoma compatibility messaging. The runtime min is macOS 14.6 (Sonoma) — that's the MACOSX_DEPLOYMENT_TARGET on the main scarf target and is intentional. Build min is Xcode 16.0 (needed for Swift 6 strict-concurrency features). The legacy CONTRIBUTING.md line had drifted to "Xcode 26.3+ / macOS 26.2+", which would have steered Sonoma contributors and users away from a build that actually runs on their box. Corrected, with a load-bearing-callout in BUILDING.md so future doc edits don't silently raise the floor again.

Migrating from 2.7.0

Sparkle will offer the update automatically. No config migration, no schema changes. Existing sessions, skills, and projects are untouched.

If you've been working around #77 by collapsing the sidebar or restarting Scarf to repopulate the Sessions list, you can stop — sessions should load reliably now.

Acknowledgements

  • @bricelb for the three v2.7.0 bug reports (#77, #78, #79) — well-instrumented reproductions including screenshots and environment details made the diagnosis straightforward.
  • @unixwzrd for #76 — the gateway-pgrep tighten, the pkill -f "hermes dashboard" direction, and the local-build.sh contributor flow are all cherry-picked from that PR.
]]>
Version 2.7.0 32 2.7.0 14.6 Tue, 05 May 2026 18:47:18 +0000

What's in 2.7.0

The biggest release since 2.6.0 — a six-week stretch covering remote-context performance, a new project authoring flow, dashboard widgets, OAuth resilience, and a top-to-bottom performance instrumentation harness that drove the bulk of the rest. 36 commits, no schema bump, no Hermes capability bump.

The throughline: Scarf got materially faster and more honest on slow remote SSH links, where 30-second sqlite timeouts and silently-empty UI used to be common. The skeleton-then-hydrate pattern, SSH cancellation propagation, and ScarfMon-driven diagnosis are the shape of how that work gets done now.


Remote-context performance — chats and Activity in seconds, not 30s timeouts

Resuming a chat on a slow remote (a 420ms-RTT droplet, an underprovisioned VPS, a tunnel through 4G) used to fetch the full message column set in one shot, which routinely tripped the 30s SSH timeout on chats with multi-page tool result blobs. The 160-message session was broken; the 30-message session was broken too. Activity didn't load at all.

v2.7 introduces a skeleton-then-hydrate pattern that bounds the wire payload by what the user actually needs to see RIGHT NOW, then fills in the heavy stuff in the background:

  • Chat skeleton. fetchSkeletonMessages selects user + assistant rows only (skips role='tool') with tool_calls / reasoning / reasoning_content hard-NULLed at the SQL level. Wire payload bounded by conversational text alone — typically a few KB. The chat appears in seconds. Background startToolHydration pages through hydrateAssistantToolCalls in 5-id batches to splice tool calls in. Tool-result CONTENT is opt-in via Settings → Display → "Load tool results in past chats" (default off); the inspector pane lazy-fetches per-result content via fetchToolResult(callId:) when you open a card.
  • Activity skeleton. fetchRecentToolCallSkeleton returns metadata-only rows (id + session_id + role + timestamp; everything else NULLed). Activity opens in <1s on remote with placeholder rows; real per-call entries swap in as paged hydration completes. New "Loading tool details…" pill in the page header surfaces hydration progress.
  • Single-id whale recovery. When a 5-id batch trips the 30s timeout (one row carries an oversized tool_calls blob — a long Edit's args, a big diff), an L1 single-id retry isolates the offending row so the rest of the batch still hydrates. Whale row stays bare; assistant message stays readable.
  • Lazy tool result loading in the inspector. Default-off avoids the bulk fetch. When you focus a tool call card, ChatInspectorPane fires loadToolResultIfMissing(callId:) which splices a single result into the message stream without re-fetching anything else.

Effect: a 160-message thinking-model session that used to time out at exactly 30s now opens in under 2 seconds with placeholder cards filling in over the next few. Activity loads in 500-800ms.

SSH cancellation that actually cancels

Task.detached { … } doesn't inherit cancellation from the awaiting parent, and Task<…> { … } (unstructured) also drops the signal. Without explicit bridging, cancelling a chat-load Task only unwinds Swift state — the underlying ssh subprocess kept running for the full 30s, pinning a remote sqlite query and a ControlMaster session slot. This produced the "third chat hangs" / "dashboard spins after rapid switching" symptom.

v2.7 wires withTaskCancellationHandler through SSHScriptRunner.run and RemoteSQLiteBackend.query so parent cancellation reaches the Process and calls proc.terminate() within 100ms. New ssh.cancelled ScarfMon event surfaces this.

In-flight coalescing for loadRecentSessions

File-watcher deltas during an active stream used to stack 2-3 parallel sessions-list reload tasks (the 500ms scheduleSessionsRefresh debounce only suppresses a pending tick, not one already executing). Subsequent callers now await the in-flight load instead of spawning a parallel SSH subprocess. New mac.loadRecentSessions.coalesced event tracks dedup hits.

Loading-state UX hardening

The Mac chat sidebar greys out and disables row taps the moment a session-switch is initiated (synchronously, before client.start() returns), with a floating ProgressView showing the current phase: "Spawning hermes acp…""Authenticating…""Loading session…""Loading history…""Ready". Pre-fix the sidebar looked engageable while the 5-7 second SSH+ACP boot was still in flight, and the user could queue up a second session-switch behind the first. New isStartingSession flag flips on user click for instant feedback.

Partial-result + mismatch + pinned-model banners

  • Partial-result banner. When the skeleton fetch trips an SSH transport failure (rather than a clean empty result), the chat surfaces "Couldn't load full chat history — the connection to server timed out" through the existing acpError triplet, plus forces hasMoreHistory = true so the "Load earlier" affordance shows up. Replaces the pre-fix silent empty transcript.
  • Model/provider mismatch banner. ModelPreflight.detectMismatch recognizes when model.default carries a <provider>/... prefix that disagrees with model.provider (e.g. anthropic/claude-sonnet-4.6 + provider: nous after switching OAuth via Credential Pools). Banner offers one-click fix in either direction.
  • Pinned-model failure hint. ACP error classifier now recognizes model_not_found / 404 messages / model is not available and surfaces "This session was created with a model the provider no longer offers — start a new chat to use your current model" so the pinned-model failure mode has a clear recovery path.
  • OAuth-completion provider swap. After a successful OAuth in Credential Pools, if the just-authed provider differs from model.provider, surface "Switch active provider to name?" with [Switch] / [Keep current] instead of auto-dismissing.

New Project from Scratch wizard + Keychain-backed cron secrets

A third project entry point alongside Browse Catalog and Add Existing Project: a wizard that scaffolds a Scarf-standard project skeleton (<project>/.scarf/dashboard.json + AGENTS.md marker block), registers it, and hands off to a chat session that auto-activates the bundled scarf-template-author skill. The skill drives the rest conversationally — widgets, optional config schema, optional cron — and writes the final files itself. Wizard stays minimal because the agent does configuration better than a multi-step form. The skill ships bundled inside Scarf.app/Contents/Resources/BuiltinSkills.bundle/ and copies into ~/.hermes/skills/ on launch (idempotent + version-gated).

Cron + Keychain — $SCARF_<SLUG>_<FIELD> env vars. Cron prompts that referenced secret-typed config fields used to get the literal keychain://... URI back when reading config.json, producing 401s. v2.7 mirrors resolved Keychain values into ~/.hermes/.env under a marker-bounded block keyed by template slug:

# scarf-secrets:begin local-news-aggregator
SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN=actual-value
SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL=https://example.com/feed
# scarf-secrets:end local-news-aggregator

Hermes already reloads ~/.hermes/.env per cron tick, so credential rotation is automatic — just edit the value in Configuration → next tick sees it. The mirror runs at every state-change point: install, post-install Configuration save, uninstall, "Remove from List", and on app launch (reconciliation pass over registered projects). Source of truth stays in the Keychain — config.json keeps keychain:// URIs unchanged. Mode 0600 enforced on ~/.hermes/.env.

Cron prompts now reference these env vars directly:

{
  "prompt": "Use the terminal: curl -sS -H \"Authorization: Bearer $SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN\" \"$SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL\" -o {{PROJECT_DIR}}/.scarf/feed.xml"
}

Migration. First launch of v2.7 walks the project registry and writes the managed block per schemaful project — automatic. Existing cron prompts you wrote against the old (broken) config.json pattern still need updating: open the cron job in Scarf's Cron sidebar and edit the prompt, or ask the agent in chat ("Update my Local News cron job's prompt to use the new env var convention") — the bundled scarf-template-author skill (now v1.1.0) documents the convention with worked examples.

Also fixes #75_NSDetectedLayoutRecursion on the Configuration form for projects whose form transitioned between stages with different intrinsic heights.


Project dashboards — file-reading widgets, sparklines, typed status

Five new widget types, project-wide auto-refresh, and a structured error card for unknown widgets. Backwards-compatible — every existing dashboard.json renders byte-identically.

  • Project-wide auto-refresh. HermesFileWatcher used to watch each project's dashboard.json specifically. v2.7 promotes that to a watch on the entire <project>/.scarf/ directory. A markdown_file or log_tail widget pointing at <project>/.scarf/reports/foo.md refreshes the moment a cron job rewrites the file. By convention, place files the dashboard reads inside .scarf/ so the watch picks them up.
  • markdown_file — renders a markdown file from disk through the same MarkdownContentView pipeline used by inline text widgets.
  • log_tail — last lines of a file (default 20, max 200), monospaced, ANSI codes stripped.
  • cron_status — last run / next run / state for one Hermes cron job by jobId, plus a small inline log tail. Read-only — Run/Pause/Resume controls stay on the Cron tab.
  • image — local file (path relative to project root) or remote url. Optional height cap. Useful for matplotlib/Plotly PNGs the cron job generates.
  • status_grid — compact NxM grid of colored cells, one per service / item, with hover labels.
  • stat widget gains inline sparklines. Optional sparkline: [Number] field. SVG-only render, dozens per dashboard cost nothing.
  • Typed status badges. list items and status_grid cells share a typed enum (success, warning, danger, info, pending, done, neutral) with lenient decode for synonyms (ok/up → success, down/error → danger). Unknown strings render as plain text.
  • Structured widget error card. Replaces the legacy "Unknown: \<type\>" placeholder with a card surfacing the title, specific reason, and a hint.
  • Schema mirror. The widget vocabulary lives once at tools/widget-schema.json; the catalog validator reads from it and enforces per-type required fields.

OAuth resilience + Credential Pools

  • Daily OAuth keepalive cron. Prevents Anthropic OAuth refresh tokens from expiring after weeks of inactivity. New cron job [scarf:oauth-keepalive] (managed by Scarf) pings Hermes on a daily cadence; the in-app Refresh All Sessions action mirrors the same path on demand.
  • Remote re-auth. Re-authenticating against a remote droplet's OAuth provider used to be blocked by the lack of a stdin path through SSHTransport. The OAuth flow now drives a remote hermes auth add correctly with stdin forwarded.
  • OAuth remove button. Per-provider remove action in Credential Pools (auth.json edit), with confirmation dialog. Companion auto-refresh of the view when auth.json changes externally (file-watcher).
  • resolve_provider_client error classification. When an auxiliary task references a provider whose credentials aren't loaded, Hermes prints resolve_provider_client: <name> requested but <Display Name> not configured to stderr — pre-fix this surfaced in chat as the opaque -32603 Internal error with no actionable detail. Now classified into a clear hint pointing at Settings → Aux Models.
  • Aux Tab unknown-task surface. When config.yaml has an auxiliary.<task> block for a task Scarf doesn't know about (newer Hermes added it; Scarf hasn't caught up), render it as a plain row with the raw provider/model values instead of dropping it silently.
  • Credential Pools refresh after OAuth sheet dismiss. Closing the OAuth sheet after a successful add now refreshes the list immediately instead of leaving the just-added pool hidden until the next file-watcher tick.

ScarfMon — performance instrumentation harness

The diagnostic surface that drove the bulk of the v2.7 perf work. Off by default; signpost-only mode (Instruments-friendly) is free; Full mode (4096-entry in-memory ring buffer + os.Logger) is a click away in Settings → Diagnostics → Performance. Wiki: https://github.com/awizemann/scarf/wiki/Performance-Monitoring

  • Phases 1-3 built the core: dispatcher + ring buffer + 3 backends, chat / transport / sqlite measure points, diagnostic counters for chat-render bursts, finalize-burst dampening.
  • Tier A + B added per-feature instrumentation: iOS file watcher, sessions list, model catalog, dashboard widgets, image encoder, message hydration.
  • Nous picker investigation localized a 60s + 120s beach-ball to a specific path (Nous catalog readCache), then killed the 120s one with dedupe + 5s timeout.
  • Tier C catch-up (this release): instrumented Memory / Skills / Cron / Curator load paths so future captures show how often these tabs cost multiple sequential SFTP RTTs on remote.
  • Per-call bytes recorded on transport + sqlite events so captures show payload sizes alongside latencies.
  • mac.emptyAssistantTurn event documents the Nous quirk where the model returns a thought stream with no body (the bubble looks like Hermes is "still thinking" but the turn already finished).

Adding a new measure point is two lines. The harness covers Mac and iOS uniformly. The "Copy as JSON" button exports the ring buffer for paste-into-issue diagnosis.


Other fixes + polish

  • Sessions sidebar reload debounce — file-watcher deltas during streaming used to flicker the sessions list. Coalesced into one trailing fetch ~500ms after the last tick.
  • Session-load pagination + race guard — switching to a small chat while a larger one is mid-fetch could last-write-wins the small chat away. Three race-checks against self.sessionId prevent the stale fetch from overwriting.
  • Sessions + previews batched — two separate SSH calls folded into one queryBatch round trip, halving the round-trips for every sidebar refresh.
  • Remote SQLite query timeout bumped 15→30s to better tolerate slow links; in-flight query coalescing dedupes concurrent identical queries.
  • Thread.sleep spin replaced with a kernel-wait via DispatchGroup for runLocal timeout; under concurrent SSH load the old loop accumulated spin-blocked threads and produced 7-second outliers in loadRecentSessions.
  • Window position + size persists across launches.
  • Sidebar reorder — Projects promoted to first section; profile chip moved under server name.
  • stop badge suppressed on metadata footer for normal turn ends (it was firing for every clean completion, looking like an error).
  • Nous picker search field + model-picker filter for the long Nous overlay model list.
  • oauth-keepalive cron create — drop the --silent flag Hermes doesn't accept.
  • Snapshot pipeline rewritten — replaced the sqlite3 .backup-then-download pipeline with direct SSH-streamed query execution (issue #74). Eliminates the multi-minute snapshot wait on multi-GB state.db files. Companion fix: pre-expand ~/ in Swift via resolvedUserHome so sqlite3 finds the DB without depending on the remote shell's tilde expansion.
  • Aux nested-YAML parser — corrected the parser so the unknown-task surface works on remote (was previously dropping aux blocks whose provider: value lived on a separate line).
  • ModelPreflight newline trim bug.whitespaces doesn't strip newlines; switched both trims to .whitespacesAndNewlines so a stray \n in a hand-edited config.yaml doesn't false-positive the mismatch banner.

What's measured today

321 ScarfCore tests pass (302 prior + 19 new ModelPreflight). New ScarfMon events documented in the Performance-Monitoring wiki.

Compatibility

  • macOS 14+ (unchanged).
  • Hermes target: still v2026.4.30 (v0.12.0). No new Hermes capability gates added.
  • Existing dashboard.json files render unchanged.
  • Existing .scarftemplate bundles install unchanged. Catalog manifest schemaVersion stays at 1/2/3 — no bump.
  • Existing ~/.hermes/.env content is preserved byte-identically — Scarf only writes inside its # scarf-secrets:begin <slug> / # scarf-secrets:end <slug> regions.
  • The skeleton-then-hydrate chat loader and SSH cancellation propagation are Mac-only in this release; ScarfGo (iOS) keeps its existing chat path.

What's deferred

  • Per-widget data sources + per-widget refresh granularity. The general "widget points at a typed data source" abstraction is the next-largest win in dashboards but materially expands the model + JS mirror + validator surface. The project-wide watch covers the common cron-driven workflow without it.
  • Cross-project health digest sidebar rollup. Counting attention-needed projects across the registry — scoped but didn't pull its weight. The typed status enum makes it cheap to add later.
  • Automatic cron-prompt rewriter on upgrade. Heuristic rewrites of free-form prompts are risky; the docs + agent-assisted path ships in v2.7. Revisit a "scan + fix" UI in v2.8 if real users miss the migration.
  • iOS New Project wizard + iOS Keychain-env mirror. ScarfGo's project surface is read-only; the wizard's chat-handoff pattern depends on Mac-only ACP plumbing.
  • iOS skeleton-then-hydrate loaders. Same data-service surfaces are public, but the iOS chat lifecycle is structured differently. Defer until iOS dogfooding shows the same payload-size pain.
  • Tier C redesigns (Memory/Skills/Cron/Curator). Instrumented in v2.7; redesign waits for capture data showing which path actually needs the skeleton-then-hydrate treatment.
]]>
Version 2.6.5 31 2.6.5 14.6 Sun, 03 May 2026 20:20:29 +0000 Version 2.6.0 29 2.6.0 14.6 Fri, 01 May 2026 13:48:15 +0000 Version 2.5.2 28 2.5.2 14.6 Wed, 29 Apr 2026 11:47:40 +0000 Version 2.5.1 27 2.5.1 14.6 Mon, 27 Apr 2026 13:38:41 +0000 Version 2.5.0 26 2.5.0 14.6 Sat, 25 Apr 2026 15:42:47 +0000 Version 2.3.0 25 2.3.0 14.6 Fri, 24 Apr 2026 01:20:51 +0000 Version 2.2.1 24 2.2.1 14.6 Thu, 23 Apr 2026 20:10:10 +0000 Version 2.2.0 23 2.2.0 14.6 Thu, 23 Apr 2026 16:31:53 +0000 Version 2.1.0 22 2.1.0 14.6 Tue, 21 Apr 2026 01:50:30 +0000 Version 2.0.2 21 2.0.2 14.6 Mon, 20 Apr 2026 22:50:02 +0000 Version 2.0.0 19 2.0.0 14.6 Sun, 19 Apr 2026 20:11:43 +0000 Version 1.6.2 18 1.6.2 14.6 Sat, 18 Apr 2026 00:22:28 +0000 Version 1.6.1 17 1.6.1 14.6 Fri, 17 Apr 2026 02:11:53 +0000