diff --git a/bin/omarchy-agent b/bin/omarchy-agent index 426ea85a..05f1e1a1 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -87,10 +87,8 @@ 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") + command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt") else command=(hermes --yolo) fi diff --git a/bin/omarchy-install-hermes-cli b/bin/omarchy-install-hermes-cli index 0c2e526e..5b6a3598 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 } +# 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" chat --help 2>/dev/null) && + grep -qF -- '--oneshot' <<<"$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,34 @@ 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 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 + 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 +# Only the marked wrapper proves the matching mise environment is ours to replace. +if installed && ! hermes_prompt_ready; then + 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" rm -f "$HOME/.local/bin/hermes" @@ -194,4 +222,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 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 5b4512f1..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,10 +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 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 @@ -474,6 +484,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" diff --git a/test/shell.d/hermes-cli-test.sh b/test/shell.d/hermes-cli-test.sh index 88319dc9..83b516e9 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:-} == "chat" && ${2:-} == "--help" ]]; then + [[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "--oneshot" +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 native prompted sessions" +OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 && + 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 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. printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes" @@ -211,6 +230,23 @@ 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" + +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 @@ -349,8 +385,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:-} == "chat" && ${2:-} == "--help" ]]; then + echo "--oneshot" +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"