From b609ae235578e648900f7fb9e8f26602c8fd557a Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 27 Aug 2026 21:31:17 -0500 Subject: [PATCH 1/6] Keep prompted Hermes sessions interactive Hermes oneshot deliberately exits after answering, which closes the agent terminal. Seed the TUI chat session instead, keep inherited flags after the subcommand for older Hermes parsers, and bind the query as one argument so dash-prefixed prompts remain data. Co-Authored-By: Codex XHigh --- bin/omarchy-agent | 5 ++--- test/shell.d/default-agent-test.sh | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/omarchy-agent b/bin/omarchy-agent index 426ea85a..80e0f182 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -87,10 +87,9 @@ codex) [[ -n ${prompt:-} ]] && command+=(-- "$prompt") ;; hermes) - # Hermes has no "start interactive, seeded with this prompt" mode. --oneshot - # answers the prompt and exits, which is the closest it offers. if [[ -n ${prompt:-} ]]; then - command=(hermes --yolo --oneshot "$prompt") + # Keep inherited flags after chat so older Hermes subparsers do not clear them. + command=(hermes chat --yolo --tui "--query=$prompt") else command=(hermes --yolo) fi diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index 5b4512f1..d46bbdb2 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -462,6 +462,7 @@ assert_launch claude claude --permission-mode auto -- "Review this project" assert_launch codex codex --approve-for-me -- "Review this project" assert_launch crush crush run "Review this project" assert_launch grok grok --permission-mode bypassPermissions -- "Review this project" +assert_launch hermes hermes chat --yolo --tui "--query=Review this project" assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project" assert_launch copilot copilot --allow-all --interactive "Review this project" pass "agent launcher adapts initial prompts for every supported agent" @@ -474,6 +475,7 @@ assert_bypass claude claude --permission-mode auto assert_bypass codex codex --approve-for-me assert_bypass crush crush --yolo assert_bypass grok grok --permission-mode bypassPermissions +assert_bypass hermes hermes --yolo assert_bypass agy agy --dangerously-skip-permissions assert_bypass copilot copilot --allow-all pass "agent launcher skips permission prompts for every supported agent" From 750dde5ed2759816aee0b8bd27fce581fa6466c8 Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 27 Aug 2026 22:37:03 -0500 Subject: [PATCH 2/6] Resume prompted Hermes sessions literally Hermes TUI startup queries execute slash, shell, interpolation, and multiline syntax before reaching the model. Run the prompt through literal one-shot mode, read its exact session ID from a private usage report, and resume that session in the TUI so arbitrary prompt text stays data while the conversation remains interactive. Co-Authored-By: Codex XHigh --- bin/omarchy-agent | 3 +- bin/omarchy-agent-hermes | 21 ++++++++ test/shell.d/default-agent-test.sh | 2 +- test/shell.d/hermes-agent-test.sh | 79 ++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100755 bin/omarchy-agent-hermes create mode 100755 test/shell.d/hermes-agent-test.sh diff --git a/bin/omarchy-agent b/bin/omarchy-agent index 80e0f182..5436ca0b 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -88,8 +88,7 @@ codex) ;; hermes) if [[ -n ${prompt:-} ]]; then - # Keep inherited flags after chat so older Hermes subparsers do not clear them. - command=(hermes chat --yolo --tui "--query=$prompt") + command=(omarchy-agent-hermes "$prompt") else command=(hermes --yolo) fi diff --git a/bin/omarchy-agent-hermes b/bin/omarchy-agent-hermes new file mode 100755 index 00000000..b40a5e67 --- /dev/null +++ b/bin/omarchy-agent-hermes @@ -0,0 +1,21 @@ +#!/bin/bash + +# omarchy:summary=Seed Hermes literally and resume the resulting interactive session +# omarchy:args= +# omarchy:hidden=true + +set -euo pipefail + +prompt=${1:?usage: omarchy-agent-hermes } +usage=$(mktemp) +trap 'rm -f "$usage"' EXIT + +# TUI startup queries pass through Hermes' slash, shell, and interpolation +# dispatcher. One-shot treats the prompt literally and records its session ID. +HERMES_SESSION_SOURCE=tui hermes --yolo --usage-file "$usage" --oneshot "$prompt" +session_id=$(jq -er '.session_id | strings | select(length > 0)' "$usage") + +rm -f "$usage" +trap - EXIT + +exec hermes chat --yolo --tui --resume "$session_id" diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index d46bbdb2..81f3985c 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -462,7 +462,7 @@ assert_launch claude claude --permission-mode auto -- "Review this project" assert_launch codex codex --approve-for-me -- "Review this project" assert_launch crush crush run "Review this project" assert_launch grok grok --permission-mode bypassPermissions -- "Review this project" -assert_launch hermes hermes chat --yolo --tui "--query=Review this project" +assert_launch hermes omarchy-agent-hermes "Review this project" assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project" assert_launch copilot copilot --allow-all --interactive "Review this project" pass "agent launcher adapts initial prompts for every supported agent" diff --git a/test/shell.d/hermes-agent-test.sh b/test/shell.d/hermes-agent-test.sh new file mode 100755 index 00000000..37b8de55 --- /dev/null +++ b/test/shell.d/hermes-agent-test.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +mock_bin="$test_tmp/bin" +oneshot_log="$test_tmp/oneshot" +resume_log="$test_tmp/resume" +source_log="$test_tmp/source" +mkdir -p "$mock_bin" + +cat >"$mock_bin/hermes" <<'SH' +#!/bin/bash + +if [[ " $* " == *" --oneshot "* ]]; then + printf '%s\0' "$@" >"$HERMES_TEST_ONESHOT_LOG" + printf '%s' "${HERMES_SESSION_SOURCE:-}" >"$HERMES_TEST_SOURCE_LOG" + + while (( $# )); do + if [[ $1 == "--usage-file" ]]; then + usage=$2 + break + fi + shift + done + + [[ ${HERMES_TEST_ONESHOT_FAIL:-false} == "false" ]] || exit 42 + [[ ${HERMES_TEST_USAGE_FAIL:-false} == "false" ]] && printf '{"session_id":"session-123"}\n' >"$usage" + printf '%s\n' response + exit +fi + +printf '%s\0' "$@" >"$HERMES_TEST_RESUME_LOG" +SH + +chmod +x "$mock_bin/hermes" + +export PATH="$mock_bin:$PATH" +export HERMES_TEST_ONESHOT_LOG="$oneshot_log" +export HERMES_TEST_RESUME_LOG="$resume_log" +export HERMES_TEST_SOURCE_LOG="$source_log" + +sentinel="$test_tmp/hermes-seed-must-stay-literal" +prompt="!Crash /quit {!touch $sentinel}"$'\ntrailing\\' +"$ROOT/bin/omarchy-agent-hermes" "$prompt" >/dev/null + +mapfile -d '' -t oneshot_args <"$oneshot_log" +(( ${#oneshot_args[@]} == 5 )) || fail "Hermes literal seed has five one-shot arguments" +[[ ${oneshot_args[0]} == "--yolo" ]] || fail "Hermes literal seed enables yolo mode" +[[ ${oneshot_args[1]} == "--usage-file" ]] || fail "Hermes literal seed requests the session report" +usage_file=${oneshot_args[2]} +[[ ${oneshot_args[3]} == "--oneshot" && ${oneshot_args[4]} == "$prompt" ]] || + fail "Hermes literal seed remains one argument" +[[ ! -e $usage_file ]] || fail "Hermes literal seed removes its session report" +[[ ! -e $sentinel ]] || fail "Hermes literal seed never executes prompt interpolation" +[[ $(<"$source_log") == "tui" ]] || fail "Hermes literal seed records an interactive session" + +mapfile -d '' -t resume_args <"$resume_log" +[[ ${resume_args[*]} == "chat --yolo --tui --resume session-123" ]] || + fail "Hermes literal seed resumes the exact completed session" +pass "Hermes sends initial prompts literally and resumes their exact session" + +: >"$resume_log" +if HERMES_TEST_ONESHOT_FAIL=true "$ROOT/bin/omarchy-agent-hermes" failure >/dev/null 2>&1; then + fail "Hermes literal seed reports a failed initial turn" +fi +[[ ! -s $resume_log ]] || fail "Hermes literal seed does not resume a failed initial turn" +pass "Hermes does not resume after a failed initial turn" + +: >"$resume_log" +if HERMES_TEST_USAGE_FAIL=true "$ROOT/bin/omarchy-agent-hermes" missing-session >/dev/null 2>&1; then + fail "Hermes literal seed requires a recorded session ID" +fi +[[ ! -s $resume_log ]] || fail "Hermes literal seed does not guess which session to resume" +pass "Hermes resumes only the session recorded by the initial turn" From 288e387a22244a8f98a30c15ee94d048dab2ba18 Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 27 Aug 2026 22:50:21 -0500 Subject: [PATCH 3/6] Preserve Hermes session workspace metadata Keep the one-shot session on Hermes' native CLI source so it records the launch directory before the exact session is resumed in the TUI. Co-Authored-By: Codex XHigh --- bin/omarchy-agent-hermes | 2 +- test/shell.d/hermes-agent-test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/omarchy-agent-hermes b/bin/omarchy-agent-hermes index b40a5e67..28675cae 100755 --- a/bin/omarchy-agent-hermes +++ b/bin/omarchy-agent-hermes @@ -12,7 +12,7 @@ trap 'rm -f "$usage"' EXIT # TUI startup queries pass through Hermes' slash, shell, and interpolation # dispatcher. One-shot treats the prompt literally and records its session ID. -HERMES_SESSION_SOURCE=tui hermes --yolo --usage-file "$usage" --oneshot "$prompt" +hermes --yolo --usage-file "$usage" --oneshot "$prompt" session_id=$(jq -er '.session_id | strings | select(length > 0)' "$usage") rm -f "$usage" diff --git a/test/shell.d/hermes-agent-test.sh b/test/shell.d/hermes-agent-test.sh index 37b8de55..7472e3fc 100755 --- a/test/shell.d/hermes-agent-test.sh +++ b/test/shell.d/hermes-agent-test.sh @@ -57,7 +57,7 @@ usage_file=${oneshot_args[2]} fail "Hermes literal seed remains one argument" [[ ! -e $usage_file ]] || fail "Hermes literal seed removes its session report" [[ ! -e $sentinel ]] || fail "Hermes literal seed never executes prompt interpolation" -[[ $(<"$source_log") == "tui" ]] || fail "Hermes literal seed records an interactive session" +[[ ! -s $source_log ]] || fail "Hermes literal seed preserves native CLI session metadata" mapfile -d '' -t resume_args <"$resume_log" [[ ${resume_args[*]} == "chat --yolo --tui --resume session-123" ]] || From 36353296aad129e81b69a6249c01253c540eecab Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 27 Aug 2026 23:01:30 -0500 Subject: [PATCH 4/6] Harden prompted Hermes session handoff Bind option-looking prompts to one-shot mode, require a successful completed usage report before resuming, replay the prompt after first-run setup, and reject Hermes runtimes that lack the session-report capability. Co-Authored-By: Codex XHigh --- bin/omarchy-agent-hermes | 16 +++++++-- bin/omarchy-install-hermes-cli | 39 ++++++++++++++++---- test/shell.d/hermes-agent-test.sh | 59 +++++++++++++++++++++++++++---- test/shell.d/hermes-cli-test.sh | 44 +++++++++++++++++++---- 4 files changed, 138 insertions(+), 20 deletions(-) diff --git a/bin/omarchy-agent-hermes b/bin/omarchy-agent-hermes index 28675cae..eb81495b 100755 --- a/bin/omarchy-agent-hermes +++ b/bin/omarchy-agent-hermes @@ -12,8 +12,20 @@ trap 'rm -f "$usage"' EXIT # TUI startup queries pass through Hermes' slash, shell, and interpolation # dispatcher. One-shot treats the prompt literally and records its session ID. -hermes --yolo --usage-file "$usage" --oneshot "$prompt" -session_id=$(jq -er '.session_id | strings | select(length > 0)' "$usage") +seed_session() { + hermes --yolo --usage-file "$usage" --oneshot="$prompt" +} + +if ! seed_session; then + if jq -e '.failed == true and (.failure | strings | startswith("No inference provider configured."))' "$usage" >/dev/null 2>&1; then + hermes setup + seed_session + else + exit 1 + fi +fi + +session_id=$(jq -er 'select(.completed == true and .failed != true) | .session_id | strings | select(length > 0)' "$usage") rm -f "$usage" trap - EXIT diff --git a/bin/omarchy-install-hermes-cli b/bin/omarchy-install-hermes-cli index 0c2e526e..f3a8614c 100755 --- a/bin/omarchy-install-hermes-cli +++ b/bin/omarchy-install-hermes-cli @@ -55,7 +55,7 @@ desktop_hermes_ready() { # And a marker left behind by an install whose venv has since gone answers # for nothing, so the command has to run, exactly as a foreign one must. - hermes_runs + hermes_prompt_ready } # Whether Hermes is really installed, not merely whether the stub exists. A @@ -88,6 +88,15 @@ hermes_runs() { timeout 15 "$HOME/.local/bin/hermes" --version >/dev/null 2>&1 } +# Prompted Omarchy sessions need Hermes' usage report to identify and validate +# the exact one-shot session before reopening it interactively. +hermes_prompt_ready() { + local help + hermes_runs && + help=$(timeout 15 "$HOME/.local/bin/hermes" --help 2>/dev/null) && + grep -qF -- '--usage-file' <<<"$help" +} + # --owns answers whether the wrapper on PATH is the one this command wrote, so # the migration and Remove Preinstalls do not each carry their own copy of the # marker and drift from it. @@ -101,12 +110,12 @@ if [[ $mode == "--check" ]]; then if desktop_owns_hermes; then if desktop_hermes_ready; then exit 0; else exit 1; fi fi - # A foreign command is ready when it runs; a broken one is not, and since it - # is not ours to replace, nothing this installer does will make it ready. + # A foreign command is ready only when it also supports prompted sessions; + # since it is not ours to replace, nothing this installer does will update it. if foreign_hermes; then - if hermes_runs; then exit 0; else exit 1; fi + if hermes_prompt_ready; then exit 0; else exit 1; fi fi - if installed; then exit 0; else exit 1; fi + if installed && hermes_prompt_ready; then exit 0; else exit 1; fi fi # Hand Hermes over to the app rather than keeping a second copy beside it. @@ -140,15 +149,29 @@ fi # The user already has a hermes of their own. Leave it be: a working one is # what the default agent will run, and a broken one is theirs to fix. if foreign_hermes; then - if hermes_runs; then + if hermes_prompt_ready; then exit 0 fi + if hermes_runs; then + echo "~/.local/bin/hermes does not support the session report Omarchy needs for prompted launches." >&2 + echo "Update it to Hermes Agent 0.18.1 or newer, then run omarchy-install-hermes-cli again." >&2 + exit 1 + fi + echo "~/.local/bin/hermes exists but is not runnable, and it was not installed by Omarchy." >&2 echo "Fix or remove it, then run omarchy-install-hermes-cli again." >&2 exit 1 fi +# An older mise environment may predate the session report. It belongs to this +# installer, so discard only that environment and let the current stub rebuild it. +if installed && ! hermes_prompt_ready; then + echo "Updating Hermes for prompted sessions..." >&2 + mise rm -g "$tool" >/dev/null 2>&1 || true + mise uninstall --all "$tool" >/dev/null 2>&1 || true +fi + mkdir -p "$HOME/.local/bin" rm -f "$HOME/.local/bin/hermes" @@ -194,4 +217,8 @@ chmod +x "$HOME/.local/bin/hermes" # hands Hermes to the GUI has to install it here rather than leave it stubbed. if [[ $mode == "--now" ]]; then "$HOME/.local/bin/hermes" --version + if ! hermes_prompt_ready; then + echo "Hermes installed without the session report Omarchy needs for prompted launches." >&2 + exit 1 + fi fi diff --git a/test/shell.d/hermes-agent-test.sh b/test/shell.d/hermes-agent-test.sh index 7472e3fc..174958d0 100755 --- a/test/shell.d/hermes-agent-test.sh +++ b/test/shell.d/hermes-agent-test.sh @@ -11,12 +11,21 @@ mock_bin="$test_tmp/bin" oneshot_log="$test_tmp/oneshot" resume_log="$test_tmp/resume" source_log="$test_tmp/source" +setup_log="$test_tmp/setup" +setup_marker="$test_tmp/setup-complete" mkdir -p "$mock_bin" cat >"$mock_bin/hermes" <<'SH' #!/bin/bash -if [[ " $* " == *" --oneshot "* ]]; then +if [[ ${1:-} == "setup" ]]; then + printf '%s\0' "$@" >"$HERMES_TEST_SETUP_LOG" + [[ ${HERMES_TEST_SETUP_FAIL:-false} == "false" ]] || exit 43 + touch "$HERMES_TEST_SETUP_MARKER" + exit +fi + +if [[ " $* " == *" --oneshot="* ]]; then printf '%s\0' "$@" >"$HERMES_TEST_ONESHOT_LOG" printf '%s' "${HERMES_SESSION_SOURCE:-}" >"$HERMES_TEST_SOURCE_LOG" @@ -28,8 +37,19 @@ if [[ " $* " == *" --oneshot "* ]]; then shift done + if [[ ${HERMES_TEST_NEEDS_SETUP:-false} == "true" && ! -e $HERMES_TEST_SETUP_MARKER ]]; then + printf '{"session_id":null,"completed":null,"failed":true,"failure":"No inference provider configured. Run hermes model."}\n' >"$usage" + exit 1 + fi + [[ ${HERMES_TEST_ONESHOT_FAIL:-false} == "false" ]] || exit 42 - [[ ${HERMES_TEST_USAGE_FAIL:-false} == "false" ]] && printf '{"session_id":"session-123"}\n' >"$usage" + if [[ ${HERMES_TEST_USAGE_FAIL:-false} == "false" ]]; then + completed=true + failed=false + [[ ${HERMES_TEST_USAGE_INCOMPLETE:-false} == "false" ]] || completed=false + [[ ${HERMES_TEST_USAGE_FAILED:-false} == "false" ]] || failed=true + printf '{"session_id":"session-123","completed":%s,"failed":%s}\n' "$completed" "$failed" >"$usage" + fi printf '%s\n' response exit fi @@ -43,18 +63,19 @@ export PATH="$mock_bin:$PATH" export HERMES_TEST_ONESHOT_LOG="$oneshot_log" export HERMES_TEST_RESUME_LOG="$resume_log" export HERMES_TEST_SOURCE_LOG="$source_log" +export HERMES_TEST_SETUP_LOG="$setup_log" +export HERMES_TEST_SETUP_MARKER="$setup_marker" sentinel="$test_tmp/hermes-seed-must-stay-literal" -prompt="!Crash /quit {!touch $sentinel}"$'\ntrailing\\' +prompt="--help !Crash /quit {!touch $sentinel}"$'\ntrailing\\' "$ROOT/bin/omarchy-agent-hermes" "$prompt" >/dev/null mapfile -d '' -t oneshot_args <"$oneshot_log" -(( ${#oneshot_args[@]} == 5 )) || fail "Hermes literal seed has five one-shot arguments" +(( ${#oneshot_args[@]} == 4 )) || fail "Hermes literal seed has four one-shot arguments" [[ ${oneshot_args[0]} == "--yolo" ]] || fail "Hermes literal seed enables yolo mode" [[ ${oneshot_args[1]} == "--usage-file" ]] || fail "Hermes literal seed requests the session report" usage_file=${oneshot_args[2]} -[[ ${oneshot_args[3]} == "--oneshot" && ${oneshot_args[4]} == "$prompt" ]] || - fail "Hermes literal seed remains one argument" +[[ ${oneshot_args[3]} == "--oneshot=$prompt" ]] || fail "Hermes literal seed binds option-looking prompts as data" [[ ! -e $usage_file ]] || fail "Hermes literal seed removes its session report" [[ ! -e $sentinel ]] || fail "Hermes literal seed never executes prompt interpolation" [[ ! -s $source_log ]] || fail "Hermes literal seed preserves native CLI session metadata" @@ -77,3 +98,29 @@ if HERMES_TEST_USAGE_FAIL=true "$ROOT/bin/omarchy-agent-hermes" missing-session fi [[ ! -s $resume_log ]] || fail "Hermes literal seed does not guess which session to resume" pass "Hermes resumes only the session recorded by the initial turn" + +for state in INCOMPLETE FAILED; do + : >"$resume_log" + if env "HERMES_TEST_USAGE_$state=true" "$ROOT/bin/omarchy-agent-hermes" "${state,,}" >/dev/null 2>&1; then + fail "Hermes literal seed rejects a reported ${state,,} initial turn" + fi + [[ ! -s $resume_log ]] || fail "Hermes literal seed does not resume a reported ${state,,} initial turn" +done +pass "Hermes resumes only completed successful initial turns" + +: >"$resume_log" +HERMES_TEST_NEEDS_SETUP=true "$ROOT/bin/omarchy-agent-hermes" setup-first >/dev/null +mapfile -d '' -t setup_args <"$setup_log" +[[ ${setup_args[*]} == "setup" ]] || fail "Hermes runs setup when no inference provider is configured" +mapfile -d '' -t resume_args <"$resume_log" +[[ ${resume_args[*]} == "chat --yolo --tui --resume session-123" ]] || + fail "Hermes replays the prompted turn after setup and resumes it" +pass "Hermes completes first-run setup before replaying the prompt" + +rm -f "$setup_marker" +: >"$resume_log" +if HERMES_TEST_NEEDS_SETUP=true HERMES_TEST_SETUP_FAIL=true "$ROOT/bin/omarchy-agent-hermes" setup-cancelled >/dev/null 2>&1; then + fail "Hermes reports a failed first-run setup" +fi +[[ ! -s $resume_log ]] || fail "Hermes does not resume when first-run setup fails" +pass "Hermes stops when first-run setup does not complete" diff --git a/test/shell.d/hermes-cli-test.sh b/test/shell.d/hermes-cli-test.sh index 88319dc9..39435aa5 100755 --- a/test/shell.d/hermes-cli-test.sh +++ b/test/shell.d/hermes-cli-test.sh @@ -26,7 +26,10 @@ SH cat >"$mock_bin/mise" <<'SH' #!/bin/bash printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG" -[[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]] && exit 0 +if [[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]]; then + printf '%s\n' "$OMARCHY_TEST_MISE_ROOT" + exit 0 +fi [[ $1 != "where" ]] SH @@ -35,6 +38,7 @@ chmod +x "$mock_bin"/* run_installer() { OMARCHY_TEST_DESKTOP_INSTALLED="$1" \ OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \ + OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \ OMARCHY_TEST_MISE_LOG="$mise_log" \ HOME="$test_home" \ PATH="$mock_bin:$PATH" \ @@ -88,10 +92,17 @@ pass "takeover removes an unhealthy mise copy" rm -rf "$test_home/.hermes" rm -f "$test_home/.local/bin/hermes" run_installer 1 --check && fail "--check reports Hermes missing before the app installs it" -# The venv command answers --version, as the real one does: foreign wrappers -# below exec it, and the installer probes them by running exactly that. +# The venv command answers the readiness probes, as the real one does: foreign +# wrappers below exec it, and the installer runs both before trusting them. mkdir -p "$test_home/.hermes/hermes-agent/venv/bin" -printf '%s\n' "#!/bin/bash" 'echo "hermes-agent 0.0.0-test"' >"$test_home/.hermes/hermes-agent/venv/bin/hermes" +cat >"$test_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH' +#!/bin/bash +if [[ ${1:-} == "--help" ]]; then + [[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "--usage-file PATH" +else + echo "hermes-agent 0.0.0-test" +fi +SH chmod +x "$test_home/.hermes/hermes-agent/venv/bin/hermes" run_installer 1 --check && fail "--check waits for the install to finish, not just the venv" touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete" @@ -123,6 +134,14 @@ run_installer 0 --now || fail "--now over a foreign hermes command returns succe fail "a foreign hermes command is left untouched" pass "a foreign hermes command is preserved and satisfies --check" +OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 --check && + fail "--check rejects a foreign Hermes without prompted-session reports" +OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 && + fail "installing refuses a foreign Hermes without prompted-session reports" +[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] || + fail "an older foreign Hermes command is left untouched" +pass "a foreign Hermes must support prompted-session reports" + # Broken foreign paths are still foreign. They cannot be used, so --check says # so and the installer refuses rather than replacing them. printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes" @@ -211,6 +230,13 @@ grep -q "stale template" "$test_home/.local/bin/hermes" && fail "reinstalling re grep -q "exec env -u UV_PYTHON mise x" "$test_home/.local/bin/hermes" || fail "the refreshed stub is the current template" pass "reinstalling refreshes the Omarchy stub" +mkdir -p "$test_tmp/mise/hermes-agent/lib/python$python_pin" +: >"$mise_log" +OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 || fail "reinstalling replaces an older owned Hermes environment" +tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "an older owned Hermes environment is removed from mise config" +tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "an older owned Hermes environment is uninstalled" +pass "reinstalling replaces an older owned Hermes environment" + # install/user/mise.sh is sourced by install/user/all.sh through run_logged, # which runs it under `bash -eE` and hands its exit code back to # omarchy-provision-user's `set -euo pipefail`. Everything that finalizes a user @@ -349,8 +375,14 @@ run_ready_check() { run_ready_check && fail "--check rejects the app's wrapper when its runtime is gone" -printf '%s\n' "#!/bin/bash" 'echo "hermes-agent 0.0.0-test"' \ - >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" +cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH' +#!/bin/bash +if [[ ${1:-} == "--help" ]]; then + echo "--usage-file PATH" +else + echo "hermes-agent 0.0.0-test" +fi +SH chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes" run_ready_check || fail "--check accepts the app's wrapper once it runs" pass "readiness runs the app's command rather than trusting its marker" From 64203cc2085717d29b7a13257bad1b48a2434cbf Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 27 Aug 2026 23:07:18 -0500 Subject: [PATCH 5/6] Keep prompted Hermes sessions local Clear inherited session-source tags for Omarchy's local one-shot process so Hermes records the launch directory before the exact session is resumed in the TUI. Co-Authored-By: Codex XHigh --- bin/omarchy-agent-hermes | 1 + test/shell.d/hermes-agent-test.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/omarchy-agent-hermes b/bin/omarchy-agent-hermes index eb81495b..93175818 100755 --- a/bin/omarchy-agent-hermes +++ b/bin/omarchy-agent-hermes @@ -7,6 +7,7 @@ set -euo pipefail prompt=${1:?usage: omarchy-agent-hermes } +unset HERMES_SESSION_SOURCE usage=$(mktemp) trap 'rm -f "$usage"' EXIT diff --git a/test/shell.d/hermes-agent-test.sh b/test/shell.d/hermes-agent-test.sh index 174958d0..3339df24 100755 --- a/test/shell.d/hermes-agent-test.sh +++ b/test/shell.d/hermes-agent-test.sh @@ -68,7 +68,7 @@ export HERMES_TEST_SETUP_MARKER="$setup_marker" sentinel="$test_tmp/hermes-seed-must-stay-literal" prompt="--help !Crash /quit {!touch $sentinel}"$'\ntrailing\\' -"$ROOT/bin/omarchy-agent-hermes" "$prompt" >/dev/null +HERMES_SESSION_SOURCE=gateway "$ROOT/bin/omarchy-agent-hermes" "$prompt" >/dev/null mapfile -d '' -t oneshot_args <"$oneshot_log" (( ${#oneshot_args[@]} == 4 )) || fail "Hermes literal seed has four one-shot arguments" From 5284be65828112b2882179797566979f005d1ace Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Fri, 28 Aug 2026 16:32:19 -0500 Subject: [PATCH 6/6] Use Hermes native prompted sessions Hermes now keeps chat queries interactive and literal to TUI control syntax, so launch it directly and let its own session flow replace the local one-shot, usage-file, and resume bridge. Gate installation on the capability added with native interactive queries, preserve unowned mise environments, and keep the unprompted launch path unchanged. Co-Authored-By: Codex XHigh --- bin/omarchy-agent | 2 +- bin/omarchy-agent-hermes | 34 -------- bin/omarchy-install-hermes-cli | 29 ++++--- test/shell.d/default-agent-test.sh | 15 +++- test/shell.d/hermes-agent-test.sh | 126 ----------------------------- test/shell.d/hermes-cli-test.sh | 24 ++++-- 6 files changed, 47 insertions(+), 183 deletions(-) delete mode 100755 bin/omarchy-agent-hermes delete mode 100755 test/shell.d/hermes-agent-test.sh diff --git a/bin/omarchy-agent b/bin/omarchy-agent index 5436ca0b..05f1e1a1 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -88,7 +88,7 @@ codex) ;; hermes) if [[ -n ${prompt:-} ]]; then - command=(omarchy-agent-hermes "$prompt") + command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt") else command=(hermes --yolo) fi diff --git a/bin/omarchy-agent-hermes b/bin/omarchy-agent-hermes deleted file mode 100755 index 93175818..00000000 --- a/bin/omarchy-agent-hermes +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Seed Hermes literally and resume the resulting interactive session -# omarchy:args= -# omarchy:hidden=true - -set -euo pipefail - -prompt=${1:?usage: omarchy-agent-hermes } -unset HERMES_SESSION_SOURCE -usage=$(mktemp) -trap 'rm -f "$usage"' EXIT - -# TUI startup queries pass through Hermes' slash, shell, and interpolation -# dispatcher. One-shot treats the prompt literally and records its session ID. -seed_session() { - hermes --yolo --usage-file "$usage" --oneshot="$prompt" -} - -if ! seed_session; then - if jq -e '.failed == true and (.failure | strings | startswith("No inference provider configured."))' "$usage" >/dev/null 2>&1; then - hermes setup - seed_session - else - exit 1 - fi -fi - -session_id=$(jq -er 'select(.completed == true and .failed != true) | .session_id | strings | select(length > 0)' "$usage") - -rm -f "$usage" -trap - EXIT - -exec hermes chat --yolo --tui --resume "$session_id" diff --git a/bin/omarchy-install-hermes-cli b/bin/omarchy-install-hermes-cli index f3a8614c..5b6a3598 100755 --- a/bin/omarchy-install-hermes-cli +++ b/bin/omarchy-install-hermes-cli @@ -88,13 +88,13 @@ hermes_runs() { timeout 15 "$HOME/.local/bin/hermes" --version >/dev/null 2>&1 } -# Prompted Omarchy sessions need Hermes' usage report to identify and validate -# the exact one-shot session before reopening it interactively. +# The chat subcommand's --oneshot opt-out arrived with native interactive -q, +# so its presence is a stable capability check without relying on a version. hermes_prompt_ready() { local help hermes_runs && - help=$(timeout 15 "$HOME/.local/bin/hermes" --help 2>/dev/null) && - grep -qF -- '--usage-file' <<<"$help" + help=$(timeout 15 "$HOME/.local/bin/hermes" chat --help 2>/dev/null) && + grep -qF -- '--oneshot' <<<"$help" } # --owns answers whether the wrapper on PATH is the one this command wrote, so @@ -154,8 +154,8 @@ if foreign_hermes; then fi if hermes_runs; then - echo "~/.local/bin/hermes does not support the session report Omarchy needs for prompted launches." >&2 - echo "Update it to Hermes Agent 0.18.1 or newer, then run omarchy-install-hermes-cli again." >&2 + echo "~/.local/bin/hermes does not support the interactive seeded sessions Omarchy needs." >&2 + echo "Update it to a Hermes Agent release with interactive chat queries, then run omarchy-install-hermes-cli again." >&2 exit 1 fi @@ -164,12 +164,17 @@ if foreign_hermes; then exit 1 fi -# An older mise environment may predate the session report. It belongs to this -# installer, so discard only that environment and let the current stub rebuild it. +# Only the marked wrapper proves the matching mise environment is ours to replace. if installed && ! hermes_prompt_ready; then - echo "Updating Hermes for prompted sessions..." >&2 - mise rm -g "$tool" >/dev/null 2>&1 || true - mise uninstall --all "$tool" >/dev/null 2>&1 || true + if ours; then + echo "Updating Hermes for prompted sessions..." >&2 + mise rm -g "$tool" >/dev/null 2>&1 || true + mise uninstall --all "$tool" >/dev/null 2>&1 || true + else + echo "A Hermes mise environment exists without an Omarchy-owned wrapper." >&2 + echo "Update or remove it explicitly, then run omarchy-install-hermes-cli again." >&2 + exit 1 + fi fi mkdir -p "$HOME/.local/bin" @@ -218,7 +223,7 @@ chmod +x "$HOME/.local/bin/hermes" if [[ $mode == "--now" ]]; then "$HOME/.local/bin/hermes" --version if ! hermes_prompt_ready; then - echo "Hermes installed without the session report Omarchy needs for prompted launches." >&2 + echo "Hermes installed without the interactive seeded sessions Omarchy needs." >&2 exit 1 fi fi diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index 81f3985c..fbd60f31 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -431,8 +431,10 @@ assert_launched() { fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}" for ((index = 0; index < ${#expected[@]}; index++)); do - [[ ${actual[$index]} == ${expected[$index]} ]] || - fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}" + case ${actual[$index]} in + "${expected[$index]}") ;; + *) fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}" ;; + esac done } @@ -462,11 +464,18 @@ assert_launch claude claude --permission-mode auto -- "Review this project" assert_launch codex codex --approve-for-me -- "Review this project" assert_launch crush crush run "Review this project" assert_launch grok grok --permission-mode bypassPermissions -- "Review this project" -assert_launch hermes omarchy-agent-hermes "Review this project" +assert_launch hermes env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=Review this project" assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project" assert_launch copilot copilot --allow-all --interactive "Review this project" pass "agent launcher adapts initial prompts for every supported agent" +literal_hermes_prompt=$' --help !Crash /quit {$(touch must-not-run)}\ntrailing\\ ' +printf '%s\n' "hermes" >"$agent_file" +omarchy-agent-prompt "$literal_hermes_prompt" +assert_launched hermes "binds its literal initial prompt" env -u HERMES_SESSION_SOURCE \ + hermes chat --yolo --tui "--query=$literal_hermes_prompt" +pass "Hermes receives prompted launches as one literal query argument" + assert_bypass pi pi assert_bypass omp omp --auto-approve assert_bypass opencode opencode --auto diff --git a/test/shell.d/hermes-agent-test.sh b/test/shell.d/hermes-agent-test.sh deleted file mode 100755 index 3339df24..00000000 --- a/test/shell.d/hermes-agent-test.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" - -test_tmp=$(mktemp -d) -trap 'rm -rf "$test_tmp"' EXIT - -mock_bin="$test_tmp/bin" -oneshot_log="$test_tmp/oneshot" -resume_log="$test_tmp/resume" -source_log="$test_tmp/source" -setup_log="$test_tmp/setup" -setup_marker="$test_tmp/setup-complete" -mkdir -p "$mock_bin" - -cat >"$mock_bin/hermes" <<'SH' -#!/bin/bash - -if [[ ${1:-} == "setup" ]]; then - printf '%s\0' "$@" >"$HERMES_TEST_SETUP_LOG" - [[ ${HERMES_TEST_SETUP_FAIL:-false} == "false" ]] || exit 43 - touch "$HERMES_TEST_SETUP_MARKER" - exit -fi - -if [[ " $* " == *" --oneshot="* ]]; then - printf '%s\0' "$@" >"$HERMES_TEST_ONESHOT_LOG" - printf '%s' "${HERMES_SESSION_SOURCE:-}" >"$HERMES_TEST_SOURCE_LOG" - - while (( $# )); do - if [[ $1 == "--usage-file" ]]; then - usage=$2 - break - fi - shift - done - - if [[ ${HERMES_TEST_NEEDS_SETUP:-false} == "true" && ! -e $HERMES_TEST_SETUP_MARKER ]]; then - printf '{"session_id":null,"completed":null,"failed":true,"failure":"No inference provider configured. Run hermes model."}\n' >"$usage" - exit 1 - fi - - [[ ${HERMES_TEST_ONESHOT_FAIL:-false} == "false" ]] || exit 42 - if [[ ${HERMES_TEST_USAGE_FAIL:-false} == "false" ]]; then - completed=true - failed=false - [[ ${HERMES_TEST_USAGE_INCOMPLETE:-false} == "false" ]] || completed=false - [[ ${HERMES_TEST_USAGE_FAILED:-false} == "false" ]] || failed=true - printf '{"session_id":"session-123","completed":%s,"failed":%s}\n' "$completed" "$failed" >"$usage" - fi - printf '%s\n' response - exit -fi - -printf '%s\0' "$@" >"$HERMES_TEST_RESUME_LOG" -SH - -chmod +x "$mock_bin/hermes" - -export PATH="$mock_bin:$PATH" -export HERMES_TEST_ONESHOT_LOG="$oneshot_log" -export HERMES_TEST_RESUME_LOG="$resume_log" -export HERMES_TEST_SOURCE_LOG="$source_log" -export HERMES_TEST_SETUP_LOG="$setup_log" -export HERMES_TEST_SETUP_MARKER="$setup_marker" - -sentinel="$test_tmp/hermes-seed-must-stay-literal" -prompt="--help !Crash /quit {!touch $sentinel}"$'\ntrailing\\' -HERMES_SESSION_SOURCE=gateway "$ROOT/bin/omarchy-agent-hermes" "$prompt" >/dev/null - -mapfile -d '' -t oneshot_args <"$oneshot_log" -(( ${#oneshot_args[@]} == 4 )) || fail "Hermes literal seed has four one-shot arguments" -[[ ${oneshot_args[0]} == "--yolo" ]] || fail "Hermes literal seed enables yolo mode" -[[ ${oneshot_args[1]} == "--usage-file" ]] || fail "Hermes literal seed requests the session report" -usage_file=${oneshot_args[2]} -[[ ${oneshot_args[3]} == "--oneshot=$prompt" ]] || fail "Hermes literal seed binds option-looking prompts as data" -[[ ! -e $usage_file ]] || fail "Hermes literal seed removes its session report" -[[ ! -e $sentinel ]] || fail "Hermes literal seed never executes prompt interpolation" -[[ ! -s $source_log ]] || fail "Hermes literal seed preserves native CLI session metadata" - -mapfile -d '' -t resume_args <"$resume_log" -[[ ${resume_args[*]} == "chat --yolo --tui --resume session-123" ]] || - fail "Hermes literal seed resumes the exact completed session" -pass "Hermes sends initial prompts literally and resumes their exact session" - -: >"$resume_log" -if HERMES_TEST_ONESHOT_FAIL=true "$ROOT/bin/omarchy-agent-hermes" failure >/dev/null 2>&1; then - fail "Hermes literal seed reports a failed initial turn" -fi -[[ ! -s $resume_log ]] || fail "Hermes literal seed does not resume a failed initial turn" -pass "Hermes does not resume after a failed initial turn" - -: >"$resume_log" -if HERMES_TEST_USAGE_FAIL=true "$ROOT/bin/omarchy-agent-hermes" missing-session >/dev/null 2>&1; then - fail "Hermes literal seed requires a recorded session ID" -fi -[[ ! -s $resume_log ]] || fail "Hermes literal seed does not guess which session to resume" -pass "Hermes resumes only the session recorded by the initial turn" - -for state in INCOMPLETE FAILED; do - : >"$resume_log" - if env "HERMES_TEST_USAGE_$state=true" "$ROOT/bin/omarchy-agent-hermes" "${state,,}" >/dev/null 2>&1; then - fail "Hermes literal seed rejects a reported ${state,,} initial turn" - fi - [[ ! -s $resume_log ]] || fail "Hermes literal seed does not resume a reported ${state,,} initial turn" -done -pass "Hermes resumes only completed successful initial turns" - -: >"$resume_log" -HERMES_TEST_NEEDS_SETUP=true "$ROOT/bin/omarchy-agent-hermes" setup-first >/dev/null -mapfile -d '' -t setup_args <"$setup_log" -[[ ${setup_args[*]} == "setup" ]] || fail "Hermes runs setup when no inference provider is configured" -mapfile -d '' -t resume_args <"$resume_log" -[[ ${resume_args[*]} == "chat --yolo --tui --resume session-123" ]] || - fail "Hermes replays the prompted turn after setup and resumes it" -pass "Hermes completes first-run setup before replaying the prompt" - -rm -f "$setup_marker" -: >"$resume_log" -if HERMES_TEST_NEEDS_SETUP=true HERMES_TEST_SETUP_FAIL=true "$ROOT/bin/omarchy-agent-hermes" setup-cancelled >/dev/null 2>&1; then - fail "Hermes reports a failed first-run setup" -fi -[[ ! -s $resume_log ]] || fail "Hermes does not resume when first-run setup fails" -pass "Hermes stops when first-run setup does not complete" diff --git a/test/shell.d/hermes-cli-test.sh b/test/shell.d/hermes-cli-test.sh index 39435aa5..83b516e9 100755 --- a/test/shell.d/hermes-cli-test.sh +++ b/test/shell.d/hermes-cli-test.sh @@ -97,8 +97,8 @@ run_installer 1 --check && fail "--check reports Hermes missing before the app i mkdir -p "$test_home/.hermes/hermes-agent/venv/bin" cat >"$test_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH' #!/bin/bash -if [[ ${1:-} == "--help" ]]; then - [[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "--usage-file PATH" +if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then + [[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "--oneshot" else echo "hermes-agent 0.0.0-test" fi @@ -135,12 +135,12 @@ run_installer 0 --now || fail "--now over a foreign hermes command returns succe pass "a foreign hermes command is preserved and satisfies --check" OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 --check && - fail "--check rejects a foreign Hermes without prompted-session reports" + fail "--check rejects a foreign Hermes without native prompted sessions" OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 && - fail "installing refuses a foreign Hermes without prompted-session reports" + fail "installing refuses a foreign Hermes without native prompted sessions" [[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] || fail "an older foreign Hermes command is left untouched" -pass "a foreign Hermes must support prompted-session reports" +pass "a foreign Hermes must support native prompted sessions" # Broken foreign paths are still foreign. They cannot be used, so --check says # so and the installer refuses rather than replacing them. @@ -237,6 +237,16 @@ tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "an older owned Hermes enviro tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "an older owned Hermes environment is uninstalled" pass "reinstalling replaces an older owned Hermes environment" +rm -f "$test_home/.local/bin/hermes" +: >"$mise_log" +OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 && + fail "installing refuses to claim an unmarked Hermes mise environment" +tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' && + fail "an unmarked Hermes mise environment is never removed" +[[ ! -e $test_home/.local/bin/hermes ]] || + fail "an unmarked Hermes mise environment is not given an Omarchy wrapper" +pass "a Hermes mise environment needs wrapper ownership before replacement" + # install/user/mise.sh is sourced by install/user/all.sh through run_logged, # which runs it under `bash -eE` and hands its exit code back to # omarchy-provision-user's `set -euo pipefail`. Everything that finalizes a user @@ -377,8 +387,8 @@ run_ready_check && fail "--check rejects the app's wrapper when its runtime is g cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH' #!/bin/bash -if [[ ${1:-} == "--help" ]]; then - echo "--usage-file PATH" +if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then + echo "--oneshot" else echo "hermes-agent 0.0.0-test" fi