diff options
| -rw-r--r-- | .ai/workflows/INDEX.org | 4 | ||||
| -rw-r--r-- | .ai/workflows/wrap-it-up.org | 54 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/INDEX.org | 4 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/wrap-it-up.org | 54 | ||||
| -rwxr-xr-x | hooks/ai-wrap-teardown.sh | 69 | ||||
| -rw-r--r-- | hooks/settings-snippet.json | 7 | ||||
| -rw-r--r-- | scripts/tests/ai-wrap-teardown-hook.bats | 101 | ||||
| -rw-r--r-- | todo.org | 36 |
8 files changed, 324 insertions, 5 deletions
diff --git a/.ai/workflows/INDEX.org b/.ai/workflows/INDEX.org index c6543ca..eef81df 100644 --- a/.ai/workflows/INDEX.org +++ b/.ai/workflows/INDEX.org @@ -18,8 +18,10 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - =helper-mode.org= — role contract for a helper instance (a second Claude in the same project as a live primary). No manual trigger; the spawn paths route to it, "you are a helper" is the manual fallback. - =first-session.org= — initialize =.ai/= for a brand-new project. - Triggers: "this is a new project", "let's set this project up". Auto-runs if =.ai/sessions/= is empty. -- =wrap-it-up.org= — end-of-session: write summary, archive, commit, push. +- =wrap-it-up.org= — end-of-session: write summary, archive, commit, push, then a phrase-dependent Step 6 teardown. Bare "wrap it up" tears the session down (kills the ai-term buffer + =aiv-<project>= tmux session via a =Stop=-hook sentinel, after the valediction flushes); a "with summary" / "and summarize" wrap keeps the buffer; "and shutdown" gates on being the only live ai-term session, then powers the machine off via an abort-able Emacs countdown. - Triggers: "wrap it up", "that's a wrap", "let's call it a wrap" + - No-teardown triggers: "wrap it up with summary", "wrap it up and summarize" + - Shutdown trigger: "wrap it up and shutdown" - =retrospective.org= — post-mortem after a tough session. - Triggers: "let's do a retrospective", "retrospective time" diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org index 7c66c60..6e89f5f 100644 --- a/.ai/workflows/wrap-it-up.org +++ b/.ai/workflows/wrap-it-up.org @@ -4,7 +4,7 @@ * Overview -This workflow defines the process for ending a Claude Code session cleanly. It finalizes the session record, commits + pushes all work, and provides a warm handoff. +This workflow defines the process for ending a Claude Code session cleanly. It finalizes the session record, commits + pushes all work, and provides a warm handoff. A bare wrap also tears the session down (kills the ai-term buffer + tmux session, restoring geometry); a qualified wrap keeps the buffer, and a shutdown wrap powers the machine off. The teardown variants are set by the trigger phrase (see Teardown mode below) and act only at the very end, in Step 6. Triggered by Craig saying "wrap it up," "that's a wrap," "let's call it a wrap," or similar. @@ -29,6 +29,18 @@ The wrap-up is complete when: The absence of =.ai/session-context.org= is the signal that the last session wrapped up cleanly. Its presence at session start means the previous session was interrupted. +* Teardown mode (set from the trigger phrase) + +The wrap itself — Steps 1 through 5 — is identical in every mode. The trigger phrase only decides what Step 6 does once commit + push and the valediction are done. Resolve the mode from the phrase before starting: + +- *Teardown* (the default) — bare "wrap it up", "that's a wrap", "let's call it a wrap". The full wrap, then Step 6 kills the ai-term buffer + the =aiv-<project>= tmux session (which takes =claude= with it) and restores the saved window geometry. This is Craig's typical end-of-day case. +- *No-teardown* — "wrap it up with summary" or "wrap it up and summarize". The full wrap, but Step 6 leaves the buffer and session intact so the summary stays readable. The explicit qualifier is what opts out of teardown. +- *Shutdown* — "wrap it up and shutdown". The full wrap, then Step 6 gates on this being the only live ai-term session and powers the machine off. Shutdown supersedes teardown (killing the buffer is moot if the box is going down). + +Why teardown waits for Step 6 and runs through a hook, never inline: teardown kills the very tmux session =claude= runs in, so an inline kill would cut the valediction off before it renders. Step 6 instead drops a sentinel after everything else is verified, and the =Stop= hook (=ai-wrap-teardown.sh=) does the actual teardown when this response ends — by which point the valediction has already been delivered. + +This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-quit=, =cj/ai-term-live-count=, =cj/ai-term-shutdown-countdown=) and on the =Stop= hook being wired in =settings.json= (=hooks/settings-snippet.json=). If =emacsclient= or the daemon is unreachable, the sentinel is cleared and the session simply stays up — teardown degrades to a no-op, never a wedge. + * The Workflow ** Step 1: Finalize the Summary @@ -462,6 +474,43 @@ ratio-local uncommitted state. Good session. Talk tomorrow. #+end_example +** Step 6: Session teardown (mode-dependent) + +The last action of the wrap, and only after Step 4's commit + push is verified and the Step 5 valediction is composed. The teardown itself happens when this response ends (via the =Stop= hook), so the valediction always renders first. Act by the mode resolved up front: + +*** No-teardown mode + +Do nothing. The buffer, the =aiv-<project>= tmux session, and =claude= all stay up so the summary stays readable. The wrap is complete. + +*** Teardown mode (default) + +Confirm commit + push succeeded (Exit Criteria 5 — never tear down over unpushed work), then drop the sentinel: + +#+begin_src bash +touch "/tmp/ai-wrap-teardown-$(basename "$PWD")" +#+end_src + +That is the whole step. Don't run any =tmux kill-session=, =emacsclient=, or buffer kill inline — the =Stop= hook reads the sentinel when this response ends and runs =cj/ai-term-quit=, which kills the =aiv-<project>= session (taking =claude= with it), kills the vterm buffer, and restores geometry. The basename of =$PWD= is the key the hook matches, so the sentinel names the session it tears down. + +*** Shutdown mode + +Confirm commit + push succeeded, then evaluate the safety gate *before* committing to the shutdown — never power the box off out from under another live session: + +#+begin_src bash +emacsclient -e '(cj/ai-term-live-count)' +#+end_src + +- *Count > 1* — another ai-term session is alive. ABORT the shutdown. List the other live =aiv-*= sessions, drop *no* sentinel, and tell Craig in the valediction that it fell back to a normal wrap (no poweroff, no teardown). This gate is the load-bearing safety of the whole feature. +- *Count = 1* — this session is the only one. Drop the shutdown sentinel: + + #+begin_src bash + touch "/tmp/ai-wrap-shutdown-$(basename "$PWD")" + #+end_src + + The =Stop= hook fires =cj/ai-term-shutdown-countdown= when this response ends: it re-checks the gate, runs an abort-able 10→1 countdown in the Emacs echo area (=C-g= cancels), then =sudo shutdown now=. Shutdown supersedes teardown — do *not* also drop the teardown sentinel. + +If =emacsclient= isn't resolvable or the daemon is down, the gate can't run — abort the shutdown, fall back to a normal wrap, and say so. Don't power off on an unverifiable gate. + * Common Mistakes to Avoid 1. *Skipping Step 1 (Summary)* — the file becomes the record; an empty Summary makes it hard to scan at catch-up @@ -497,5 +546,8 @@ Before considering wrap-up complete: - [ ] Current branch pushed to ALL remotes (verified with =git remote -v=) - [ ] All other local branches with a tracking upstream pushed to their remote - [ ] Any untracked-upstream branches surfaced for manual =git push -u= +- [ ] Step 6 teardown matches the trigger phrase: no-teardown leaves the buffer; teardown drops only =/tmp/ai-wrap-teardown-<project>=; shutdown gates on =cj/ai-term-live-count= = 1 and drops only =/tmp/ai-wrap-shutdown-<project>= +- [ ] No teardown/shutdown sentinel was dropped before commit + push was verified +- [ ] Shutdown aborted (fell back to normal wrap, logged in the valediction) when another =aiv-*= session was live or the gate couldn't run - [ ] Commit message follows format (no =session:=, no Claude attribution) - [ ] Valediction delivered (brief, specific, warm) diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org index c6543ca..eef81df 100644 --- a/claude-templates/.ai/workflows/INDEX.org +++ b/claude-templates/.ai/workflows/INDEX.org @@ -18,8 +18,10 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - =helper-mode.org= — role contract for a helper instance (a second Claude in the same project as a live primary). No manual trigger; the spawn paths route to it, "you are a helper" is the manual fallback. - =first-session.org= — initialize =.ai/= for a brand-new project. - Triggers: "this is a new project", "let's set this project up". Auto-runs if =.ai/sessions/= is empty. -- =wrap-it-up.org= — end-of-session: write summary, archive, commit, push. +- =wrap-it-up.org= — end-of-session: write summary, archive, commit, push, then a phrase-dependent Step 6 teardown. Bare "wrap it up" tears the session down (kills the ai-term buffer + =aiv-<project>= tmux session via a =Stop=-hook sentinel, after the valediction flushes); a "with summary" / "and summarize" wrap keeps the buffer; "and shutdown" gates on being the only live ai-term session, then powers the machine off via an abort-able Emacs countdown. - Triggers: "wrap it up", "that's a wrap", "let's call it a wrap" + - No-teardown triggers: "wrap it up with summary", "wrap it up and summarize" + - Shutdown trigger: "wrap it up and shutdown" - =retrospective.org= — post-mortem after a tough session. - Triggers: "let's do a retrospective", "retrospective time" diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org index 7c66c60..6e89f5f 100644 --- a/claude-templates/.ai/workflows/wrap-it-up.org +++ b/claude-templates/.ai/workflows/wrap-it-up.org @@ -4,7 +4,7 @@ * Overview -This workflow defines the process for ending a Claude Code session cleanly. It finalizes the session record, commits + pushes all work, and provides a warm handoff. +This workflow defines the process for ending a Claude Code session cleanly. It finalizes the session record, commits + pushes all work, and provides a warm handoff. A bare wrap also tears the session down (kills the ai-term buffer + tmux session, restoring geometry); a qualified wrap keeps the buffer, and a shutdown wrap powers the machine off. The teardown variants are set by the trigger phrase (see Teardown mode below) and act only at the very end, in Step 6. Triggered by Craig saying "wrap it up," "that's a wrap," "let's call it a wrap," or similar. @@ -29,6 +29,18 @@ The wrap-up is complete when: The absence of =.ai/session-context.org= is the signal that the last session wrapped up cleanly. Its presence at session start means the previous session was interrupted. +* Teardown mode (set from the trigger phrase) + +The wrap itself — Steps 1 through 5 — is identical in every mode. The trigger phrase only decides what Step 6 does once commit + push and the valediction are done. Resolve the mode from the phrase before starting: + +- *Teardown* (the default) — bare "wrap it up", "that's a wrap", "let's call it a wrap". The full wrap, then Step 6 kills the ai-term buffer + the =aiv-<project>= tmux session (which takes =claude= with it) and restores the saved window geometry. This is Craig's typical end-of-day case. +- *No-teardown* — "wrap it up with summary" or "wrap it up and summarize". The full wrap, but Step 6 leaves the buffer and session intact so the summary stays readable. The explicit qualifier is what opts out of teardown. +- *Shutdown* — "wrap it up and shutdown". The full wrap, then Step 6 gates on this being the only live ai-term session and powers the machine off. Shutdown supersedes teardown (killing the buffer is moot if the box is going down). + +Why teardown waits for Step 6 and runs through a hook, never inline: teardown kills the very tmux session =claude= runs in, so an inline kill would cut the valediction off before it renders. Step 6 instead drops a sentinel after everything else is verified, and the =Stop= hook (=ai-wrap-teardown.sh=) does the actual teardown when this response ends — by which point the valediction has already been delivered. + +This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-quit=, =cj/ai-term-live-count=, =cj/ai-term-shutdown-countdown=) and on the =Stop= hook being wired in =settings.json= (=hooks/settings-snippet.json=). If =emacsclient= or the daemon is unreachable, the sentinel is cleared and the session simply stays up — teardown degrades to a no-op, never a wedge. + * The Workflow ** Step 1: Finalize the Summary @@ -462,6 +474,43 @@ ratio-local uncommitted state. Good session. Talk tomorrow. #+end_example +** Step 6: Session teardown (mode-dependent) + +The last action of the wrap, and only after Step 4's commit + push is verified and the Step 5 valediction is composed. The teardown itself happens when this response ends (via the =Stop= hook), so the valediction always renders first. Act by the mode resolved up front: + +*** No-teardown mode + +Do nothing. The buffer, the =aiv-<project>= tmux session, and =claude= all stay up so the summary stays readable. The wrap is complete. + +*** Teardown mode (default) + +Confirm commit + push succeeded (Exit Criteria 5 — never tear down over unpushed work), then drop the sentinel: + +#+begin_src bash +touch "/tmp/ai-wrap-teardown-$(basename "$PWD")" +#+end_src + +That is the whole step. Don't run any =tmux kill-session=, =emacsclient=, or buffer kill inline — the =Stop= hook reads the sentinel when this response ends and runs =cj/ai-term-quit=, which kills the =aiv-<project>= session (taking =claude= with it), kills the vterm buffer, and restores geometry. The basename of =$PWD= is the key the hook matches, so the sentinel names the session it tears down. + +*** Shutdown mode + +Confirm commit + push succeeded, then evaluate the safety gate *before* committing to the shutdown — never power the box off out from under another live session: + +#+begin_src bash +emacsclient -e '(cj/ai-term-live-count)' +#+end_src + +- *Count > 1* — another ai-term session is alive. ABORT the shutdown. List the other live =aiv-*= sessions, drop *no* sentinel, and tell Craig in the valediction that it fell back to a normal wrap (no poweroff, no teardown). This gate is the load-bearing safety of the whole feature. +- *Count = 1* — this session is the only one. Drop the shutdown sentinel: + + #+begin_src bash + touch "/tmp/ai-wrap-shutdown-$(basename "$PWD")" + #+end_src + + The =Stop= hook fires =cj/ai-term-shutdown-countdown= when this response ends: it re-checks the gate, runs an abort-able 10→1 countdown in the Emacs echo area (=C-g= cancels), then =sudo shutdown now=. Shutdown supersedes teardown — do *not* also drop the teardown sentinel. + +If =emacsclient= isn't resolvable or the daemon is down, the gate can't run — abort the shutdown, fall back to a normal wrap, and say so. Don't power off on an unverifiable gate. + * Common Mistakes to Avoid 1. *Skipping Step 1 (Summary)* — the file becomes the record; an empty Summary makes it hard to scan at catch-up @@ -497,5 +546,8 @@ Before considering wrap-up complete: - [ ] Current branch pushed to ALL remotes (verified with =git remote -v=) - [ ] All other local branches with a tracking upstream pushed to their remote - [ ] Any untracked-upstream branches surfaced for manual =git push -u= +- [ ] Step 6 teardown matches the trigger phrase: no-teardown leaves the buffer; teardown drops only =/tmp/ai-wrap-teardown-<project>=; shutdown gates on =cj/ai-term-live-count= = 1 and drops only =/tmp/ai-wrap-shutdown-<project>= +- [ ] No teardown/shutdown sentinel was dropped before commit + push was verified +- [ ] Shutdown aborted (fell back to normal wrap, logged in the valediction) when another =aiv-*= session was live or the gate couldn't run - [ ] Commit message follows format (no =session:=, no Claude attribution) - [ ] Valediction delivered (brief, specific, warm) 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" } + ] + } ] } } diff --git a/scripts/tests/ai-wrap-teardown-hook.bats b/scripts/tests/ai-wrap-teardown-hook.bats new file mode 100644 index 0000000..05c49f1 --- /dev/null +++ b/scripts/tests/ai-wrap-teardown-hook.bats @@ -0,0 +1,101 @@ +#!/usr/bin/env bats +# hooks/ai-wrap-teardown.sh — Stop hook that tears down the ai-term session +# (or powers off) after a wrap-up, gated on a sentinel wrap-it-up drops. On a +# normal stop (no sentinel) it is a silent no-op. The emacsclient call is +# stubbed here so the test records the elisp form without a live daemon. + +setup() { + REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" + SCRIPT="$REPO_ROOT/hooks/ai-wrap-teardown.sh" + TMPDIR_T="$(mktemp -d)" + PROJ="proj-$$-$BATS_TEST_NUMBER" # unique so /tmp sentinels don't collide + CWD="$TMPDIR_T/$PROJ" + mkdir -p "$CWD" + TEARDOWN_SENTINEL="/tmp/ai-wrap-teardown-${PROJ}" + SHUTDOWN_SENTINEL="/tmp/ai-wrap-shutdown-${PROJ}" + + # Stub emacsclient on PATH: record the elisp form it was called with. + BIN="$TMPDIR_T/bin" + mkdir -p "$BIN" + EC_LOG="$TMPDIR_T/emacsclient.log" + cat >"$BIN/emacsclient" <<EOF +#!/usr/bin/env bash +# args: -e <form> +shift # drop -e +printf '%s\n' "\$1" >> "$EC_LOG" +EOF + chmod +x "$BIN/emacsclient" +} + +teardown() { + rm -rf "$TMPDIR_T" + rm -f "$TEARDOWN_SENTINEL" "$SHUTDOWN_SENTINEL" +} + +run_hook() { + # invoke with the stubbed emacsclient on PATH, feeding Stop-hook JSON + printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \ + | PATH="$BIN:$PATH" bash "$SCRIPT" +} + +@test "no sentinel: silent no-op, emacsclient never called" { + run run_hook + [ "$status" -eq 0 ] + [ -z "$output" ] + [ ! -f "$EC_LOG" ] +} + +@test "teardown sentinel: calls cj/ai-term-quit with the project basename" { + : > "$TEARDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + grep -q "cj/ai-term-quit \"$PROJ\"" "$EC_LOG" +} + +@test "teardown sentinel is removed after firing" { + : > "$TEARDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + [ ! -f "$TEARDOWN_SENTINEL" ] +} + +@test "shutdown sentinel: calls cj/ai-term-shutdown-countdown" { + : > "$SHUTDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + grep -q "cj/ai-term-shutdown-countdown" "$EC_LOG" +} + +@test "shutdown supersedes teardown when both sentinels exist" { + : > "$TEARDOWN_SENTINEL" + : > "$SHUTDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + grep -q "cj/ai-term-shutdown-countdown" "$EC_LOG" + ! grep -q "cj/ai-term-quit" "$EC_LOG" + [ ! -f "$TEARDOWN_SENTINEL" ] + [ ! -f "$SHUTDOWN_SENTINEL" ] +} + +@test "emacsclient absent: clears the sentinel and exits 0 (graceful)" { + : > "$TEARDOWN_SENTINEL" + status=0 + output="$(printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \ + | PATH="/usr/bin:/bin" bash "$SCRIPT")" || status=$? + [ "$status" -eq 0 ] + [ ! -f "$TEARDOWN_SENTINEL" ] +} + +@test "falls back to PWD basename when cwd is absent from JSON" { + # No cwd key: hook uses $PWD. Run from CWD so basename resolves to PROJ. + : > "$TEARDOWN_SENTINEL" + run env "PATH=$BIN:$PATH" bash -c "cd '$CWD' && printf '{}' | bash '$SCRIPT'" + [ "$status" -eq 0 ] + grep -q "cj/ai-term-quit \"$PROJ\"" "$EC_LOG" +} + +@test "emits no stderr noise on a normal stop" { + err="$(printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \ + | PATH="$BIN:$PATH" bash "$SCRIPT" 2>&1 >/dev/null)" + [ -z "$err" ] +} @@ -47,12 +47,46 @@ CLOSED: [2026-06-23 Tue] :END: Built per the Ready spec: =process-inbox= + =monitor-inbox= + =inbox-zero= merged into one =inbox.org= engine (shared core + process/monitor/roam modes + the interactive =auto inbox zero= =/loop= mode); =triage-intake= and =no-approvals= stay separate. Callers repointed (INDEX, protocols, startup Phase C, wrap-up Step 3), old files deleted, stale-ref grep clean, workflow-integrity + sync-check + full suite green. The fully-unattended =/schedule= cron pass is vNext (see the =[#D]= task above). [[file:docs/inbox-workflow-consolidation-spec.org][spec]]. -** TODO [#B] wrap-it-up teardown + "wrap it up and shutdown" :feature: +** DOING [#B] wrap-it-up teardown + "wrap it up and shutdown" :feature: :PROPERTIES: :CREATED: [2026-06-23 Tue] :END: Two additions to =wrap-it-up.org=, designed by Craig (home, 2026-06-23). Item 1: bare "wrap it up" also tears down the session — kill the =aiv-<proj>= tmux session (takes =claude= with it), kill the vterm buffer, restore geometry. Teardown is the default; "wrap it up with summary" wraps without teardown (keeps the buffer readable). Must fire from a Stop/SessionEnd hook via a sentinel file, decoupled and last, so the valediction flushes before the session dies, and strictly after commit+push is verified. Item 2: "wrap it up and shutdown" → wrap, then a hard blocking gate (abort unless this is the only live =aiv-*= session), then an abort-able 10→1 countdown, then =sudo shutdown now=. Countdown can't run through the Bash tool (stdout buffers — prints all ten at once); needs a detached tty writer or an Emacs =run-at-time= timer. Companion: =cj/ai-term-quit= (and optional =cj/ai-term-live-count=) must live in =.emacs.d/modules/ai-term.el= — route there via inbox-send when building so both sides land together. Open decisions for Craig first: qualifier wording ("with summary" vs "and summarize"), countdown home (tty script vs Emacs timer), session-count mechanism (=tmux ls= / =pgrep claude= / helper). Shared-asset, review-gated. Proposal: [[file:docs/design/2026-06-23-wrap-teardown-shutdown-proposal.org][proposal]]. From home 2026-06-23. +*** 2026-06-23 Tue @ 23:31:59 -0400 Built the rulesets side; companion routed to .emacs.d +Craig's three decisions (2026-06-23): non-destructive qualifier is *both* "with summary" and "and summarize"; countdown is an Emacs =run-at-time= timer; the gate uses =cj/ai-term-live-count=. Built and pushed: =hooks/ai-wrap-teardown.sh= (Stop hook, sentinel-gated, 8 bats tests green), =hooks/settings-snippet.json= Stop wiring, =wrap-it-up.org= Teardown-mode section + Step 6 + checklist, INDEX trigger update. Architecture: both teardown and shutdown fire from the Stop hook via a basename-keyed sentinel (=/tmp/ai-wrap-teardown-<proj>=, =/tmp/ai-wrap-shutdown-<proj>=) dropped only at the end of Step 6 after commit+push, so the valediction flushes first. No bin script — the gate/countdown/teardown are all =emacsclient= calls into the companion. Companion spec (=cj/ai-term-quit=, =cj/ai-term-live-count=, =cj/ai-term-shutdown-countdown=) routed to .emacs.d via inbox-send. Remaining: .emacs.d lands the three functions, then the manual end-to-end validation below. Task stays DOING until both sides verify. + +*** TODO Manual testing and validation :test: +What we're verifying: the wrap-teardown + shutdown feature end to end, once =.emacs.d/modules/ai-term.el= has the three companion functions and the =Stop= hook is installed (=make install-hooks= + the =settings-snippet.json= Stop block in =~/.claude/settings.json=). These need a live Emacs daemon + tmux + an =aiv-<proj>= ai-term session; they can't be driven from a script. + +**** Bare "wrap it up" tears down after the valediction +What we're verifying: teardown is the default and fires only after the valediction renders. +- In an ai-term =aiv-<proj>= session, say "wrap it up". +- Watch the wrap run (summary, archive, commit, push, valediction). +Expected: the valediction renders in full first, THEN the vterm buffer + =aiv-<proj>= tmux session + =claude= all disappear and the saved window geometry is restored. =/tmp/ai-wrap-teardown-<proj>= does not linger afterward. + +**** "wrap it up with summary" / "and summarize" keeps the buffer +What we're verifying: the explicit qualifier opts out of teardown. +- Say "wrap it up with summary" (then, separately, "wrap it up and summarize"). +Expected: the wrap completes (commit + push + archive), but the buffer and session stay up and readable. No teardown. + +**** "wrap it up and shutdown" aborts when another session is live +What we're verifying: the safety gate refuses to power off out from under another session. +- Start a second =aiv-*= ai-term session in another project. +- In the first, say "wrap it up and shutdown". +Expected: the wrap completes but the shutdown is refused; the other live =aiv-*= session is listed, and the valediction says it fell back to a normal wrap. No poweroff, no teardown, no sentinel dropped. + +**** "wrap it up and shutdown" as the sole session powers off (cancellable) +What we're verifying: the happy path and the abort window. +- With this as the only =aiv-*= session, say "wrap it up and shutdown". +- During the countdown, first run it once and cancel with =C-g=; then run it again and let it complete. (Stub =sudo shutdown now= to an echo while validating so the box doesn't actually power off.) +Expected: after commit + push, a 10→1 countdown renders one-per-second in the Emacs echo area; =C-g= cancels it cleanly (no shutdown); letting it finish fires =shutdown=. + +**** Teardown never precedes a verified push +What we're verifying: no sentinel is dropped before commit + push succeeds. +- Trigger a teardown wrap in a state where the push would fail (e.g. temporarily point the remote somewhere unreachable). +Expected: the wrap surfaces the push failure and stops; no =/tmp/ai-wrap-*-<proj>= sentinel is created, so no teardown fires. + ** DONE [#C] inbox-zero: delete empty roam entries on triage :feature: CLOSED: [2026-06-23 Tue] :PROPERTIES: |
