Repository navigation
Expand file tree
/
Copy pathhm
More file actions
executable file
·666 lines (632 loc) · 39.1 KB
/
Copy pathhm
File metadata and controls
executable file
·666 lines (632 loc) · 39.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
#!/usr/bin/env bash
# hm — one ergonomic terminal-native command over the whole hermes-max stack.
#
# Pure DISPATCH over the existing scripts — every verb maps to a scripts/*.sh that
# STILL runs directly on its own (`./scripts/start-all.sh` etc. unchanged). `hm` is
# sugar, the scripts are the substance. Runnable as `./hm` from the repo root, or
# install onto PATH (`hm install`, or via bootstrap.sh).
#
# hm up [--free|--full] start all MCPs (+ embed/rerank hint) scripts/start-all.sh
# --free = Profile A (own a GPU: local drives, free planner)
# --full = Profile B (no GPU: economic API drives + plans)
# advanced MODEs: --full-local|--frontier-local|--frontier|
# --local (see docs/modes.md); default = current/free
# + --free-uplift|--no-free-uplift (plugin toggle)
# hm mode [name] show / switch the cost-quality posture config/modes.yaml
# no arg = show; --list = table; <name> = switch live
# hm down stop everything (PID + port fallback) scripts/stop-all.sh
# hm restart [srv|all] restart one server or all scripts/restart.sh
# hm status UP/DOWN · port · PID · uptime · health scripts/status.sh
# hm watch live tool-call stream scripts/watch.sh
# hm observe live waterfall: where wall time goes scripts/observe.sh
# hm logs [server] tail a server's log (or the live log) ~/.hermes-max/logs/
# hm run "task" launch hermes with a task hermes
# hm ui [--port N] localhost web view (L0/L1 + live cost) over ui/server
# the existing livelog+ledger; opens a browser (Tier 1)
# hm snapshot <name> snapshot RAG+KG+corpus stores scripts/snapshot-stores.sh
# hm restore <name> restore a named snapshot scripts/restore-stores.sh
# hm dev [--MODE] the tmux cockpit (hermes + watch + status) (this script)
# hm attach reattach to the running tmux cockpit tmux attach
# hm health pass/fail healthcheck (for scripting) scripts/healthcheck.sh
# hm preflight validate the whole stack before a task scripts/preflight.sh
# (PASS/FAIL/WARN; auto-fixes; exit 1 on BLOCKING)
# hm smoke real ~15-min end-to-end agent proof scripts/integration-smoke.sh
# hm summary per-task tool-call summary + bottleneck split scripts/run-summary.sh
# hm bottleneck full-vs-bare timing eval scripts/bottleneck-eval.sh
# hm cost [window] fabric ledger ($0.000000): provider/model/role, lib/inference
# free-vs-paid, remaining free RPD (+legacy conductor)
# hm eval [capability] prove every feature works in the REAL agent scripts/eval-battery.sh
# loop (asserts real effects); one capability if named
# hm setup first-run: copy config trinity + show providers scripts/setup.sh
# hm install symlink hm onto PATH (~/.local/bin)
set -uo pipefail
HM_SELF="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
REPO_ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)"
SCRIPTS="${REPO_ROOT}/scripts"
TMUX_SESSION="hermes-max"
LOG_DIR="${HERMES_MAX_LOG_DIR:-${HOME}/.hermes-max/logs}"
LOG_DIR="${LOG_DIR/#\~/$HOME}"
# In HM_PRINT mode, echo the command that WOULD run instead of executing it. Used
# by the smoke test to prove `hm <verb>` maps to the right standalone script,
# without side effects.
_dispatch() {
if [ -n "${HM_PRINT:-}" ]; then echo "$*"; exit 0; fi
exec "$@"
}
_port_up() { timeout 1 bash -c ">/dev/tcp/127.0.0.1/$1" 2>/dev/null; }
# Probe the ACTUAL $VLLM_BASE_URL (local, LAN, or Tailscale — wherever it points)
# and classify by REACHABILITY, not location. Echoes: "<state> <host> <model>"
# local — reachable on a loopback host (GPU on this box)
# remote — reachable on another host (GPU on another box; Tailscale/LAN)
# none — unset or not answering
# The endpoint is never assumed to be on :8001/localhost; we read and probe it.
_probe_vllm() {
local base host state model
base="${VLLM_BASE_URL:-}"
if [ -z "${base}" ]; then echo "none - -"; return; fi
host="$(printf '%s' "${base}" | sed -E 's#^[a-zA-Z]+://([^/:]+).*#\1#')"
model="$(curl -fsS -m 4 -H 'User-Agent: hermes-max/1.0' "${base%/}/models" 2>/dev/null \
| sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
if [ -n "${model}" ] || curl -fsS -m 4 -H 'User-Agent: hermes-max/1.0' "${base%/}/models" >/dev/null 2>&1; then
case "${host}" in localhost|127.0.0.1|::1|0.0.0.0) state=local ;; *) state=remote ;; esac
else
state=none
fi
echo "${state} ${host:-?} ${model:-?}"
}
# Start the embed (:8002) + rerank (:8003) serves, backgrounded, if not already up.
# DEFAULT ON for the gpu_local profile (the model serves the dense lane + reranker);
# opt out with HM_WITH_EMBED=0 / HM_WITH_RERANK=0. On lean_cloud they stay off.
_start_serves() {
local profile="${DEPLOY_PROFILE:-gpu_local}"
local want_embed="${HM_WITH_EMBED:-}" want_rerank="${HM_WITH_RERANK:-}"
if [ "${profile}" = "gpu_local" ]; then
[ -z "${want_embed}" ] && want_embed=1
[ -z "${want_rerank}" ] && want_rerank=1
fi
mkdir -p "${LOG_DIR}"
if [ "${want_embed}" = "1" ]; then
if _port_up "${EMBED_PORT:-8002}"; then echo " • embed already up on :${EMBED_PORT:-8002}"
else echo " • starting embed serve on :${EMBED_PORT:-8002} (background)"
nohup "${REPO_ROOT}/serve-embed.sh" >>"${LOG_DIR}/embed.log" 2>&1 & fi
fi
if [ "${want_rerank}" = "1" ]; then
if _port_up "${RERANK_PORT:-8003}"; then echo " • rerank already up on :${RERANK_PORT:-8003}"
else echo " • starting rerank serve on :${RERANK_PORT:-8003} (background)"
nohup "${REPO_ROOT}/serve-rerank.sh" >>"${LOG_DIR}/rerank.log" 2>&1 & fi
fi
}
# ── conductor MODE system (--local | --free | --full | --frontier) ────────────
# Default --full. Each mode is a SUPERSET of the one below and is presence-gated:
# a tier whose required key is absent falls through to the next-lower tier, so the
# system always resolves to the highest tier whose keys are actually present. The
# mode is the CEILING; the conductor's per-subtask gating decides actual use.
HMX_ENV_FILE="${HMX_ENV_FILE:-${REPO_ROOT}/.env}"
HMX_RUN_DIR="${HMX_RUN_DIR:-${HOME}/.hermes-max/run}" # pidfiles (MCP servers + web UI)
HMX_MODE_FILE="${HERMES_MAX_STATE_DIR:-${HOME}/.hermes-max}/conductor/mode"
HMX_MODE_FILE="${HMX_MODE_FILE/#\~/$HOME}"
# A key is present if non-empty in the live shell env OR non-empty in .env.
_key_present() {
local k="$1"
[ -n "${!k:-}" ] && return 0
grep -qE "^${k}=[^[:space:]#]" "${HMX_ENV_FILE}" 2>/dev/null
}
_paid_present() { _key_present DEEPINFRA_API_KEY || _key_present FIREWORKS_API_KEY \
|| _key_present TOGETHER_API_KEY || _key_present DEEPSEEK_API_KEY || _key_present MOONSHOT_API_KEY; }
_free_present() { _key_present CEREBRAS_API_KEY || _key_present GROQ_API_KEY || _key_present GEMINI_API_KEY; }
# Resolve a requested mode to the highest tier whose required key is present,
# warning (to stderr) on each fall-through. Echoes the effective mode on stdout.
_resolve_mode() {
local m="$1"
while :; do
case "${m}" in
frontier) _key_present ANTHROPIC_API_KEY && break
echo " ⚠ --frontier needs ANTHROPIC_API_KEY (absent) → falling back to --full (Opus tier OFF)" >&2; m=full ;;
full) _paid_present && break
echo " ⚠ --full needs a paid synth key (DeepInfra/Fireworks/Together/DeepSeek/Moonshot) (absent) → --free" >&2; m=free ;;
free) _free_present && break
echo " ⚠ --free needs a free key (Cerebras/Groq/Gemini) (absent) → --local" >&2; m=local ;;
local) break ;;
*) m=full ;;
esac
done
echo "${m}"
}
_mode_tiers_line() {
case "$1" in
local) echo "local only — fully sovereign, \$0 cloud" ;;
free) echo "local + free draft/steer (Cerebras/Groq/Gemini)" ;;
full) echo "local + free + DeepSeek V4 steer/synth (paid, ~\$10/mo)" ;;
frontier) echo "local + free + DeepSeek + SPARING Opus 4.8 escalation (3-gated, ~\$12-15/mo)" ;;
esac
}
# Apply a mode: resolve fall-through, persist to .env (backed up) + the live mode
# file, export for freshly-started servers. Sets HMX_MODE_CHANGED=1 if .env changed
# (the caller restarts the conductor so the running server picks up the new mode).
HMX_MODE_CHANGED=0
HMX_EFFECTIVE_MODE=""
_apply_mode() {
local requested="${1:-full}" eff cur
eff="$(_resolve_mode "${requested}")"
HMX_EFFECTIVE_MODE="${eff}"
mkdir -p "$(dirname "${HMX_MODE_FILE}")"
printf '%s\n' "${eff}" > "${HMX_MODE_FILE}"
cur="$(grep -E '^CONDUCTOR_MODE=' "${HMX_ENV_FILE}" 2>/dev/null | head -1 | sed 's/^CONDUCTOR_MODE=//; s/[[:space:]].*//')"
if [ "${cur}" != "${eff}" ]; then
if [ -f "${HMX_ENV_FILE}" ]; then
cp -p "${HMX_ENV_FILE}" "${HMX_ENV_FILE}.bak.mode.$(date +%s 2>/dev/null || echo bak)" 2>/dev/null || true
if grep -qE '^CONDUCTOR_MODE=' "${HMX_ENV_FILE}"; then
sed -i "s@^CONDUCTOR_MODE=.*@CONDUCTOR_MODE=${eff} # set by hm up (local/free/full/frontier)@" "${HMX_ENV_FILE}"
else
printf 'CONDUCTOR_MODE=%s # set by hm up (local/free/full/frontier)\n' "${eff}" >> "${HMX_ENV_FILE}"
fi
fi
HMX_MODE_CHANGED=1
fi
export CONDUCTOR_MODE="${eff}"
if [ "${eff}" != "${requested}" ]; then
echo " • requested --${requested}, resolved to --${eff} (key fall-through)"
fi
echo " ▸ conductor mode: ${eff} — $(_mode_tiers_line "${eff}")"
}
# Extract a --local|--free|--full|--frontier flag from args; echo the mode (or
# 'full' default). Other args are ignored (start-all takes none meaningfully).
_mode_from_args() {
local m=full a
for a in "$@"; do case "${a}" in
--local) m=local ;; --free) m=free ;; --full) m=full ;; --frontier) m=frontier ;;
esac; done
echo "${m}"
}
# ── inference MODE (modes.yaml posture toggle) ─────────────────────────────────
# The user-facing posture: free | full-local | full | frontier-local | frontier |
# local. ONE word reassigns the coding role chains (who plans, who executes). The
# mode name persists to ~/.hermes-max/mode (read live by lib/inference); we also
# sync CONDUCTOR_MODE to the mode's spend ceiling so the conductor agrees.
_inf() { ( cd "${REPO_ROOT}" && python3 -m lib.inference.modes_cli "$@" ); }
# Echo a recognized --<mode> flag's name, or '' if none present.
_inf_mode_from_args() {
local a
for a in "$@"; do case "${a}" in
--free) echo free; return ;;
--full-local) echo full-local; return ;;
--full) echo full; return ;;
--frontier-local) echo frontier-local; return ;;
--frontier) echo frontier; return ;;
--local) echo local; return ;;
esac; done
}
# Apply a posture mode: persist + print coverage, sync the conductor ceiling, and
# warn (do NOT silently fall back) if it executes locally but no vLLM is up.
_apply_inf_mode() {
local name="$1" meta ceiling gpu
_inf set "${name}" || return 1
meta="$(_inf meta "${name}" 2>/dev/null)"; ceiling="${meta%% *}"; gpu="${meta##* }"
[ -n "${ceiling}" ] && _apply_mode "${ceiling}" >/dev/null 2>&1
if [ "${gpu}" = "1" ]; then
# Probe the real endpoint (local/LAN/Tailscale) — only warn if NONE is reachable.
set -- $(_probe_vllm); _vstate="$1"; _vhost="$2"; _vmodel="$3"
if [ "${_vstate}" = "none" ]; then
echo " ⚠ '${name}' executes locally but no vLLM endpoint is reachable at ${VLLM_BASE_URL:-<VLLM_BASE_URL unset>}."
echo " Stand vLLM up (local or remote), or pick a no-GPU posture: hm mode full (V4-Flash executes, no GPU)"
fi
# (a reachable remote endpoint is a LIVE driver — set_mode.sh prints the REMOTE GPU line)
fi
# Option A: atomically swap the Hermes agent-loop backend to this posture's
# executor (local vLLM, or DeepInfra V4-Flash). Reversible; backs up first.
# Skip with HM_NO_HERMES_SWAP=1 (config-only changes, no live-agent swap).
if [ -z "${HM_NO_HERMES_SWAP:-}" ] && [ -f "${SCRIPTS}/set_mode.sh" ]; then
"${SCRIPTS}/set_mode.sh" "${name}" || true
fi
echo " ▸ conductor ceiling synced to: ${ceiling}"
}
# Persist the free_uplift toggle (plugins/free_uplift) into .env.
_set_uplift() {
local val="$1" # true|false
[ -f "${HMX_ENV_FILE}" ] || return 0
if grep -qE '^INFERENCE_MODE_FREE_UPLIFT=' "${HMX_ENV_FILE}"; then
sed -i "s@^INFERENCE_MODE_FREE_UPLIFT=.*@INFERENCE_MODE_FREE_UPLIFT=${val} # set by hm@" "${HMX_ENV_FILE}"
else
printf 'INFERENCE_MODE_FREE_UPLIFT=%s # set by hm (plugins/free_uplift)\n' "${val}" >> "${HMX_ENV_FILE}"
fi
export INFERENCE_MODE_FREE_UPLIFT="${val}"
echo " ▸ free-uplift: ${val} (plugins/free_uplift)"
}
# Short, grouped help — the common path only (progressive disclosure). The full
# surface is one flag away: `hm help --all`.
_help_short() {
cat <<'EOF'
hermes-max — agentic coding harness
Pick your mode
have a GPU? hm mode free-full-local free cascade plans ($0), V4-Pro fallback,
local executes — best GPU mode (recommended)
hm mode full-local reliable V4-Pro planner, local executes (~$1-3/mo)
no GPU? hm mode full V4-Pro plans + V4-Flash executes (~$17/mo)
want $0 always? hm mode free free cascade plans + local executes (quality varies)
(then: hm up · hm run "task" · hm dev)
Getting started
hm up [--free|--full] start the stack in a profile/mode
--free = own a GPU (Profile A) · --full = no GPU (Profile B)
hm down stop everything
hm status what's running + active mode + today's spend
Using it
hm run "task" run the agent on a task (or just launch `hermes`)
hm dev the cockpit: agent + live activity + status, one window
hm ui localhost web view: live timeline + cost, in your browser
hm cost spend breakdown ($0.000000, free-vs-paid)
Health & modes
hm health check endpoints, providers, roster
hm mode [name|--list] show or switch profile/mode
More: hm help --all every command, including advanced/diagnostic verbs
hm <verb> --help one-paragraph help for any verb
EOF
}
# The full surface — extracted from the header comment block (DRY: the comment is
# the authoritative per-verb doc), from line 3 up to the `set -uo pipefail` line.
_help_all() {
sed -n '3,/^set -uo pipefail/p' "${HM_SELF}" | sed -e '/^set -uo pipefail/d' -e 's/^# \{0,1\}//'
}
# Route `hm help` / `hm` (no args) to the short help; `hm help --all` to the full one.
usage() {
case " $* " in *" --all "*|*" -a "*|*" all "*) _help_all ;; *) _help_short ;; esac
}
# One-paragraph help for an individual verb (`hm <verb> --help`).
_verb_help() {
case "$1" in
up) echo "hm up [--free|--full|--MODE] [--free-uplift] — start the whole stack in a profile/mode. --free = Profile A (you own a GPU: local model executes, free planner); --full = Profile B (no GPU: economic API drives and plans). Advanced modes: --full-local --frontier-local --frontier --local (docs/modes.md). Prints which providers are present, which roles are satisfiable, the Hermes backend set, and any roster warnings." ;;
down) echo "hm down — stop every MCP server (by PID file, port fallback) and the embed/rerank serves. Idempotent; confirms every port is free." ;;
restart) echo "hm restart [server|all] — stop then start one named server (e.g. 'hm restart research') or the whole stack, re-running health." ;;
status) echo "hm status — the single 'is it OK and what's it costing' view: per-server UP/DOWN · port · PID · uptime · health, plus the active mode and today's spend." ;;
mode) echo "hm mode [name|--list] — show or switch the cost/quality posture live (no restart). Names: $(_inf list 2>/dev/null | sed -n '2,7p' | awk '{print $1}' | tr '\n' ' '). See docs/modes.md." ;;
dev) echo "hm dev [--MODE] — the one-window tmux cockpit: a hermes pane, a live activity (watch) pane, and a compact status pane (mode + spend). Detachable; reattach with 'hm attach'. tmux absent → prints manual instructions." ;;
attach) echo "hm attach — reattach to the running 'hermes-max' tmux cockpit started by hm dev." ;;
run) echo "hm run \"task\" — launch the Hermes agent on a task (exec's 'hermes'). Equivalent to running 'hermes' directly." ;;
native) echo "hm native \"task\" [type] — conductor plans (writes a signed PLAN.md) then hands the RAW prompt to NATIVE hermes with the full TUI. Hermes self-directs with PLAN.md as context (the lighter sibling of 'hm run', which enforces strict execute-the-plan). Runs an MCP preflight (warn-only); degrades gracefully if the conductor is down. Also callable directly as bin/hm-native." ;;
ui-test) echo "hm ui-test — PART V actuation equivalence test: drives a real bounded agent turn through the web UI's own API headlessly and asserts every action surfaced in the SSE visual-flow, real artifacts landed on disk, verify went green, the handback fired, a follow-up turn actuated, and zero secrets leaked. Proves the UI ACTUATES the agent (a complete replacement for hm dev + hermes), not just visualizes it. Runs in --mode free; needs the stack up (hm up)." ;;
ui) echo "hm ui [--port N] [--no-open] [--token [T]] — open the localhost web view: L0 ambient + L1 timeline + L2 span tree + live cost, fed by the existing livelog/ledger/OTLP over SSE. Auto-opens your browser and prints one line (http://localhost:7080). Port is sticky (~/.hermes-max/ui.conf; next free port if taken). No token needed on localhost — hardened by 127.0.0.1 bind + Origin/Host checks + a SameSite CSRF cookie. --token adds a bearer for remote/Tailscale exposure. Read-only. Ctrl-C to stop." ;;
studio) echo "hm studio [build] — Hermes Studio, the Tauri desktop appliance (studio/). No arg = run the dev build (opens the native window, sidecars the Python server). 'hm studio build' (or 'hm studio:build') produces the Linux .deb. Sets HERMES_MAX_ROOT to this repo so the app finds ui/server." ;;
cost) echo "hm cost [today|week|month] — spend breakdown at \$0.000000 precision: by provider/model/role, the free-vs-paid split, and remaining daily free budget per model." ;;
health) echo "hm health — pass/fail healthcheck for scripting: endpoints reachable, providers present, and the full model-roster confirmation (warns on a deprecated/missing id; never gates)." ;;
preflight) echo "hm preflight — validate the whole stack before a task (PASS/FAIL/WARN); auto-fixes the common killers; exit 1 on a BLOCKING failure." ;;
watch) echo "hm watch — the raw live tool-call stream: every call, heartbeat, fallback, and routing decision. (Cockpit uses this; for debugging.)" ;;
observe) echo "hm observe — the live waterfall view of where wall-clock time actually went. (For debugging.)" ;;
logs) echo "hm logs [server] — tail a server's log under ~/.hermes-max/logs/ (or the live log with no arg)." ;;
smoke) echo "hm smoke — a real ~15-minute end-to-end agent proof that drives every component once." ;;
regression) echo "hm regression — the regression eval gate." ;;
summary) echo "hm summary — per-task tool-call summary + the inference/tool-work/artificial bottleneck split." ;;
bottleneck) echo "hm bottleneck — run the SAME task FULL vs BARE and print both timing splits → bottleneck_report.md." ;;
eval) echo "hm eval [capability] — prove every feature works in a REAL agent turn (asserts a real artifact effect, not a 200). Name one capability for fast iteration." ;;
snapshot) echo "hm snapshot <name> — snapshot the RAG index + KG db + corpus stores to ~/.hermes-max/snapshots/<name>." ;;
restore) echo "hm restore <name> — restore a named store snapshot (the current state is auto-backed-up first)." ;;
setup) echo "hm setup — first-run wiring: copy the config trinity into ~/.hermes-max/ and print which providers are present." ;;
install) echo "hm install — symlink hm onto PATH (~/.local/bin/hm)." ;;
*) return 1 ;;
esac
}
_install() {
local bindir="${HOME}/.local/bin"
mkdir -p "${bindir}"
ln -sf "${HM_SELF}" "${bindir}/hm"
echo "✓ linked ${bindir}/hm -> ${HM_SELF}"
case ":${PATH}:" in *":${bindir}:"*) : ;;
*) echo " note: ${bindir} is not on PATH — add it: export PATH=\"${bindir}:\$PATH\"" ;;
esac
}
_attach() {
if [ -n "${HM_PRINT:-}" ]; then echo "tmux attach -t ${TMUX_SESSION}"; exit 0; fi
command -v tmux >/dev/null 2>&1 || { echo "✗ tmux not installed"; exit 1; }
tmux has-session -t "${TMUX_SESSION}" 2>/dev/null || { echo "no '${TMUX_SESSION}' session — start it with: hm dev"; exit 1; }
exec tmux attach -t "${TMUX_SESSION}"
}
# Resolve the sticky web-UI port (ui.conf, else default 7080).
_ui_port() {
local conf="${HERMES_MAX_CONFIG_DIR:-${HOME}/.hermes-max}/ui.conf"
if [ -f "${conf}" ]; then
local p; p="$(grep -oE '[0-9]{2,5}' "${conf}" 2>/dev/null | head -1)"
[ -n "${p}" ] && { echo "${p}"; return; }
fi
echo "7080"
}
# Stop the backgrounded web UI started by `hm dev` (by pidfile).
_stop_ui() {
local f="${HMX_RUN_DIR}/ui.pid"
if [ -f "${f}" ]; then
kill "$(cat "${f}" 2>/dev/null)" 2>/dev/null || true
rm -f "${f}"
echo " • web UI stopped"
fi
}
# `hm dev` — daemonize the stack: background MCP servers (start-all, pidfiled) + the
# web UI (backgrounded, pidfile), then RETURN to the shell. No tmux, no blocking.
# Native execution is `hm run` (exec into hermes); the tmux cockpit is `hm cockpit`.
_dev_daemon() {
if [ -n "${HM_PRINT:-}" ]; then echo "${SCRIPTS}/start-all.sh + ui.server (daemon)"; exit 0; fi
# 1. MCP stack up (idempotent; start-all backgrounds each server with a pidfile)
"${SCRIPTS}/healthcheck.sh" >/dev/null 2>&1 || "${SCRIPTS}/start-all.sh" || true
_start_serves 2>/dev/null || true
# 2. web UI backgrounded with a pidfile (skip if already up)
mkdir -p "${HMX_RUN_DIR}"
local uipid="${HMX_RUN_DIR}/ui.pid" port; port="$(_ui_port)"
if [ -f "${uipid}" ] && kill -0 "$(cat "${uipid}" 2>/dev/null)" 2>/dev/null; then
echo " • web UI already running (pid $(cat "${uipid}"))"
else
# nohup exec-replaces itself with python, so $! is python's real pid (PYTHONPATH
# makes `ui`/`lib` importable without changing hm's cwd).
HMX_UI_NO_OPEN=1 PYTHONPATH="${REPO_ROOT}:${PYTHONPATH:-}" \
nohup python3 -m ui.server --no-open --port "${port}" >"${LOG_DIR}/ui.log" 2>&1 &
echo $! > "${uipid}"
fi
# 3. report + return (no blocking)
echo "✓ Stack ready. Web UI: http://localhost:${port}"
echo " next: hm run \"<task>\" · hm status · hm stop · hm cockpit (tmux view)"
}
# The one-command tmux cockpit: hermes pane + live-watch pane + status pane.
# Detachable, reattach-from-anywhere (hm attach), 24/7. If the session already
# exists, just attach. tmux absent → clear manual instructions (never a hard fail).
_dev() {
if [ -n "${HM_PRINT:-}" ]; then echo "tmux new-session -s ${TMUX_SESSION} (hermes | watch | status)"; exit 0; fi
if ! command -v tmux >/dev/null 2>&1; then
cat <<EOF
tmux is not installed — the cockpit needs it. Install it (apt: 'sudo apt install tmux',
brew: 'brew install tmux'), or run the panes manually in three terminals:
1) ${SCRIPTS}/start-all.sh # bring the stack up
2) ${SCRIPTS}/watch.sh # live tool-call stream
3) watch -n3 ${SCRIPTS}/status.sh # stack health
4) hermes # the agent prompt
The individual scripts work without tmux — hm dev is only the convenience cockpit.
EOF
exit 1
fi
# already running → just attach (from anywhere, incl. over the network)
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
echo "• '${TMUX_SESSION}' already running — attaching"
exec tmux attach -t "${TMUX_SESSION}"
fi
# posture mode (explicit --<mode> switches; else keep the persisted posture)
_m="$(_inf_mode_from_args "$@")"
if [ -n "${_m}" ]; then _apply_inf_mode "${_m}"
else _apply_inf_mode "$(_inf name 2>/dev/null || echo free)"; fi
# pre-flight validation FIRST — auto-fixes the killers (timeouts, skills,
# embed/rerank) and brings the stack up; BLOCKING failure aborts the cockpit so
# we never launch the agent into a misconfigured stack. Override with HM_FORCE=1.
if [ -f "${SCRIPTS}/preflight.sh" ]; then
if ! "${SCRIPTS}/preflight.sh" --fix; then
if [ -n "${HM_FORCE:-}" ]; then
echo " ⚠ preflight reported BLOCKING failures — continuing anyway (HM_FORCE=1)"
else
echo " ✗ preflight found BLOCKING failures — fix them, or re-run with HM_FORCE=1 to override."
exit 1
fi
fi
fi
# bring servers up if not already healthy (backgrounded inside start-all)
"${SCRIPTS}/healthcheck.sh" >/dev/null 2>&1 || "${SCRIPTS}/start-all.sh" || true
if [ "${HMX_MODE_CHANGED}" = "1" ] && timeout 1 bash -c ">/dev/tcp/127.0.0.1/${MCP_ESCALATION_PORT:-9105}" 2>/dev/null; then
"${SCRIPTS}/restart.sh" escalation >/dev/null 2>&1 || true
fi
# embed + rerank serves (gpu_local default) so the cockpit's status pane shows them
_start_serves || true
# cockpit cost baseline: snapshot today's spend now so the status pane can show
# "this session" as a delta (best-effort; absent → session reads as today).
mkdir -p "${HOME}/.hermes-max/ui" 2>/dev/null || true
( cd "${REPO_ROOT}" && python3 -c "from lib.inference import ledger; print(ledger.report('today')['total_usd'])" 2>/dev/null ) \
> "${HOME}/.hermes-max/ui/cockpit_base" 2>/dev/null || true
# pane 1 (large): an interactive shell ready for `hermes`
tmux new-session -d -s "${TMUX_SESSION}" -n cockpit -x 220 -y 50
tmux send-keys -t "${TMUX_SESSION}" \
"echo '── hermes pane — type: hermes ──'; command -v hermes >/dev/null && echo '(hermes ready)' || echo '(hermes not on PATH)'" C-m
# pane 2: the structured live activity stream (server · tool · input · status ·
# timing + research fan-out sub-entries); falls back to the plain watch tail.
tmux split-window -h -t "${TMUX_SESSION}"
tmux send-keys -t "${TMUX_SESSION}" \
"python3 ${SCRIPTS}/cockpit_livelog.py 2>/dev/null || ${SCRIPTS}/watch.sh" C-m
# pane 3: auto-refreshing status (use watch if present, else a sleep loop)
tmux split-window -v -t "${TMUX_SESSION}"
# status pane: the live run-context block (mode · cost · task · phase · rung ·
# elapsed) on top, then the stack server table.
_statpane="python3 ${SCRIPTS}/cockpit_status.py 2>/dev/null || (cd ${REPO_ROOT} && python3 -m lib.inference.modes_cli status-line); ${SCRIPTS}/status.sh"
if command -v watch >/dev/null 2>&1; then
tmux send-keys -t "${TMUX_SESSION}" "watch -t -n 4 '${_statpane}'" C-m
else
tmux send-keys -t "${TMUX_SESSION}" \
"while true; do clear; ${_statpane}; sleep 4; done" C-m
fi
tmux select-pane -t "${TMUX_SESSION}.0"
[ -n "${HM_NO_ATTACH:-}" ] && { echo "✓ cockpit '${TMUX_SESSION}' started (detached). hm attach to join."; return 0; }
exec tmux attach -t "${TMUX_SESSION}"
}
# Sourcing with HM_LIB_ONLY=1 defines the helper functions and stops before the
# dispatch — so the mode-resolution logic can be unit-tested in isolation.
if [ -n "${HM_LIB_ONLY:-}" ]; then return 0 2>/dev/null || exit 0; fi
cmd="${1:-}"; shift || true
# `hm <verb> --help` → one-paragraph verb help, before any dispatch/side effect.
# 'mode' handles its own -h/--help; an empty/help verb falls through to grouped help.
case "${1:-}" in
-h|--help)
case "${cmd}" in
""|help|mode) : ;;
*) if _verb_help "${cmd}"; then exit 0; fi ;;
esac
;;
esac
case "${cmd}" in
up)
if [ -n "${HM_PRINT:-}" ]; then echo "${SCRIPTS}/start-all.sh"; exit 0; fi
# First-run wiring on demand: `hm up --setup` copies the config trinity.
case " $* " in *" --setup "*) [ -f "${SCRIPTS}/setup.sh" ] && "${SCRIPTS}/setup.sh" || true ;; esac
# free_uplift toggle (plugins/free_uplift)
case " $* " in *" --free-uplift "*) _set_uplift true ;; *" --no-free-uplift "*) _set_uplift false ;; esac
# Posture: an explicit --<mode> flag switches; no flag keeps the persisted
# posture (default 'free' on first run) and never silently escalates to paid.
_m="$(_inf_mode_from_args "$@")"
if [ -n "${_m}" ]; then _apply_inf_mode "${_m}"
else _apply_inf_mode "$(_inf name 2>/dev/null || echo free)"; fi
# Raise Hermes' own MCP call timeouts (the 120s long-run killer) before the
# stack comes up — idempotent, no-op once applied. Never fails `hm up`.
[ -f "${SCRIPTS}/fix-hermes-timeouts.sh" ] && \
"${SCRIPTS}/fix-hermes-timeouts.sh" || true
"${SCRIPTS}/start-all.sh"
# If the conductor was already running, restart it so it picks up the new mode
# (mode is read from CONDUCTOR_MODE in .env at launch). No-op if mode unchanged.
if [ "${HMX_MODE_CHANGED}" = "1" ] && timeout 1 bash -c ">/dev/tcp/127.0.0.1/${MCP_ESCALATION_PORT:-9105}" 2>/dev/null; then
echo " • mode changed → restarting conductor (escalation) to apply it"
"${SCRIPTS}/restart.sh" escalation >/dev/null 2>&1 || true
fi
# embed (:8002) + rerank (:8003) serves auto-start with the stack on gpu_local
# (the dense lane + reranker); opt out with HM_WITH_EMBED=0 / HM_WITH_RERANK=0.
_start_serves
# roster: warn (never gate) if a configured model id is deprecated/missing
( cd "${REPO_ROOT}" && python3 -m lib.inference.roster --warn-only 2>/dev/null ) || true
# plugins: register optional capabilities (free_uplift) if their conditions hold
( cd "${REPO_ROOT}" && python3 -m plugins.load_plugins 2>/dev/null ) || true
# ── one-screen start summary: mode + role satisfiability + present providers.
# (The Hermes backend was printed above by set_mode; roster warnings too.)
echo
echo "════════ hermes-max is up ════════"
( cd "${REPO_ROOT}" && python3 -m lib.inference.modes_cli show ) 2>/dev/null || true
echo " providers present (a missing key silently drops that rung):"
( cd "${REPO_ROOT}" && python3 -m lib.inference.modes_cli providers ) 2>/dev/null \
| awk 'NR==1{next} {printf " %-16s %s\n", $1, $2 $3}' || true
echo " next: hermes (or: hm dev for the cockpit · hm status · hm cost)"
;;
mode)
sub="${1:-}"
case "${sub}" in
""|show) _inf show ;;
--list|list) _inf list ;;
-h|--help) echo "hm mode [show|--list|<name>] names: $(_inf list 2>/dev/null | sed -n '2,7p' | awk '{print $1}' | tr '\n' ' ')" ;;
*)
if [ -n "${HM_PRINT:-}" ]; then echo "hm mode set ${sub}"; exit 0; fi
_apply_inf_mode "${sub}" || exit 2
# If the conductor is already running, restart it so the new ceiling applies.
if [ "${HMX_MODE_CHANGED}" = "1" ] && timeout 1 bash -c ">/dev/tcp/127.0.0.1/${MCP_ESCALATION_PORT:-9105}" 2>/dev/null; then
echo " • restarting conductor (escalation) to apply the new ceiling"
"${SCRIPTS}/restart.sh" escalation >/dev/null 2>&1 || true
fi
;;
esac
;;
down) _stop_ui; _dispatch "${SCRIPTS}/stop-all.sh" "$@" ;;
stop) if [ -n "${HM_PRINT:-}" ]; then echo "stop ui + ${SCRIPTS}/stop-all.sh"; exit 0; fi
_stop_ui; "${SCRIPTS}/stop-all.sh" "$@" ;;
restart) _dispatch "${SCRIPTS}/restart.sh" "$@" ;;
status) _dispatch "${SCRIPTS}/status.sh" "$@" ;;
watch) _dispatch "${SCRIPTS}/watch.sh" "$@" ;;
observe) _dispatch "${SCRIPTS}/observe.sh" "$@" ;;
health) _dispatch "${SCRIPTS}/healthcheck.sh" "$@" ;;
preflight) _dispatch "${SCRIPTS}/preflight.sh" "$@" ;;
smoke) _dispatch "${SCRIPTS}/integration-smoke.sh" "$@" ;;
ui-test) _dispatch "${SCRIPTS}/ui-actuation-test.sh" "$@" ;;
regression) _dispatch "${SCRIPTS}/regression-eval.sh" "$@" ;;
summary) _dispatch "${SCRIPTS}/run-summary.sh" "$@" ;;
bottleneck) _dispatch "${SCRIPTS}/bottleneck-eval.sh" "$@" ;;
cost)
if [ -n "${HM_PRINT:-}" ]; then echo "lib.inference cost + ${SCRIPTS}/conductor-report.sh"; exit 0; fi
( cd "${REPO_ROOT}" && python3 -m lib.inference.modes_cli cost "$@" ) || true
if [ -f "${SCRIPTS}/conductor-report.sh" ]; then
echo; echo "── legacy conductor ledger ──"; "${SCRIPTS}/conductor-report.sh" || true
fi
;;
eval) _dispatch "${SCRIPTS}/eval-battery.sh" "$@" ;;
snapshot) _dispatch "${SCRIPTS}/snapshot-stores.sh" "$@" ;;
restore) _dispatch "${SCRIPTS}/restore-stores.sh" "$@" ;;
logs)
srv="${1:-live}"
if [ "${srv}" = "live" ]; then f="${LOG_DIR}/live.log"; else f="${LOG_DIR}/${srv#mcp-}.log"; fi
if [ -n "${HM_PRINT:-}" ]; then echo "tail -F ${f}"; exit 0; fi
[ -f "${f}" ] || { echo "no log at ${f}"; exit 1; }
exec tail -n "${HM_LOG_LINES:-60}" -F "${f}"
;;
run)
if [ -n "${HM_PRINT:-}" ]; then echo "hermes $*"; exit 0; fi
command -v hermes >/dev/null 2>&1 || { echo "✗ 'hermes' not on PATH — install/activate Hermes first."; exit 127; }
# Raw hermes flags (e.g. `hm run --continue …`) pass straight through.
case "${1:-}" in -*) exec hermes "$@" ;; esac
_hm_prompt="$*"
_hm_plan="${PWD}/PLAN.md"
# Escape hatch: HM_NO_PREPLAN=1 runs the legacy raw-prompt path (no enforced split).
if [ -n "${HM_NO_PREPLAN:-}" ]; then exec hermes --yolo -z "${_hm_prompt}"; fi
# Fix 5: register the run so the web UI discovers it within ~1s (works without the
# shell wrapper; .env sourced so the registry has the mode).
_hm_run_id=""
if [ -f "${SCRIPTS}/register_run.py" ]; then
_hm_run_id="$( ( set -a; [ -f "${HMX_ENV_FILE}" ] && . "${HMX_ENV_FILE}" 2>/dev/null; set +a
python3 "${SCRIPTS}/register_run.py" "${PWD}" -z "${_hm_prompt}" ) 2>/dev/null )"
export HM_CURRENT_RUN_ID="${_hm_run_id}"
fi
# Fix 3: conductor plans FIRST; preplan ALWAYS leaves a PLAN.md (signed, or a minimal
# local-fallback if every rung 429'd after retries) so the run never dead-ends.
if [ -f "${SCRIPTS}/preplan.py" ]; then
( set -a; [ -f "${HMX_ENV_FILE}" ] && . "${HMX_ENV_FILE}" 2>/dev/null; set +a
python3 "${SCRIPTS}/preplan.py" "${PWD}" "${_hm_prompt}" ) || true
fi
if ! grep -qiE '^##[[:space:]]*Plan authored by:.*via conductor' "${_hm_plan}" 2>/dev/null; then
echo " ⚠ PLAN.md not conductor-signed (conductor unavailable) — proceeding; verify warns, not blocks."
fi
# The EXECUTION instruction hermes receives — NEVER the raw design brief (this is what
# enforces the split). Names the mid-loop tools: reasoning_escalation (stuck/HIGH) and
# review_and_adapt (a step is impossible as written).
_hm_exec="Read PLAN.md in ${PWD} first, then execute it. Do NOT replan or redesign — the architecture is already decided in PLAN.md. Execute each step. For any step marked 'complexity: HIGH', call reasoning_escalation BEFORE implementing it and follow the guidance. If a step proves IMPOSSIBLE as written, call review_and_adapt with the issue and step number — do not attempt impossible implementations and do not spin. If you get stuck, call reasoning_escalation rather than over-reasoning — ask and act. Verify with pytest; done when the plan's DONE_CONDITION is met."
# ── interactive default: exec into native hermes, which OWNS the terminal ──
# `exec` replaces this process with hermes — no subprocess, no pipe, no blank
# screen. Hermes streams natively (tokens, tool_progress, banner). The registered
# run keeps the SAME pid (exec), so the UI's pid-liveness marks it done on exit.
# (Native one-shot; the run is autonomous + the conductor plugin orchestrates. For
# the interactive REPL with slash-commands, run `hermes chat` in this dir directly.)
if [ "${HM_HEADLESS:-0}" = "0" ]; then
exec hermes --yolo -z "${_hm_exec}"
fi
# ── headless (CI/cron): tmux window so a non-interactive caller doesn't block ──
_hm_pf="$(mktemp 2>/dev/null || echo /tmp/hmx_exec.$$)"; printf '%s' "${_hm_exec}" > "${_hm_pf}"
_hm_done="python3 '${SCRIPTS}/complete_run.py' '${_hm_run_id}' 2>/dev/null"
if command -v tmux >/dev/null 2>&1; then
_win="run-$(basename "${PWD}")-$(date +%H%M%S 2>/dev/null || echo run)"
_cmd="cd '${PWD}' && hermes --yolo -z \"\$(cat '${_hm_pf}')\"; rm -f '${_hm_pf}'; ${_hm_done}; echo '--- done ---'; read _"
tmux new-window -t "${TMUX_SESSION}" -n "${_win}" "${_cmd}" 2>/dev/null \
|| tmux new-session -d -s "${TMUX_SESSION}" -n "${_win}" "${_cmd}"
echo "✓ headless run in tmux '${TMUX_SESSION}:${_win}' — attach: tmux attach -t ${TMUX_SESSION}"
exit 0
fi
# headless, no tmux: run inline to completion.
hermes --yolo -z "${_hm_exec}"; _rc=$?
rm -f "${_hm_pf}"; [ -n "${_hm_run_id}" ] && python3 "${SCRIPTS}/complete_run.py" "${_hm_run_id}" 2>/dev/null
exit ${_rc}
;;
native)
if [ -n "${HM_PRINT:-}" ]; then echo "${REPO_ROOT}/bin/hm-native $*"; exit 0; fi
exec "${REPO_ROOT}/bin/hm-native" "$@"
;;
setup) _dispatch "${SCRIPTS}/setup.sh" "$@" ;;
studio|studio:build)
# Hermes Studio — the Tauri desktop appliance (studio/). `hm studio` runs the
# dev build; `hm studio build` (or `hm studio:build`) produces the .deb.
command -v npm >/dev/null 2>&1 || { echo "✗ npm not found — required for Hermes Studio."; exit 127; }
[ -d "${REPO_ROOT}/studio/node_modules" ] || \
( echo " • installing Studio deps (one-time)…"; cd "${REPO_ROOT}/studio" && npm install --no-fund --no-audit )
export HERMES_MAX_ROOT="${REPO_ROOT}" # the app sidecars the Python server from here
if [ "${cmd}" = "studio:build" ] || [ "${1:-}" = "build" ]; then
cd "${REPO_ROOT}/studio" && exec npm run tauri build
fi
cd "${REPO_ROOT}/studio" && exec npm run tauri dev
;;
ui)
if [ -n "${HM_PRINT:-}" ]; then echo "python3 -m ui.server $*"; exit 0; fi
command -v python3 >/dev/null 2>&1 || { echo "✗ python3 not found — required for 'hm ui'."; exit 127; }
if [ ! -f "${REPO_ROOT}/ui/web/dist/index.html" ]; then
echo " • frontend not built yet — building once (cd ui/web && npm install && npm run build)…"
if command -v npm >/dev/null 2>&1; then
( cd "${REPO_ROOT}/ui/web" && npm install --no-fund --no-audit >/dev/null 2>&1 && npm run build >/dev/null 2>&1 ) \
|| echo " ⚠ frontend build failed — the server still runs and the API works; build manually in ui/web."
else
echo " ⚠ npm not found — the server runs and the API works, but the page needs 'npm run build' in ui/web."
fi
fi
cd "${REPO_ROOT}" && exec python3 -m ui.server "$@"
;;
dev) _dev_daemon "$@" ;;
cockpit) _dev "$@" ;;
attach) _attach ;;
install) _install ;;
""|-h|--help|help) usage "$@" ;;
*) echo "hm: unknown verb '${cmd}'"; echo; _help_short; exit 2 ;;
esac