aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/capture-guard
blob: 19583092cd8dd8d448225bca491bc72632a878cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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