diff --git a/Makefile b/Makefile index 6d0c3f7..2d57a63 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SCRIPTS = preflight.sh provision.sh setup-user.sh sync-code.sh destroy.sh \ - files/remote-setup.sh files/claude-notify.tmpl + files/remote-setup.sh files/claude-notify.tmpl files/codex-notify.tmpl .PHONY: check residue preflight provision setup sync all destroy diff --git a/README.md b/README.md index 224b6e7..1b59b61 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,8 @@ Optional: enable **Tailscale Serve** on your tailnet for HTTPS preview URLs — - **`claude` in any repo** pushes to your phone only when it's genuinely waiting on you — idle after handing control back, or blocked on a decision — plus turn failures. It deliberately does *not* ping on every completed turn (that floods during autonomous multi-step work), and stays - quiet while you're active in tmux. + quiet while you're active in tmux. Codex (when `INSTALL_CODEX=1`) pushes the same way when it + hands a turn back — it exposes only a turn-complete event, but that's its genuine "over to you" moment. ## Rebuild and teardown diff --git a/files/codex-notify.tmpl b/files/codex-notify.tmpl new file mode 100644 index 0000000..65b71d6 --- /dev/null +++ b/files/codex-notify.tmpl @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# OpenAI Codex CLI notify program -> Pushover push notifications. +# Wired via `notify = [""]` in ~/.codex/config.toml. Codex passes +# the event JSON as the FINAL ARGV argument (not stdin), and currently emits +# only "agent-turn-complete" — there is no idle/error/needs-input event to +# filter on (unlike Claude Code's hooks), so this pushes when Codex hands a +# turn back to you. That's the genuine "over to you" moment; Codex does not +# flood with intermediate stops. Presence-suppressed while you're active in the +# tmux session, exactly like the Claude hook. +# Rendered from codex-notify.tmpl by setup-user.sh (placeholders from secrets.env). +PO_TOKEN="__PUSHOVER_TOKEN__" +PO_USER="__PUSHOVER_USER__" +PRESENCE_WINDOW=60 + +# Codex hands the payload as the last argument; fall back to stdin if a future +# version ever switches, so we don't go silently dead. +payload="${1:-}" +[ -z "$payload" ] && payload=$(cat 2>/dev/null) +[ -z "$payload" ] && exit 0 + +type=$(jq -r '.type // empty' <<<"$payload" 2>/dev/null) +[ "$type" = "agent-turn-complete" ] || exit 0 + +# Codex's payload carries the project dir and the message it ended on. +proj=$(jq -r '.cwd // empty' <<<"$payload" 2>/dev/null); proj=$(basename "${proj:-$PWD}") +body=$(jq -r '."last-assistant-message" // empty' <<<"$payload" 2>/dev/null | tr '\n\t' ' ' | sed 's/ */ /g' | head -c 280) +body=${body:-"Turn complete — over to you"} + +# Presence check: if a tmux client on this session saw activity in the last +# $PRESENCE_WINDOW seconds, you're at the terminal — stay quiet. +if [ -n "$TMUX" ] && command -v tmux >/dev/null 2>&1; then + sess=$(tmux display-message -p '#{session_name}' 2>/dev/null) + last=$(tmux list-clients -t "$sess" -F '#{client_activity}' 2>/dev/null | sort -rn | head -1) + if [ -n "$last" ] && [ $(( $(date +%s) - last )) -lt "$PRESENCE_WINDOW" ]; then + exit 0 + fi +fi + +# The split literal keeps the render sed from rewriting this guard (see the +# Claude hook for the full explanation). +if [ -n "$PO_TOKEN" ] && [ "$PO_TOKEN" != "__PUSHOVER""_TOKEN__" ]; then + curl -s -o /dev/null --max-time 5 \ + --form-string "token=$PO_TOKEN" --form-string "user=$PO_USER" \ + --form-string "title=Codex · $proj" --form-string "message=$body" \ + --form-string "priority=1" --form-string "sound=intermission" \ + https://api.pushover.net/1/messages.json || true +fi +exit 0 diff --git a/files/remote-setup.sh b/files/remote-setup.sh index f3581fe..e3c15e4 100755 --- a/files/remote-setup.sh +++ b/files/remote-setup.sh @@ -64,6 +64,18 @@ if [ "$(cat "$S/install-codex" 2>/dev/null)" = 1 ]; then command -v bwrap >/dev/null \ || sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install bubblewrap >/dev/null 2>&1 \ || echo "WARN: bubblewrap install failed (codex falls back to a bundled copy)" + # Phone notifications (same Pushover pipe as Claude): Codex calls a `notify` + # program on agent-turn-complete. Wire it via config.toml's root `notify` key, + # which per TOML must precede any [tables] — so prepend it; idempotent. + install -m 700 "$S/codex-notify" ~/.local/bin/codex-notify + mkdir -p ~/.codex + if ! grep -q '^notify' ~/.codex/config.toml 2>/dev/null; then + if [ -f ~/.codex/config.toml ]; then + sed -i "1i notify = [\"$HOME/.local/bin/codex-notify\"]" ~/.codex/config.toml + else + echo "notify = [\"$HOME/.local/bin/codex-notify\"]" > ~/.codex/config.toml + fi + fi fi echo "== git identity ==" diff --git a/setup-user.sh b/setup-user.sh index 4f4e332..15fe706 100755 --- a/setup-user.sh +++ b/setup-user.sh @@ -37,6 +37,9 @@ cp files/config.fish files/tmux.conf files/fnm.fish files/remote-setup.sh "$stag sed -e "s|__PUSHOVER_TOKEN__|${PUSHOVER_TOKEN:-}|g" \ -e "s|__PUSHOVER_USER__|${PUSHOVER_USER:-}|g" \ files/claude-notify.tmpl > "$staging/claude-notify" +sed -e "s|__PUSHOVER_TOKEN__|${PUSHOVER_TOKEN:-}|g" \ + -e "s|__PUSHOVER_USER__|${PUSHOVER_USER:-}|g" \ + files/codex-notify.tmpl > "$staging/codex-notify" sed -e "s|__DEVBOX_NAME__|$DEVBOX_NAME|g" files/vite-hosts.fish > "$staging/vite-hosts.fish" sed -e "s|__DEV_USER__|$DEV_USER|g" files/claude-settings.json > "$staging/claude-settings.json" printf '%s' "${GIT_NAME:-}" > "$staging/git-name"