diff options
Diffstat (limited to 'claude-templates')
57 files changed, 592 insertions, 131 deletions
diff --git a/claude-templates/.ai/notes.org b/claude-templates/.ai/notes.org index 42ea8df..c16863b 100644 --- a/claude-templates/.ai/notes.org +++ b/claude-templates/.ai/notes.org @@ -1,5 +1,5 @@ #+TITLE: Claude Code Notes - [Project Name] -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: [Date] * About This File diff --git a/claude-templates/.ai/protocols.org b/claude-templates/.ai/protocols.org index 5e18ab9..5cd69d4 100644 --- a/claude-templates/.ai/protocols.org +++ b/claude-templates/.ai/protocols.org @@ -1,5 +1,5 @@ #+TITLE: Claude Code Protocols -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2025-11-05 * About This File @@ -367,6 +367,12 @@ Craig's shell aliases =ls= to =exa=, which prints nothing to non-TTY pipes (e.g. - Applies to =ls -la=, =ls -t=, glob expansions piped through =ls=, and any =ls= invocation whose output gets read programmatically. - Symptom if forgotten: the Bash tool returns empty output and you mistakenly conclude the directory is empty. +*** zsh does not word-split unquoted variables +The Bash tool runs zsh, which (unlike bash) does not split an unquoted =$var= on whitespace. =chrome $urls= passes all the space-joined URLs as one malformed argument. + +- Loop over the values, use an array, or force the split with =${=var}=. +- Symptom if forgotten: a command that "works in bash" gets one garbled argument and fails, often silently (from the takuzu session, 2026-07-11). + ** Miscellaneous Information - Craig currently lives in New Orleans, LA - Craig's phone number: 510-316-9357 @@ -406,14 +412,27 @@ Full usage: =notify --help= or see =~/.local/bin/notify= - =atq= - list all scheduled alarms - =atrm [number]= - remove an alarm by its queue number -** Paging Craig — desktop vs. away from the machine +** Paging Craig — the agent pager -"Page me" has two channels; pick by where Craig is. +"Page me" has two channels; pick by where Craig is. Both work from any agent runtime — nothing here is Claude-specific. - *At his laptop/desktop* — desktop =notify ... --persist= (above). It reaches him on the machine and stays up until dismissed. -- *Away from his laptop/desktop* — page his phone over Signal via the *signal-mcp* tool =send_message_to_user=, addressed to Craig's account UUID =b1b5601e-6126-47f8-afaa-0a59f5188fde= (his primary number reads as unregistered in Signal's directory — never page a phone number). The message goes out from the dedicated pager account (+15045173983) and fires a normal mobile push. This is the live cross-device path, verified working 2026-06-30. -Do *not* use the old =page-signal= shell script — it was removed from the rulesets canonical 2026-06-12 and its =~/.local/bin/page-signal= symlink no longer exists. The signal-mcp tool is the only supported Signal path; =notify --persist= is the only supported desktop path. + #+begin_src bash + notify info "Title" "Message" --persist + #+end_src + +- *Away from his laptop/desktop* — page his phone over Signal with the *agent pager*: + + #+begin_src bash + agent-page "Message for Craig's phone" + #+end_src + + =agent-page= (in =~/.local/bin= via the rulesets install) sends from the dedicated pager identity (+15045173983, registered in velox's signal-cli) to Craig's Signal account UUID, firing a normal mobile push. On velox it sends directly; on any other tailnet machine it ssh-relays the send to velox. Verified end to end 2026-07-13. Never page Craig's phone *number* — it reads as unregistered in Signal's directory; the script already targets the UUID. + + Caveats: velox must be up and on the tailnet (the script says so and names the desktop fallback when the relay fails), and the signal-cli account wants a periodic =receive= — both tracked on the rulesets Signal-pager task, which owns the full runbook. + +On velox, Claude sessions may also have the *signal-mcp* tool (=send_message_to_user=, same pager identity) — fine to use there, but it exists only in velox's local MCP config, so =agent-page= is the portable habit. Do *not* use the old =page-signal= shell script (removed 2026-06-12). * Session Protocols diff --git a/claude-templates/.ai/references/calendar-reference.org b/claude-templates/.ai/references/calendar-reference.org index b44c0f1..5791b08 100644 --- a/claude-templates/.ai/references/calendar-reference.org +++ b/claude-templates/.ai/references/calendar-reference.org @@ -1,5 +1,5 @@ #+TITLE: Calendar Reference -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings Tool recipes, authentication, and credentials for Craig's calendar setup. Three access methods, in order of preference. diff --git a/claude-templates/.ai/scripts/lint-org.el b/claude-templates/.ai/scripts/lint-org.el index 90b1b1d..55727ef 100644 --- a/claude-templates/.ai/scripts/lint-org.el +++ b/claude-templates/.ai/scripts/lint-org.el @@ -2,16 +2,19 @@ ;; ;; Usage: ;; emacs --batch -q -l lint-org.el FILE.org [FILE.org ...] +;; report only (the default) — categorize without modifying the file. +;; A linter reports, it doesn't write; mutation requires --fix. +;; --check is accepted as an explicit alias of this default. +;; +;; emacs --batch -q -l lint-org.el --fix FILE.org [FILE.org ...] ;; apply mechanical fixes in place, emit judgment items on stdout for the ;; command layer to walk ;; -;; emacs --batch -q -l lint-org.el --check FILE.org [FILE.org ...] -;; report only — categorize without modifying the file -;; -;; emacs --batch -q -l lint-org.el --followups-file=PATH FILE.org +;; emacs --batch -q -l lint-org.el --fix --followups-file=PATH FILE.org ;; apply mechanical fixes; if any judgment items remain, append them to ;; PATH as an org section dated today. Used by wrap-it-up to defer the ;; judgment walk to the next morning's review without blocking the wrap. +;; (--followups-file only writes in --fix mode.) ;; ;; Mechanical categories (auto-fixed): ;; item-number add [@N] directive to drifted bullets @@ -66,7 +69,9 @@ Each plist has :kind (mechanical-fixed | judgment), :line, :checker, :msg. Mechanical entries from --check mode also carry :preview t.") (defvar lo-check-only nil - "Non-nil means run in report-only mode — no buffer writes.") + "Non-nil means run in report-only mode — no buffer writes. +The CLI defaults this to t (a linter reports, it doesn't write); +`--fix' is what enables writes on a command-line run.") (defvar lo-current-file nil "Path of the file currently being processed.") (defvar lo-followups-file nil @@ -355,24 +360,40 @@ logical row, matching wrap-org-table.el's grouping." (defun lo--check-tables () "Scan the current buffer for org tables violating the table standard. -Emits one judgment item per violating table." +Emits one judgment item per violating table. Pipe-led lines inside +#+begin_/#+end_ blocks are content (ASCII art, shell pipes), not tables, +and are skipped — the same block rule `wot-process-file' applies." (save-excursion (goto-char (point-min)) - (while (re-search-forward "^[ \t]*|" nil t) - (let ((start-line (line-number-at-pos)) - (lines nil)) - (beginning-of-line) - (while (and (not (eobp)) (looking-at "[ \t]*|")) - (push (buffer-substring-no-properties (line-beginning-position) - (line-end-position)) - lines) + (let ((in-block nil)) ; the open block's type, e.g. "example" — nil outside + (while (not (eobp)) + (cond + ;; Type-matched close only: literal #+end_src quoted inside an + ;; example block must not clear the flag (see wot-process-file). + ((and (not in-block) + (looking-at "^[ \t]*#\\+begin_\\([^ \t\n]+\\)")) + (setq in-block (downcase (match-string 1))) + (forward-line 1)) + ((and in-block + (looking-at-p (format "^[ \t]*#\\+end_%s\\([ \t]\\|$\\)" + (regexp-quote in-block)))) + (setq in-block nil) (forward-line 1)) - (let ((violations (lo--table-violations (nreverse lines)))) - (when violations - (lo--emit-judgment - 'org-table-standard start-line - (format "table violates the org-table standard: %s — wrap-org-table.el reflows it" - (string-join violations "; "))))))))) + ((and (not in-block) (looking-at-p "^[ \t]*|")) + (let ((start-line (line-number-at-pos)) + (lines nil)) + (while (and (not (eobp)) (looking-at "[ \t]*|")) + (push (buffer-substring-no-properties (line-beginning-position) + (line-end-position)) + lines) + (forward-line 1)) + (let ((violations (lo--table-violations (nreverse lines)))) + (when violations + (lo--emit-judgment + 'org-table-standard start-line + (format "table violates the org-table standard: %s — wrap-org-table.el reflows it" + (string-join violations "; "))))))) + (t (forward-line 1))))))) ;;; --------------------------------------------------------------------------- ;;; level-2 dated-header check (claude-rules/todo-format.md) @@ -677,6 +698,13 @@ After printing, also append judgments to `lo-followups-file' when set." ;;; CLI (defun lo-main () + ;; Report-only is the CLI default; --fix is the only way a command-line run + ;; writes to disk. The old mutate-by-default reformatted five files in one + ;; pass before anyone confirmed anything (work project, 2026-07-09). + (setq lo-check-only t) + (when (member "--fix" command-line-args-left) + (setq lo-check-only nil) + (setq command-line-args-left (delete "--fix" command-line-args-left))) (when (member "--check" command-line-args-left) (setq lo-check-only t) (setq command-line-args-left (delete "--check" command-line-args-left))) @@ -688,7 +716,7 @@ After printing, also append judgments to `lo-followups-file' when set." (setq command-line-args-left (delete followups command-line-args-left)))) (if (null command-line-args-left) (progn - (princ "Usage: emacs --batch -q -l lint-org.el [--check] [--followups-file=PATH] FILE.org ...\n") + (princ "Usage: emacs --batch -q -l lint-org.el [--fix] [--check] [--followups-file=PATH] FILE.org ...\n") (kill-emacs 1)) (let ((files command-line-args-left)) (setq command-line-args-left nil) @@ -707,7 +735,7 @@ this file without firing the CLI dispatch — under `ert-run-tests-batch-and-exi the trailing args are things like `-f ert-run-tests-batch-and-exit'." (and command-line-args-left (cl-every (lambda (a) - (cond ((member a '("--check")) t) + (cond ((member a '("--check" "--fix")) t) ((string-prefix-p "--followups-file=" a) t) ((string-prefix-p "-" a) nil) (t (file-readable-p a)))) diff --git a/claude-templates/.ai/scripts/task-review-staleness.sh b/claude-templates/.ai/scripts/task-review-staleness.sh index ed43712..50e0257 100755 --- a/claude-templates/.ai/scripts/task-review-staleness.sh +++ b/claude-templates/.ai/scripts/task-review-staleness.sh @@ -19,9 +19,14 @@ # deeper headings, and cookie-less headings are not review units. # # A task is stale (count mode), and sorts oldest (list mode), when its -# :LAST_REVIEWED: property is missing or unparseable (NIL sorts first), or -# when its age strictly exceeds the threshold (age > N days; age == N is -# still fresh). +# :LAST_REVIEWED: property is missing (NIL sorts first) or when its age +# strictly exceeds the threshold (age > N days; age == N is still fresh). +# +# :LAST_REVIEWED: accepts a bare date (2026-07-09) or an org-native +# timestamp ([2026-07-09 Thu] or <2026-07-09 Thu ...>); both normalize to +# the ISO date. A value that is present but parses to neither is a data +# error: the script warns loudly to stderr (file:line:value) and leaves it +# out of the stale count rather than silently treating it as never-reviewed. set -euo pipefail @@ -46,7 +51,7 @@ num="$2" # only the property drawer between a qualifying heading and the next heading # is scanned. extract_tasks() { - awk ' + awk -v fname="$todo_file" ' function flush() { if (in_task) printf "%s\t%s\t%s\n", hline, (have_lr ? lr : "NONE"), heading } @@ -65,7 +70,21 @@ extract_tasks() { v = $0 sub(/^[ \t]*:LAST_REVIEWED:[ \t]*/, "", v) sub(/[ \t]*$/, "", v) - lr = v; have_lr = 1 + raw = v + # Accept org-native stamps: strip a leading [ or < (inactive/active + # timestamp bracket) and take the leading ISO date, so [2026-07-09 Thu] + # and 2026-07-09 both normalize to 2026-07-09. + sub(/^[[<]/, "", v) + if (match(v, /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/)) { + lr = substr(v, RSTART, RLENGTH); have_lr = 1 + } else { + # Present but unparseable — a data error, not "never reviewed". Warn + # loudly (file:line:value) instead of silently folding it into the + # stale count, where a re-review in the same bad format would never + # drop the number and nothing would explain why. + printf "task-review-staleness: %s:%d: unparseable :LAST_REVIEWED: %s (expected YYYY-MM-DD or [YYYY-MM-DD Day])\n", fname, NR, raw > "/dev/stderr" + lr = "INVALID"; have_lr = 1 + } next } END { flush() } @@ -98,9 +117,16 @@ while IFS=$'\t' read -r hline value heading; do continue fi - # Unparseable date → treat as NIL (stale). + # Malformed stamp: extract_tasks already warned. A data error is not + # "never reviewed", so it stays out of the stale count. + if [ "$value" = "INVALID" ]; then + continue + fi + + # A shape-valid but impossible date (e.g. 2026-13-45) slips past the awk + # regex; warn and skip rather than silently counting it. if ! rev_epoch=$(date -d "$value" +%s 2>/dev/null); then - count=$((count + 1)) + echo "task-review-staleness: $todo_file: unparseable :LAST_REVIEWED: $value" >&2 continue fi diff --git a/claude-templates/.ai/scripts/tests/lint-org-cli.bats b/claude-templates/.ai/scripts/tests/lint-org-cli.bats index d457696..b9faef6 100644 --- a/claude-templates/.ai/scripts/tests/lint-org-cli.bats +++ b/claude-templates/.ai/scripts/tests/lint-org-cli.bats @@ -20,6 +20,24 @@ teardown() { [[ "$output" == *"lint-org: file="* ]] } +@test "lint-org.el default invocation is report-only — file untouched" { + # bare #+begin_src is a mechanical fix (→ #+begin_example) that the old + # default applied on disk; a linter reports, it doesn't write + printf '* H\n\n#+begin_src\nx\n#+end_src\n' > "$TMPFILE" + before="$(cat "$TMPFILE")" + run emacs --batch -q -l "$SCRIPTS_DIR/lint-org.el" "$TMPFILE" + [ "$status" -eq 0 ] + [[ "$output" == *"would-fix"* ]] + [ "$(cat "$TMPFILE")" = "$before" ] +} + +@test "lint-org.el --fix applies mechanical fixes on disk" { + printf '* H\n\n#+begin_src\nx\n#+end_src\n' > "$TMPFILE" + run emacs --batch -q -l "$SCRIPTS_DIR/lint-org.el" --fix "$TMPFILE" + [ "$status" -eq 0 ] + grep -q '#+begin_example' "$TMPFILE" +} + @test "wrap-org-table.el loads and runs without -L on the load path" { run emacs --batch -q -l "$SCRIPTS_DIR/wrap-org-table.el" --width=120 "$TMPFILE" [ "$status" -eq 0 ] diff --git a/claude-templates/.ai/scripts/tests/task-review-staleness.bats b/claude-templates/.ai/scripts/tests/task-review-staleness.bats index 488b023..79aad79 100644 --- a/claude-templates/.ai/scripts/tests/task-review-staleness.bats +++ b/claude-templates/.ai/scripts/tests/task-review-staleness.bats @@ -49,6 +49,16 @@ task_unreviewed() { printf '** %s [#%s] %s\nBody.\n\n' "$keyword" "$prio" "$title" >> "$TODO" } +# Emit a qualifying task whose LAST_REVIEWED is an org-native inactive +# timestamp — [YYYY-MM-DD Day] — matching the CREATED:/CLOSED: cookies that +# sit in the same drawer. The date is derived from an ISO date via `date`. +task_reviewed_org() { + local keyword="$1" prio="$2" title="$3" isodate="$4" + local org="[$(date -d "$isodate" '+%F %a')]" + printf '** %s [#%s] %s\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\nBody.\n\n' \ + "$keyword" "$prio" "$title" "$org" >> "$TODO" +} + # ---- Normal cases ---------------------------------------------------- @test "staleness: empty file reports zero" { @@ -85,6 +95,20 @@ task_unreviewed() { [ "$output" = "2" ] } +@test "staleness: org-native bracketed LAST_REVIEWED parses — recent is fresh" { + task_reviewed_org TODO A "Reviewed five days ago, org stamp" "$D5" + run bash "$SCRIPT" "$TODO" 30 + [ "$status" -eq 0 ] + [ "$output" = "0" ] +} + +@test "staleness: org-native bracketed LAST_REVIEWED parses — old is stale" { + task_reviewed_org TODO A "Reviewed forty days ago, org stamp" "$D40" + run bash "$SCRIPT" "$TODO" 30 + [ "$status" -eq 0 ] + [ "$output" = "1" ] +} + # ---- Boundary cases -------------------------------------------------- @test "staleness: age exactly equal to threshold is fresh" { @@ -136,9 +160,23 @@ task_unreviewed() { [ "$output" = "0" ] } -@test "staleness: malformed LAST_REVIEWED is treated as stale" { +@test "staleness: malformed LAST_REVIEWED warns to stderr and is not counted" { task_reviewed TODO A "Bad date" "not-a-date" - run bash "$SCRIPT" "$TODO" 30 + # stdout carries only the count — the malformed stamp is not folded in. + run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null" + [ "$status" -eq 0 ] + [ "$output" = "0" ] + # stderr carries the loud warning naming the offending value. + run bash -c "bash '$SCRIPT' '$TODO' 30 2>&1 1>/dev/null" + [ "$status" -eq 0 ] + [[ "$output" == *"not-a-date"* ]] + [[ "$output" == *"LAST_REVIEWED"* ]] +} + +@test "staleness: malformed stamp is excluded while real stale tasks still count" { + task_reviewed TODO A "Real stale" "$D40" + task_reviewed TODO B "Broken stamp" "garbage" + run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null" [ "$status" -eq 0 ] [ "$output" = "1" ] } @@ -161,6 +199,17 @@ task_unreviewed() { [[ "${lines[2]}" == *"Reviewed recently"* ]] } +@test "staleness --list: org-native bracketed stamp sorts by its real date" { + task_reviewed TODO A "Bare recent" "$D5" + task_reviewed_org TODO B "Org-stamped old" "$D40" + run bash "$SCRIPT" --list "$TODO" 10 + [ "$status" -eq 0 ] + # The org-bracketed old stamp must sort ahead of the bare recent one — + # proof it parsed to a real date rather than falling to 0000-00-00. + [[ "${lines[0]}" == *"Org-stamped old"* ]] + [[ "${lines[1]}" == *"Bare recent"* ]] +} + @test "staleness --list: takes only the requested count" { task_unreviewed TODO A "First" task_reviewed TODO B "Second" "$D40" diff --git a/claude-templates/.ai/scripts/tests/test-lint-org.el b/claude-templates/.ai/scripts/tests/test-lint-org.el index d14879f..8e3e190 100644 --- a/claude-templates/.ai/scripts/tests/test-lint-org.el +++ b/claude-templates/.ai/scripts/tests/test-lint-org.el @@ -620,6 +620,29 @@ followups file on the next run." ;;; --------------------------------------------------------------------------- ;;; org-table-standard check (width budget + rules between rows) +(ert-deftest lo-table-inside-example-block-not-flagged () + "Pipe-led ASCII art inside an example block is not a table; no judgment." + (let* ((run (lo-test--run + "* H\n\n#+begin_example\n| client |----->| server |\n| box | | box |\n#+end_example\n" + 1 t)) + (judgments (lo-test--judgments (plist-get run :issues)))) + (should-not (memq 'org-table-standard (lo-test--checkers judgments))))) + +(ert-deftest lo-table-inside-src-block-not-flagged () + "Shell pipes inside a src block are not a table; no judgment." + (let* ((run (lo-test--run + "* H\n\n#+begin_src sh\n| sort\n| uniq -c\n#+end_src\n" 1 t)) + (judgments (lo-test--judgments (plist-get run :issues)))) + (should-not (memq 'org-table-standard (lo-test--checkers judgments))))) + +(ert-deftest lo-real-table-after-block-still-flagged () + "Block safety must not mask a genuine violation later in the file." + (let* ((run (lo-test--run + "* H\n\n#+begin_example\n| art |\n#+end_example\n\n| a | b |\n| 1 | 2 |\n" + 1 t)) + (judgments (lo-test--judgments (plist-get run :issues)))) + (should (memq 'org-table-standard (lo-test--checkers judgments))))) + (ert-deftest lo-table-over-budget-emits-judgment () "A table line rendering wider than 120 surfaces as an org-table-standard judgment." (let* ((wide (make-string 130 ?x)) diff --git a/claude-templates/.ai/scripts/tests/test-wrap-org-table.el b/claude-templates/.ai/scripts/tests/test-wrap-org-table.el index 8d1ecb6..0b3b375 100644 --- a/claude-templates/.ai/scripts/tests/test-wrap-org-table.el +++ b/claude-templates/.ai/scripts/tests/test-wrap-org-table.el @@ -186,3 +186,45 @@ (should (string-match-p "Prose before\\." content)) (should (string-match-p "Prose after\\." content)))) (delete-file file)))) + +;;; --------------------------------------------------------------------------- +;;; block safety — pipe lines inside #+begin_/#+end_ blocks are never tables + +(defconst wot-test--block-content + "#+begin_example +| client |----->| server | +| box | | box | +#+end_example +" + "An example block whose ASCII-art lines start with pipes.") + +(defun wot-test--process-content (content budget) + "Write CONTENT to a temp file, run `wot-process-file' at BUDGET, return result." + (let ((file (make-temp-file "wot-test" nil ".org"))) + (unwind-protect + (progn + (with-temp-file file (insert content)) + (wot-process-file file budget) + (with-temp-buffer (insert-file-contents file) (buffer-string))) + (delete-file file)))) + +(ert-deftest wot-process-file-leaves-example-block-byte-identical () + (let ((content (concat "* Diagram\n\n" wot-test--block-content))) + (should (equal (wot-test--process-content content 120) content)))) + +(ert-deftest wot-process-file-reformats-table-but-not-block () + (let* ((content (concat "* Doc\n\n" wot-test--block-content "\n" + wot-test--wide-input)) + (result (wot-test--process-content content 40))) + (should (string-match-p (regexp-quote wot-test--block-content) result)) + (should (string-match-p (regexp-quote wot-test--wide-expected) result)))) + +(ert-deftest wot-process-file-skips-pipes-in-src-block () + (let ((content "* Pipeline\n\n#+begin_src sh\n| sort\n| uniq -c\n#+end_src\n")) + (should (equal (wot-test--process-content content 120) content)))) + +(ert-deftest wot-process-file-literal-inner-end-marker-stays-in-block () + "A literal #+end_src quoted inside an example block must not close it." + (let ((content (concat "* Doc\n\n#+begin_example\n#+begin_src sh\nx\n" + "#+end_src\n| art |----| art |\n#+end_example\n"))) + (should (equal (wot-test--process-content content 120) content)))) diff --git a/claude-templates/.ai/scripts/wrap-org-table.el b/claude-templates/.ai/scripts/wrap-org-table.el index ddbea65..173e44d 100644 --- a/claude-templates/.ai/scripts/wrap-org-table.el +++ b/claude-templates/.ai/scripts/wrap-org-table.el @@ -228,22 +228,40 @@ continuation lines merge back into their logical row before re-wrapping." ;;; file layer (defun wot-process-file (file &optional budget) - "Reformat every org table in FILE in place to BUDGET width." + "Reformat every org table in FILE in place to BUDGET width. +Pipe-led lines inside #+begin_/#+end_ blocks (example, src, quote, …) are +content, not tables — ASCII art in an example block once got mangled into a +bordered table — so block regions are skipped verbatim." (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) - (while (re-search-forward "^[ \t]*|" nil t) - (let ((start (line-beginning-position))) - (while (and (not (eobp)) - (save-excursion (beginning-of-line) - (looking-at "[ \t]*|"))) + (let ((in-block nil)) ; the open block's type, e.g. "example" — nil outside + (while (not (eobp)) + (cond + ;; Only the matching #+end_<type> closes a block: an example block + ;; often quotes literal #+begin_src/#+end_src lines, and a boolean + ;; flag would let that inner literal end-marker re-expose the rest + ;; of the block to reformatting. + ((and (not in-block) + (looking-at "^[ \t]*#\\+begin_\\([^ \t\n]+\\)")) + (setq in-block (downcase (match-string 1))) (forward-line 1)) - (let* ((end (point)) - (table (buffer-substring-no-properties start end)) - (reformatted (wot-reformat-table-string table budget))) - (delete-region start end) - (goto-char start) - (insert reformatted)))) + ((and in-block + (looking-at-p (format "^[ \t]*#\\+end_%s\\([ \t]\\|$\\)" + (regexp-quote in-block)))) + (setq in-block nil) + (forward-line 1)) + ((and (not in-block) (looking-at-p "^[ \t]*|")) + (let ((start (point))) + (while (and (not (eobp)) (looking-at-p "^[ \t]*|")) + (forward-line 1)) + (let* ((end (point)) + (table (buffer-substring-no-properties start end)) + (reformatted (wot-reformat-table-string table budget))) + (delete-region start end) + (goto-char start) + (insert reformatted)))) + (t (forward-line 1))))) (write-region (point-min) (point-max) file))) ;;; --------------------------------------------------------------------------- @@ -289,7 +307,21 @@ so the ERT suite can `require' this file without firing the CLI dispatch." (t (file-readable-p a)))) command-line-args-left))) -(when (and noninteractive (wot--cli-invocation-p)) +(defun wot--entry-script-p () + "Non-nil when wrap-org-table.el itself was named on the command line. +lint-org.el `require's this file, and a load-triggered dispatch would run +the table reformatter over lint-org's file arguments — that's how a lint +invocation once reformatted the files it was only supposed to report on. +Only dispatch when a -l/--load argument names this very file." + (and load-file-name + (cl-loop for (flag arg) on command-line-args + thereis (and (member flag '("-l" "--load")) + (stringp arg) + (file-exists-p arg) + (string= (file-truename (expand-file-name arg)) + (file-truename load-file-name)))))) + +(when (and noninteractive (wot--entry-script-p) (wot--cli-invocation-p)) (wot-main)) (provide 'wrap-org-table) diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org index 88721ed..b031dbe 100644 --- a/claude-templates/.ai/workflows/INDEX.org +++ b/claude-templates/.ai/workflows/INDEX.org @@ -1,5 +1,5 @@ #+TITLE: Workflow Index -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-04-25 * Purpose @@ -110,8 +110,8 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - Situational triggers: "broadcast the <event> to all projects", "broadcast that <situation>", "let every project know I'll be away ..." - =flashcard-review.org= — review an org-drill flashcard file, restructure cards to question-form headings (no answer hints), audit content accuracy against project source-of-truth via subagent, rewrite source preserving SRS state, regenerate the Anki =.apkg= to =~/sync/phone/anki/=. Person cards use "Who is X? Tell me about their Y."; talking-points cards stay as-is. Script behavior: =flashcard-to-anki.py= strips =:PROPERTIES:= drawers + =SCHEDULED:= / =DEADLINE:= planning lines from Anki output. - Triggers: "review the flashcards", "update the flashcards", "review the drill deck", "update the drill deck", "refresh the Anki cards", "let's run the flashcard-review workflow" -- =page-me.org= — set a timed notification. - - Triggers: anything containing the word "page" used as a verb ("page me", "page me in 10 minutes", "page me at 3pm") +- =page-me.org= — set a timed notification (desktop =notify=; phone via =agent-page= when Craig is away). + - Triggers: anything containing the word "page" used as a verb ("page me", "page me in 10 minutes", "page me at 3pm", "page my phone") - =status-check.org= — proactive long-running-job updates. - Triggers: "keep me posted on this", "provide status checks on this job", "let me know when it's done", "monitor this for me". Auto: any job estimated 10+ min. - =create-workflow.org= — define a new workflow. diff --git a/claude-templates/.ai/workflows/add-calendar-event.org b/claude-templates/.ai/workflows/add-calendar-event.org index 2650fb7..5dd6c42 100644 --- a/claude-templates/.ai/workflows/add-calendar-event.org +++ b/claude-templates/.ai/workflows/add-calendar-event.org @@ -1,5 +1,5 @@ #+TITLE: Add Calendar Event Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-01 * Overview diff --git a/claude-templates/.ai/workflows/broadcast.org b/claude-templates/.ai/workflows/broadcast.org index 60e9ed1..cc14f00 100644 --- a/claude-templates/.ai/workflows/broadcast.org +++ b/claude-templates/.ai/workflows/broadcast.org @@ -1,5 +1,5 @@ #+TITLE: Broadcast Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-29 * Overview diff --git a/claude-templates/.ai/workflows/clean-todo.org b/claude-templates/.ai/workflows/clean-todo.org index a1b2af5..48d3084 100644 --- a/claude-templates/.ai/workflows/clean-todo.org +++ b/claude-templates/.ai/workflows/clean-todo.org @@ -1,5 +1,5 @@ #+TITLE: Clean-Todo Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-11 * Overview diff --git a/claude-templates/.ai/workflows/code-quality.org b/claude-templates/.ai/workflows/code-quality.org index 2406f4c..3ac3e9d 100644 --- a/claude-templates/.ai/workflows/code-quality.org +++ b/claude-templates/.ai/workflows/code-quality.org @@ -1,5 +1,5 @@ #+TITLE: Code-Quality Sweep Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-28 * Overview diff --git a/claude-templates/.ai/workflows/daily-prep.org b/claude-templates/.ai/workflows/daily-prep.org index b6989e7..3103bc7 100644 --- a/claude-templates/.ai/workflows/daily-prep.org +++ b/claude-templates/.ai/workflows/daily-prep.org @@ -1,5 +1,5 @@ #+TITLE: Daily Prep Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-11 * Overview diff --git a/claude-templates/.ai/workflows/delete-calendar-event.org b/claude-templates/.ai/workflows/delete-calendar-event.org index 5bb92a1..7de0086 100644 --- a/claude-templates/.ai/workflows/delete-calendar-event.org +++ b/claude-templates/.ai/workflows/delete-calendar-event.org @@ -1,5 +1,5 @@ #+TITLE: Delete Calendar Event Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-01 * Overview diff --git a/claude-templates/.ai/workflows/edit-calendar-event.org b/claude-templates/.ai/workflows/edit-calendar-event.org index 662f0b4..27a9dd3 100644 --- a/claude-templates/.ai/workflows/edit-calendar-event.org +++ b/claude-templates/.ai/workflows/edit-calendar-event.org @@ -1,5 +1,5 @@ #+TITLE: Edit Calendar Event Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-01 * Overview diff --git a/claude-templates/.ai/workflows/email-assembly.org b/claude-templates/.ai/workflows/email-assembly.org index 003459c..699dbc0 100644 --- a/claude-templates/.ai/workflows/email-assembly.org +++ b/claude-templates/.ai/workflows/email-assembly.org @@ -1,5 +1,5 @@ #+TITLE: Email Assembly Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-01-29 * Overview diff --git a/claude-templates/.ai/workflows/extract-email.org b/claude-templates/.ai/workflows/extract-email.org index 3a70bea..c68bafe 100644 --- a/claude-templates/.ai/workflows/extract-email.org +++ b/claude-templates/.ai/workflows/extract-email.org @@ -1,5 +1,5 @@ #+TITLE: Extract Email Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-06 * Overview diff --git a/claude-templates/.ai/workflows/find-email.org b/claude-templates/.ai/workflows/find-email.org index 0ef9615..d71ed3e 100644 --- a/claude-templates/.ai/workflows/find-email.org +++ b/claude-templates/.ai/workflows/find-email.org @@ -1,5 +1,5 @@ #+TITLE: Find Email Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-01 * Overview diff --git a/claude-templates/.ai/workflows/first-session.org b/claude-templates/.ai/workflows/first-session.org index 60118a2..147026f 100644 --- a/claude-templates/.ai/workflows/first-session.org +++ b/claude-templates/.ai/workflows/first-session.org @@ -1,5 +1,5 @@ #+TITLE: First Session Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings Run this workflow on the first Claude Code session for a new project. It establishes the git/.ai policy, orients Claude to the diff --git a/claude-templates/.ai/workflows/flashcard-review.org b/claude-templates/.ai/workflows/flashcard-review.org index 31027b3..09af348 100644 --- a/claude-templates/.ai/workflows/flashcard-review.org +++ b/claude-templates/.ai/workflows/flashcard-review.org @@ -1,5 +1,5 @@ #+TITLE: Drill Deck Review Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-30 * Overview diff --git a/claude-templates/.ai/workflows/helper-mode.org b/claude-templates/.ai/workflows/helper-mode.org index cdec200..a6acfa7 100644 --- a/claude-templates/.ai/workflows/helper-mode.org +++ b/claude-templates/.ai/workflows/helper-mode.org @@ -1,5 +1,5 @@ #+TITLE: Helper Mode Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-15 * Overview diff --git a/claude-templates/.ai/workflows/inbox.org b/claude-templates/.ai/workflows/inbox.org index acfd11d..3bd9335 100644 --- a/claude-templates/.ai/workflows/inbox.org +++ b/claude-templates/.ai/workflows/inbox.org @@ -1,5 +1,5 @@ #+TITLE: Inbox Workflow (Engine) -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-23 * Overview diff --git a/claude-templates/.ai/workflows/journal-entry.org b/claude-templates/.ai/workflows/journal-entry.org index 3f476a7..c70dfe8 100644 --- a/claude-templates/.ai/workflows/journal-entry.org +++ b/claude-templates/.ai/workflows/journal-entry.org @@ -1,5 +1,5 @@ #+TITLE: Journal Entry Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2025-11-07 * Overview diff --git a/claude-templates/.ai/workflows/meeting-prep.org b/claude-templates/.ai/workflows/meeting-prep.org index 162ae30..563328b 100644 --- a/claude-templates/.ai/workflows/meeting-prep.org +++ b/claude-templates/.ai/workflows/meeting-prep.org @@ -1,5 +1,5 @@ #+TITLE: Meeting-Prep Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-10 * Overview diff --git a/claude-templates/.ai/workflows/meeting-prep.pre-wire.org b/claude-templates/.ai/workflows/meeting-prep.pre-wire.org index 6a156c0..3e27c2a 100644 --- a/claude-templates/.ai/workflows/meeting-prep.pre-wire.org +++ b/claude-templates/.ai/workflows/meeting-prep.pre-wire.org @@ -1,5 +1,5 @@ #+TITLE: Meeting-Prep — Pre-Wire Method (supporting doc) -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-10 Supporting document for the [[file:meeting-prep.org][meeting-prep workflow]]'s Phase 3.5. The workflow carries the condensed, in-flow version of pre-wiring; this file is the full Manager Tools method, kept beside the workflow (same name + =.pre-wire= suffix) so it travels with the workflow. Source casts: "How to Prewire a Meeting" (2007) and "Peer Prewire" (2015). diff --git a/claude-templates/.ai/workflows/no-approvals.org b/claude-templates/.ai/workflows/no-approvals.org index 9e1c894..5f54b96 100644 --- a/claude-templates/.ai/workflows/no-approvals.org +++ b/claude-templates/.ai/workflows/no-approvals.org @@ -1,5 +1,5 @@ #+TITLE: No-Approvals Mode -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-28 * Overview diff --git a/claude-templates/.ai/workflows/open-tasks.org b/claude-templates/.ai/workflows/open-tasks.org index 02a0847..205d95c 100644 --- a/claude-templates/.ai/workflows/open-tasks.org +++ b/claude-templates/.ai/workflows/open-tasks.org @@ -1,5 +1,5 @@ #+TITLE: Open Tasks Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-04-25 * Overview diff --git a/claude-templates/.ai/workflows/page-me.org b/claude-templates/.ai/workflows/page-me.org index 8069830..bfa92c6 100644 --- a/claude-templates/.ai/workflows/page-me.org +++ b/claude-templates/.ai/workflows/page-me.org @@ -1,5 +1,5 @@ #+TITLE: Page Me Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-01-31 #+UPDATED: 2026-02-27 @@ -113,6 +113,19 @@ notify info "Page" "Your message here" --persist The =--persist= flag keeps the notification on screen until manually dismissed. All page-me notifications should use =--persist= by default. +** Paging Craig's phone (away from the machine) + +The timed =notify= alarm above is the desktop channel. When Craig is away from the machine (or asks to be paged "on my phone"), use the agent pager instead — a Signal push to his phone from any machine or agent runtime: + +#+begin_src bash +agent-page "Build finished — ready for your eyes" + +# Timed phone page: same at-daemon pattern, different channel +echo "agent-page 'Meeting starts in 5'" | at 3:25pm +#+end_src + +Channel selection and the pager's mechanics live in protocols.org "Paging Craig — the agent pager". When in doubt, fire both: the desktop notification persists for whenever he returns, the phone push reaches him now. + ** Managing Alarms #+begin_src bash diff --git a/claude-templates/.ai/workflows/process-meeting-transcript.org b/claude-templates/.ai/workflows/process-meeting-transcript.org index 4dd340f..d0806ad 100644 --- a/claude-templates/.ai/workflows/process-meeting-transcript.org +++ b/claude-templates/.ai/workflows/process-meeting-transcript.org @@ -1,5 +1,5 @@ #+TITLE: Process Meeting Transcript Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-03 * Overview @@ -10,16 +10,16 @@ This workflow defines the process for processing meeting recordings from start t Trigger this workflow when: - Craig says "process the transcript" or "process the recording" or similar -- New recording files (.mkv) appear in ~/sync/recordings/ after meetings +- New recording files (.mkv, .m4a, or .flac) appear in ~/sync/recordings/ after meetings - Craig wants to process meeting recordings into labeled transcripts * Prerequisites -- Recording file(s) exist in ~/sync/recordings/ (*.mkv) +- Recording file(s) exist in ~/sync/recordings/ (*.mkv, *.m4a, or *.flac) - Calendar files available at ~/.emacs.d/data/*cal.org for meeting titles - AssemblyAI transcription script at ~/.emacs.d/scripts/assemblyai-transcribe - AssemblyAI API key stored in ~/.authinfo.gpg (machine api.assemblyai.com) -- ffmpeg available for audio extraction +- ffmpeg available for audio extraction (video .mkv only; .m4a and .flac skip extraction) * The Workflow @@ -43,13 +43,13 @@ Classification is per recording, not per session — a single run can carry this Find and match recording files with calendar events. *Run sub-steps 1 and 3 (recording list + calendar dump) as a single parallel batch* — they're independent. Sub-step 2 (parse timestamps) and sub-step 4 (matching) work from those two outputs in-memory, so they're sequential after the batch. -1. **List recordings:** Find all recording files in ~/sync/recordings/ (video .mkv or audio-only .m4a) +1. **List recordings:** Find all recording files in ~/sync/recordings/ (video .mkv or audio-only .m4a / .flac) #+begin_src bash - ls -la ~/sync/recordings/*.mkv ~/sync/recordings/*.m4a 2>/dev/null + ls -la ~/sync/recordings/*.mkv ~/sync/recordings/*.m4a ~/sync/recordings/*.flac 2>/dev/null #+end_src - Audio-only recordings (.m4a) are used when no screen content is expected. These skip Step 3 (audio extraction) since they're already in a transcribable format. + Audio-only recordings (.m4a or lossless .flac) are used when no screen content is expected. These skip Step 3 (audio extraction) since they're already in a transcribable format. FLAC is the recorder's current audio format — its frames are self-contained, so an interrupted recording still decodes. -2. **Extract timestamps:** Parse date/time from each filename (format: YYYY-MM-DD-HH-MM-SS.mkv or .m4a) +2. **Extract timestamps:** Parse date/time from each filename (format: YYYY-MM-DD-HH-MM-SS.mkv, .m4a, or .flac) 3. **Match with calendar:** Check ~/.emacs.d/data/*cal.org for meetings at those times #+begin_src bash @@ -74,7 +74,7 @@ Per =cross-project.md=: transcribe and label here, then deliver the *labeled tra ** Step 3: Extract Audio (video recordings only) -*Skip this step for .m4a files* — they are already audio and can go directly to transcription. +*Skip this step for .m4a and .flac files* — they are already audio and can go directly to transcription. For .mkv video recordings, extract audio for transcription: @@ -96,11 +96,11 @@ Output: /tmp/FILENAME.m4a (temporary, deleted after transcription) #+begin_src bash # For .mkv files (audio was extracted to /tmp/): ~/.emacs.d/scripts/assemblyai-transcribe /tmp/FILENAME.m4a > ~/sync/recordings/FILENAME.txt - # For .m4a files (transcribe directly): + # For .m4a or .flac files (transcribe directly — AssemblyAI accepts both natively): ~/.emacs.d/scripts/assemblyai-transcribe ~/sync/recordings/FILENAME.m4a > ~/sync/recordings/FILENAME.txt #+end_src -2. **Clean up:** Delete intermediate .m4a file after successful transcription (only for .mkv extractions — do NOT delete original .m4a recordings) +2. **Clean up:** Delete intermediate .m4a file after successful transcription (only for .mkv extractions — do NOT delete original .m4a or .flac recordings) #+begin_src bash rm /tmp/FILENAME.m4a #+end_src @@ -181,13 +181,13 @@ Present the speaker identification table to Craig for confirmation: ** Step 9: Copy Recording to Meetings Folder -1. Ensure engagement meetings folder exists and patterns are in .gitignore (~*/meetings/*.mkv~ and ~*/meetings/*.m4a~) +1. Ensure engagement meetings folder exists and patterns are in .gitignore (~*/meetings/*.mkv~, ~*/meetings/*.m4a~, and ~*/meetings/*.flac~) 2. Copy the recording file with descriptive name: #+begin_src bash # Video recordings: cp ~/sync/recordings/YYYY-MM-DD-HH-MM-SS.mkv {engagement}/meetings/YYYY-MM-DD_HH-MM-meeting-name.mkv - # Audio-only recordings: + # Audio-only recordings (.m4a or .flac — copy with the original extension): cp ~/sync/recordings/YYYY-MM-DD-HH-MM-SS.m4a {engagement}/meetings/YYYY-MM-DD_HH-MM-meeting-name.m4a #+end_src Example: ~deepsat/meetings/2026-02-03_11-02-standup-ipm-grooming.mkv~ diff --git a/claude-templates/.ai/workflows/read-calendar-events.org b/claude-templates/.ai/workflows/read-calendar-events.org index be66bf4..5eac529 100644 --- a/claude-templates/.ai/workflows/read-calendar-events.org +++ b/claude-templates/.ai/workflows/read-calendar-events.org @@ -1,5 +1,5 @@ #+TITLE: Read Calendar Events Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-01 * Overview diff --git a/claude-templates/.ai/workflows/readability-audit.org b/claude-templates/.ai/workflows/readability-audit.org index 8223a03..90ad366 100644 --- a/claude-templates/.ai/workflows/readability-audit.org +++ b/claude-templates/.ai/workflows/readability-audit.org @@ -1,5 +1,5 @@ #+TITLE: Readability Audit Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-28 * Overview diff --git a/claude-templates/.ai/workflows/rename-artifact.org b/claude-templates/.ai/workflows/rename-artifact.org index 7b9f15b..a8d1246 100644 --- a/claude-templates/.ai/workflows/rename-artifact.org +++ b/claude-templates/.ai/workflows/rename-artifact.org @@ -1,5 +1,5 @@ #+TITLE: Rename an .ai Artifact -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-31 * Summary diff --git a/claude-templates/.ai/workflows/send-email.org b/claude-templates/.ai/workflows/send-email.org index 065f925..82d2286 100644 --- a/claude-templates/.ai/workflows/send-email.org +++ b/claude-templates/.ai/workflows/send-email.org @@ -1,5 +1,5 @@ #+TITLE: Email Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-01-26 * Overview diff --git a/claude-templates/.ai/workflows/session-harvest.org b/claude-templates/.ai/workflows/session-harvest.org index c48d689..54a7c09 100644 --- a/claude-templates/.ai/workflows/session-harvest.org +++ b/claude-templates/.ai/workflows/session-harvest.org @@ -1,5 +1,5 @@ #+TITLE: Session-Harvest Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-11 * Overview diff --git a/claude-templates/.ai/workflows/spec-create.org b/claude-templates/.ai/workflows/spec-create.org index 1249181..39758a0 100644 --- a/claude-templates/.ai/workflows/spec-create.org +++ b/claude-templates/.ai/workflows/spec-create.org @@ -1,5 +1,5 @@ #+TITLE: Spec-Create Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-09 * Overview @@ -47,6 +47,8 @@ Capture, in this order: ** Phase 2 — Design, alternatives, decisions 1. *Design* — overview first, then detail. Write the reasoning as *prose, not bullet dumps* — prose exposes weak logic that bullets let you hide. Use bullets only for genuinely enumerable lists. When the thing has an interface, use the *two-altitude* split (Rust RFC): explain it once for a user/caller, once for an implementer. + + *Non-trivial UI.* When the deliverable is a real UI (a panel, a multi-control surface, an interacting visual layout — not a single dialog, a CLI flag, or a one-off prompt), the design isn't settled on the page. Run the research → ~5 distinct working-prototype directions → iterate-one-to-final process in =claude-rules/ui-prototyping.md= before treating the UI design as done, and add a =Prototype iterations= subsection under the spec's status heading linking every iteration (final linked in the design section). A UI design decision moves to =DONE= only once it's been seen working in a prototype. 2. *Alternatives considered* — the load-bearing section authors skip and reviewers need most. For each option, force a why-not with the MADR grammar: "Good, because… / Bad, because… / Neutral, because…". Even one rejected option, with the reason, beats presenting one path as inevitable. 3. *Decisions* — capture each real choice as an org =TODO= task carrying an inline mini-ADR (Nygard's spine): - The heading is =** TODO <Decision name>=. It flips to =DONE= when the decision-maker agrees with the call; until then it stays =TODO=. diff --git a/claude-templates/.ai/workflows/spec-review.org b/claude-templates/.ai/workflows/spec-review.org index d4998eb..0da8e65 100644 --- a/claude-templates/.ai/workflows/spec-review.org +++ b/claude-templates/.ai/workflows/spec-review.org @@ -134,6 +134,7 @@ Work the spec against these. Each is a source of concrete findings, not a box to - *Security & privacy.* API keys safe? Debug logs leaking secrets or private issue text? Confirmations before mutating shared workspace objects? Personal vs shared distinguished? Local files holding sensitive descriptions/comments? Anything to redact from messages/logs? Any work-tracker integration may handle private company data. - *UX & accessibility.* Discoverable commands? Recoverable mistakes? Prompts ordered to the task? Safe, useful defaults? Informative-not-noisy status messages? Does the UI avoid implying unsupported actions are supported? Match the upstream product's permissions/concepts? Are customizations named in user language, with clear defaults and docstrings? For Emacs packages, command names, completion candidates, buffer layout, defcustom names, and message wording *are* the UX. - *Operational-panel UI traps.* Applies when the spec covers a user-facing panel, dialog, or control surface; skip otherwise. Lists that mix saved, current, and generated items must name each item's source. Refresh or scan actions must not gate data that could be shown immediately. Add-forms must not ask the user to retype values the system already discovered. Destructive confirmations read in future tense before the action and verified-result tense after it. Diagnostics, performance, logging, and repair affordances are reviewed as one coherent flow before extra pages or buttons are added. A popup launched from a bar, tray, or tool surface should visually belong to that launcher. (Promoted from archsetup's Waybar network-panel review, 2026-06-30.) +- *Prototype process for non-trivial UI.* Applies when the deliverable is a real UI (a panel, a multi-control surface, an interacting visual layout — not a single dialog or CLI flag); skip otherwise. Verify the =claude-rules/ui-prototyping.md= process ran: category research is cited in Goals/Design, the final prototype is linked in the design section, a =Prototype iterations= subsection under the status heading lists every pass, and each UI design decision is backed by a prototype it was seen working in rather than asserted on the page. A non-trivial-UI spec with decisions but no prototype evidence is a =:blocking:= finding. - *Test strategy and coverage.* Characterization tests before behavior changes? Pure functions to unit-test? API responses needing fixtures? Command flows needing stubs? Regression tests for prior bugs? Boundary/error cases? What's covered elsewhere and shouldn't be re-tested? Which existing tests must change? How is coverage generated, summarized, and used to find untested/refactor-worthy code? Prefer tests that lock contracts: representation shape, query compilation, sync no-op, conflict refusal, pagination, dirty-buffer protection, log redaction, and long-running/slow-operation behavior via fakes rather than flaky live dependencies. - *Observability & operations.* How does a user see what the package is doing? Progress messages for long ops? Useful, safe debug logging? Are logs structured enough to isolate issues from a bug report? Are commands provided to inspect/clear caches, test connectivity, diagnose backends/tools, copy redacted debug info, or reproduce command invocations? How are terminal states discovered: completion, failure, partial success, stalled/hung, cancelled, cleanup-unverified, and "needs user action"? Does the product notify only when useful, avoid noisy success spam, and keep non-success states visible until acknowledged? For generated org files, headers should often carry source, filter/view name, refresh time, count, truncation state. - *Comparable-product sentiment.* When there are obvious adjacent products, research what users love and hate about them from official docs plus current community reports. Do not cargo-cult their feature set; translate findings into the spec's scope. For each loved behavior, say whether the spec provides it, intentionally omits it, or defers it. For each hated behavior, say whether the spec avoids, resolves, inherits, or accepts it. diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org index 4e3d417..929d482 100644 --- a/claude-templates/.ai/workflows/startup.org +++ b/claude-templates/.ai/workflows/startup.org @@ -1,5 +1,5 @@ #+TITLE: Startup Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-04-25 * Summary diff --git a/claude-templates/.ai/workflows/status-check.org b/claude-templates/.ai/workflows/status-check.org index efff16d..4a9972c 100644 --- a/claude-templates/.ai/workflows/status-check.org +++ b/claude-templates/.ai/workflows/status-check.org @@ -1,5 +1,5 @@ #+TITLE: Status Check Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-02 * Overview diff --git a/claude-templates/.ai/workflows/summarize-emails.org b/claude-templates/.ai/workflows/summarize-emails.org index 6ac5e6f..c9c7001 100644 --- a/claude-templates/.ai/workflows/summarize-emails.org +++ b/claude-templates/.ai/workflows/summarize-emails.org @@ -1,5 +1,5 @@ #+TITLE: Summarize Emails Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-14 * Overview diff --git a/claude-templates/.ai/workflows/suspend.org b/claude-templates/.ai/workflows/suspend.org index 1c16bb9..3691f60 100644 --- a/claude-templates/.ai/workflows/suspend.org +++ b/claude-templates/.ai/workflows/suspend.org @@ -1,5 +1,5 @@ #+TITLE: Session Suspend Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-28 * Overview diff --git a/claude-templates/.ai/workflows/sync-email.org b/claude-templates/.ai/workflows/sync-email.org index 52a7caf..863b400 100644 --- a/claude-templates/.ai/workflows/sync-email.org +++ b/claude-templates/.ai/workflows/sync-email.org @@ -1,5 +1,5 @@ #+TITLE: Sync Email Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-02-01 * Overview diff --git a/claude-templates/.ai/workflows/task-audit.org b/claude-templates/.ai/workflows/task-audit.org index 7d2b758..aa50176 100644 --- a/claude-templates/.ai/workflows/task-audit.org +++ b/claude-templates/.ai/workflows/task-audit.org @@ -1,5 +1,5 @@ #+TITLE: Task Audit Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-22 * Overview diff --git a/claude-templates/.ai/workflows/task-review.org b/claude-templates/.ai/workflows/task-review.org index ba1571a..7ea2e8e 100644 --- a/claude-templates/.ai/workflows/task-review.org +++ b/claude-templates/.ai/workflows/task-review.org @@ -1,5 +1,5 @@ #+TITLE: Task Review Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-20 * Overview @@ -92,7 +92,7 @@ Set =:LAST_REVIEWED:= to today's date (from above) in the task's =:PROPERTIES:= Body... #+end_example -The exact date string matters: =task-review-staleness.sh= and the wrap-up health check both parse =:LAST_REVIEWED: YYYY-MM-DD=. +Format: =:LAST_REVIEWED:= takes a bare ISO date (=2026-05-20=) or an org-native inactive timestamp (=[2026-05-20 Tue]=, matching the =CREATED:=/=CLOSED:= cookies beside it); =task-review-staleness.sh= and the wrap-up health check normalize both to the date. A value that is neither is a data error — the staleness script warns loudly to stderr (naming the file, line, and value) and leaves the task out of the stale count rather than silently reporting a freshly-reviewed task as never-reviewed. Stamp a clean date and the warning never fires. *** Killing a task diff --git a/claude-templates/.ai/workflows/triage-intake.cmail.org b/claude-templates/.ai/workflows/triage-intake.cmail.org index d818c72..8d8abfb 100644 --- a/claude-templates/.ai/workflows/triage-intake.cmail.org +++ b/claude-templates/.ai/workflows/triage-intake.cmail.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake — cmail (Proton) Source -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-26 # Source plugin for the triage-intake engine. See triage-intake.org for the diff --git a/claude-templates/.ai/workflows/triage-intake.github-prs.org b/claude-templates/.ai/workflows/triage-intake.github-prs.org index c1bc796..644421c 100644 --- a/claude-templates/.ai/workflows/triage-intake.github-prs.org +++ b/claude-templates/.ai/workflows/triage-intake.github-prs.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake — Personal GitHub PRs Source -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-26 # Source plugin for the triage-intake engine. See triage-intake.org for the diff --git a/claude-templates/.ai/workflows/triage-intake.org b/claude-templates/.ai/workflows/triage-intake.org index a5a3bda..fde67a7 100644 --- a/claude-templates/.ai/workflows/triage-intake.org +++ b/claude-templates/.ai/workflows/triage-intake.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake Workflow (Engine) -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-01 * Summary @@ -414,7 +414,7 @@ The sentinel is checked into git, but git tracks content, not mtime — so an mt Craig, via the work project's same-day handoff: "we only need to report if anything's changed when we do triage intake." Sweep summaries report deltas only — a new invite, a new/moved/cancelled event, a new message needing attention. Unchanged sources get no block (the "Calendar — quiet" roll-call is retired), and an all-quiet sweep renders as a single "HH:MM sweep: no changes" line. Failures keep their loud banner (never folded into the no-change line) and the suggested-actions line stays when actions are queued. Same ruling: the telegram plugin's dev-community group traffic is dropped from reports entirely unless Craig asks (see that plugin's 2026-06-11 note). **** 2026-06-10: Loud failure surfacing (Phase C item 0 + Common Mistake 9) -Craig: "highlight any failures in daily triage loudly. I get important communication from all these channels." Trigger: the 2026-06-10 sweep shipped with Signal silently missing — a standalone receive hung on the account lock while the signel daemon owned it, and the failure looked identical to a quiet source. Failures now lead the summary in a ⚠ SCAN FAILED banner; the telegram plugin's failure path points at this rule. +Craig: "highlight any failures in daily triage loudly. I get important communication from all these channels." Trigger: the 2026-06-10 sweep shipped with Signal silently missing — a standalone receive hung on the signal-cli account lock while another client held it, and the failure looked identical to a quiet source. Failures now lead the summary in a ⚠ SCAN FAILED banner; the telegram plugin's failure path points at this rule. **** 2026-05-26: Refactor into engine + source plugins Split the monolithic workflow into a source-agnostic engine (this file) and per-source plugins named =triage-intake.<source>.org=. The engine carries the anchor/sentinel logic, the four-bucket model, the Phase A-D orchestration, the todo.org persistence convention, and the exit criteria. Each source's scan/classify/render/action knowledge moved to its own plugin. General plugins (personal-gmail, personal-calendar, cmail, github-prs) live in =.ai/workflows/= and are template-synced; project-specific plugins (a work project's Linear, work Gmail, work Slack, enterprise PRs) live in the project's =.ai/project-workflows/= and are never synced. Phase 0 globs *both* directories — the loud requirement, because missing the project dir silently halves the sweep. Naming convention: first dot is the engine/plugin boundary, deeper dots reserved for sub-adapters. This removed all DeepSat/Linear specifics from the engine; they become work-project plugins. diff --git a/claude-templates/.ai/workflows/triage-intake.personal-calendar.org b/claude-templates/.ai/workflows/triage-intake.personal-calendar.org index bf7d543..b5ee67a 100644 --- a/claude-templates/.ai/workflows/triage-intake.personal-calendar.org +++ b/claude-templates/.ai/workflows/triage-intake.personal-calendar.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake — Personal Calendar Source -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-26 # Source plugin for the triage-intake engine. See triage-intake.org for the diff --git a/claude-templates/.ai/workflows/triage-intake.personal-gmail.org b/claude-templates/.ai/workflows/triage-intake.personal-gmail.org index aa0554d..7fb1231 100644 --- a/claude-templates/.ai/workflows/triage-intake.personal-gmail.org +++ b/claude-templates/.ai/workflows/triage-intake.personal-gmail.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake — Personal Gmail Source -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-26 # Source plugin for the triage-intake engine. See triage-intake.org for the @@ -21,10 +21,29 @@ Personal Gmail unread in the inbox since the anchor: mcp__google-docs-personal__listMessages q="is:unread in:inbox after:<anchor-epoch>" maxResults=100 #+end_src -⚠ *Express the cutoff as the literal UNIX epoch* — =after:1778856990=, not =after:YYYY/MM/DD=. Gmail's =after:YYYY/MM/DD= operator only supports day resolution; the =YYYY/MM/DD HH:MM:SS= form is NOT valid syntax — Gmail parses the space as a term separator, treats =HH:MM:SS= as a search term that never matches, and returns 0 results, silently masking unread mail. The engine supplies =<anchor-epoch>= because this source declares =ANCHOR: epoch=. +⚠ *Express every anchor cutoff as the literal UNIX epoch* — =after:1784177122= and =before:1784177122= for the same anchor, never the =YYYY/MM/DD= form. This governs *both* anchored queries: the scan above and the backlog-residue probe below. They must meet at the same instant or mail falls between them permanently. Gmail's day-resolution operators fail two different ways: =after:YYYY/MM/DD HH:MM:SS= is not valid syntax at all — Gmail parses the space as a term separator, treats =HH:MM:SS= as a search term that never matches, and returns 0 results, silently masking unread mail — while =before:YYYY/MM/DD= is valid but excludes the named day entirely, so pairing it with a second-resolution scan leaves the whole anchor day covered by neither query. The engine supplies =<anchor-epoch>= because this source declares =ANCHOR: epoch=. + +The rule binds the *anchor* windows only. The date-slice walk below deliberately uses =before:<oldest-full-day-seen>= at day resolution — safe there because consecutive slices overlap and get deduped by message id. ⚠ *Do NOT add =-category:promotions -category:social=.* That filter masked 67 promo+social messages across two runs (2026-05-04, 2026-05-06), both needing a follow-up sweep. Pull the full unfiltered set; the trash-leaning bias in Classify handles promotions and social directly. +⚠ *The MCP caps at =maxResults=100= and exposes NO =pageToken= parameter.* The response carries a =nextPageToken=, but the tool can't consume it, so a pile over 100 is silently truncated — the tail below the cap never gets classified, and every later anchored sweep skips it (it predates the new anchor). This is exactly how a 300+ backlog accumulated invisibly by 2026-07-08. Two consequences: + +- *Never treat a 100-row result as complete.* When a scan returns exactly 100, walk the tail in *date slices*: re-query with =before:<oldest-full-day-seen>= (day resolution), repeat until a page returns fewer than 100, dedupe by message id across slices (the day-resolution boundary overlaps). +- *Never report =resultSizeEstimate= as a count.* It's unreliable — observed stuck at "201" across three different queries whose real union exceeded 300. + +*** Backlog-residue check (every sweep — cheap, mandatory) + +The anchored scan is blind to anything unread from *before* the anchor. After it, run one probe for pre-anchor residue: + +#+begin_src text +mcp__google-docs-personal__listMessages q="is:unread in:inbox before:<anchor-epoch>" maxResults=5 +#+end_src + +The cutoff is the epoch, matching the scan's =after:<anchor-epoch>= — see the epoch rule above. + +If it returns any messages, surface one loud line in the sweep summary: "Backlog: unread predating the anchor exists (N+ shown; date-slice to inventory)" and offer a backlog sweep. Never fold the residue into a quiet sweep — an anchored "no changes" claim is only true for the window the scan saw. (Added 2026-07-08 after ~300 pre-anchor unread accumulated unseen; the probe returns actual messages, so it works where the estimate lies. Shipped with a day-resolution cutoff that hid the entire anchor day; fixed to epoch 2026-07-16 after a home sweep reported the backlog clear while two July-15 messages sat unread.) + ** Classify Bias: *trash-leaning* — personal Gmail is high noise volume. diff --git a/claude-templates/.ai/workflows/triage-intake.telegram.org b/claude-templates/.ai/workflows/triage-intake.telegram.org index 9caa4e1..5039a8b 100644 --- a/claude-templates/.ai/workflows/triage-intake.telegram.org +++ b/claude-templates/.ai/workflows/triage-intake.telegram.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake — Telegram Source -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-09 # Source plugin for the triage-intake engine. See triage-intake.org for the diff --git a/claude-templates/.ai/workflows/work-the-backlog.org b/claude-templates/.ai/workflows/work-the-backlog.org index b0666e7..090841d 100644 --- a/claude-templates/.ai/workflows/work-the-backlog.org +++ b/claude-templates/.ai/workflows/work-the-backlog.org @@ -1,5 +1,5 @@ #+TITLE: Work the Backlog -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-07-02 * Overview @@ -152,7 +152,7 @@ With paging on, fire one page when the set is done or the cap is hit — end-of- notify info "Page" "<project>: <N> done, <M> remaining — <one-line summary>" --persist #+end_src -=--persist= keeps it on screen until dismissed, and =info= is the page-me urgency convention (persistent but never crash-scary). The page fires when the set completes *or* the cap stops the run — either way exactly once. The message carries the project name, the completed count, and the remaining count (with skipped tasks noted in the run summary) so Craig can confirm ready and name the next project in one reply. There is no separate page-signal call — =notify= is the paging surface. +=--persist= keeps it on screen until dismissed, and =info= is the page-me urgency convention (persistent but never crash-scary). The page fires when the set completes *or* the cap stops the run — either way exactly once. The message carries the project name, the completed count, and the remaining count (with skipped tasks noted in the run summary) so Craig can confirm ready and name the next project in one reply. =notify= is the desktop paging surface; a run that expects Craig to be away also fires =agent-page= with the same message (the Signal phone channel — protocols.org "Paging Craig — the agent pager"). * Metrics diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org index d0c4e75..5ce88a5 100644 --- a/claude-templates/.ai/workflows/wrap-it-up.org +++ b/claude-templates/.ai/workflows/wrap-it-up.org @@ -1,5 +1,5 @@ #+TITLE: Session Wrap-Up Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-04-20 * Overview @@ -202,9 +202,14 @@ else followups=".ai/lint-followups.org" fi [ -f todo.org ] && emacs --batch -q -l .ai/scripts/lint-org.el \ - --followups-file="$followups" todo.org + --fix --followups-file="$followups" todo.org #+end_src +The =--fix= flag is required for the writes: lint-org's CLI default is +report-only (a linter reports, it doesn't write), and this wrap-up pass is +the deliberate exception that applies fixes — its diff rides the wrap-up +commit for review. + =lint-org= runs =org-lint= over =todo.org=, auto-applies four mechanical categories (=item-number= counters, bare =#+begin_src= → =#+begin_example=, multi-line planning-info merged onto one line, =**X.**= → =*X.*=), and diff --git a/claude-templates/AGENTS.md b/claude-templates/AGENTS.md new file mode 100644 index 0000000..97fd001 --- /dev/null +++ b/claude-templates/AGENTS.md @@ -0,0 +1,19 @@ +# Agent Entry Point + +You are this project's agent, whichever model or harness is running you. + +1. If the project has `.ai/protocols.org`, read it now and follow all + instructions. It is the single entry point for session behavior: + startup, session logging, inbox processing, wrap-up. +2. Behavioral rules live in `.claude/rules/*.md` in the project (when + present) and `~/.claude/rules/*.md` globally. Read them; they bind + every session regardless of harness. +3. A `/name` reference in any rule or workflow (`/voice`, + `/review-code`, `/brainstorm`, ...) resolves to a file: + `~/.claude/skills/<name>/SKILL.md` or `~/.claude/commands/<name>.md`. + Read that file and follow it. If it is absent, say so and apply the + rule's documented fallback rather than skipping the gate. +4. Harness mechanics these files assume (hooks, `/clear`, popup tools) + may not exist in your harness. Degrade per each rule's own fallback + language. Never silently skip a verification or approval gate because + the tooling that enforces it is missing. diff --git a/claude-templates/bin/agent-page b/claude-templates/bin/agent-page new file mode 100755 index 0000000..11a5264 --- /dev/null +++ b/claude-templates/bin/agent-page @@ -0,0 +1,48 @@ +#!/bin/bash +# agent-page — page Craig's phone over Signal, from any machine or agent runtime. +# +# Usage: agent-page <message...> +# +# The pager identity (+15045173983) is registered in velox's signal-cli, so +# velox sends directly and every other machine ssh-relays the send over the +# tailnet. The recipient is Craig's Signal account UUID — his phone number +# reads as unregistered in Signal's directory, so never page the number. +# Verified end to end 2026-07-13 (phone push confirmed). +# +# This is the AWAY channel. At his desk, use the desktop channel instead: +# notify info "Title" "Message" --persist +# See protocols.org "Paging Craig" for choosing between them. +# +# Known caveats (tracked on the rulesets Signal-pager task): velox must be +# up and reachable on the tailnet, and the signal-cli account wants a +# periodic `receive` (staleness warnings appear otherwise). +# +# Source: ~/code/rulesets/claude-templates/bin/agent-page +# Install: make -C ~/code/rulesets install + +PAGER_ACCOUNT="+15045173983" +CRAIG_UUID="b1b5601e-6126-47f8-afaa-0a59f5188fde" +VELOX_HOST="velox.tailf3bb8c.ts.net" + +if [ $# -eq 0 ]; then + echo "usage: agent-page <message...>" >&2 + exit 2 +fi + +msg="$*" + +if [ "$(uname -n)" = "velox" ]; then + signal-cli -a "$PAGER_ACCOUNT" send -m "$msg" "$CRAIG_UUID" + rc=$? +else + # printf %q hardens the message for the remote shell. + ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \ + "$VELOX_HOST" \ + "signal-cli -a $PAGER_ACCOUNT send -m $(printf '%q' "$msg") $CRAIG_UUID" + rc=$? +fi + +if [ "$rc" -ne 0 ]; then + echo "agent-page: phone page failed (velox down or unreachable?) — fall back to the desktop channel: notify info 'Page' '<message>' --persist" >&2 +fi +exit "$rc" diff --git a/claude-templates/bin/ai b/claude-templates/bin/ai index 994dc1f..cf17875 100755 --- a/claude-templates/bin/ai +++ b/claude-templates/bin/ai @@ -1,16 +1,23 @@ #!/bin/bash -# ai — Claude Code session launcher (unified aix + hey) +# ai — agent session launcher (unified aix + hey) # # Usage: -# ai Select one or more projects via fzf and open each in -# an 'ai' tmux session window (creates session if needed). +# ai Pick the agent first (claude, codex/ChatGPT, or any +# local ollama model), then select one or more projects +# via fzf; each opens in an 'ai' tmux session window. # Git-aware: fetches, annotates with ↑/↓/dirty, auto-pulls -# clean-and-behind repos before opening. +# clean-and-behind repos before opening. --runtime or +# AI_RUNTIME skips the agent pick. # # ai <dir>... Single-project mode. Opens each given directory directly # in the 'ai' session (new window or switch to existing). # Use '.' for current directory. Git prep per dir. # +# ai --runtime <rt> Launch with a different agent CLI: claude (default), +# codex, or local (codex --oss against this machine's +# ollama; model per AI_LOCAL_MODEL, default gpt-oss:120b). +# Also settable via AI_RUNTIME. +# # ai --attach Attach to the existing 'ai' session without changes. # # ai -h | --help Show this help. @@ -25,7 +32,56 @@ # would kill the script. SESSION="ai" -CLAUDE_CMD="claude" +RUNTIME="${AI_RUNTIME:-claude}" +LOCAL_MODEL="${AI_LOCAL_MODEL:-gpt-oss:120b}" + +# Map the runtime name to the agent CLI a pane launches. All three take the +# opening instructions as a positional prompt. AGENT_BIN is the binary the +# dependency check probes; AGENT_CMD is the full launch command (the local +# runtime rides codex's open-source provider against the machine's ollama — +# model per AI_LOCAL_MODEL, default gpt-oss:120b, verified on ratio's +# Strix Halo 2026-07-13). +resolve_agent_cmd() { + case "$RUNTIME" in + claude) AGENT_BIN="claude"; AGENT_CMD="claude" ;; + codex) AGENT_BIN="codex"; AGENT_CMD="codex" ;; + local) AGENT_BIN="codex"; AGENT_CMD="codex --oss --local-provider=ollama -m $LOCAL_MODEL" ;; + *) + echo "ai: unknown runtime '$RUNTIME' — valid runtimes: claude, codex, local" >&2 + exit 2 + ;; + esac +} + +# One line per launchable agent, claude first (Enter-Enter keeps the old +# muscle memory). Local models appear only when both codex (the CLI that +# drives them) and a live ollama answer; a dead server just drops the lines. +build_runtime_choices() { + command -v claude >/dev/null 2>&1 && echo "claude — Claude Code" + command -v codex >/dev/null 2>&1 && echo "codex — ChatGPT (Codex CLI)" + if command -v codex >/dev/null 2>&1 && command -v ollama >/dev/null 2>&1; then + timeout 3 ollama list 2>/dev/null | tail -n +2 | awk 'NF {print "local:" $1 " — ollama"}' + fi +} + +# Interactive runtime pick for the bare-`ai` flow. Sets RUNTIME (and +# LOCAL_MODEL for a local pick) and re-resolves the agent command. +# Returns 1 when the pick is cancelled. +pick_runtime() { + local choice + choice=$(build_runtime_choices | fzf --height=30% --reverse --prompt='agent> ') || return 1 + [ -z "$choice" ] && return 1 + case "$choice" in + claude*) RUNTIME="claude" ;; + codex*) RUNTIME="codex" ;; + local:*) + RUNTIME="local" + LOCAL_MODEL="${choice#local:}" + LOCAL_MODEL="${LOCAL_MODEL%% *}" + ;; + esac + resolve_agent_cmd +} # Run in the pane's shell just before Claude launches. `stty susp undef` clears # the tty's SIGTSTP (C-z) character for this pane only, so an accidental C-z is @@ -46,16 +102,18 @@ build_instructions() { } usage() { - sed -n '2,20p' "$0" | sed 's|^# \?||' + sed -n '2,23p' "$0" | sed 's|^# \?||' exit 0 } -for cmd in fzf tmux claude; do - if ! command -v "$cmd" &>/dev/null; then - echo "ai: $cmd is not installed" >&2 - exit 1 - fi -done +check_deps() { + for cmd in fzf tmux "$AGENT_BIN"; do + if ! command -v "$cmd" &>/dev/null; then + echo "ai: $cmd is not installed" >&2 + exit 1 + fi + done +} # ---------- shared helpers ---------- @@ -73,7 +131,7 @@ create_window() { wid=$(tmux new-window -a -t "$SESSION:{end}" -n "$name" -c "$dir" -P -F '#{window_id}') sleep 0.1 instructions=$(build_instructions "$name") - tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter echo "$wid" } @@ -319,7 +377,7 @@ single_mode() { local instructions wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') instructions=$(build_instructions "$name") - tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter fi sort_windows @@ -379,7 +437,7 @@ multi_mode() { local instructions first_wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') instructions=$(build_instructions "$name") - tmux send-keys -t "$first_wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$first_wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter for entry in "${selected[@]:1}"; do dir="${entry/#\~/$HOME}" name="$(basename "$dir")" @@ -403,19 +461,78 @@ multi_mode() { attach_session } +# Print the launch command a real run would send to the pane, then exit. +# Exists for the launcher's bats tests: exercises runtime resolution and the +# opening line with no tmux or fzf involved. +print_launch_mode() { + local arg="$1" dir name + dir="$(cd "$arg" 2>/dev/null && pwd)" || { echo "ai: cannot access '$arg'" >&2; exit 1; } + if [ ! -f "$dir/.ai/protocols.org" ]; then + echo "ai: $dir has no .ai/protocols.org — not an agent-template project" >&2 + exit 1 + fi + name="$(basename "$dir")" + printf '%s "%s"\n' "$AGENT_CMD" "$(build_instructions "$name")" + exit 0 +} + # ---------- dispatch ---------- +print_launch="" +runtime_explicit="${AI_RUNTIME:+1}" +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + ;; + --runtime) + [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex, local" >&2; exit 2; } + RUNTIME="$2" + runtime_explicit=1 + shift 2 + ;; + --runtime=*) + RUNTIME="${1#--runtime=}" + runtime_explicit=1 + shift + ;; + --print-launch) + print_launch=1 + shift + ;; + --print-runtimes) + build_runtime_choices + exit 0 + ;; + *) + break + ;; + esac +done + +resolve_agent_cmd + +if [ -n "$print_launch" ]; then + [ $# -eq 0 ] && { echo "ai: --print-launch needs a project directory" >&2; exit 2; } + print_launch_mode "$1" +fi + case "${1:-}" in - -h|--help) - usage - ;; --attach) + check_deps attach_mode ;; "") + # Bare `ai`: pick the agent first (skipped when --runtime or AI_RUNTIME + # already chose), then the familiar project multi-select. + if [ -z "$runtime_explicit" ]; then + pick_runtime || exit 0 + fi + check_deps multi_mode ;; *) + check_deps for arg in "$@"; do single_mode "$arg" done |
