aboutsummaryrefslogtreecommitdiff
path: root/hooks
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 23:34:18 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 23:34:18 -0400
commitf87f59cc9eb1dd492be5b55870271d67245c1fdd (patch)
tree18bab578f4026f9f85878bd0bfc53d147eab470a /hooks
parente5aab199cd4c83f357ff5190139ddf2994ac28a3 (diff)
downloadrulesets-f87f59cc9eb1dd492be5b55870271d67245c1fdd.tar.gz
rulesets-f87f59cc9eb1dd492be5b55870271d67245c1fdd.zip
feat(wrap): add session teardown and shutdown to wrap-it-up
A bare "wrap it up" now tears the session down after the valediction: it kills the ai-term buffer and the aiv-<project> tmux session (which takes claude with it) and restores geometry. "wrap it up with summary" or "and summarize" keeps the buffer. "wrap it up and shutdown" gates on this being the only live ai-term session, then powers the machine off through an abort-able Emacs countdown. Teardown can't run inline because it kills the session claude runs in, so the valediction would never flush. Step 6 instead drops a basename-keyed sentinel after commit+push is verified, and a new Stop hook (ai-wrap-teardown.sh) does the teardown when the response ends, by which point the valediction has rendered. The hook is a no-op on every normal stop because the sentinel only exists after a teardown wrap. The runtime lives in .emacs.d/modules/ai-term.el (cj/ai-term-quit, cj/ai-term-live-count, cj/ai-term-shutdown-countdown), and the rulesets side calls it via emacsclient. I routed that companion to .emacs.d, so the feature is end-to-end once it lands. The hook has 8 bats tests. The live teardown and shutdown paths are a manual checklist in todo.org. Built from the proposal. I went with both summary qualifiers, the Emacs-timer countdown, and the live-count gate. Claude-Session: https://claude.ai/code/session_017PtX1nt1rtYVATuzmzBS4f
Diffstat (limited to 'hooks')
-rwxr-xr-xhooks/ai-wrap-teardown.sh69
-rw-r--r--hooks/settings-snippet.json7
2 files changed, 76 insertions, 0 deletions
diff --git a/hooks/ai-wrap-teardown.sh b/hooks/ai-wrap-teardown.sh
new file mode 100755
index 0000000..6133075
--- /dev/null
+++ b/hooks/ai-wrap-teardown.sh
@@ -0,0 +1,69 @@
+#!/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
+
+# Stop-hook stdin JSON carries cwd; basename it to the project / aiv- session.
+cwd="$(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
+}
+
+# Shutdown supersedes teardown when both are somehow present.
+if [ -f "$shutdown_sentinel" ]; then
+ rm -f "$shutdown_sentinel" "$teardown_sentinel"
+ fire '(cj/ai-term-shutdown-countdown)'
+ exit 0
+fi
+
+if [ -f "$teardown_sentinel" ]; then
+ rm -f "$teardown_sentinel"
+ fire "(cj/ai-term-quit \"${proj}\")"
+ exit 0
+fi
+
+exit 0
diff --git a/hooks/settings-snippet.json b/hooks/settings-snippet.json
index a5f9d9c..0f0e784 100644
--- a/hooks/settings-snippet.json
+++ b/hooks/settings-snippet.json
@@ -30,6 +30,13 @@
{ "type": "command", "command": "~/.claude/hooks/destructive-bash-confirm.py" }
]
}
+ ],
+ "Stop": [
+ {
+ "hooks": [
+ { "type": "command", "command": "~/.claude/hooks/ai-wrap-teardown.sh" }
+ ]
+ }
]
}
}