Files
scarf/appcast.xml
T

623 lines
66 KiB
XML
Raw Normal View History

2026-04-16 18:09:32 -07:00
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Scarf Updates</title>
<link>https://awizemann.github.io/scarf/appcast.xml</link>
<description>Scarf macOS app updates</description>
<language>en</language>
2026-05-08 12:56:11 +02:00
<item>
<title>Version 2.7.5</title>
<sparkle:version>34</sparkle:version>
<sparkle:shortVersionString>2.7.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 08 May 2026 10:56:09 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.5</h2>
<p>A feature release that lifts Scarf&#x27;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.</p>
<h3>New features</h3>
<h4>Mac</h4>
<ul>
<li><strong>Drag-and-drop Kanban board</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanBoardView.swift">scarf/Features/Kanban/Views/KanbanBoardView.swift</a>). Five visible columns — Triage / Up Next (<code>todo</code> + <code>ready</code>) / Running / Blocked / Done — collapsing Hermes&#x27;s seven status values into a layout that doesn&#x27;t waste space on <code>ready</code>, 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 <code>kanban dispatch</code> (the dispatcher then spawns a worker), drop-on-Blocked opens a sheet asking for a reason and calls <code>kanban block</code>, drop-on-Done opens a result sheet and calls <code>kanban complete</code>, blocked → running chains <code>unblock</code> + <code>dispatch</code>. 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.</li>
</ul>
<ul>
<li><strong>Side-pane inspector</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanInspectorPane.swift">KanbanInspectorPane.swift</a>). 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 <code>hermes kanban show &lt;id&gt;</code> data: <strong>Comments</strong> (with an inline composer that calls <code>kanban comment</code>), <strong>Events</strong> (the <code>task_events</code> log with per-kind glyphs), <strong>Runs</strong> (one row per attempt with outcome badge + summary + error), and <strong>Log</strong> — the worker&#x27;s captured stdout/stderr from <code>hermes kanban log &lt;id&gt;</code>, polled every 2 s while the task is running with a &quot;● streaming&quot; 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 <code>claim</code> 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 &quot;Archive&quot; tooltip explicitly notes Hermes has no hard-delete: archived tasks remain in <code>~/.hermes/kanban.db</code> and are recoverable via the &quot;Show archived&quot; toggle until <code>hermes kanban gc</code> runs.</li>
</ul>
<ul>
<li><strong>Inspector auto-refresh.</strong> 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&#x27;s 2 s poll runs separately and self-cancels the moment the task transitions out of <code>running</code>.</li>
</ul>
<ul>
<li><strong>Inline assignee picker on the inspector header.</strong> The assignee badge is a clickable menu — set means a <code>.brand</code> (rust) chip, unassigned means a <code>.warning</code> (yellow) chip so the eye catches it instantly. Tapping opens a menu of every known profile (union of <code>~/.hermes/profiles/</code>, current task assignees, and the active local profile from <code>HermesProfileResolver</code>) plus an &quot;Unassigned&quot; option. Selection routes through <code>kanban assign</code> and immediately follows with <code>kanban dispatch</code> so the task gets picked up promptly. Solves the &quot;I assigned a profile but nothing happened&quot; gap end-to-end without the user touching a terminal.</li>
</ul>
<ul>
<li><strong>Health banner in the inspector.</strong> Surfaces two conditions that previously left users staring at a stuck task with no explanation. <strong>Yellow</strong> when the task is unassigned in <code>ready</code> / <code>todo</code>: <em>&quot;Won&#x27;t run automatically — Hermes&#x27;s dispatcher silently skips tasks with no assignee.&quot;</em> The dispatcher&#x27;s own <code>--json</code> output literally lists these under <code>skipped_unassigned</code>; we now surface that to the human. <strong>Red</strong> when the most-recently-completed run ended in a non-success outcome (<code>stale_lock</code> / <code>crashed</code> / <code>gave_up</code> / <code>timed_out</code> / <code>spawn_failed</code> / <code>reclaimed</code> / <code>failed</code>): banner displays the outcome label + the raw <code>error</code> field from the run record, so you don&#x27;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 <code>running</code>, the previous outcome is stale signal and the Log tab&#x27;s live stream is the right thing to look at.</li>
</ul>
<ul>
<li><strong>Card-level signals.</strong> Cards in <code>running</code> get a 2 px <code>ScarfColor.info</code> left edge + a subtle title shimmer so live work is obvious at a glance. Blocked cards get a 2 px <code>ScarfColor.warning</code> 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 <code>ready</code> / <code>todo</code> with no assignee get a yellow ⚠ glyph in the title row with a tooltip explaining the dispatcher won&#x27;t pick them up — same signal as the inspector banner, just at the board level so triage is one keypress away.</li>
</ul>
<ul>
<li><strong><code>Board | List</code> toggle at the top of the route.</strong> The v2.6 read-only list view is preserved in <code>KanbanListView.swift</code> 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 <code>@AppStorage</code>.</li>
</ul>
<ul>
<li><strong>New Task sheet</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanCreateSheet.swift">KanbanCreateSheet.swift</a>). Title, body (markdown supported), assignee (defaults to <code>HermesProfileResolver.activeProfileName()</code> so newly-created tasks actually run), workspace kind (segmented <code>Scratch / Worktree / Project Dir</code>; locked to Project Dir on per-project boards), priority slider, comma-separated skills with autocomplete from <code>~/.hermes/skills/</code>, optional tenant (hidden on per-project boards — the slug is implicit), and a &quot;Send to triage&quot; toggle. Submit fires <code>kanban create --json</code> and immediately follows with <code>kanban dispatch</code> so an assigned task transitions <code>ready</code> → <code>running</code> within seconds rather than waiting for the gateway dispatcher&#x27;s internal cycle.</li>
</ul>
<ul>
<li><strong>Kanban moved from Manage → Monitor in the sidebar.</strong> It&#x27;s runtime work-in-progress, not configuration. Sits between Activity and the rest of Manage so users see &quot;what&#x27;s happening right now&quot; at a glance.</li>
</ul>
<h4>Per-project Kanban</h4>
<ul>
<li><strong><code>DashboardTab.kanban</code> on every project</strong>, capability-gated on <code>HermesCapabilities.hasKanban</code>. Renders a project-scoped <code>KanbanBoardView</code> filtered to the project&#x27;s tenant slug. Workspace defaults in the New Task sheet are pre-pinned to <code>dir:&lt;project.path&gt;</code>. Empty state explains the project doesn&#x27;t have any tasks yet and offers a &quot;New Task&quot; CTA — the empty board IS the discovery surface.</li>
</ul>
<ul>
<li><strong>Tenant minting via <a href="scarf/scarf/Core/Services/KanbanTenantResolver.swift">KanbanTenantResolver</a>.</strong> Each Scarf project gets a stable <code>scarf:&lt;slug&gt;</code> tenant minted on first kanban interaction and persisted to <code>&lt;project&gt;/.scarf/manifest.json</code> (new optional <code>kanbanTenant</code> field on <code>ProjectTemplateManifest</code>). Slug rules: lowercased, hyphenated, ≤ 48 chars, <code>scarf:</code> prefix to avoid collision with hand-typed tenants. Once minted, the tenant is <strong>immutable across rename</strong> — tasks already on the board carry the original slug, so renaming the project doesn&#x27;t orphan them. Bare projects (no manifest) get a sentinel manifest written with <code>id: scarf/&lt;project-id&gt;</code> + <code>version: 0.0.0</code> + just the <code>kanbanTenant</code> set; the <code>ProjectAgentContextService</code> reader recognizes the sentinel and refuses to surface it as a &quot;Template&quot; line in the AGENTS.md block, so the project doesn&#x27;t suddenly start advertising a fake template to the agent.</li>
</ul>
<ul>
<li><strong>Agent-side tenant injection.</strong> <a href="scarf/scarf/Core/Services/ProjectAgentContextService.swift">ProjectAgentContextService.renderBlock</a> emits a &quot;Kanban tenant&quot; line inside the <code>&lt;!-- scarf-project --&gt;</code> markers in <code>&lt;project&gt;/AGENTS.md</code> whenever a tenant exists, instructing the agent to pass <code>--tenant scarf:&lt;slug&gt;</code> on <code>hermes kanban create</code>. <code>ChatViewModel.startACPSession</code> already calls <code>refresh(for:)</code> 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 <code>--tenant</code> lands the task in the global &quot;Untagged&quot; group rather than failing — acceptable v2.7.5 behavior.</li>
</ul>
<ul>
<li><strong><code>kanban_summary</code> dashboard widget</strong> (<a href="scarf/scarf/Features/Projects/Views/Widgets/KanbanSummaryWidgetView.swift">KanbanSummaryWidgetView.swift</a>). New widget kind for project dashboards: shows the top three <code>running</code> / <code>blocked</code> / <code>todo</code> tasks for the project&#x27;s tenant by priority, plus a glance footer (<code>&quot;12 todo · 3 running · 5 blocked&quot;</code>) sourced from <code>kanban stats</code>. Polls every 10 s while the dashboard is foregrounded. Widget vocabulary registered in <a href="tools/widget-schema.json">tools/widget-schema.json</a> and rendered on the catalog site via <a href="site/widgets.js">site/widgets.js</a>; template authors can drop a <code>{ kind: kanban_summary, max_rows: 3 }</code> block into <code>dashboard.json</code>.</li>
</ul>
<h4>iOS / iPadOS</h4>
<ul>
<li><strong>Read-only Kanban tab on <code>ProjectDetailView</code></strong> (<a href="scarf/Scarf%20iOS/Kanban/ScarfGoKanbanView.swift">Scarf iOS/Kanban/ScarfGoKanbanView.swift</a>). Same five-column collapse rendered as a horizontally-paged segmented <code>Picker</code> 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 <code>NavigationStack</code> detail sheet (<a href="scarf/Scarf%20iOS/Kanban/ScarfGoKanbanDetailSheet.swift">ScarfGoKanbanDetailSheet.swift</a>) 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 <code>.headline</code> (not <code>ScarfFont</code>) so Dynamic Type works; chrome (badges) stays on <code>ScarfBadge</code> for fixed visual weight per the project&#x27;s iOS conventions.</li>
</ul>
<h4>ScarfCore</h4>
<ul>
<li><strong><code>KanbanService</code> actor</strong> (<a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift">Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift</a>) — pure-I/O Sendable actor wrapping every Hermes v0.12 verb (<code>list / show / runs / stats / assignees / create / assign / claim / comment / complete / block / unblock / archive / dispatch / link / unlink / log</code>). Dispatches each CLI invocation through <code>Task.detached(priority: .utility)</code> matching the existing concurrency conventions. Errors land in <a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Models/KanbanError.swift">KanbanError</a> and surface as inline banners (not modal alerts) since the board is high-frequency. The &quot;no matching tasks&quot; stdout sentinel is normalized to <code>[]</code> rather than thrown.</li>
</ul>
<ul>
<li><strong>Pure transition planner.</strong> <code>KanbanService.plan(for: KanbanTransition)</code> is a synchronous function that maps a <code>(from, to)</code> column pair to the right verb sequence — <code>(.upNext, .running) → [.dispatch]</code>, <code>(.blocked, .running) → [.unblock, .dispatch]</code>, etc. Disallowed transitions throw <code>KanbanError.forbiddenTransition</code> with a user-actionable reason. The planner is fully tested in <code>KanbanModelsTests.swift</code>. Critically: <code>dispatch</code> (not <code>claim</code>) is the verb used for Up-Next → Running. Hermes&#x27;s <code>claim</code> is documented as &quot;manual alternative to the dispatcher&quot; and assumes the caller spawns the worker themselves — Scarf doesn&#x27;t, so calling <code>claim</code> from drag-drop reserved tasks but never spawned work, and the dispatcher reclaimed them ~15 minutes later (<code>stale_lock</code>). <code>dispatch</code> is the right primitive for a GUI client.</li>
</ul>
<ul>
<li><strong>Cross-platform <a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanTenantReader.swift">KanbanTenantReader</a>.</strong> Read-only projection over <code>&lt;project&gt;/.scarf/manifest.json</code>&#x27;s <code>kanbanTenant</code> field. The full <code>ProjectTemplateManifest</code> 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.</li>
</ul>
<ul>
<li><strong>Timestamp decoding tolerates both shapes.</strong> Hermes emits <code>created_at</code> / <code>started_at</code> / <code>completed_at</code> / <code>last_heartbeat_at</code> 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 <code>decodeUnixIntegerTimestamps</code> in <code>KanbanModelsTests</code>.</li>
</ul>
<ul>
<li><strong><code>KanbanBoardViewModel</code> optimistic merge.</strong> Holds <code>optimisticOverrides: [taskId: status]</code> 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&#x27;t snap the card back to its old column. On CLI failure the override is removed and the message lands in the inline banner.</li>
</ul>
<h3>Dispatch + assignee fixes</h3>
<p>A diagnostic round driving real tasks end-to-end exposed a connected bug pattern that the polish pass closed:</p>
<ul>
<li><strong>Hermes&#x27;s dispatcher silently skips unassigned tasks</strong> — its <code>kanban dispatch --json</code> output literally lists them under a <code>skipped_unassigned</code> key and moves on. Tasks created without an assignee sat in <code>ready</code> 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 &quot;Unassigned&quot; chip + warning banner, every <code>ready</code> / <code>todo</code> card without an assignee gets a ⚠ glyph + tooltip, and the inspector&#x27;s inline assignee picker fixes it in one click.</li>
</ul>
<ul>
<li><strong>Drag-to-Running used to call <code>claim</code></strong>, which is a manual alternative to the dispatcher. Status flipped to <code>running</code>, but no worker spawned (Scarf doesn&#x27;t host workers), and 15 minutes later the dispatcher reclaimed the task with a <code>stale_lock</code> outcome. Replaced with <code>dispatch</code> end-to-end so the gateway-running dispatcher actually does the spawning.</li>
</ul>
<ul>
<li><strong><code>hermes kanban assignees</code> empty-state was leaking into the picker.</strong> The CLI prints a literal sentinel <code>(no assignees — create a profile with hermes -p &lt;name&gt; setup)</code> when the table is empty; the parser was tokenizing it on whitespace and offering <code>(no</code> as a profile in the menu. Parser now skips the sentinel, validates each candidate against <code>^[a-zA-Z0-9_-]+$</code>, and falls back cleanly to the active local profile when the table is empty.</li>
</ul>
<ul>
<li><strong><code>spawn_failed</code> from &quot;executable not found on PATH&quot;</strong> — most subtle of the lot. macOS GUI apps inherit a launch-services PATH (<code>/usr/bin:/bin:/usr/sbin:/sbin</code>) that doesn&#x27;t include <code>~/.local/bin</code> (where pipx installs <code>hermes</code>) or <code>/opt/homebrew/bin</code>. Scarf was finding <code>hermes</code> for its own invocation via the absolute-path resolver in <code>HermesPathSet.hermesBinaryCandidates</code>, but when the dispatcher then spawned a worker process, that worker inherited Scarf&#x27;s GUI PATH and couldn&#x27;t find <code>hermes</code> by name — recording an <code>outcome=spawn_failed</code> run with the exact &quot;executable not found on PATH&quot; message. <code>LocalTransport</code> now grows an <code>environmentEnricher</code> static (mirroring <code>SSHTransport.environmentEnricher</code>) wired by <code>scarfApp.swift</code> to the same <code>HermesFileService.enrichedEnvironment()</code> login-shell probe the SSH transport uses. Every local subprocess Scarf spawns now sees the user&#x27;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: <code>subprocessEnvironment(forExecutable:)</code> also unconditionally prepends the executable&#x27;s parent directory to PATH, so the fix works even if the enricher hasn&#x27;t been wired (early startup, tests).</li>
</ul>
<h3>Migrating from 2.7.1</h3>
<p>Sparkle will offer the update automatically. No config migration, no schema changes — <code>~/.hermes/kanban.db</code> 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.</p>
<p>If you have an existing project with a Scarf-managed <code>manifest.json</code>, the new optional <code>kanbanTenant</code> field is added on next mint and lives alongside any template-author config schema without touching it. Templates do not ship <code>kanbanTenant</code> (it&#x27;s user-machine-scoped state); the export pipeline strips it.</p>
<p>If you&#x27;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&#x27;s no migration step. Tasks created without an assignee in v2.6 will now show the yellow &quot;Unassigned&quot; warning until you fix them through the inline picker.</p>
<h3>Known limitations</h3>
<ul>
<li><strong>Within-column reorder is not supported.</strong> Hermes has no <code>update</code> verb and no <code>position</code> column on the tasks table — <code>priority</code> is write-once at create time. Sort order inside each column is <code>priority DESC, created_at DESC</code>, matching the dispatcher&#x27;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 <code>update --priority</code> verb.</li>
</ul>
<ul>
<li><strong>No live <code>watch</code> streaming yet.</strong> 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. <code>hermes kanban watch --json</code> event streaming + reconnect-with-backoff lands in v2.8 along with iOS write surfaces.</li>
</ul>
<ul>
<li><strong>No bulk re-tag for legacy NULL-tenant tasks.</strong> Tasks created before this release (assignee or no assignee) appear in the global &quot;Untagged&quot; group on the global board. Hermes has no <code>tenant</code> mutation verb post-create, so retagging would be archive + recreate — too destructive to ship in this release.</li>
</ul>
<h3>Acknowledgements</h3>
<ul>
<li>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 <code>claim</code> vs <code>dispatch</code> distinction, the silent <code>skipped_unassigned</code> behavior, the <code>(no</code> parse leak, the integer-vs-ISO timestamp shape, and the stale &quot;Last run&quot; banner during a fresh attempt all surfaced from driving real tasks and watching what actually happened.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.5/Scarf-v2.7.5-Universal.zip"
sparkle:edSignature="6QLmonqLdavcU3+u7NE3oYL4Iui4wZZ0r9OWM+kj0uJ3tM32C14N35g7kXmADvo50YONIAmxqfkYWo/AIX70AA=="
length="19346988"
type="application/octet-stream" />
</item>
2026-05-07 12:51:55 +02:00
<item>
<title>Version 2.7.1</title>
<sparkle:version>33</sparkle:version>
<sparkle:shortVersionString>2.7.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 07 May 2026 10:51:54 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.1</h2>
<p>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.</p>
<h3>Bug fixes</h3>
<h4>Mac</h4>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/77">#77</a> — Sessions screen renders empty even when Dashboard reports sessions exist.</strong> v2.7.0 folded the Sessions tab&#x27;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&#x27;s 1664 KB pipe-buffer threshold; without a concurrent reader draining the pipe, the remote <code>sqlite3 -json</code> blocked, the script never finished, our 30-second timeout fired, and the call returned an empty result. <code>SSHScriptRunner</code> now drains stdout/stderr concurrently with the running process via <code>FileHandle.readabilityHandler</code>, 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.</li>
</ul>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/78">#78</a> — Skills &quot;What&#x27;s New&quot; pill contradicts the Updates sub-tab.</strong> The pill at the top of the Skills page was rendering on every sub-tab, including Updates. It counts <strong>local</strong> file deltas since the user last clicked &quot;Mark as seen&quot; (e.g. &quot;18 new&quot; = 18 skills landed on disk that you haven&#x27;t acknowledged), while the Updates body runs <code>hermes skills check</code> to find skills with newer <strong>upstream</strong> versions available — a different concept. Two surfaces using the word &quot;update&quot; 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 &quot;X <strong>changed</strong> since you last looked&quot; instead of &quot;X updated&quot; so the local-file vocabulary doesn&#x27;t collide with upstream-update vocabulary anywhere on the page.</li>
</ul>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/79">#79</a> — Skills hub search returns nothing for terms visible in Browse.</strong> With the source picker on &quot;All Sources&quot;, <code>hermes skills search &lt;query&gt;</code> (no <code>--source</code> flag) routes through Hermes&#x27;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 <code>honcho</code> would show up in Browse and disappear in search. Same picker, same query, contradictory results. Rather than chase Hermes&#x27;s index gaps, &quot;All Sources&quot; search now means &quot;filter what you can already see&quot;: 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 <code>hermes skills search --source &lt;s&gt;</code> for full upstream search semantics. Five new tests cover the filter behavior.</li>
</ul>
<ul>
<li><strong><code>hermesPIDResult()</code> — narrow the Hermes &quot;is it running?&quot; probe to the gateway.</strong> Previously <code>pgrep -f hermes</code>, which matched any process with &quot;hermes&quot; in its argv: chat sessions Scarf itself spawns, <code>hermes -z</code> one-shots, log tails, even the README in an editor. The Dashboard &quot;Hermes is running&quot; badge could read true even when the gateway daemon was down. Tightened to a regex that matches only the gateway shape — <code>python -m hermes_cli.main gateway run …</code> and <code>/path/to/hermes gateway run …</code>. All callers (DashboardViewModel, HealthViewModel, SettingsViewModel, scarfApp, stopHermes) want the gateway PID specifically. Cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a> — thanks to <a href="https://github.com/unixwzrd">@unixwzrd</a> for the diagnosis and regex.</li>
</ul>
<ul>
<li><strong><code>HealthViewModel.stopDashboard()</code> — stop the dashboard by port, not <code>pkill -f</code>.</strong> External-instance fallback used to be <code>pkill -f &quot;hermes dashboard&quot;</code>, broad enough to match shell history, log tails, README readers — anything with the substring in its argv. Now <code>lsof -tiTCP:&lt;port&gt; -sTCP:LISTEN</code> resolves the PID actually bound to the dashboard port and only that one process gets <code>SIGTERM</code>. Trusting the port is correct here: Scarf owns the configured port and the user-visible intent is &quot;stop the thing on this port.&quot; Direction cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a>; the <code>-c hermes</code> filter from the original was dropped because Hermes installs as a Python shebang script and the kernel COMM is <code>python</code>, not <code>hermes</code> — <code>-c hermes</code> would silently miss every standard install.</li>
</ul>
<h3>Documentation + tooling</h3>
<ul>
<li><strong><code>scripts/local-build.sh</code> + <code>BUILDING.md</code> for contributor builds.</strong> 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 <code>ditto</code> to <code>/Applications/scarf.app</code> on explicit y/N. The canonical Release universal CLI in <code>README.md</code> is unchanged — <code>local-build.sh</code> is an alternative for contributors, not a replacement for the shipping build. Cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a>.</li>
</ul>
<ul>
<li><strong><code>BUILDING.md</code> + <code>CONTRIBUTING.md</code> — restored Sonoma compatibility messaging.</strong> The runtime min is <strong>macOS 14.6 (Sonoma)</strong> — that&#x27;s the <code>MACOSX_DEPLOYMENT_TARGET</code> on the main <code>scarf</code> target and is intentional. Build min is <strong>Xcode 16.0</strong> (needed for Swift 6 strict-concurrency features). The legacy CONTRIBUTING.md line had drifted to &quot;Xcode 26.3+ / macOS 26.2+&quot;, 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&#x27;t silently raise the floor again.</li>
</ul>
<h3>Migrating from 2.7.0</h3>
<p>Sparkle will offer the update automatically. No config migration, no schema changes. Existing sessions, skills, and projects are untouched.</p>
<p>If you&#x27;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.</p>
<h3>Acknowledgements</h3>
<ul>
<li><a href="https://github.com/bricelb">@bricelb</a> for the three v2.7.0 bug reports (<a href="https://github.com/awizemann/scarf/issues/77">#77</a>, <a href="https://github.com/awizemann/scarf/issues/78">#78</a>, <a href="https://github.com/awizemann/scarf/issues/79">#79</a>) — well-instrumented reproductions including screenshots and environment details made the diagnosis straightforward.</li>
<li><a href="https://github.com/unixwzrd">@unixwzrd</a> for <a href="https://github.com/awizemann/scarf/pull/76">#76</a> — the gateway-pgrep tighten, the <code>pkill -f &quot;hermes dashboard&quot;</code> direction, and the <code>local-build.sh</code> contributor flow are all cherry-picked from that PR.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.1/Scarf-v2.7.1-Universal.zip"
sparkle:edSignature="P9whuwJ264TN1XLaUNvzQK+yKmK2fiyccgKa6TixK3mkIPyVl2XrwPhyHgoMFOQ/c14l+4sizWolx67BSwXBAg=="
length="18806277"
type="application/octet-stream" />
</item>
2026-05-05 20:47:20 +02:00
<item>
<title>Version 2.7.0</title>
<sparkle:version>32</sparkle:version>
<sparkle:shortVersionString>2.7.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Tue, 05 May 2026 18:47:18 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.0</h2>
<p>The biggest release since 2.6.0 — a six-week stretch covering <strong>remote-context performance</strong>, a <strong>new project authoring flow</strong>, <strong>dashboard widgets</strong>, <strong>OAuth resilience</strong>, and a top-to-bottom <strong>performance instrumentation harness</strong> that drove the bulk of the rest. 36 commits, no schema bump, no Hermes capability bump.</p>
<p>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.</p>
<hr>
<h3>Remote-context performance — chats and Activity in seconds, not 30s timeouts</h3>
<p>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&#x27;t load at all.</p>
<p>v2.7 introduces a <strong>skeleton-then-hydrate pattern</strong> that bounds the wire payload by what the user actually needs to see RIGHT NOW, then fills in the heavy stuff in the background:</p>
<ul>
<li><strong>Chat skeleton.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift"><code>fetchSkeletonMessages</code></a> selects user + assistant rows only (skips <code>role=&#x27;tool&#x27;</code>) with <code>tool_calls</code> / <code>reasoning</code> / <code>reasoning_content</code> hard-NULLed at the SQL level. Wire payload bounded by conversational text alone — typically a few KB. The chat appears in seconds. Background <code>startToolHydration</code> pages through <code>hydrateAssistantToolCalls</code> in 5-id batches to splice tool calls in. Tool-result CONTENT is <strong>opt-in</strong> via Settings → Display → &quot;Load tool results in past chats&quot; (default off); the inspector pane lazy-fetches per-result content via <code>fetchToolResult(callId:)</code> when you open a card.</li>
<li><strong>Activity skeleton.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift"><code>fetchRecentToolCallSkeleton</code></a> returns metadata-only rows (id + session_id + role + timestamp; everything else NULLed). Activity opens in &lt;1s on remote with placeholder rows; real per-call entries swap in as paged hydration completes. New &quot;Loading tool details…&quot; pill in the page header surfaces hydration progress.</li>
<li><strong>Single-id whale recovery.</strong> When a 5-id batch trips the 30s timeout (one row carries an oversized <code>tool_calls</code> blob — a long Edit&#x27;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.</li>
<li><strong>Lazy tool result loading in the inspector.</strong> Default-off avoids the bulk fetch. When you focus a tool call card, ChatInspectorPane fires <code>loadToolResultIfMissing(callId:)</code> which splices a single result into the message stream without re-fetching anything else.</li>
</ul>
<p>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.</p>
<h4>SSH cancellation that actually cancels</h4>
<p><code>Task.detached { … }</code> doesn&#x27;t inherit cancellation from the awaiting parent, and <code>Task&lt;…&gt; { … }</code> (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 &quot;third chat hangs&quot; / &quot;dashboard spins after rapid switching&quot; symptom.</p>
<p>v2.7 wires <code>withTaskCancellationHandler</code> through <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/SSHScriptRunner.swift"><code>SSHScriptRunner.run</code></a> and <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/Backends/RemoteSQLiteBackend.swift"><code>RemoteSQLiteBackend.query</code></a> so parent cancellation reaches the <code>Process</code> and calls <code>proc.terminate()</code> within 100ms. New <code>ssh.cancelled</code> ScarfMon event surfaces this.</p>
<h4>In-flight coalescing for <code>loadRecentSessions</code></h4>
<p>File-watcher deltas during an active stream used to stack 2-3 parallel sessions-list reload tasks (the 500ms <code>scheduleSessionsRefresh</code> 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 <code>mac.loadRecentSessions.coalesced</code> event tracks dedup hits.</p>
<h4>Loading-state UX hardening</h4>
<p>The Mac chat sidebar greys out and disables row taps the moment a session-switch is initiated (synchronously, before <code>client.start()</code> returns), with a floating ProgressView showing the current phase: <strong>&quot;Spawning hermes acp…&quot;</strong> → <strong>&quot;Authenticating…&quot;</strong> → <strong>&quot;Loading session…&quot;</strong> → <strong>&quot;Loading history…&quot;</strong> → <strong>&quot;Ready&quot;</strong>. 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 <code>isStartingSession</code> flag flips on user click for instant feedback.</p>
<h4>Partial-result + mismatch + pinned-model banners</h4>
<ul>
<li><strong>Partial-result banner.</strong> When the skeleton fetch trips an SSH transport failure (rather than a clean empty result), the chat surfaces &quot;Couldn&#x27;t load full chat history — the connection to <em>server</em> timed out&quot; through the existing <code>acpError</code> triplet, plus forces <code>hasMoreHistory = true</code> so the &quot;Load earlier&quot; affordance shows up. Replaces the pre-fix silent empty transcript.</li>
<li><strong>Model/provider mismatch banner.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/ModelPreflight.swift"><code>ModelPreflight.detectMismatch</code></a> recognizes when <code>model.default</code> carries a <code>&lt;provider&gt;/...</code> prefix that disagrees with <code>model.provider</code> (e.g. <code>anthropic/claude-sonnet-4.6</code> + <code>provider: nous</code> after switching OAuth via Credential Pools). Banner offers one-click fix in either direction.</li>
<li><strong>Pinned-model failure hint.</strong> ACP error classifier now recognizes <code>model_not_found</code> / <code>404 messages</code> / <code>model is not available</code> and surfaces &quot;This session was created with a model the provider no longer offers — start a new chat to use your current model&quot; so the pinned-model failure mode has a clear recovery path.</li>
<li><strong>OAuth-completion provider swap.</strong> After a successful OAuth in Credential Pools, if the just-authed provider differs from <code>model.provider</code>, surface &quot;Switch active provider to <em>name</em>?&quot; with [Switch] / [Keep current] instead of auto-dismissing.</li>
</ul>
<hr>
<h3>New Project from Scratch wizard + Keychain-backed cron secrets</h3>
<p>A <strong>third project entry point</strong> alongside Browse Catalog and Add Existing Project: a wizard that scaffolds a Scarf-standard project skeleton (<code>&lt;project&gt;/.scarf/dashboard.json</code> + AGENTS.md marker block), registers it, and hands off to a chat session that auto-activates the bundled <code>scarf-template-author</code> 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 <code>Scarf.app/Contents/Resources/BuiltinSkills.bundle/</code> and copies into <code>~/.hermes/skills/</code> on launch (idempotent + version-gated).</p>
<p><strong>Cron + Keychain — <code>$SCARF_&lt;SLUG&gt;_&lt;FIELD&gt;</code> env vars.</strong> Cron prompts that referenced <code>secret</code>-typed config fields used to get the literal <code>keychain://...</code> URI back when reading <code>config.json</code>, producing 401s. v2.7 mirrors resolved Keychain values into <code>~/.hermes/.env</code> under a marker-bounded block keyed by template slug:</p>
<pre><code># 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</code></pre>
<p>Hermes already reloads <code>~/.hermes/.env</code> 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, &quot;Remove from List&quot;, and on app launch (reconciliation pass over registered projects). Source of truth stays in the Keychain — <code>config.json</code> keeps <code>keychain://</code> URIs unchanged. Mode 0600 enforced on <code>~/.hermes/.env</code>.</p>
<p>Cron prompts now reference these env vars directly:</p>
<pre><code>{
&quot;prompt&quot;: &quot;Use the terminal: curl -sS -H \&quot;Authorization: Bearer $SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN\&quot; \&quot;$SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL\&quot; -o {{PROJECT_DIR}}/.scarf/feed.xml&quot;
}</code></pre>
<p><strong>Migration.</strong> 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) <code>config.json</code> pattern still need updating: open the cron job in Scarf&#x27;s Cron sidebar and edit the prompt, or ask the agent in chat (&quot;Update my Local News cron job&#x27;s prompt to use the new env var convention&quot;) — the bundled <code>scarf-template-author</code> skill (now v1.1.0) documents the convention with worked examples.</p>
<p>Also fixes <a href="https://github.com/awizemann/scarf/issues/75">#75</a> — <code>_NSDetectedLayoutRecursion</code> on the Configuration form for projects whose form transitioned between stages with different intrinsic heights.</p>
<hr>
<h3>Project dashboards — file-reading widgets, sparklines, typed status</h3>
<p>Five new widget types, project-wide auto-refresh, and a structured error card for unknown widgets. Backwards-compatible — every existing <code>dashboard.json</code> renders byte-identically.</p>
<ul>
<li><strong>Project-wide auto-refresh.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesFileWatcher.swift"><code>HermesFileWatcher</code></a> used to watch each project&#x27;s <code>dashboard.json</code> specifically. v2.7 promotes that to a watch on the entire <code>&lt;project&gt;/.scarf/</code> directory. A <code>markdown_file</code> or <code>log_tail</code> widget pointing at <code>&lt;project&gt;/.scarf/reports/foo.md</code> refreshes the moment a cron job rewrites the file. <strong>By convention, place files the dashboard reads inside <code>.scarf/</code></strong> so the watch picks them up.</li>
<li><strong><code>markdown_file</code></strong> — renders a markdown file from disk through the same <code>MarkdownContentView</code> pipeline used by inline <code>text</code> widgets.</li>
<li><strong><code>log_tail</code></strong> — last <code>lines</code> of a file (default 20, max 200), monospaced, ANSI codes stripped.</li>
<li><strong><code>cron_status</code></strong> — last run / next run / state for one Hermes cron job by <code>jobId</code>, plus a small inline log tail. Read-only — Run/Pause/Resume controls stay on the Cron tab.</li>
<li><strong><code>image</code></strong> — local file (<code>path</code> relative to project root) or remote <code>url</code>. Optional <code>height</code> cap. Useful for matplotlib/Plotly PNGs the cron job generates.</li>
<li><strong><code>status_grid</code></strong> — compact NxM grid of colored cells, one per service / item, with hover labels.</li>
<li><strong><code>stat</code> widget gains inline sparklines.</strong> Optional <code>sparkline: [Number]</code> field. SVG-only render, dozens per dashboard cost nothing.</li>
<li><strong>Typed status badges.</strong> <code>list</code> items and <code>status_grid</code> cells share a typed enum (<code>success</code>, <code>warning</code>, <code>danger</code>, <code>info</code>, <code>pending</code>, <code>done</code>, <code>neutral</code>) with lenient decode for synonyms (<code>ok</code>/<code>up</code> → success, <code>down</code>/<code>error</code> → danger). Unknown strings render as plain text.</li>
<li><strong>Structured widget error card.</strong> Replaces the legacy &quot;Unknown: \&lt;type\&gt;&quot; placeholder with a card surfacing the title, specific reason, and a hint.</li>
<li><strong>Schema mirror.</strong> The widget vocabulary lives once at <a href="https://github.com/awizemann/scarf/blob/main/tools/widget-schema.json"><code>tools/widget-schema.json</code></a>; the catalog validator reads from it and enforces per-type required fields.</li>
</ul>
<hr>
<h3>OAuth resilience + Credential Pools</h3>
<ul>
<li><strong>Daily OAuth keepalive cron.</strong> Prevents Anthropic OAuth refresh tokens from expiring after weeks of inactivity. New cron job <code>[scarf:oauth-keepalive]</code> (managed by Scarf) pings Hermes on a daily cadence; the in-app Refresh All Sessions action mirrors the same path on demand.</li>
<li><strong>Remote re-auth.</strong> Re-authenticating against a remote droplet&#x27;s OAuth provider used to be blocked by the lack of a stdin path through SSHTransport. The OAuth flow now drives a remote <code>hermes auth add</code> correctly with stdin forwarded.</li>
<li><strong>OAuth remove button.</strong> Per-provider remove action in Credential Pools (auth.json edit), with confirmation dialog. Companion auto-refresh of the view when <code>auth.json</code> changes externally (file-watcher).</li>
<li><strong><code>resolve_provider_client</code> error classification.</strong> When an auxiliary task references a provider whose credentials aren&#x27;t loaded, Hermes prints <code>resolve_provider_client: &lt;name&gt; requested but &lt;Display Name&gt; not configured</code> to stderr — pre-fix this surfaced in chat as the opaque <code>-32603 Internal error</code> with no actionable detail. Now classified into a clear hint pointing at Settings → Aux Models.</li>
<li><strong>Aux Tab unknown-task surface.</strong> When <code>config.yaml</code> has an <code>auxiliary.&lt;task&gt;</code> block for a task Scarf doesn&#x27;t know about (newer Hermes added it; Scarf hasn&#x27;t caught up), render it as a plain row with the raw provider/model values instead of dropping it silently.</li>
<li><strong>Credential Pools refresh after OAuth sheet dismiss.</strong> 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.</li>
</ul>
<hr>
<h3>ScarfMon — performance instrumentation harness</h3>
<p>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</p>
<ul>
<li><strong>Phases 1-3</strong> built the core: dispatcher + ring buffer + 3 backends, chat / transport / sqlite measure points, diagnostic counters for chat-render bursts, finalize-burst dampening.</li>
<li><strong>Tier A + B</strong> added per-feature instrumentation: iOS file watcher, sessions list, model catalog, dashboard widgets, image encoder, message hydration.</li>
<li><strong>Nous picker investigation</strong> localized a 60s + 120s beach-ball to a specific path (Nous catalog <code>readCache</code>), then killed the 120s one with dedupe + 5s timeout.</li>
<li><strong>Tier C catch-up</strong> (this release): instrumented Memory / Skills / Cron / Curator load paths so future captures show how often these tabs cost multiple sequential SFTP RTTs on remote.</li>
<li><strong>Per-call bytes recorded</strong> on transport + sqlite events so captures show payload sizes alongside latencies.</li>
<li><strong><code>mac.emptyAssistantTurn</code> event</strong> documents the Nous quirk where the model returns a thought stream with no body (the bubble looks like Hermes is &quot;still thinking&quot; but the turn already finished).</li>
</ul>
<p>Adding a new measure point is two lines. The harness covers Mac and iOS uniformly. The &quot;Copy as JSON&quot; button exports the ring buffer for paste-into-issue diagnosis.</p>
<hr>
<h3>Other fixes + polish</h3>
<ul>
<li><strong>Sessions sidebar reload debounce</strong> — file-watcher deltas during streaming used to flicker the sessions list. Coalesced into one trailing fetch ~500ms after the last tick.</li>
<li><strong>Session-load pagination + race guard</strong> — switching to a small chat while a larger one is mid-fetch could last-write-wins the small chat away. Three race-checks against <code>self.sessionId</code> prevent the stale fetch from overwriting.</li>
<li><strong>Sessions + previews batched</strong> — two separate SSH calls folded into one <code>queryBatch</code> round trip, halving the round-trips for every sidebar refresh.</li>
<li><strong>Remote SQLite query timeout</strong> bumped 15→30s to better tolerate slow links; in-flight query coalescing dedupes concurrent identical queries.</li>
<li><strong><code>Thread.sleep</code> spin replaced</strong> with a kernel-wait via <code>DispatchGroup</code> for <code>runLocal</code> timeout; under concurrent SSH load the old loop accumulated spin-blocked threads and produced 7-second outliers in <code>loadRecentSessions</code>.</li>
<li><strong>Window position + size</strong> persists across launches.</li>
<li><strong>Sidebar reorder</strong> — Projects promoted to first section; profile chip moved under server name.</li>
<li><strong><code>stop</code> badge suppressed</strong> on metadata footer for normal turn ends (it was firing for every clean completion, looking like an error).</li>
<li><strong>Nous picker search field</strong> + <code>model-picker</code> filter for the long Nous overlay model list.</li>
<li><strong><code>oauth-keepalive</code> cron create</strong> — drop the <code>--silent</code> flag Hermes doesn&#x27;t accept.</li>
<li><strong>Snapshot pipeline rewritten</strong> — replaced the <code>sqlite3 .backup</code>-then-download pipeline with direct SSH-streamed query execution (issue <a href="https://github.com/awizemann/scarf/issues/74">#74</a>). Eliminates the multi-minute snapshot wait on multi-GB state.db files. Companion fix: pre-expand <code>~/</code> in Swift via <code>resolvedUserHome</code> so sqlite3 finds the DB without depending on the remote shell&#x27;s tilde expansion.</li>
<li><strong>Aux nested-YAML parser</strong> — corrected the parser so the unknown-task surface works on remote (was previously dropping aux blocks whose <code>provider:</code> value lived on a separate line).</li>
<li><strong><code>ModelPreflight</code> newline trim bug</strong> — <code>.whitespaces</code> doesn&#x27;t strip newlines; switched both trims to <code>.whitespacesAndNewlines</code> so a stray <code>\n</code> in a hand-edited config.yaml doesn&#x27;t false-positive the mismatch banner.</li>
</ul>
<hr>
<h3>What&#x27;s measured today</h3>
<p>321 ScarfCore tests pass (302 prior + 19 new ModelPreflight). New ScarfMon events documented in the <a href="https://github.com/awizemann/scarf/wiki/Performance-Monitoring">Performance-Monitoring wiki</a>.</p>
<h3>Compatibility</h3>
<ul>
<li>macOS 14+ (unchanged).</li>
<li>Hermes target: still <strong>v2026.4.30 (v0.12.0)</strong>. No new Hermes capability gates added.</li>
<li>Existing <code>dashboard.json</code> files render unchanged.</li>
<li>Existing <code>.scarftemplate</code> bundles install unchanged. Catalog manifest schemaVersion stays at 1/2/3 — no bump.</li>
<li>Existing <code>~/.hermes/.env</code> content is preserved byte-identically — Scarf only writes inside its <code># scarf-secrets:begin &lt;slug&gt;</code> / <code># scarf-secrets:end &lt;slug&gt;</code> regions.</li>
<li>The skeleton-then-hydrate chat loader and SSH cancellation propagation are <strong>Mac-only</strong> in this release; ScarfGo (iOS) keeps its existing chat path.</li>
</ul>
<h3>What&#x27;s deferred</h3>
<ul>
<li><strong>Per-widget data sources + per-widget refresh granularity.</strong> The general &quot;widget points at a typed data source&quot; 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.</li>
<li><strong>Cross-project health digest sidebar rollup.</strong> Counting attention-needed projects across the registry — scoped but didn&#x27;t pull its weight. The typed status enum makes it cheap to add later.</li>
<li><strong>Automatic cron-prompt rewriter on upgrade.</strong> Heuristic rewrites of free-form prompts are risky; the docs + agent-assisted path ships in v2.7. Revisit a &quot;scan + fix&quot; UI in v2.8 if real users miss the migration.</li>
<li><strong>iOS New Project wizard + iOS Keychain-env mirror.</strong> ScarfGo&#x27;s project surface is read-only; the wizard&#x27;s chat-handoff pattern depends on Mac-only ACP plumbing.</li>
<li><strong>iOS skeleton-then-hydrate loaders.</strong> Same data-service surfaces are public, but the iOS chat lifecycle is structured differently. Defer until iOS dogfooding shows the same payload-size pain.</li>
<li><strong>Tier C redesigns (Memory/Skills/Cron/Curator).</strong> Instrumented in v2.7; redesign waits for capture data showing which path actually needs the skeleton-then-hydrate treatment.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.0/Scarf-v2.7.0-Universal.zip"
sparkle:edSignature="JfHK1sKbP3ubbznXx3uY/a3kY2szkWpTUQmtHJE54Uv970T5PxgIHCTwsimqtZCPepUTv2qK4TRQfqHJBKUmCA=="
length="18793474"
type="application/octet-stream" />
</item>
2026-05-03 22:20:31 +02:00
<item>
<title>Version 2.6.5</title>
<sparkle:version>31</sparkle:version>
<sparkle:shortVersionString>2.6.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sun, 03 May 2026 20:20:29 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.6.5/Scarf-v2.6.5-Universal.zip"
sparkle:edSignature="2hYRNX/z+mQ7TjlHfDAiTHiP/Rk1BoJRjjefPvutooiptwOK6EyYq/9crnHYZYe8xvEQTDMmAvsPz4vYz823Cg=="
length="18257858"
type="application/octet-stream" />
</item>
2026-05-01 15:48:16 +02:00
<item>
<title>Version 2.6.0</title>
<sparkle:version>29</sparkle:version>
<sparkle:shortVersionString>2.6.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 01 May 2026 13:48:15 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.6.0/Scarf-v2.6.0-Universal.zip"
sparkle:edSignature="gI1uwJAvmwdlvngLcsSQFtMDsHyI6+DWN23ZjOX/BYmX+BqKu0XLuAXL1tztZV9RF1l5PG+vNVWGnq6ivqVQCQ=="
length="18031081"
type="application/octet-stream" />
</item>
2026-04-29 13:47:41 +02:00
<item>
<title>Version 2.5.2</title>
<sparkle:version>28</sparkle:version>
<sparkle:shortVersionString>2.5.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Wed, 29 Apr 2026 11:47:40 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.2/Scarf-v2.5.2-Universal.zip"
sparkle:edSignature="tPgE0ajkNs+OYuYGgB8jVLtY/tGTaUJlrJCf5I5AbwCU2R+Zu08D+pLB17q01A3AgwYfYTLZbnoTG+xO4ys8DQ=="
length="17367761"
type="application/octet-stream" />
</item>
2026-04-27 15:38:43 +02:00
<item>
<title>Version 2.5.1</title>
<sparkle:version>27</sparkle:version>
<sparkle:shortVersionString>2.5.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Mon, 27 Apr 2026 13:38:41 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.1/Scarf-v2.5.1-Universal.zip"
sparkle:edSignature="OArax2dY25Q7ZRFYGcviaGmCQCJsIugcBdjTET//mJ4XTT/FnnPQoSTIYaQrsV+mwFZU//G75q9PwPtnNsPiCA=="
length="17090983"
type="application/octet-stream" />
</item>
2026-04-25 17:42:48 +02:00
<item>
<title>Version 2.5.0</title>
<sparkle:version>26</sparkle:version>
<sparkle:shortVersionString>2.5.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 25 Apr 2026 15:42:47 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.0/Scarf-v2.5.0-Universal.zip"
sparkle:edSignature="YnHpGMIiL8jyDn3+h8B7Gqzrlz8SXDSyiUXGm9DD6BIkRfYfzi3AVatkxfBLMvoVhlqPGKIhKsB8ybqopgjpCw=="
length="16994785"
type="application/octet-stream" />
</item>
2026-04-24 03:20:53 +02:00
<item>
<title>Version 2.3.0</title>
<sparkle:version>25</sparkle:version>
<sparkle:shortVersionString>2.3.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 24 Apr 2026 01:20:51 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.3.0/Scarf-v2.3.0-Universal.zip"
sparkle:edSignature="vae/hUTU7UOSY/LYU/pt1A9wnbKgvp22+e2peGA/clmloaA22gxCnBX5JALT1w93eHYMLOtvdSf5OrNHyogHDQ=="
length="14783787"
type="application/octet-stream" />
</item>
2026-04-23 22:10:12 +02:00
<item>
<title>Version 2.2.1</title>
<sparkle:version>24</sparkle:version>
<sparkle:shortVersionString>2.2.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 23 Apr 2026 20:10:10 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.2.1/Scarf-v2.2.1-Universal.zip"
sparkle:edSignature="nFdO9t2wWWKeXehQCm7btr7kzCtmDDg4xsvrm4Z24fqOB+Y9ffYcIBr+e9pqnLSIJJ6r/lYcyz5FlkUMjYXyCw=="
length="17868308"
type="application/octet-stream" />
</item>
2026-04-23 18:31:55 +02:00
<item>
<title>Version 2.2.0</title>
<sparkle:version>23</sparkle:version>
<sparkle:shortVersionString>2.2.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 23 Apr 2026 16:31:53 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.2.0/Scarf-v2.2.0-Universal.zip"
sparkle:edSignature="mGuKLJbcugMTKdSlrkgYKjpVAaXY8CsMVhCiAo/O7b4K8S/fRaK7ZZNdbLtfSGndrbVWcSU0IIhaB1rBGaPOBQ=="
length="17867934"
type="application/octet-stream" />
</item>
2026-04-20 18:50:31 -07:00
<item>
<title>Version 2.1.0</title>
<sparkle:version>22</sparkle:version>
<sparkle:shortVersionString>2.1.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Tue, 21 Apr 2026 01:50:30 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.1.0/Scarf-v2.1.0-Universal.zip"
sparkle:edSignature="kllR3yC/Cze1W9fSM+WRIE5YVObubEGmV629hAvxzVhvVIJ9n+qa00WOAC3YakZLEKX46DmowEMQf5ikqQGODQ=="
length="17243337"
type="application/octet-stream" />
</item>
2026-04-20 15:50:03 -07:00
<item>
<title>Version 2.0.2</title>
<sparkle:version>21</sparkle:version>
<sparkle:shortVersionString>2.0.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Mon, 20 Apr 2026 22:50:02 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.0.2/Scarf-v2.0.2-Universal.zip"
sparkle:edSignature="3BF7PzLqO835wr+cCEA0Ls4kfp2hNwgDmM8YlvUKM9R/GwTs+tgtSPLTwfr1UyXNH/83uNRuSeZM8iNgV4dPDA=="
length="17063811"
type="application/octet-stream" />
</item>
2026-04-19 13:16:49 -07:00
<item>
<title>Version 2.0.0</title>
<sparkle:version>19</sparkle:version>
<sparkle:shortVersionString>2.0.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sun, 19 Apr 2026 20:11:43 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.0.0/Scarf-v2.0.0-Universal.zip"
sparkle:edSignature="QTgPwFgHk5SbUYKrgg412o/Yc7ZhSiow/33EoWnN+julyEVw/0MwemjHNcwcX+aVQJV4arMp7xGVMzaVV/j1CQ=="
length="16906164"
type="application/octet-stream" />
</item>
2026-04-17 17:22:29 -07:00
<item>
<title>Version 1.6.2</title>
<sparkle:version>18</sparkle:version>
<sparkle:shortVersionString>1.6.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 18 Apr 2026 00:22:28 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v1.6.2/Scarf-v1.6.2-Universal.zip"
sparkle:edSignature="wtf0QjTKCvYq8BZW1meeWdWk8GMsbYopfM/DNiYw8ImdgmX6X8jN5+bIG9KvzYv0VgZ0la8ssSliiz7zdJA2CQ=="
length="16570465"
type="application/octet-stream" />
</item>
2026-04-16 20:03:56 -07:00
<item>
<title>Version 1.6.1</title>
<sparkle:version>17</sparkle:version>
<sparkle:shortVersionString>1.6.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 17 Apr 2026 02:11:53 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v1.6.1/Scarf-v1.6.1-Universal.zip"
sparkle:edSignature="hoYDb7VRQ+YDNUox1kf7eYbhckOJWYLEi8ZPfBZG59qK4L/5N2mmgV7jOCLriHkNx0F4mvM9UK8UQZIkxsX1DA=="
length="16566934"
type="application/octet-stream" />
</item>
2026-04-16 18:09:32 -07:00
</channel>
</rss>