GUI-launched Scarf inherits macOS's launch-services PATH (`/usr/bin:/bin:/usr/sbin:/sbin`). Scarf itself finds `hermes` via absolute-path resolution in `HermesPathSet.hermesBinaryCandidates`, but when the kanban dispatcher (a child of Scarf) tries to spawn a worker, the worker inherits the same stripped PATH and Hermes's spawn machinery prints `\`hermes\` executable not found on PATH. Install Hermes Agent or activate its venv before running the kanban dispatcher.` — recording `outcome=spawn_failed` on the run. `LocalTransport` now mirrors `SSHTransport.environmentEnricher`: adds an `environmentEnricher: (() -> [String: String])?` static, and applies it to every subprocess. `scarfApp.swift` wires it at launch to the same `HermesFileService.enrichedEnvironment()` login-shell probe (`zsh -l -i` → `zsh -l` fallback) the SSH transport already uses, so subprocesses see `~/.local/bin`, `/opt/homebrew/bin`, and the user's credential env vars. Defense-in-depth: `subprocessEnvironment(forExecutable:)` always prepends the executable's own directory to PATH if missing — covers early-startup paths and test harnesses where the enricher hasn't been wired yet. Two new tests in `KanbanModelsTests` lock in: 1. The fallback (no enricher → executable's dir lands on PATH) 2. The enricher win for PATH + the empty-string-aware copy semantics for credential env vars (process env happens to set `ANTHROPIC_API_KEY=""` as an empty string in some environments; the enricher's non-empty value must still take effect) Release notes for v2.7.5 updated to document the fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
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 onready, 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 fireskanban dispatch(the dispatcher then spawns a worker), drop-on-Blocked opens a sheet asking for a reason and callskanban block, drop-on-Done opens a result sheet and callskanban complete, blocked → running chainsunblock+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 callskanban comment), Events (thetask_eventslog with per-kind glyphs), Runs (one row per attempt with outcome badge + summary + error), and Log — the worker's captured stdout/stderr fromhermes 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 isclaimrebranded 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.dband are recoverable via the "Show archived" toggle untilhermes kanban gcruns. -
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 fromHermesProfileResolver) plus an "Unassigned" option. Selection routes throughkanban assignand immediately follows withkanban dispatchso 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--jsonoutput literally lists these underskipped_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 rawerrorfield 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 torunning, 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
runningget a 2 pxScarfColor.infoleft edge + a subtle title shimmer so live work is obvious at a glance. Blocked cards get a 2 pxScarfColor.warningleft 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 inready/todowith 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 | Listtoggle at the top of the route. The v2.6 read-only list view is preserved inKanbanListView.swiftand 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 (segmentedScratch / 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 fireskanban create --jsonand immediately follows withkanban dispatchso an assigned task transitionsready→runningwithin 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.kanbanon every project, capability-gated onHermesCapabilities.hasKanban. Renders a project-scopedKanbanBoardViewfiltered to the project's tenant slug. Workspace defaults in the New Task sheet are pre-pinned todir:<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 optionalkanbanTenantfield onProjectTemplateManifest). 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 withid: scarf/<project-id>+version: 0.0.0+ just thekanbanTenantset; theProjectAgentContextServicereader 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.mdwhenever a tenant exists, instructing the agent to pass--tenant scarf:<slug>onhermes kanban create.ChatViewModel.startACPSessionalready callsrefresh(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--tenantlands the task in the global "Untagged" group rather than failing — acceptable v2.7.5 behavior. -
kanban_summarydashboard widget (KanbanSummaryWidgetView.swift). New widget kind for project dashboards: shows the top threerunning/blocked/todotasks for the project's tenant by priority, plus a glance footer ("12 todo · 3 running · 5 blocked") sourced fromkanban 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 intodashboard.json.
iOS / iPadOS
- Read-only Kanban tab on
ProjectDetailView(Scarf iOS/Kanban/ScarfGoKanbanView.swift). Same five-column collapse rendered as a horizontally-paged segmentedPickerof 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 → modalNavigationStackdetail 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(notScarfFont) so Dynamic Type works; chrome (badges) stays onScarfBadgefor fixed visual weight per the project's iOS conventions.
ScarfCore
-
KanbanServiceactor (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 throughTask.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 throwKanbanError.forbiddenTransitionwith a user-actionable reason. The planner is fully tested inKanbanModelsTests.swift. Critically:dispatch(notclaim) is the verb used for Up-Next → Running. Hermes'sclaimis documented as "manual alternative to the dispatcher" and assumes the caller spawns the worker themselves — Scarf doesn't, so callingclaimfrom drag-drop reserved tasks but never spawned work, and the dispatcher reclaimed them ~15 minutes later (stale_lock).dispatchis the right primitive for a GUI client. -
Cross-platform KanbanTenantReader. Read-only projection over
<project>/.scarf/manifest.json'skanbanTenantfield. The fullProjectTemplateManifesttype 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_atetc. 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 bydecodeUnixIntegerTimestampsinKanbanModelsTests. -
KanbanBoardViewModeloptimistic merge. HoldsoptimisticOverrides: [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 --jsonoutput literally lists them under askipped_unassignedkey and moves on. Tasks created without an assignee sat inreadyindefinitely 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, everyready/todocard 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 torunning, but no worker spawned (Scarf doesn't host workers), and 15 minutes later the dispatcher reclaimed the task with astale_lockoutcome. Replaced withdispatchend-to-end so the gateway-running dispatcher actually does the spawning. -
hermes kanban assigneesempty-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(noas 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_failedfrom "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 installshermes) or/opt/homebrew/bin. Scarf was findinghermesfor its own invocation via the absolute-path resolver inHermesPathSet.hermesBinaryCandidates, but when the dispatcher then spawned a worker process, that worker inherited Scarf's GUI PATH and couldn't findhermesby name — recording anoutcome=spawn_failedrun with the exact "executable not found on PATH" message.LocalTransportnow grows anenvironmentEnricherstatic (mirroringSSHTransport.environmentEnricher) wired byscarfApp.swiftto the sameHermesFileService.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
updateverb and nopositioncolumn on the tasks table —priorityis write-once at create time. Sort order inside each column ispriority 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 anupdate --priorityverb. -
No live
watchstreaming 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 --jsonevent 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
tenantmutation 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
claimvsdispatchdistinction, the silentskipped_unassignedbehavior, the(noparse 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.