Reflect the three post-initial-commit fixes:
- log-noise suppression (skill.yaml / optional-file 'No such file'
warnings no longer spam Console via the new Result-returning readers)
- diagnostics script now stdin-pipes to sh -s instead of sh -c <script>
argv, so it runs as one sh process with variable scope preserved
- pill UX: replaced colored dot with state-specific SF Symbol
(checkmark / stethoscope / arrows / triangle), removed custom
background, kept .principal placement for centering
Also expanded the 'Known follow-ups' section so users know what's
explicitly deferred post-2.0.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rolling back the .primaryAction placement (the pill shifted right and
lost its centered position in the toolbar). The "funny background with
shadow" visible in the toolbar is macOS's own .principal emphasis bezel
— not something Scarf draws, and not something we can cleanly hide
without disabling the toolbar surface itself. The native bezel is the
pill's frame; we just have to make the pill's interior read well inside
it.
Two changes to make the pill itself look like a toolbar tool inside
that bezel:
- Drop the colored dot, replace with a state-specific SF Symbol. The
icon's shape signals clickability (looks like a tool button), and its
color signals state (green/orange/yellow/red hierarchical). Less
"status chip", more "toolbar button with status".
- Icons per state:
- connected → checkmark.circle.fill (click to re-probe)
- degraded → stethoscope (click to run diagnostics, matches the
stethoscope on the Manage Servers row)
- idle → arrow.triangle.2.circlepath (checking/retry)
- error → exclamationmark.triangle.fill (click for stderr)
Horizontal padding = 4 so the icon-and-label sit balanced inside the
bezel rather than pushed up against its edges.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macOS applies a centered emphasis bezel (light capsule + drop shadow)
to ToolbarItem(placement: .principal) — visible in screenshots as a
doubly-framed "capsule behind the pill" look. The pill itself doesn't
own that background; the toolbar placement does.
.primaryAction (right side of the toolbar) has no decorative
background, so the pill renders as just the colored dot + label text
directly on the toolbar surface. Fits the intended minimal look.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toolbar item already draws its own bezel for the principal-placement
slot; painting a `Color.secondary.opacity(0.08)` capsule on top gave the
pill a doubly-framed look. Drop the pill's background + the padding that
was only there to fit inside the capsule. The dot + label now sit
directly on the toolbar's native surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix (direct ssh argv, bypassing transport.runProcess) got
us from 0/14 to 7/14, but \$H was empty everywhere it was referenced —
the user's 7/14 report showed:
- probe 4 (hermesHomeConfigured): PASS with empty detail
- probe 5 (hermesDirExists FAIL): "not a directory:" (empty after colon)
- probe 11 (sqlite3CanOpenStateDB FAIL): 'unable to open "/state.db"'
Root cause: `ssh host -- /bin/sh -c <script>` doesn't travel as three
argv entries to the remote. ssh concatenates them with single spaces
into one command string and sends that to the remote's LOGIN shell.
The login shell then runs `$LOGIN_SHELL -c "$string"`, and bash's
parser treats unquoted newlines inside `$string` as command separators.
So the first newline splits the script: `/bin/sh -c H="..."` becomes
one command (which runs in an ephemeral sh subprocess that exits
immediately), and every subsequent line runs in the login shell with
no \$H set.
TestConnectionProbe happens to still work because its downstream lines
don't depend on an assignment from the first line — but the diagnostic
script's \$H is used everywhere, so the entire script is effectively
running with \$H="".
Fix: pipe the script into `/bin/sh -s` on stdin via ssh's own stdin
channel. `sh -s` reads a shell program from stdin and executes it in
one process, variable scope preserved. Implementation uses
Process.standardInput with a Pipe, writing the script after proc.run()
and closing the write end so sh sees EOF. Same as
`cat script.sh | ssh host -- /bin/sh -s` from the command line.
Also: raw-output disclosure panel in the diagnostics sheet now shows
whenever ANY probe fails, not only when all fail. Partial failures are
the most common failure mode and the raw stdout is the only way to see
why a specific detail came back the way it did.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First-run of diagnostics against a working Mardon returned 0/14 passing
with "(no output)" for every probe — including the trivial "emit
connectivity PASS" that the script emits unconditionally. That meant the
script wasn't executing as written; the parser saw `__END__` but no
probe lines.
Root cause: SSHTransport.runProcess wraps every argument through
`remotePathArg`, which is designed for PATHS (it rewrites `~/` to
`$HOME/` and double-quotes the result with backslash-escapes). Passing
a multi-line shell script with embedded `"$1"` / `"$2"` / `"$3"` and
`printf '\n'` escape sequences through that is corruption — the remote
sh -c receives a scrambled script and silently emits nothing.
TestConnectionProbe already works around this: it builds the ssh argv
directly (ssh host -- /bin/sh -c <script>) so the script travels as a
single opaque argv entry and ssh forwards it to the remote shell
unchanged.
Mirror that approach. RemoteDiagnosticsViewModel.execute now:
- For remote contexts: builds ssh argv directly (ControlMaster-aware,
uses the same socket as SSHTransport so it's effectively free after
the first connection), then passes /bin/sh -c <script> as argv.
- For local contexts: spawns /bin/sh -c <script> via Process directly.
Also surfaces raw stdout/stderr/exit-code in a disclosure panel at the
bottom of the sheet, visible only when ALL probes fail. Makes any
future transport-level breakage self-diagnosing: the user sees exactly
what the remote returned, not just "(no output)" rows.
Expose SSHTransport.controlDirPath (already static) as a public helper
so the diagnostics probe reuses the same ControlMaster socket as the
connection itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Result-returning readers I added for the v2.0.1 diagnostics surface
were logging EVERY failure, including routine "file doesn't exist" cases
— e.g. skill.yaml files under ~/.hermes/skills/*/ that are optional
metadata, gateway_state.json before Hermes has started, memories/USER.md
on fresh installs.
In practice this meant the Platforms view and similar feature loaders
that walk directories and read optional files now spam the Console with
warnings on every refresh. That's noisier than useful and actively hides
the signal (permission denied, connection failure, sqlite3 missing) we
added the logging to surface.
readFileDataResult now detects the "no such file" case via either:
- TransportError.fileIO(_, "No such file...") from SSHTransport
- NSCocoaErrorDomain code 260 (NSFileNoSuchFileError) from FileManager
- NSPOSIXErrorDomain code 2 (ENOENT)
and suppresses the warning log for those paths. The Result.failure is
still returned, so any caller that cares (Dashboard's banner, Remote
Diagnostics) can still distinguish missing from present-but-unreadable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three users reported on day-one of v2.0 that SSH connections showed a
green "Connected" pill but every data view read as empty / "not running"
/ "not configured". The common thread across Docker, homelab VM, and
Ubuntu VPS setups: file-access failures on the remote that Scarf
silently swallowed into nil/empty defaults.
Stop swallowing errors
- HermesFileService gains Result-returning variants for the four
dashboard-critical readers: loadConfigResult, loadGatewayStateResult,
hermesPIDResult, plus readFileResult / readFileDataResult as
primitives. Each logs os.Logger warnings on failure. Legacy nil-
returning signatures remain as thin forwarders.
- HermesDataService.open records lastOpenError with humanized hints
for the top three failure modes — sqlite3 not installed, permission
denied, file not found. Each maps to concrete remediation (`apt
install sqlite3`, "check file perms", "set Hermes data directory").
Dashboard surfaces the error
- DashboardViewModel collects errors from every loader into
lastReadError, only on remote contexts (local skips the banner).
- DashboardView renders an orange banner above the stats with the
specific error text, a copy-selectable detail, and a "Run
Diagnostics…" button.
New Remote Diagnostics sheet (stethoscope icon)
- RemoteDiagnosticsViewModel runs 14 checks in one SSH round-trip via
a pipe-delimited "KEY|STATUS|DETAIL" protocol. Covers: SSH
connectivity, remote user/$HOME, Hermes dir existence + readability,
config.yaml readability + actual read (distinct from just `test -e`
which can't detect permission issues), state.db readability, sqlite3
binary presence, sqlite3 open test, hermes binary on non-login AND
login PATH, pgrep availability.
- Each probe row shows a targeted hint on fail (e.g. "check perms on
~/.hermes", "apt install sqlite3", "move PATH export from .bashrc
to .zshenv"). A Copy Full Report button dumps plain-text output
for GitHub issues.
- Accessible from Manage Servers (stethoscope button per row) and
directly from the yellow pill.
Yellow "degraded" connection state
- ConnectionStatusViewModel.Status gains .degraded(reason:) between
.connected and .error. After tier-1 `true` passes, the probe runs
tier-2 `test -r $HOME/.hermes/config.yaml` in the same SSH round-
trip. On tier-2 fail, pill is orange with "Connected — can't read
Hermes state" tooltip.
- Clicking a degraded pill opens Remote Diagnostics directly. Exactly
the symptom in #19 is now one click from a specific answer.
Auto-suggest remoteHome for non-default installs
- TestConnectionProbe.TestResult.success gains suggestedRemoteHome:
String?. When state.db isn't found at the configured path, the
probe also checks /var/lib/hermes/.hermes, /opt/hermes/.hermes,
/home/hermes/.hermes, /root/.hermes — the common alternates for
systemd services, Docker containers, and single-user VPSes — and
surfaces the first hit as a "Use this" suggestion in Add Server.
- AddServerSheet relabels "Remote ~/.hermes override" to "Hermes data
directory" with an explanation of when you'd use it.
README
- New "Remote setup requirements" subsection lists the four concrete
prereqs (SSH, sqlite3, pgrep, read access to ~/.hermes).
- New "Troubleshooting remote connections" paragraph describes the
diagnostics sheet and remoteHome auto-suggest for the two most
common failure modes.
Releases
- releases/v2.0.1/RELEASE_NOTES.md for the GitHub release body.
- Ship via `./scripts/release.sh 2.0.1`.
Closes#19.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings multi-window + multi-server + remote-SSH support to main,
plus the full correctness/UX/concurrency polish pass.
Two commits land:
- 00ca722 feat: multi-window + remote SSH server support (Phases 0-4)
- 5920923 feat: v2.0 — correctness + UX polish on multi-server + remote SSH
See releases/v2.0.0/RELEASE_NOTES.md for the user-facing summary.
The multi-window / multi-server / remote-SSH work that landed in
00ca722 (feat: multi-window + remote SSH server support (Phases 0-4))
was feature-complete but accumulated rough edges during dogfooding
against a remote Mac mini. This commit finishes the 2.0 release:
correctness fixes on remote, a chat-view UX overhaul, and a Swift 6
complete-concurrency sweep across the service layer.
Correctness on remote
- Kill the WAL-error spam: snapshotSQLite now runs `PRAGMA
journal_mode=DELETE` on the remote temp DB before scp, so the
pulled file is self-contained. Open remote snapshots with
`file:...?immutable=1` URI as defense-in-depth, and drop the
pointless `PRAGMA journal_mode=WAL` from HermesDataService.open.
- loadSessionHistory and refreshMessages now force a fresh snapshot
via refresh(), so resuming a session on a remote shows messages
persisted since launch (previously stuck on the first snapshot).
- New SnapshotCoordinator actor dedupes concurrent snapshotSQLite
calls per ServerID — Dashboard + Sessions + Activity no longer
issue three parallel SSH backups for the same fetch.
- ACP cwd comes from the remote's $HOME (probed once, cached per
server in UserHomeCache), not the local Mac's NSHomeDirectory().
- Typing into a blank Chat always creates a new session. The old
auto-resume-most-recent fallback was picking up cron-spawned
sessions that Hermes had already GC'd, producing silent prompt
failures.
- handlePromptComplete surfaces non-success stopReasons ("refusal",
"error", "max_tokens") as a system message so failed prompts no
longer sit under a forever-spinning "Agent working…".
Chat UX
- Replace six racing onChange-driven scrollTo calls with
`.defaultScrollAnchor(.bottom)` alone. Manual proxy.scrollTo
against a LazyVStack that hadn't finished laying out was
overshooting into whitespace. Layout-pass-integrated anchor
behaves correctly at stream start and finish.
- Remove ContentUnavailableView swap in RichChatView — it tore down
the whole ScrollView hierarchy on first message. Empty state now
lives inside the scroll view.
- continueLastSession surfaces an acpError banner if open() fails,
instead of silently returning.
Lifecycle hygiene
- ServerRegistry.removeServer closes the server's SSH ControlMaster
(`ssh -O exit`), prunes its snapshot cache dir, and invalidates
UserHomeCache for that ID. App launch sweeps orphan snapshot dirs
whose UUIDs aren't in the registry anymore.
- NSWorkspace.activateFileViewerSelecting (backup-saved-to dialog)
gated on !context.isRemote; remote surfaces the remote path in the
saveMessage instead of silently no-op'ing on a nonexistent local
path.
Swift 6 concurrency — 230 warnings → 1
- Mark ServerContext, HermesPathSet, ServerTransport (protocol),
LocalTransport, SSHTransport, HermesFileService, and every value-
type accessor as `nonisolated`. Prevents AppKit-import-driven
MainActor inference from bleeding onto data-only types.
- Hand-written Codable conformances (vs. synthesized) for
ACPRequest, ACPRawMessage, ACPError, GatewayState, PlatformState,
HermesCronJob, CronSchedule, CronJobsFile, AuthFile, AuthEntry.
Synthesized inits were inferred @MainActor by Swift 6's default-
isolation rule; hand-written ones are explicitly nonisolated.
- Captured-var refactors in MCPServerEditorViewModel, PluginsView
Model, LocalTransport.watchPaths. Thread.sleep → Task.sleep in
TestConnectionProbe.
- Remaining warning is AnyCodable.value mutation in init(from:) —
Any-typed storage can't be strictly Sendable; acknowledged via
@unchecked Sendable.
ACP adapter upstream bug (not fixed here, but handled)
- Hermes's ACP adapter returns JSON-RPC success `{"result":{}}` for
session/load on a missing session, logging the warning only to
stderr. Scarf can't distinguish "loaded" from "silently missing"
at that layer; the stopReason=refusal surfacing above catches the
downstream symptom. Upstream issue worth filing.
Release docs
- releases/v2.0.0/RELEASE_NOTES.md with full user-facing breakdown.
- README.md "What's New" bumped to 2.0 with a multi-server section.
Compatibility table adds v0.10.0 as verified.
- GitHub repo description updated (via `gh repo edit`) to call out
multi-server + remote SSH.
35 files changed, +809/-350.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the ability to manage multiple Hermes installations — local and
remote over SSH — from the same Scarf app, each in its own window.
Architecture:
- ServerContext value type carries per-server identity + paths through
every VM and service. ContentView routes serverContext into each
feature view's init; all 22 routed views thread it through to their
@State VMs.
- ServerTransport protocol with LocalTransport (FileManager/Process/
FSEvents) and SSHTransport (system ssh + scp + ControlMaster).
Services were ported from direct Foundation I/O to transport-routed
helpers so the same code runs against local or remote.
- WindowGroup(for: ServerID.self) gives each window its own
AppCoordinator + HermesFileWatcher + ChatViewModel. File menu has
Open Server commands with keyboard shortcuts (⌘1..⌘9). MenuBarExtra
fans out per-server with start/stop/restart controls.
- ServerRegistry persists connections to ~/Library/Application
Support/scarf/servers.json. Add Server sheet probes the remote with
ssh -v to capture the full handshake on failure.
- Connection-status pill in remote-window toolbars with silent reconnect
(3s retry on first failure, escalate to red after 2 consecutive),
known-hosts-mismatch + ssh-add hint cards with copy buttons.
Concurrency / UX hardening (the parts learned the hard way during
dogfooding — captured in the feedback memory):
- ServerContext exposes context.readText / readData / writeText /
fileExists / runHermes / openInLocalEditor as the canonical I/O
surface. Every VM uses these; never raw FileManager / Process() /
NSWorkspace.open with a Hermes path.
- SSHTransport.remotePathArg rewrites ~/foo to "$HOME/foo" so paths
expand correctly inside the sh -c command we build (POSIX shells
don't expand ~ inside any quotes).
- Heavy VM load() methods detach to a background task and commit
results back via MainActor.run, so synchronous ssh round-trips don't
beach-ball the UI. Applied to Dashboard, Memory, Settings,
MCPServers, Cron, Plugins, Personalities, QuickCommands, Skills,
Gateway, Health, CredentialPools.
- LoadingOverlay modifier shows a spinner over empty/stale section
content during background reloads.
- enrichedShellEnv (zsh -l -i probe, up to 8s) is now warmed at app
launch off-main so first MainActor caller doesn't block.
- Drop the file watcher's 5s heartbeat — FSEvents covers real changes
and the heartbeat was triggering wasted reloads across every
subscribing view.
Chat polish:
- ChatViewModel.hermesBinaryExists is a stored bool probed once at
init, not a sync transport call evaluated on every body re-render.
- MessageGroupView identifies assistant bubbles by array offset rather
than message.id, so the streaming → finalized id transition no
longer destroys + recreates the bubble.
- Static scroll anchor in RichChatMessageList prevents two onChange
handlers from racing on isWorking flips.
Branch state: feature complete, in active dogfooding. Plan + per-phase
status live at ~/.claude/plans/we-developed-an-application-harmonic-stroustrup.md;
the four hard-won transport/concurrency rules are saved in the
ServerContext-pattern feedback memory for future sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The script was creating the GitHub release before pushing main, which
caused gh to auto-create the v<VERSION> tag at the then-current origin
HEAD (one commit behind the bump, since main hadn't been pushed yet).
The subsequent `git push origin v<VERSION>` was then rejected as
non-fast-forward, leaving the remote tag pointing at the wrong commit.
Caught during v1.6.2. The remote tag for v1.6.2 was force-corrected to
12610fa (the bump commit); the release artifacts themselves were always
correct.
New order: push main → tag main locally → push tag → gh release create.
Gh will now find the tag already on origin and attach to the right
commit. Non-destructive: a retry-safe release can always be resumed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CLAUDE.md's release-notes convention says "write them to
releases/v<version>/RELEASE_NOTES.md BEFORE running the script" — but
the script's git-clean preflight rejected any working-tree state
including that exact file as untracked. Chicken-and-egg: you couldn't
follow the documented flow.
Preflight now whitelists releases/v<VERSION>/RELEASE_NOTES.md as the one
allowed untracked path. Everything else still fails the check.
Caught while running v1.6.2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stops the release script's git-clean preflight from tripping on the
zips + appcast-entry.xml that every release run produces under
releases/v<VERSION>/. GitHub Releases hosts the actual downloads; there's
no reason to commit ~30 MB of binaries per release into git history.
RELEASE_NOTES.md stays tracked — it's committed as part of the version
bump by the release script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a Releases section so future Claude sessions (and teammates) don't
have to rediscover the release workflow. Documents:
- The single entry point: `./scripts/release.sh <ver> [--draft]`
- What the script does end-to-end
- The release notes convention (write them before running)
- A handful of canonical prompts the user can type
- Pointers to deeper prerequisite docs (README, script header)
Deliberately brief — detail lives in README and the personal auto-memory
at reference_release_process.md. CLAUDE.md's job here is just to make
the entry point discoverable on session start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The orange "No AI provider credentials detected" banner was firing on the
Chat tab whenever no session was selected, even for users whose
credentials were configured and working. The banner only disappeared
when a session started — not because credentials were actually found,
but because the banner's `!hasActiveProcess` gate flipped to false once
ACP launched.
Root cause: `HermesFileService.hasAnyAICredential()` inspected only the
shell environment and `~/.hermes/.env`, while Hermes itself resolves
credentials from two additional places Scarf had never learned about:
- `~/.hermes/auth.json` — the Credential Pools file written by the
Configure → Credential Pools UI (the blessed v1.6 flow)
- `~/.hermes/config.yaml` — embedded `api_key:` under auxiliary.<task>
and delegation
The preflight now checks all four locations. For auth.json we parse the
JSON and look for any `credential_pool.<provider>[*].access_token` that
is non-empty. For config.yaml we line-scan for `api_key:` leaves with a
non-empty value, matching the defensive style of the existing .env
scanner (no YAML parser needed in a nonisolated function).
Also updated the banner subtitle to point users at Credential Pools
before .env, since the former is the blessed in-app flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each release now produces two distribution zips:
- Scarf-vX.X.X-Universal.zip (arm64 + x86_64, recommended)
- Scarf-vX.X.X-ARM64.zip (arm64 only, ~14% smaller)
Both are independently archived, exported with Developer ID, notarized,
and stapled via a new build_variant helper. The appcast still points at
the Universal zip since it works on all supported macs; ARM64 is an
alternative manual download for Apple Silicon users who want the smaller
file.
README updated to list both variants.
Prompted by the v1.6.1 release shipping only Universal; the ARM64 zip
for v1.6.1 was produced ad-hoc and uploaded to the existing release.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drafts skip the appcast push and main tag, so a draft release won't
show up in users' Sparkle update feed and v1.6.0 stays "latest" until
explicitly promoted. The signed appcast entry is saved to the release
dir for later manual promotion.
Also adds release notes file convention: releases/v<VERSION>/RELEASE_NOTES.md
is auto-included in the version-bump commit and used as the GitHub
release body.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Xcode exports the bundle as scarf.app because PRODUCT_NAME = $TARGET_NAME
and the target is lowercase "scarf". Users expect Scarf.app in their
/Applications folder. Renaming the bundle wrapper preserves the
signature (codesign signs contents, not the wrapper directory name).
Caught during a build+sign+verify dry run before the first notarized
release.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A fresh-install user reported Scarf chat only worked while `hermes chat`
was also running in Terminal. ACP connected successfully but sending a
message errored. `~/.hermes/logs/errors.log` showed the real cause:
RuntimeError: No Anthropic credentials found. Set ANTHROPIC_TOKEN or
ANTHROPIC_API_KEY, run 'claude setup-token', or authenticate with
'claude /login'.
The terminal workaround masked the bug because the terminal-launched
`hermes` inherits the user's shell env (ANTHROPIC_* exports, Keychain
session) while a Finder/Dock-launched Scarf subprocess does not.
Scarf's previous PATH-only enrichment (commit b2a29ab) fixed binary
discovery but not credential propagation.
Five changes:
1. Propagate credential env vars from the login shell.
HermesFileService.enrichedEnvironment() now harvests a conservative
allowlist of AI-provider keys (ANTHROPIC_API_KEY/TOKEN/BASE_URL,
OPENAI_*, OPENROUTER_*, GEMINI/GOOGLE/GROQ/MISTRAL/XAI API keys,
CLAUDE_CODE_OAUTH_TOKEN) alongside PATH. Uses one `zsh` probe with
null-delimited `printf` so values with newlines survive, cached for
the process lifetime.
2. Two-attempt shell probe catches nvm/asdf/mise PATH.
Previous `zsh -l` missed `.zshrc`-exported PATH (nvm). New probe
first tries `zsh -l -i` (login + interactive, sources .zshrc) with
prompt frameworks defanged (TERM=dumb, empty PS1/PROMPT,
POWERLEVEL9K_INSTANT_PROMPT=off, STARSHIP_DISABLE=1,
ZSH_DISABLE_COMPFIX=true) and a 5s timeout; falls back to `zsh -l`
with 3s; finally to hardcoded defaults.
3. Resolve `hermes` binary across install locations.
HermesPaths.hermesBinary is now computed, walking pipx
(~/.local/bin), Apple Silicon brew (/opt/homebrew/bin), Intel brew
/ manual (/usr/local/bin), and ~/.hermes/bin. Returns the first
executable match or the pipx default for "Expected at …"
diagnostics. All 10+ callsites (ACPClient, scarfApp, Health /
Gateway / Tools / Sessions / QuickCommands / Personalities /
Settings / WhatsAppSetup / OAuthFlow / CredentialPools
ViewModels) auto-migrate with zero edits.
HermesFileService.hermesBinaryPath() shares the same candidate
list as the source of truth.
4. Surface the real failure in the chat UI.
ACPClient keeps a 50-line ring buffer of subprocess stderr
(previously only sent to os_log). New ACPErrorHint.classify pattern-
matches the common fresh-install failures — "No credentials found",
"No such file or directory: 'npx'", rate-limit — and returns a short
human hint. ChatView gains an errorBanner between toolbar and chat
area showing the hint + raw message + a "Show details" disclosure
with the stderr tail in a selectable monospaced view, plus a
clipboard-copy button.
5. Preflight credential check.
HermesFileService.hasAnyAICredential() scans the enriched env and
~/.hermes/.env for any known provider key. ChatViewModel exposes
`missingCredentials`; the banner becomes a pre-emptive warning
("No AI provider credentials detected — add ANTHROPIC_API_KEY to
~/.hermes/.env or your shell profile") before the user even hits
Send. HermesFileWatcher already watches ~/.hermes/.env, so edits
re-trigger preflight automatically.
Incidental cleanup: recordACPFailure(_:client:context:) folds the
per-site `logger.error` calls, removing three `_ = msg` suppressions.
Dead `enrichedPath` alias removed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Sparkle 2 auto-updates and a local release script that produces
signed, notarized, stapled builds for GitHub distribution. App Store
submission was rejected because Scarf spawns the user-installed hermes
binary and reads ~/.hermes/ directly — both forbidden by App Sandbox —
so we commit to the GitHub-release path properly.
- Sparkle SPM dep wired into the app target (link-only; hardened-runtime
entitlement disable-library-validation lets Sparkle load at runtime).
- Tracked Info.plist with SUFeedURL, SUPublicEDKey, and daily check
interval; replaces the auto-generated plist so Sparkle keys live in
version control rather than pbxproj INFOPLIST_KEY_* noise.
- UpdaterService wraps SPUStandardUpdaterController and is injected via
.environment(). Menu bar, standard app menu (CommandGroup after
.appInfo), and a new Updates section in Settings → General each call
updater.checkForUpdates().
- scripts/release.sh runs the full pipeline: version bump → universal
archive → Developer ID export → notarytool submit (keychain profile
scarf-notary) → staple → appcast EdDSA sign → gh-pages push → gh
release → tag. scripts/ExportOptions.plist pins manual Developer ID
signing for team 3Q6X2L86C4.
- README: removes the right-click-Open workaround (notarized builds
don't need it), notes Sparkle, adds a Releases section describing
the pipeline and signing prerequisites.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorganize the Features section to match the app's sidebar (Monitor, Interact,
Configure, Manage, Project Dashboards, System) so readers find features the
same way they find them in the app. Add a "What's New in 1.6" callout with
links to the release notes.
Binaries: ARM64 (15 MB) and Universal (19 MB). Both signed with the Apple
Development identity (Team 3Q6X2L86C4). Universal contains both arm64 and
x86_64 slices verified with lipo.
Major expansion of Scarf's Hermes platform coverage. Settings is now a 10-tab
layout exposing ~60 previously hidden config fields. A new "Configure" sidebar
section groups per-platform setup, personality management, quick commands,
credential pools, plugins, webhooks, and profile switching.
## Highlights
- **Platforms feature** — Native GUI setup for all 13 messaging platforms
(Telegram, Discord, Slack, WhatsApp, Signal, Email, Matrix, Mattermost,
Feishu, iMessage, Home Assistant, Webhook, CLI). Per-platform forms write
credentials to ~/.hermes/.env and behavior toggles to ~/.hermes/config.yaml.
WhatsApp and Signal use an inline SwiftTerm terminal for QR/link pairing.
- **Credential Pools** — Provider-aware add/remove with proper type handling.
OAuth flow uses Process + pipes to extract the authorization URL, open the
browser explicitly, and accept the code via a form field. Fixes the Anthropic
OAuth failure where the code had nowhere to be entered.
- **Model Picker** — Hierarchical provider -> model picker backed by
~/.hermes/models_dev_cache.json (111 providers, every major model). Used in
Settings -> General and Delegation. "Custom..." escape hatch for unlisted IDs.
- **Settings as tabs** — 10 tabs (General, Display, Agent, Terminal, Browser,
Voice, Memory, Aux Models, Security, Advanced). HermesConfig grew from 32 to
~90 fields via grouped sub-structs. All new fields round-trip through
`hermes config set`.
- **Extended existing features** — Cron (create/edit/pause/resume/run-now/
delete), Skills (Browse Hub + Updates tabs), Health (run `hermes dump` and
`hermes debug share` with confirmation dialog), Sessions (rename/delete/
export/export-all).
## Bug fixes
- Tools platform picker showed only CLI (was reading a nonexistent
`platform_toolsets:` YAML section). Now enumerates KnownPlatforms.all with
live connectivity dots from gateway_state.json.
- Credentials add with --api-key was triggering OAuth for providers like
Anthropic because --type was missing. Now always passes --type api-key.
- Remove-by-index used 0-based indexing; hermes CLI expects 1-based. Fixed.
- Various CLI parser fragility issues (plugins, profiles, skills hub, webhooks)
replaced with structured file reads or proper box-drawn table parsers.
## New core services
- HermesEnvService — reads/writes ~/.hermes/.env atomically, preserves
comments, commented-out keys get enabled in-place on save, values with
spaces/specials get quoted, unset commented out (non-destructive).
- ModelCatalogService — decodes the models.dev cache into typed providers and
models with context/cost/release-date metadata.
- OAuthFlowController — manages the OAuth Process subprocess: extracts the
auth URL via regex, opens the browser, pipes the code back via stdin,
detects success/failure markers in output.
## New sidebar structure
Monitor / Projects / Interact / **Configure (new)** / Manage
The Configure section gathers the setup-style features that used to require
the CLI: Platforms, Personalities, Quick Commands, Credential Pools, Plugins,
Webhooks, Profiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two related bugs surfaced when testing MCP servers that spawn npx, node,
python, etc. from Homebrew/nvm/asdf/mise installs.
1. MCP test reported success even when the connection failed.
`hermes mcp test <server>` exits 0 even when the inner connection
fails — it prints the error to stdout instead. Scarf trusted the
exit code and rendered a green checkmark while the output said
"✗ Connection failed: [Errno 2] No such file or directory: 'npx'".
Fix: also scan output for ✗, "Connection failed", "No such file or
directory", and "Error:" markers.
2. .app launches start with a minimal PATH that excludes Homebrew.
When Scarf is launched from Finder/Dock, ProcessInfo's PATH is
`/usr/bin:/bin:/usr/sbin:/sbin` — no /opt/homebrew/bin, no
/usr/local/bin, no nvm/asdf/mise shims. Hermes inherits this and
can't find npx/node/python when spawning MCP server subprocesses.
Fix: query the user's login shell PATH once via `/bin/zsh -lc 'echo
$PATH'`, cache it on HermesFileService, and inject it into both
`runHermesCLI` and the ACP subprocess. Falls back to a sane default
covering both Apple Silicon and Intel Homebrew if zsh query fails.
Bumps version to 1.5.8 (build 10). Includes signed Universal + ARM64
binaries.
Fixes a bug where adding a second MCP server caused the first to disappear
from the list view, and any args containing YAML reserved characters (e.g.
"@modelcontextprotocol/server-fetch") corrupted the config file.
Three root causes in HermesFileService MCP YAML patching:
1. extractMCPBlock extended through trailing comments to EOF when
mcp_servers was the last top-level key in config.yaml. Trailing
comments became part of the "block", so subsequent inserts landed
at end-of-file rather than inside the entry.
2. patchMCPServerField's entry boundary similarly absorbed trailing
blanks/comments, making the entry "own" everything until the next
sibling — or until EOF for the last entry.
3. yamlScalar did not quote values starting with YAML reserved
indicators (@ * & ? | > ! % , [ ] { } < ` ' "). Args like
"@modelcontextprotocol/server-fetch" were written bare, producing
invalid YAML that broke subsequent reads/writes.
Fix: trim trailing blanks/comments off both the block and the entry
in the locator/extractor; quote any scalar starting with a reserved
first character.
Bumps version to 1.5.7 (build 9). Includes signed Universal + ARM64
binaries.
Note: users with an already-corrupted ~/.hermes/config.yaml from the
1.5.6 bug should manually clean up their mcp_servers block (delete the
orphan args at end of file) before upgrading. New writes will be clean.
The first 1.5.6 zips contained `linker-signed` bundles with no Sealed
Resources, plus a stray nested scarf.app from a case-insensitive cp.
macOS Gatekeeper rejected the ARM64 download as "damaged"; the
Universal one ran only because the user had already trusted it.
Now both bundles are properly ad-hoc-signed (`Sealed Resources
version=2`) with the hardened runtime preserved. Sizes dropped
significantly (Universal 33MB→16MB, ARM64 27MB→13MB) because the
nested junk is gone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Includes the MCP Servers management UI shipped in 219bca2:
- Add via curated presets (GitHub, Linear, Notion, Sentry, Stripe, …)
or fully custom (stdio command + args, or HTTP URL with bearer auth)
- Per-server detail view: enable/disable, env + headers editor,
tool include/exclude filters, resources/prompts toggles, request
and connect timeouts, OAuth token detection + clearing
- One-click "Test Connection" runs `hermes mcp test` and surfaces
the discovered tool list
- Gateway-restart banner after config changes that need a reload
README updated with the MCP Servers section, the new MCPServers/
feature module entry, and the `hermes mcp` + `mcp-tokens/` entries
in the Data Sources table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ship Hermes v0.9.0 compatibility plus new features (log component
filter, session pill, Fast Mode, Backup/Restore, iMessage, /compress,
Discord threads). README lists both universal and ARM64 downloads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Log parser: session-ID tag in v0.9.0 log format is now an optional
capture group; session pill renders inline and tap-filters the view.
- Logs: component filter (Gateway/Agent/Tools/CLI/Cron) and bounded
logger column with middle truncation.
- Gateway stop: uses `hermes gateway stop` CLI (v0.9.0's launchctl
bootout fix) with SIGTERM as fallback.
- HermesConfig: new keys for Fast Mode (service_tier), gateway notify
interval, force IPv4, context engine, interim assistant messages,
and Honcho eager init (camelCase per PR #6995).
- Settings: new Performance, Network, Advanced, and Backup & Restore
sections that call `hermes backup` / `hermes import` off the main
actor; robust zip-path extraction via regex.
- Platforms: iMessage (BlueBubbles) added to KnownPlatforms and
icon map.
- Cron: Discord thread delivery (`discord:chat:thread`) renders as
"Discord thread X in Y".
- Chat: `/compress <focus>` button appears when ACP advertises the
command; optional focus sheet sends through existing prompt path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add cumulative token tracking from ACP prompt results with fallback
display when DB has no data yet. Improve scroll-to-bottom reliability
with an external trigger for "Return to Active Session" and onAppear
auto-scroll. Show per-session cost in the dashboard session list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Synchronous Process.run()/waitUntilExit() calls on the main thread blocked
SwiftUI's render loop, causing toggle controls to appear as solid blue
rectangles instead of proper switches. All hermes subprocess and file I/O
calls are now async via Task.detached, toggle uses optimistic state update
for immediate visual feedback, and pipe file handles are properly closed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sessions were silently dying and losing chat history because:
- Pipe write errors (EPIPE) were completely undetected — broken pipe
writes via Task.detached { handle.write() } failed silently, leaving
the app unaware the subprocess had crashed
- Reconnection fell back to newSession() when loadSession() failed,
creating a blank session and permanently losing all conversation context
- No message reconciliation after reconnect — DB-persisted messages
were never re-fetched, so the UI stayed stale/incomplete
- Keepalive sent bare "\n" which caused json.loads("") parse errors
in the ACP library every 30 seconds, destabilizing the connection
- TERM=xterm-256color was set on a pipe-based subprocess, risking
terminal escape sequence pollution in the JSON-RPC stream
Fixes:
- Replace FileHandle.write() with POSIX Darwin.write() + SIGPIPE
suppression for immediate broken-pipe detection at all write sites
- Send valid JSON-RPC notification {"jsonrpc":"2.0","method":"$/ping"}
as keepalive instead of bare newlines
- Never fall back to newSession() during reconnection — try
resumeSession then loadSession, fail visibly if both fail
- Add reconcileWithDB() to merge DB-persisted messages with local
state after successful reconnection
- Finalize streaming messages immediately on disconnect so partial
content is preserved before reconnection begins
- Use SIGINT instead of SIGTERM for graceful Python subprocess shutdown
- Remove TERM env var from ACP subprocess environment
- Consolidate disconnect cleanup into single idempotent method
- Add isHandlingDisconnect guard against double-handling
- Increase reconnect attempts from 3 to 5 with capped backoff
- Add "Reconnect" button to toolbar error state
Also: bump version to 1.5.1, set deployment target to macOS 14.6
(Sonoma), and update README with rich chat/ACP features, process
controls, skill editing, and corrected system requirements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement a rich chat interface powered by the Hermes ACP (Agent
Communication Protocol) over JSON-RPC stdio pipes, with comprehensive
connection stability:
- ACPClient actor: manages hermes acp subprocess lifecycle, JSON-RPC
transport, event streaming via AsyncStream, and session management
- ACPMessages: full event parsing for message chunks, thought chunks,
tool calls, permission requests, and prompt completion
- RichChatViewModel: streaming message display with live updates,
tool result rendering, and message grouping
- ChatViewModel: ACP session orchestration, auto-start on first
message, and terminal mode fallback
Connection stability fixes:
- Non-blocking pipe writes via Task.detached to prevent actor deadlock
- Read loop cleanup (handleReadLoopEnded) finishes event stream and
fails pending requests on EOF instead of hanging silently
- 30s request timeouts on control messages via watchdog Task pattern
- Keepalive: writes \n to stdin every 30s to detect dead processes
via EPIPE before the next user action
- Health monitor: polls process.isRunning every 5s as belt-and-suspenders
- Auto-reconnect: retries up to 3 times with exponential backoff
(1s/2s/4s), restores session, only shows error after all retries fail
- connectionLost event displays system message in chat on failure
- Proper stderr pipe management: stored task reference, closed in stop()
- Idempotent cleanup across handleReadLoopEnded, handleTermination,
and handleConnectionDied via actor serialization and nil guards
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline AttributedString(markdown:) in RichMessageBubble with
the shared MarkdownContentView for consistent styled rendering of
headers, lists, blockquotes, and inline formatting in chat messages.
Code blocks continue to use CodeBlockView with its copy button.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add rich chat interface with iMessage-style message bubbles, terminal
toggle, session info bar, code block rendering with copy button, and
tool call cards. Supports both terminal and rich chat display modes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add tool result display to the Activity detail pane. When selecting a
tool call, the inspector now shows Arguments → Output → Assistant
Message, giving full visibility into what was requested, what came back,
and how the assistant interpreted it.
- Add fetchToolResult(callId:) query to HermesDataService
- Fetch tool result on entry selection in ActivityViewModel
- Display output in styled monospaced box in detail pane
- Render assistant message with MarkdownContentView
Closes#12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The config.yaml uses YAML empty string literal (provider: '') which the
parser reads as the literal string '' rather than an empty string. Strip
surrounding quotes before checking so '' and "" are treated as empty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a custom MarkdownContentView that renders markdown with visual
styling — large headers, styled code blocks with language labels,
bullet and numbered lists, blockquotes with colored borders, and
horizontal rules. YAML frontmatter in skill files is hidden.
Markdown rendering added to:
- Memory view (MEMORY.md, USER.md) with live preview in editor
- Skills view (.md files) with new edit/save capability
- Session messages (assistant responses)
- Dashboard text widgets
Other changes:
- Shared MarkdownRenderer utility for inline formatting
- Split-pane editors (raw markdown left, live preview right)
- saveSkillContent() in HermesFileService with path validation
- Line breaks preserved in non-markdown content (Key: Value format)
Closes#11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add hermesPID() and stopHermes() to HermesFileService for process
signal management via SIGTERM
- Add process control bar to Health view with running status, PID
display, and Start/Stop/Restart buttons
- Add Start/Stop/Restart Hermes quick actions to menu bar
- Start launches gateway, stop sends SIGTERM, restart combines both
Closes#10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce a new structured chat view as an alternative to the SwiftTerm
terminal. Users can switch between raw terminal and rich chat modes via a
segmented picker in the toolbar. The rich view polls state.db for messages
and renders them as conversation bubbles with markdown, code blocks,
expandable tool call cards, reasoning sections, and a live session info bar
showing tokens, cost, and model. The terminal process stays alive in both
modes — in rich mode it runs hidden while user input from the text field is
piped to its stdin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auto-detect v0.7.0 database schema with backward compat for older DBs
- Surface reasoning tokens, actual cost, and billing provider from sessions
- Display model reasoning/thinking content in session message bubbles
- Add cost tracking to Dashboard, Insights, and session detail views
- Fix FTS5 search crash on dotted terms (e.g., "config.yaml", "v0.7.0")
- Add missing platforms: Home Assistant, Webhook, Matrix
- Consolidate platform icon mapping into shared KnownPlatforms.icon(for:)
- Map execute_code tool to ToolKind.execute
- Add Settings UI for reasoning effort, approval mode, show cost
- Show memory provider warning when external provider (Honcho) is active
- Replace fragile manual HermesSession init with withTitle() helper
- De-duplicate formatTokens utility function
- Bump version to 1.4.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All builds were reporting version 1.0 because the Xcode project version
was never updated from its default. Fixes#5, fixes#7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>