Skip to content
Open
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
4 changes: 1 addition & 3 deletions bin/omarchy-agent
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 38 additions & 6 deletions bin/omarchy-install-hermes-cli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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
15 changes: 13 additions & 2 deletions test/shell.d/default-agent-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
54 changes: 48 additions & 6 deletions test/shell.d/hermes-cli-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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" \
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"