aboutsummaryrefslogtreecommitdiff
path: root/hooks/ai-wrap-teardown.sh
diff options
context:
space:
mode:
Diffstat (limited to 'hooks/ai-wrap-teardown.sh')
-rwxr-xr-xhooks/ai-wrap-teardown.sh110
1 files changed, 110 insertions, 0 deletions
diff --git a/hooks/ai-wrap-teardown.sh b/hooks/ai-wrap-teardown.sh
new file mode 100755
index 0000000..ca73ae6
--- /dev/null
+++ b/hooks/ai-wrap-teardown.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+# Stop hook: tear down the ai-term session (or power off) AFTER a wrap-up.
+#
+# wrap-it-up.org drops a sentinel only at the very end of a teardown- or
+# shutdown-mode wrap, once commit+push is verified and the valediction is
+# delivered. This hook fires when Claude stops responding; on every NORMAL
+# stop there is no sentinel, so it is a silent no-op. Only after a wrap that
+# requested teardown does the matching sentinel exist, and only then does this
+# hook act — which is what keeps a routine end-of-turn from killing the
+# session.
+#
+# Decoupling via the Stop hook (rather than an inline workflow step) is what
+# lets the valediction flush before the session dies: teardown kills the very
+# tmux session Claude runs in, so it cannot happen inline.
+#
+# Two sentinels, keyed by the project basename (one ai-term session per
+# project, named aiv-<basename>):
+# /tmp/ai-wrap-teardown-<basename> -> cj/ai-term-quit "<basename>"
+# kill the aiv-<basename> tmux session (takes claude with it), kill the
+# vterm buffer, restore geometry. Defined in .emacs.d/modules/ai-term.el.
+# /tmp/ai-wrap-shutdown-<basename> -> cj/ai-term-shutdown-countdown
+# abort-able 10->1 echo-area countdown, then sudo shutdown now. Shutdown
+# supersedes teardown (killing the buffer is moot if powering off).
+#
+# The sentinel is removed BEFORE the emacsclient call: cj/ai-term-quit kills
+# the tmux session this hook runs inside, so the hook process may not survive
+# the call. Clearing first guarantees the sentinel never lingers to re-fire on
+# a later session in the same project. The emacs daemon is a separate process,
+# so it completes the teardown even when the hook is cut off mid-call.
+#
+# emacsclient absent or daemon unreachable: clear the sentinel and exit 0. The
+# session simply stays up — graceful degradation, never a wedge.
+#
+# Wire in ~/.claude/settings.json (see hooks/settings-snippet.json):
+#
+# "Stop": [
+# { "hooks": [
+# { "type": "command",
+# "command": "~/.claude/hooks/ai-wrap-teardown.sh" } ] } ]
+set -u
+
+payload="$(cat)"
+
+# Stop-hook stdin JSON carries cwd; basename it to the project / aiv- session.
+cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty' 2>/dev/null)"
+[ -z "$cwd" ] && cwd="$PWD"
+proj="$(basename "$cwd")"
+
+teardown_sentinel="/tmp/ai-wrap-teardown-${proj}"
+shutdown_sentinel="/tmp/ai-wrap-shutdown-${proj}"
+
+fire() {
+ # $1 = elisp form. Best-effort: only when emacsclient resolves.
+ command -v emacsclient >/dev/null 2>&1 || return 0
+ emacsclient -e "$1" >/dev/null 2>&1 || true
+}
+
+# A sentinel means wrap-up claimed completion. Re-prove that claim immediately
+# before consuming it: the certificate binds a prior strict check to HEAD, and
+# verify also performs a fresh strict check to catch late writes.
+verify_wrap() {
+ local gate="${GIT_WORKTREE_GATE:-}" detail
+ if [ -z "$gate" ]; then
+ gate="$(command -v git-worktree-gate 2>/dev/null || true)"
+ fi
+ if [ -z "$gate" ] || [ ! -x "$gate" ]; then
+ gate="$HOME/code/rulesets/claude-templates/bin/git-worktree-gate"
+ fi
+ if [ ! -x "$gate" ]; then
+ detail="wrap blocked: git-worktree-gate is unavailable; install rulesets tooling and retry"
+ else
+ detail="$("$gate" verify "$cwd" 2>&1)" && return 0
+ fi
+
+ # Codex and Claude consume different Stop-hook response fields. Codex
+ # command-hook payloads always include model; Claude's do not.
+ if printf '%s' "$payload" | jq -e '.model? != null' >/dev/null 2>&1; then
+ jq -n --arg reason "$detail" \
+ '{continue:false, stopReason:$reason, systemMessage:$reason}'
+ else
+ jq -n --arg reason "$detail" \
+ '{decision:"block", reason:$reason}'
+ fi
+ return 1
+}
+
+consume_certificate() {
+ local dir
+ dir="$(git -C "$cwd" rev-parse --absolute-git-dir 2>/dev/null)" || return 0
+ rm -f "$dir/ai-wrap-clean"
+}
+
+# Shutdown supersedes teardown when both are somehow present.
+if [ -f "$shutdown_sentinel" ]; then
+ verify_wrap || exit 0
+ rm -f "$shutdown_sentinel" "$teardown_sentinel"
+ consume_certificate
+ fire '(cj/ai-term-shutdown-countdown)'
+ exit 0
+fi
+
+if [ -f "$teardown_sentinel" ]; then
+ verify_wrap || exit 0
+ rm -f "$teardown_sentinel"
+ consume_certificate
+ fire "(cj/ai-term-quit \"${proj}\")"
+ exit 0
+fi
+
+exit 0