aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/capture-guard
diff options
context:
space:
mode:
Diffstat (limited to '.ai/scripts/capture-guard')
-rwxr-xr-x.ai/scripts/capture-guard55
1 files changed, 55 insertions, 0 deletions
diff --git a/.ai/scripts/capture-guard b/.ai/scripts/capture-guard
new file mode 100755
index 0000000..1958309
--- /dev/null
+++ b/.ai/scripts/capture-guard
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# capture-guard — detect live org-capture buffers visiting a target file
+# before a workflow edits that file on disk.
+#
+# Editing a file on disk while Emacs has an indirect org-capture buffer
+# cloned from it reverts the base buffer underneath the capture, wedging it:
+# the capture can no longer finalize cleanly with C-c C-c, and a freshly-typed
+# item can be lost or written back against post-edit content. inbox-zero
+# Phase D edits ~/org/roam/inbox.org, the file Craig captures into constantly,
+# so it calls this guard first. See claude-rules/emacs.md.
+#
+# Usage: capture-guard [TARGET_FILE] (default ~/org/roam/inbox.org)
+# exit 0 — safe to edit: no Emacs, daemon unreachable, or no capture buffer
+# visits TARGET_FILE.
+# exit 1 — a live capture buffer visits TARGET_FILE; its name(s) printed to
+# stdout, comma-separated.
+#
+# Conservative by construction: any uncertainty (no Emacs, query failure)
+# resolves to "safe," so the guard never blocks a workflow that would have
+# been fine. It only stops the one case it can positively confirm.
+
+set -euo pipefail
+
+TARGET="${1:-$HOME/org/roam/inbox.org}"
+
+# No Emacs to collide with → nothing to guard against.
+command -v emacsclient >/dev/null 2>&1 || exit 0
+emacsclient -e t >/dev/null 2>&1 || exit 0
+
+# Names of capture buffers whose base buffer visits TARGET. file-equal-p
+# normalizes symlinks and ./.. so the match survives path spelling; it also
+# returns nil when TARGET doesn't exist, which collapses to "safe" below.
+lisp='(let ((target (expand-file-name "'"$TARGET"'")))
+ (mapconcat (function buffer-name)
+ (seq-filter
+ (lambda (b)
+ (and (string-prefix-p "CAPTURE" (buffer-name b))
+ (let* ((base (or (buffer-base-buffer b) b))
+ (f (buffer-file-name base)))
+ (and f (file-equal-p f target)))))
+ (buffer-list))
+ ","))'
+
+bufs="$(emacsclient -e "$lisp" 2>/dev/null)" || exit 0
+
+# emacsclient prints an elisp string with surrounding double quotes:
+# "" for none, "CAPTURE-inbox.org,..." for matches. Strip them.
+bufs="${bufs#\"}"
+bufs="${bufs%\"}"
+
+if [ -n "$bufs" ]; then
+ echo "$bufs"
+ exit 1
+fi
+exit 0