Files
scarf/releases/v2.7.5/RELEASE_NOTES.md
T
Alan Wizemann adcc984091 feat(kanban): full read/write board with per-project tenants
Lifts Scarf's Kanban surface from the v2.6 read-only list to a
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.

Why now: the v2.6 list was a placeholder shipped while upstream Kanban
collab was still mid-rework. v0.12 stabilized the 27-verb CLI; this
release makes Scarf a real GUI client for it. Driving real tasks
end-to-end exposed and closed a connected bug pattern (claim vs
dispatch, silent skipped_unassigned, integer-vs-ISO timestamps,
parser-leaked "(no" sentinel) that would have shipped as latent UX
papercuts otherwise.

ScarfCore: KanbanService actor (Sendable, pure I/O) wrapping every
verb; KanbanTenantReader cross-platform manifest projection; eight
new model types (TaskDetail, Comment, Event, Run, Stats, Assignee,
CreateRequest, Filters); KanbanError; pure transition planner that
maps drag-drop column changes to verb sequences, tested against
canonical Hermes JSON fixtures.

Mac: KanbanBoardView orchestrator with five-column drag-drop layout,
optimistic-merge state, KanbanInspectorPane side-pane (Comments /
Events / Runs / Log tabs, Log streams worker stdout every 2s while
running), inline assignee picker, health banner for unassigned and
last-failed-run states. New Task sheet defaults to active profile
and auto-fires kanban dispatch on submit. Sidebar moved Kanban from
Manage to Monitor. Read-only KanbanListView preserved as Board|List
toggle for narrow windows / accessibility.

Per-project: DashboardTab.kanban tab on every project gated on
hasKanban; KanbanTenantResolver mints scarf:<slug> tenants on first
interaction and persists to .scarf/manifest.json (immutable across
rename); ProjectAgentContextService surfaces the tenant in the
AGENTS.md scarf-managed block so agents pass --tenant <slug> on
kanban create. New kanban_summary dashboard widget; vocabulary
mirrored in tools/widget-schema.json and site/widgets.js.

iOS: read-only board on the project tab via paged single-column
Picker, modal detail sheet with Comments / Events / Runs. Mutations
+ drag-drop deferred to v2.8.

Tests: 19 new pure-logic tests covering decoding, planner verb
mapping, argv assembly, glance string formatting, and parser
rejection of the kanban assignees empty-state sentinel. All 348
ScarfCore tests pass.

Constraints documented in CLAUDE.md: no within-column reorder
(Hermes has no update --priority verb); no live watch streaming
yet (5s polling for board, 2s for log); no bulk re-tag for legacy
NULL-tenant tasks. Pre-v0.12 Hermes hosts gracefully hide the
surface end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:59:21 +02:00

17 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 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.

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.