Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@
# container needs the key for whichever provider you configured in
# ~/.hermes/config.yaml. Common options:
#
# Anthropic (Claude): https://console.anthropic.com/settings/keys
# OpenAI Codex / OpenAI-compatible: configure through `hermes setup` / `hermes model`
# OpenAI (GPT / o-series): https://platform.openai.com/api-keys
# OpenRouter (many models, free tier available): https://openrouter.ai/keys
# Google (Gemini): https://aistudio.google.com/app/apikey
# Ollama / local: No key needed — just run `ollama serve`
#
# Uncomment ONLY the key(s) for the providers you actually use.

# ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# OPENROUTER_API_KEY=sk-or-v1-...
# GOOGLE_API_KEY=AIza...
Expand Down Expand Up @@ -51,8 +50,8 @@
# Set this if hermes-agent is installed elsewhere
# HERMES_AGENT_PATH=/path/to/hermes-agent

# Server port (default: 3002)
# PORT=3002
# Server port (default: 3000)
# PORT=3000

# ══════════════════════════════════════════════════════════════
# Security
Expand Down
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

> Not a chat wrapper. A complete workspace — orchestrate agents, browse memory, manage skills, and control everything from one interface.

> **v2 — zero-fork.** Clone, don't fork. Runs on vanilla [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent) installed via Nous's own installer. Chat, sessions, memory, skills, jobs, MCP, terminal, dashboard, Agent View, and Operations are all in vanilla parity. **Conductor** currently requires an additional dashboard plugin not in upstream yet — the UI shows a clear placeholder when that endpoint isn't available ([#262](https://github.com/outsourc-e/hermes-workspace/issues/262)). Everything else works with zero patches.
> **v2 — zero-fork.** Clone, don't fork. Runs on vanilla [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent) installed via Nous's own installer. Chat, sessions, memory, skills, jobs, MCP, terminal, dashboard, Agent View, and Operations are all in vanilla parity. **Conductor** uses the dashboard mission API when available and falls back to Workspace-native Swarm dispatch (`mode: native-swarm`) when the dashboard endpoint is absent, preserving zero-fork behavior ([#262](https://github.com/outsourc-e/hermes-workspace/issues/262)).

![Hermes Workspace](./docs/screenshots/splash.png)

Expand Down Expand Up @@ -48,7 +48,7 @@ Start here: [docs/swarm/](./docs/swarm/)
- 🔌 **MCP** — Full /mcp page (catalog + marketplace + sources), or fallback to local config CRUD
- 📁 **Files + Terminal** — Full workspace file browser with Monaco; cross-platform PTY terminal
- 🎮 **Operations** — Multi-agent dashboard with profile presets (Sage/Trader/Builder/Scribe/Ops) and 'Needs setup' detection
- 📡 **Conductor** — Mission dispatch + decomposition (requires upstream dashboard plugin, see [#262](https://github.com/outsourc-e/hermes-workspace/issues/262))
- 📡 **Conductor** — Mission dispatch + decomposition with dashboard-backed missions when available and Workspace-native Swarm fallback otherwise
- 👥 **Agent View** — Live agent panel in chat with avatar, queue, history, usage meter
- 🐝 **Swarm Mode** — Persistent tmux-backed Hermes Agent workers with role-based dispatch
- 🗄️ **Dashboard** — Aggregated overview: sessions, model mix, cost ledger, attention card, ops strip
Expand Down Expand Up @@ -213,8 +213,7 @@ HERMES_API_URL=http://127.0.0.1:8642

# Optional: provider keys the Hermes Agent gateway can read at runtime.
# You only need the key(s) for whichever provider(s) you actually use.
# ANTHROPIC_API_KEY=*** # Anthropic
# OPENAI_API_KEY=sk-... # GPT / o-series
# OPENAI_API_KEY=sk-... # GPT / o-series / OpenAI-compatible
# OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter (incl. free models)
# GOOGLE_API_KEY=AIza... # Gemini
# (Ollama / LM Studio / local servers don't need a key)
Expand Down Expand Up @@ -320,7 +319,7 @@ The Docker setup runs both the **Hermes Agent gateway** and **Hermes Workspace**

- **Docker**
- **Docker Compose**
- **Anthropic API Key** — [Get one here](https://console.anthropic.com/settings/keys) (required for the agent gateway)
- **A configured Hermes Agent model provider** — run `hermes setup` / `hermes model`, or provide a key for whichever provider you use. This workspace does not require Anthropic.

### Step 1: Configure Environment

Expand All @@ -334,8 +333,7 @@ Edit `.env` and add **at least one** LLM provider key — whichever provider you

```env
# Pick one (or more). You do NOT need all of these.
# ANTHROPIC_API_KEY=*** # Anthropic
# OPENAI_API_KEY=sk-... # GPT / o-series
# OPENAI_API_KEY=sk-... # GPT / o-series / OpenAI-compatible
# OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter (free models available)
# GOOGLE_API_KEY=AIza... # Gemini
```
Expand Down Expand Up @@ -586,7 +584,7 @@ Verify: `curl http://localhost:8642/health` should return `{"status": "ok"}`.

v2+ runs on vanilla `hermes-agent`. **No fork required.** The upstream ships every endpoint the workspace needs for chat, sessions, memory, skills, config, jobs, MCP, terminal, and Agent View.

**One known exception:** **Conductor** uses a dashboard plugin that hasn't landed upstream yet. When the workspace detects the missing endpoint, the Conductor screen shows a clear "Upstream not ready" placeholder with a link to [issue #262](https://github.com/outsourc-e/hermes-workspace/issues/262) instead of failing mid-action. Everything else works.
**Conductor note:** when the dashboard mission API is available, Workspace uses it directly. When that endpoint is absent, Workspace uses its native Swarm fallback and returns `mode: native-swarm`. The fallback dispatches through Workspace Swarm workers, keeps status available through `/api/conductor-spawn?missionId=...`, and cancels through `/api/conductor-stop`.

If you're pinned to an older `hermes-agent` version and missing core endpoints, the workspace will degrade gracefully to **portable mode** with basic chat — upgrade upstream to restore full features.

Expand All @@ -598,7 +596,7 @@ If using Docker Compose and getting auth errors:

```bash
grep -E '_API_KEY' .env
# Should show one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, GOOGLE_API_KEY, ...
# Should show one of: OPENAI_API_KEY, OPENROUTER_API_KEY, GOOGLE_API_KEY, or another provider key you intentionally use.
```

(hermes-agent reads whichever key matches the provider configured in `~/.hermes/config.yaml`.)
Expand Down Expand Up @@ -661,13 +659,13 @@ The Docker setup runs both automatically — no action needed if using `docker c
| Mobile PWA + Tailscale | Install as native-feeling app on any device |
| Themes | Hermes / Nous / Bronze / Slate / Mono (light + dark) |
| Capability gates | Graceful 'upstream not ready' placeholders |
| Multi-provider | Anthropic, OpenAI, OpenRouter, Google, Ollama, LM Studio, vLLM, Atomic Chat |
| Multi-provider | OpenAI/OpenAI-compatible, OpenRouter, Google, Ollama, LM Studio, vLLM, Atomic Chat, and other Hermes-supported providers |

### In progress 🔨

| Feature | Status |
|---|---|
| Conductor missions | Workspace UI is shipped; awaiting upstream dashboard plugin (see [#262](https://github.com/outsourc-e/hermes-workspace/issues/262)) |
| Conductor missions | Workspace UI is shipped; uses dashboard mission API when available and Workspace-native Swarm fallback otherwise (see [#262](https://github.com/outsourc-e/hermes-workspace/issues/262)) |
| Native Desktop App (Electron) | Spec'd; PWA install path works today |

### Coming 🔜
Expand Down
14 changes: 7 additions & 7 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Common setup issues and how to fix them.
**Fix:**

```bash
# Find your claude env file
claude config env-path
# Find your Hermes env file
hermes config env-path
# Usually: ~/.hermes/.env

# Check for the key
Expand Down Expand Up @@ -43,7 +43,7 @@ After fixing, restart the gateway: `hermes gateway run --replace`

**Checklist (in order):**

1. Is the gateway running? `pgrep -af "claude.*gateway"`
1. Is the gateway running? `hermes gateway status` or `pgrep -af "hermes.*gateway"`
2. Is port 8642 bound? `curl -sf http://127.0.0.1:8642/health`
3. Is Workspace `.env` correct? `grep HERMES_API_URL ~/hermes-workspace/.env`
- Should be: `HERMES_API_URL=http://127.0.0.1:8642`
Expand Down Expand Up @@ -122,10 +122,10 @@ This means the Vite SSR server tried `GET /api/gateway-status` which internally
If nothing above helps, run this and share the output:

```bash
echo "=== claude version ===" && claude --version 2>&1
echo "=== claude env path ===" && claude config env-path 2>&1
echo "=== claude env (redacted) ===" && grep -E "^(API_SERVER|CLAUDE_)" "$(claude config env-path 2>/dev/null || echo ~/.hermes/.env)" 2>&1
echo "=== gateway process ===" && pgrep -af "claude.*gateway" 2>&1 || echo "not running"
echo "=== hermes version ===" && hermes --version 2>&1
echo "=== hermes env path ===" && hermes config env-path 2>&1
echo "=== hermes env (redacted) ===" && grep -E "^(API_SERVER|HERMES_|CLAUDE_)" "$(hermes config env-path 2>/dev/null || echo ~/.hermes/.env)" 2>&1
echo "=== gateway process ===" && pgrep -af "hermes.*gateway" 2>&1 || echo "not running"
echo "=== port 8642 ===" && (ss -tlnp 2>/dev/null || lsof -iTCP:8642 -sTCP:LISTEN 2>/dev/null) | grep 8642 || echo "not bound"
echo "=== health check ===" && curl -sf http://127.0.0.1:8642/health 2>&1 || echo "not reachable"
echo "=== workspace .env ===" && grep CLAUDE ~/hermes-workspace/.env 2>&1 || echo "no .env"
Expand Down
40 changes: 33 additions & 7 deletions src/components/swarm/swarm-health-strip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ type WorkerHealth = {
model: string
provider: string
recentAuthErrors: number
recentFallbacks: number
lastErrorAt: string | null
lastErrorMessage: string | null
lastFallbackAt: string | null
lastFallbackMessage: string | null
modelAuthStatus: 'ready' | 'primary-auth-failed' | 'fallback-active' | 'not-configured' | 'unknown'
primaryAuthOk: boolean | null
fallbackActive: boolean
fallbackProvider: string | null
fallbackModel: string | null
}

type HealthResponse = {
Expand All @@ -33,6 +41,11 @@ type HealthResponse = {
totalWorkers: number
wrappersConfigured: number
totalAuthErrors24h: number
totalFallbacks24h: number
workersUsingFallback: number
workersPrimaryAuthFailed: number
degraded: boolean
warnings: string[]
distinctModels: string[]
distinctProviders: string[]
}
Expand Down Expand Up @@ -110,6 +123,9 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |
const workspaceModel = data?.workspaceModel ?? '—'
const apiUrl = data?.agentApiUrl ?? data?.claudeApiUrl ?? '—'
const totalAuthErrors = data?.summary.totalAuthErrors24h ?? 0
const totalFallbacks = data?.summary.totalFallbacks24h ?? 0
const degraded = data?.summary.degraded ?? false
const warnings = data?.summary.warnings ?? []
const wrappersConfigured = data?.summary.wrappersConfigured ?? 0
const totalWorkers = data?.summary.totalWorkers ?? 0
const distinctModels = data?.summary.distinctModels ?? []
Expand All @@ -120,7 +136,7 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |
<div className="rounded-2xl border border-emerald-400/15 bg-[#08110d] p-4 text-emerald-50/85">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<HugeiconsIcon icon={CheckmarkCircle02Icon} size={14} className="text-emerald-300" />
<HugeiconsIcon icon={degraded ? AlertCircleIcon : CheckmarkCircle02Icon} size={14} className={degraded ? 'text-amber-300' : 'text-emerald-300'} />
<span className="text-[11px] uppercase tracking-[0.18em] text-emerald-200/80">Swarm health</span>
</div>
<div className="flex items-center gap-2 text-[11px] text-emerald-100/55">
Expand Down Expand Up @@ -148,13 +164,23 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |
<HealthTile icon={FlashIcon} label="Provider" value={provider} />
<HealthTile icon={FlashIcon} label="Wrappers" value={`${wrappersConfigured}/${totalWorkers}`} />
<HealthTile
icon={totalAuthErrors === 0 ? CheckmarkCircle02Icon : AlertCircleIcon}
label="Auth errors 24h"
value={String(totalAuthErrors)}
tone={totalAuthErrors === 0 ? 'good' : 'warn'}
icon={totalFallbacks === 0 ? CheckmarkCircle02Icon : AlertCircleIcon}
label="Fallbacks 24h"
value={String(totalFallbacks)}
tone={totalFallbacks === 0 ? 'good' : 'warn'}
/>
</div>

{degraded ? (
<div className="mt-3 rounded-lg border border-amber-400/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
<div className="font-semibold">Primary model readiness degraded.</div>
<div className="mt-1 text-amber-100/80">
Auth errors: {totalAuthErrors}. Fallbacks: {totalFallbacks}. Reply smoke tests can pass on fallback; fix primary auth before production swarm work.
</div>
{warnings.length > 0 ? <div className="mt-1 text-amber-100/70">{warnings.join(' ')}</div> : null}
</div>
) : null}

<div className="mt-3 flex flex-wrap items-center gap-2 text-[11px] text-emerald-100/55">
<span>Gateway: <span className="text-emerald-50">{apiUrl}</span></span>
{distinctModels.length > 0 ? (
Expand All @@ -164,8 +190,8 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |

<div className="mt-3 flex flex-wrap items-center justify-between gap-2 rounded-xl border border-emerald-400/15 bg-emerald-500/5 px-3 py-2">
<div className="text-[11px] text-emerald-100/70">
Smoke test: dispatch a tiny prompt to{' '}
<span className="font-semibold text-emerald-100">{pingTarget ?? 'no worker'}</span> and confirm a real reply.
Reply smoke test: dispatch a tiny prompt to{' '}
<span className="font-semibold text-emerald-100">{pingTarget ?? 'no worker'}</span>. This confirms a reply, not primary-model readiness.
</div>
<button
type="button"
Expand Down
1 change: 1 addition & 0 deletions src/hooks/use-crew-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type CrewPlatformInfo = {
export type CrewMember = {
id: string
displayName: string
humanLabel?: string
role: string
specialty?: string
mission?: string
Expand Down
21 changes: 21 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import { Route as ApiLocalProvidersRouteImport } from './routes/api/local-provid
import { Route as ApiIntegrationsRouteImport } from './routes/api/integrations'
import { Route as ApiHistoryRouteImport } from './routes/api/history'
import { Route as ApiGatewayStatusRouteImport } from './routes/api/gateway-status'
import { Route as ApiGatewayReprobeRouteImport } from './routes/api/gateway-reprobe'
import { Route as ApiFilesRouteImport } from './routes/api/files'
import { Route as ApiEventsRouteImport } from './routes/api/events'
import { Route as ApiCrewStatusRouteImport } from './routes/api/crew-status'
Expand Down Expand Up @@ -487,6 +488,11 @@ const ApiGatewayStatusRoute = ApiGatewayStatusRouteImport.update({
path: '/api/gateway-status',
getParentRoute: () => rootRouteImport,
} as any)
const ApiGatewayReprobeRoute = ApiGatewayReprobeRouteImport.update({
id: '/api/gateway-reprobe',
path: '/api/gateway-reprobe',
getParentRoute: () => rootRouteImport,
} as any)
const ApiFilesRoute = ApiFilesRouteImport.update({
id: '/api/files',
path: '/api/files',
Expand Down Expand Up @@ -836,6 +842,7 @@ export interface FileRoutesByFullPath {
'/api/crew-status': typeof ApiCrewStatusRoute
'/api/events': typeof ApiEventsRoute
'/api/files': typeof ApiFilesRoute
'/api/gateway-reprobe': typeof ApiGatewayReprobeRoute
'/api/gateway-status': typeof ApiGatewayStatusRoute
'/api/history': typeof ApiHistoryRoute
'/api/integrations': typeof ApiIntegrationsRoute
Expand Down Expand Up @@ -968,6 +975,7 @@ export interface FileRoutesByTo {
'/api/crew-status': typeof ApiCrewStatusRoute
'/api/events': typeof ApiEventsRoute
'/api/files': typeof ApiFilesRoute
'/api/gateway-reprobe': typeof ApiGatewayReprobeRoute
'/api/gateway-status': typeof ApiGatewayStatusRoute
'/api/history': typeof ApiHistoryRoute
'/api/integrations': typeof ApiIntegrationsRoute
Expand Down Expand Up @@ -1102,6 +1110,7 @@ export interface FileRoutesById {
'/api/crew-status': typeof ApiCrewStatusRoute
'/api/events': typeof ApiEventsRoute
'/api/files': typeof ApiFilesRoute
'/api/gateway-reprobe': typeof ApiGatewayReprobeRoute
'/api/gateway-status': typeof ApiGatewayStatusRoute
'/api/history': typeof ApiHistoryRoute
'/api/integrations': typeof ApiIntegrationsRoute
Expand Down Expand Up @@ -1237,6 +1246,7 @@ export interface FileRouteTypes {
| '/api/crew-status'
| '/api/events'
| '/api/files'
| '/api/gateway-reprobe'
| '/api/gateway-status'
| '/api/history'
| '/api/integrations'
Expand Down Expand Up @@ -1369,6 +1379,7 @@ export interface FileRouteTypes {
| '/api/crew-status'
| '/api/events'
| '/api/files'
| '/api/gateway-reprobe'
| '/api/gateway-status'
| '/api/history'
| '/api/integrations'
Expand Down Expand Up @@ -1502,6 +1513,7 @@ export interface FileRouteTypes {
| '/api/crew-status'
| '/api/events'
| '/api/files'
| '/api/gateway-reprobe'
| '/api/gateway-status'
| '/api/history'
| '/api/integrations'
Expand Down Expand Up @@ -1636,6 +1648,7 @@ export interface RootRouteChildren {
ApiCrewStatusRoute: typeof ApiCrewStatusRoute
ApiEventsRoute: typeof ApiEventsRoute
ApiFilesRoute: typeof ApiFilesRoute
ApiGatewayReprobeRoute: typeof ApiGatewayReprobeRoute
ApiGatewayStatusRoute: typeof ApiGatewayStatusRoute
ApiHistoryRoute: typeof ApiHistoryRoute
ApiIntegrationsRoute: typeof ApiIntegrationsRoute
Expand Down Expand Up @@ -2193,6 +2206,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiGatewayStatusRouteImport
parentRoute: typeof rootRouteImport
}
'/api/gateway-reprobe': {
id: '/api/gateway-reprobe'
path: '/api/gateway-reprobe'
fullPath: '/api/gateway-reprobe'
preLoaderRoute: typeof ApiGatewayReprobeRouteImport
parentRoute: typeof rootRouteImport
}
'/api/files': {
id: '/api/files'
path: '/api/files'
Expand Down Expand Up @@ -2826,6 +2846,7 @@ const rootRouteChildren: RootRouteChildren = {
ApiCrewStatusRoute: ApiCrewStatusRoute,
ApiEventsRoute: ApiEventsRoute,
ApiFilesRoute: ApiFilesRoute,
ApiGatewayReprobeRoute: ApiGatewayReprobeRoute,
ApiGatewayStatusRoute: ApiGatewayStatusRoute,
ApiHistoryRoute: ApiHistoryRoute,
ApiIntegrationsRoute: ApiIntegrationsRoute,
Expand Down
Loading