aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/design/module-inventory.org2
-rw-r--r--init.el2
-rw-r--r--modules/ai-term.el21
-rw-r--r--modules/linear-config.el58
-rw-r--r--modules/pearl-config.el66
-rw-r--r--tests/test-ai-term--close.el31
-rw-r--r--tests/test-init-module-headers.el2
-rw-r--r--todo.org407
8 files changed, 362 insertions, 227 deletions
diff --git a/docs/design/module-inventory.org b/docs/design/module-inventory.org
index 385bdbd5..2d4baf81 100644
--- a/docs/design/module-inventory.org
+++ b/docs/design/module-inventory.org
@@ -220,7 +220,7 @@ owns the intentional end-of-startup buffer-bury timer.
| Module | Layer | Cat | Current | Target | Runtime requires | Top-level side effects | Direct load |
|--------+-------+-----+---------+--------+------------------+------------------------+-------------|
-| =linear-config= | 3 | D/P | eager | command | system-lib | package config | yes |
+| =pearl-config= | 3 | D/P | eager | command | system-lib | package config | yes |
| =local-repository= | 4 | O/D/P | eager | command | elpa-mirror | none | yes |
| =lorem-optimum= | 4 | O/L | eager | command | cl-lib | none | yes |
| =mail-config= | 3 | D/P | eager | command | user-constants, system-lib, mu4e-attachments, keybindings | cj/email-map under cj/custom-keymap, add-hook, 2 advice, 1 global key | yes |
diff --git a/init.el b/init.el
index 390de45e..fb6d55af 100644
--- a/init.el
+++ b/init.el
@@ -73,7 +73,7 @@
(require 'diff-config) ;; diff and merge functionality w/in Emacs
(require 'erc-config) ;; seamless IRC client
(require 'slack-config) ;; slack client via emacs-slack
-(require 'linear-config) ;; Linear.app issue tracking (deepsat workspace)
+(require 'pearl-config) ;; Linear.app issue tracking via pearl (deepsat + craigjennings)
(require 'telega-config) ;; telegram client via telega.el (TDLib in docker)
(require 'signal-config) ;; signal client via forked signel + signal-cli
(require 'eshell-config) ;; emacs shell configuration
diff --git a/modules/ai-term.el b/modules/ai-term.el
index 1384f812..baf752fe 100644
--- a/modules/ai-term.el
+++ b/modules/ai-term.el
@@ -54,8 +54,10 @@
;; instead of toggling the current one.
;; - M-F9 `cj/ai-term-close' -- gracefully close an agent: kill its
;; tmux session (stopping the agent process), then its terminal
-;; buffer and window. Confirms first. Targets the current
-;; agent, the sole live agent, or prompts among several.
+;; buffer. Its window stays in the layout (swapped to the
+;; working buffer), so closing never collapses a split. Confirms
+;; first. Targets the current agent, the sole live agent, or
+;; prompts among several.
;; - C-S-F9 `cj/ai-term-close' -- same close command, second binding.
;; (M-F9 is the primary; C-S-F9 may be swallowed by the
;; Wayland/PGTK layer on some machines.)
@@ -859,12 +861,14 @@ down."
(error nil)))
(defun cj/--ai-term-close-buffer (buffer)
- "Gracefully tear down AI-term BUFFER: tmux session, window, buffer.
+ "Gracefully tear down AI-term BUFFER: tmux session, then buffer.
Derives the tmux session name from BUFFER's `default-directory' (the
project dir the terminal was created in) and kills it so the agent
-process stops. Deletes BUFFER's window when it's shown and isn't the
-only window in its frame, then kills BUFFER (suppressing the
+process stops. When BUFFER is shown, swaps its window to a non-agent
+buffer (the working file) rather than deleting the window -- closing an
+agent must not collapse the user's window layout; the F9 hide toggle is
+what collapses the split. Then kills BUFFER (suppressing the
process-still-running prompt -- the session is already down). No-op
when BUFFER isn't an AI-term buffer."
(when (cj/--ai-term-buffer-p buffer)
@@ -872,8 +876,11 @@ when BUFFER isn't an AI-term buffer."
(cj/--ai-term-tmux-session-name
(buffer-local-value 'default-directory buffer)))
(let ((win (get-buffer-window buffer)))
- (when (and win (> (length (window-list (window-frame win) 'never)) 1))
- (delete-window win)))
+ (when (window-live-p win)
+ (with-selected-window win
+ (switch-to-buffer
+ (or (cj/--ai-term-most-recent-non-agent-buffer)
+ (other-buffer buffer t))))))
(let ((kill-buffer-query-functions nil))
(kill-buffer buffer))))
diff --git a/modules/linear-config.el b/modules/linear-config.el
deleted file mode 100644
index 8fbae30c..00000000
--- a/modules/linear-config.el
+++ /dev/null
@@ -1,58 +0,0 @@
-;;; linear-config.el --- Linear.app integration -*- lexical-binding: t; -*-
-;; author: Craig Jennings <c@cjennings.net>
-
-;;; Commentary:
-;;
-;; Layer: 3 (Domain Workflow).
-;; Category: D/P.
-;; Load shape: deferred (command-loaded).
-;; Top-level side effects: package configuration via use-package.
-;; Runtime requires: none.
-;; Direct test load: no.
-;;
-;; Near-vanilla pearl setup: close to what pearl's README documents for a
-;; first-time install (local checkout instead of a package archive), with two
-;; deliberate tweaks layered on after dogfooding the out-of-box experience — a
-;; global C-; L prefix (see below) and the shorter assignee @-tag.
-;;
-;; pearl owns its own keymap. `pearl-mode' turns on automatically in any buffer
-;; pearl renders (it carries a `#+LINEAR-SOURCE' header) and binds the whole
-;; command surface under `pearl-keymap-prefix' (default "C-; L"). This config
-;; also binds that same `pearl-prefix-map' globally under C-; L (`:bind-keymap'),
-;; so the full command surface is reachable from any buffer; the first press
-;; autoloads pearl. `M-x pearl-menu' / `M-x pearl-list-issues' still work too.
-;;
-;; Authentication: the Linear personal API key is read from authinfo.gpg. Add:
-;; machine api.linear.app login apikey password lin_api_YOURKEYHERE
-;; Generate it in Linear: Settings -> Security & access -> Personal API keys.
-
-;;; Code:
-
-(use-package pearl
- :ensure nil ;; local checkout, not from an archive
- :load-path "~/code/pearl"
- :commands (pearl-menu pearl-list-issues pearl-create-issue pearl-run-linear-view)
- ;; Bind pearl's command map globally under C-; L, so the full surface is
- ;; reachable from any buffer (not only inside a pearl-rendered one). The
- ;; first press autoloads pearl; it's the same `pearl-prefix-map' that
- ;; `pearl-mode' binds in-buffer, so behavior is identical everywhere.
- :bind-keymap ("C-; L" . pearl-prefix-map)
- :custom
- (pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory))
- ;; Shorten the assignee @-tag to the first name only (e.g. @first instead of
- ;; @first_last), trading disambiguation for a tighter tag line.
- (pearl-assignee-tag-short t)
- ;; Optional defaults — uncomment and fill in to skip the prompts. Set them
- ;; HERE, at init level, not via M-x pearl-set-default-view /
- ;; pearl-set-default-team: those persist through `customize-save-variable',
- ;; and this config redirects `custom-file' to a throwaway temp file
- ;; (system-defaults.el), so a setter's value is discarded on the next
- ;; restart. These :custom lines re-apply on every startup instead.
- ;; (pearl-default-view "My active work") ;; the local view `C-; L l' opens
- ;; (pearl-default-team-id "9fca2cf6-390c-4102-a9ff-f94a4ed823c5") ;; DeepSat SE; skips the team prompt on create / by-project
- :config
- (setq pearl-api-key
- (auth-source-pick-first-password :host "api.linear.app")))
-
-(provide 'linear-config)
-;;; linear-config.el ends here
diff --git a/modules/pearl-config.el b/modules/pearl-config.el
new file mode 100644
index 00000000..b58812ee
--- /dev/null
+++ b/modules/pearl-config.el
@@ -0,0 +1,66 @@
+;;; pearl-config.el --- Linear.app integration via pearl -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;;
+;; Layer: 3 (Domain Workflow).
+;; Category: D/P.
+;; Load shape: deferred (command-loaded).
+;; Top-level side effects: package configuration via use-package.
+;; Runtime requires: none.
+;; Direct test load: no.
+;;
+;; Near-vanilla pearl setup (local checkout instead of a package archive), in
+;; multi-account mode: two Linear workspaces, deepsat (work) and craigjennings
+;; (personal), named by Linear's own urlKey. Each account renders to its own
+;; Org file, deepsat.pearl.org / craigjennings.pearl.org, so they never collide.
+;; `M-x pearl-switch-account' swaps the active one; the mode line shows it.
+;;
+;; pearl owns its own keymap. `pearl-mode' turns on automatically in any buffer
+;; pearl renders (it carries a `#+LINEAR-SOURCE' header) and binds the whole
+;; command surface under `pearl-keymap-prefix' (default "C-; L"). This config
+;; also binds that same `pearl-prefix-map' globally under C-; L (`:bind-keymap'),
+;; so the full command surface is reachable from any buffer; the first press
+;; autoloads pearl. `M-x pearl-menu' / `M-x pearl-list-issues' still work too.
+;;
+;; Authentication: each account reads its key from authinfo.gpg by a distinct
+;; login under the api.linear.app host:
+;; machine api.linear.app login apikey password lin_api_<deepsat key>
+;; machine api.linear.app login pearl-personal password lin_api_<personal key>
+;; Generate keys in Linear: Settings -> Security & access -> Personal API keys.
+
+;;; Code:
+
+(use-package pearl
+ :ensure nil ;; local checkout, not from an archive
+ :load-path "~/code/pearl"
+ :commands (pearl-menu pearl-list-issues pearl-create-issue
+ pearl-run-linear-view pearl-switch-account)
+ ;; Bind pearl's command map globally under C-; L, so the full surface is
+ ;; reachable from any buffer (not only inside a pearl-rendered one). The
+ ;; first press autoloads pearl; it's the same `pearl-prefix-map' that
+ ;; `pearl-mode' binds in-buffer, so behavior is identical everywhere.
+ :bind-keymap ("C-; L" . pearl-prefix-map)
+ :custom
+ ;; Shorten the assignee @-tag to the first name only (e.g. @first instead of
+ ;; @first_last), trading disambiguation for a tighter tag line.
+ (pearl-assignee-tag-short t)
+ ;; Two workspaces, keyed by Linear's urlKey. Each resolves its API key from
+ ;; authinfo.gpg by its own login (see Commentary), renders to its own Org
+ ;; file, and carries a default team so create / by-project skip the prompt.
+ (pearl-accounts
+ '(("deepsat"
+ :api-key-source (:auth-source :host "api.linear.app" :user "apikey")
+ :org-file "~/org/gtd/deepsat.pearl.org"
+ :default-team-id "9fca2cf6-390c-4102-a9ff-f94a4ed823c5") ;; DeepSat SE
+ ("craigjennings"
+ :api-key-source (:auth-source :host "api.linear.app" :user "pearl-personal")
+ :org-file "~/org/gtd/craigjennings.pearl.org"
+ :default-team-id "ee285e6c-fcc9-4dd6-9292-c47f2df75b82"))) ;; Pearl
+ ;; Which workspace pearl opens into. Dogfooding the personal account through
+ ;; Sunday; flip back to "deepsat" to make work primary again (one string), or
+ ;; switch per-session at runtime with `M-x pearl-switch-account'.
+ (pearl-default-account "craigjennings"))
+
+(provide 'pearl-config)
+;;; pearl-config.el ends here
diff --git a/tests/test-ai-term--close.el b/tests/test-ai-term--close.el
index 654e85f0..4098c091 100644
--- a/tests/test-ai-term--close.el
+++ b/tests/test-ai-term--close.el
@@ -13,7 +13,9 @@
(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'ai-term)
+(require 'testutil-ghostel-buffers)
(ert-deftest test-ai-term--kill-tmux-session-runs-kill-session ()
"Normal: invokes `tmux kill-session -t <session>'."
@@ -58,6 +60,35 @@
(should (buffer-live-p buf)))
(when (buffer-live-p buf) (kill-buffer buf)))))
+(ert-deftest test-ai-term--close-buffer-keeps-window-split ()
+ "Regression: closing an agent in a split keeps its window in the layout,
+showing a non-agent buffer, instead of deleting the split. Craig's M-F9
+annoyance -- a close must not tear down the window arrangement (the F9 hide
+toggle is what collapses the split; close should not)."
+ (cj/test--kill-agent-buffers)
+ (let ((work (get-buffer-create "*test-close-keep-work*"))
+ (agent (get-buffer-create "agent [close-keep]")))
+ (with-current-buffer agent (setq-local default-directory "/tmp/close-keep/"))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) work)
+ (let ((agent-win (split-window (selected-window) nil 'below)))
+ (set-window-buffer agent-win agent)
+ (should-not (one-window-p))
+ (cl-letf (((symbol-function 'cj/--ai-term-kill-tmux-session)
+ (lambda (_s) 0)))
+ (cj/--ai-term-close-buffer agent))
+ ;; The window survives the close ...
+ (should (window-live-p agent-win))
+ (should-not (one-window-p))
+ ;; ... now showing a non-agent buffer ...
+ (should-not (cj/--ai-term-buffer-p (window-buffer agent-win)))
+ ;; ... and the agent buffer itself is gone.
+ (should-not (buffer-live-p agent))))
+ (when (get-buffer "*test-close-keep-work*") (kill-buffer "*test-close-keep-work*"))
+ (cj/test--kill-agent-buffers))))
+
(ert-deftest test-ai-term--close-target-current-agent-buffer ()
"Normal: returns the current buffer when it is an agent buffer."
(let ((buf (get-buffer-create "agent [cur]")))
diff --git a/tests/test-init-module-headers.el b/tests/test-init-module-headers.el
index 2680a19c..bbda2388 100644
--- a/tests/test-init-module-headers.el
+++ b/tests/test-init-module-headers.el
@@ -113,7 +113,7 @@
"jumper"
"latex-config"
;; Batch 9 — Remaining domain / integration / optional modules (Layer 2-4)
- "linear-config"
+ "pearl-config"
"local-repository"
"lorem-optimum"
"mail-config"
diff --git a/todo.org b/todo.org
index beaa08b1..4936ec78 100644
--- a/todo.org
+++ b/todo.org
@@ -41,15 +41,52 @@ Tags are additive. For example, a small wrong-behavior fix can be
=:bug:quick:=, and a feature that requires internal restructuring can be
=:feature:refactor:=.
* Emacs Open Work
-** TODO [#C] Pearl vanilla dogfooding follow-ups :pearl:cleanup:
+** TODO [#B] Dashboard keybinding changes :quick:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-05
+:LAST_REVIEWED: 2026-06-06
+:END:
+pressing g has should refresh. find another binding for Telegram.
+** TODO [#A] Calibre Open Work :calibre:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-06
:END:
-From the pearl-session handoff (2026-06-02) after =modules/linear-config.el= was reduced to a vanilla pearl setup (commit 09b3b13). Open items to revisit after dogfooding pearl's out-of-box config:
+Parent grouping the open Calibre / ebook-workflow issues; close each child independently. The EPUB reading-width tasks were already resolved (2026-05-12/14).
+
+*** DOING Calibre bookmark title format :feature:solo:quick:
+When I hit m in calibre, I'm making my place in the book with a bookmark.
+While sometimes, the books look fine: "The A.B.C. Murders - Agatha Christie.epub"
+Sometimes they look not so good: Engines of Logic_ Mathematicians and the O - Martin Davis.pdf or Software Architecture_ The Hard Parts _ Mo - Neal Ford.pdf
+
+What I would like to do is to have the bookmarks be saved in the following format:
+
+Author, Title [no extension]. Underscores should be stripped.
-- Decide whether to restore any conveniences dropped in the vanilla rework: default team (DeepSat SE id, in a commented =:custom= block in linear-config.el), the custom keymap, the lazy-key advice. Weigh the eager-:config key read (fires the GPG prompt on first pearl command vs never-at-startup).
-- Confirm linear-config.el's module-header fields (Layer / Runtime requires / Direct test load) pass any module-doc linter this repo runs.
-- custom-file interaction: =system-defaults.el= redirects =custom-file= to a throwaway temp, so pearl's =customize-save-variable= persistence (default view/team) holds for the session but vanishes on restart. Set those via init-level =:custom= instead. (A pearl-side task was filed to harden pearl's persistence against a disabled custom-file.)
+Root cause: in a nov buffer =m= is =bookmark-set= (rebound at calibredb-epub-config.el:311); nov's =nov-bookmark-make-record= names the record =(buffer-name)= -- the EPUB filename.
+
+Implemented 2026-06-06. Source decision: parse the *filename*, not the embedded EPUB metadata -- under Calibre's "<Title> - <Author>.epub" naming the filename is more complete (the embedded metadata had truncated titles, author-sort "Last, First" forms, and lost punctuation; see the separate metadata-cleanup task). A =:filter-return= advice on =nov-bookmark-make-record= rebuilds the name from the record's filename: split on the last " - " into title/author, restore the colon Calibre sanitized to "_ " (-> ": "), reorder to "Author, Title". Pure helpers =cj/--nov-clean-title= + =cj/--nov-bookmark-name-from-file= in =modules/calibredb-epub-config.el=; 10 ERT tests in =tests/test-calibredb-epub-config--bookmark-name.el=. Live in the daemon.
+
+Existing bookmarks: the 3 nov bookmarks in =~/sync/org/emacs_bookmarks= were renamed by hand (one-pass, in the daemon + saved; backup at =emacs_bookmarks.bak-2026-06-06=): "Edward Kanterian, Frege: A Guide for the Perplexed", "Agatha Christie, The A.B.C. Murders", "Edward Abbey, The Fool's Progress: An Honest Novel".
+
+Awaiting Craig's manual confirm: make a NEW bookmark (open an EPUB, hit m) and check the default name is "Author, Title" from the filename.
+
+*** DOING [#A] Reconsider Calibre keybindings :feature:ux:
+Relocated from the global capture inbox 2026-06-06. Want a discoverable set of keybindings (visible in which-key) for the most frequent calibredb workflows:
+- Switch to a library (e.g. Literature), sort by last name, scroll the list.
+- Scope/filter the list in place, keeping the current library scope:
+ - by format (e.g. epubs only)
+ - by author last name (exact == or ^begins-with some text)
+ - sort by title, publication date, or group by format
+- One key pops up the selected book's description in a bottom-30% buffer, dismissed with q (same display pattern as the signel chat dock).
+- RET opens the book in the appropriate viewer.
+Survey finding 2026-06-06: calibredb already binds almost all of this in calibredb-search-mode-map (S/L library, g filter [f format, a author, t tag, d date], o sort [t title, a author, p pubdate, f format], RET open) and even ships transient menus (? = calibredb-dispatch, g, o). The real problem was discoverability -- they are top-level single keys (which-key never pops up) and Craig didn't know ? opened a menu. calibredb-quick-look is macOS-only; the detail view (v -> *calibredb-entry*, q quits) is the description but opens full-window.
+
+Implemented 2026-06-06 in =modules/calibredb-epub-config.el=:
+- A curated transient =cj/calibredb-menu= (library switch; filter format/author/reset; sort author/title/pubdate/format; open; describe; H = full calibredb-dispatch) bound to =?= in calibredb-search-mode-map. calibredb's own full dispatch moved to =H=. Defined in the use-package =:config= (needs the elpa transient, which batch doesn't load) -- the "? brings up a curated help menu" convention.
+- Bottom-30% description dock: =calibredb-show-entry-switch= -> =pop-to-buffer= + a =display-buffer-alist= rule for =*calibredb-entry*= (display-buffer-at-bottom, height 0.3); =cj/calibredb-describe-at-point= shows the entry without switching focus so q dismisses it. Same pattern as the signel chat dock.
+1 ERT test (the describe command; the transient/bindings/dock need the elpa transient + live calibredb, verified in the daemon). Author "begins-with" is covered well enough by g a's completing-read over "Last, First"; a true regex filter was not built. Awaiting Craig's manual verify (M-B -> ? menu; d/v docked description; H full menu).
+
+*** TODO Embed Calibre DB metadata into the EPUB files :data:maintenance:
+Surfaced 2026-06-06 while building the bookmark naming: the metadata embedded in the EPUB files' OPF is worse than Calibre's database metadata. nov reads the embedded OPF and got truncated titles ("Frege" vs the filename's "Frege: A Guide for the Perplexed"), author-sort "Last, First" forms ("Christie, Agatha"), and lost punctuation ("A.B.C." -> "A B C"). The filenames (from Calibre's curated DB) are the good copy. Fix on the Calibre side: select all (or by library), run "Edit metadata -> Embed metadata into book files" so the DB metadata is written into each EPUB's OPF. Consider auditing author vs author_sort first. After embedding, the in-file metadata matches the library and any tool reading the files (nov, other readers, re-imports) gets the good data. Not an Emacs task; Calibre-side bulk maintenance.
** TODO [#B] TTY-accessible personal C-; keymap :feature:ux:solo:quick:
:PROPERTIES:
@@ -66,9 +103,9 @@ Easy prefix candidates (home-row-leaning, TTY-safe), same leaf keys under each:
While in here, audit individual leaf chords for other non-TTY keys (any =C-RET=, super/hyper bindings — terminals can't send super/hyper either) and note or remap them. Verify the result in an actual =emacs -nw= / =emacsclient -nw= frame, not just GUI. Relates to the standing "org-mode keybinding consolidation" reminder.
-** DOING [#B] Signal client — forked signel :feature:
+** DOING [#B] Signel Client Open Work
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-05
+:LAST_REVIEWED: 2026-06-06
:END:
Parent task for the Emacs Signal client. Engine: signal-cli (linked secondary device). Front end: a fork of signel at =~/code/signel=, wired through =modules/signal-config.el=. Design: [[file:docs/design/signal-client.org][docs/design/signal-client.org]]. Child issues below.
@@ -110,125 +147,7 @@ Verified: (1) new contract test =test-signal-config-prefix-map-registered-under-
*** 2026-05-28 Thu @ 03:09:18 -0500 Chat buffer docks bottom 30% and C-c C-k cancels
=display-buffer-alist= entry in =modules/signal-config.el= matches =^\*Signel: = chat buffers and routes them through =display-buffer-at-bottom= with =window-height . 0.3=, so the chat docks to the bottom 30% of the frame. The signel fork's =signel-chat= switched from =switch-to-buffer= to =pop-to-buffer= so the rule can apply (=switch-to-buffer= ignores =display-buffer-alist=). =C-c C-c= was already bound to =signel--send-input= in the mode; =C-c C-k= now binds =signel--cancel-input=, a new fork helper that clears the editable region between =signel--input-marker= and =point-max= and then calls =quit-window=. Buffer stays alive so chat history above the marker survives revisits; cleared input means the next visit lands on a fresh prompt. Five ERT tests in =tests/test-signel-cancel-input.el= (clears pending, empty-area no-op, quit-window called, buffer preserved, keymap binding) and two new tests in =tests/test-signal-config.el= (entry shape + regex match set). Dotemacs commit 998e9c7a, fork commit df02d79.
-** TODO [#B] Emacs Manual Testing and Validation :verify:
-SCHEDULED: <2026-05-29 Fri>
-:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
-:END:
-
-Hand-verify checklist Craig walks one item at a time after the relevant code lands. Each child names what is being verified, the exact steps to run, and the observable expected result. On pass, the child gets marked or deleted. On fail, the actual behavior gets logged under the step and the child is promoted to a top-level =TODO= bug per the verification.md handoff rule.
-
-Walk started 2026-05-28 (tests 1 + 2 verified — surfaced two Signel bugs along the way, both fixed before continuing). Deferred to 2026-05-29: test 3 onward needs sending an actual Signal message, too late at night to be polite about it. Picker → chat buffer opens cleanly; the send half is what remains to exercise.
-
-*** 2026-05-28 Thu @ 02:13:55 -0500 Verified: connect starts the daemon (after fix)
-=C-; M SPC= → "Signel connected." in echo area; =M-x list-processes= shows =signal-rpc= running (PID 1775279, command =/usr/bin/signal-cli -a +1510...=). Two bugs surfaced and fixed during the verify:
-- The =with-eval-after-load 'keybindings= binding at =signal-config.el:280= didn't take effect on a fresh Emacs restart; a live-reload of =signal-config.el= activated the =C-; M= prefix. Logged as a separate top-level TODO for follow-up (load-order or use-package interaction).
-- =cj/signel--ensure-started= referenced =signel--process-name= before signel had been autoloaded — the bare forward-declared =(defvar signel--process-name)= didn't actually bind the variable. Fix: added =(require 'signel)= at the top of the function (=signal-config.el:170=) so the package loads before any of its private variables are read. New ERT test =test-signal-config-ensure-started-requires-signel= captures the bug.
-
-*** 2026-05-28 Thu @ 02:16:45 -0500 Verified: picker opens with contact names
-=C-; M m= → minibuffer opened within ~1s, "Note to Self" pinned at the top, the 94 Signal contacts followed labeled "Name (+number)". Picker behavior matches spec. Surfaced a follow-up on the chat buffer that opens after a pick — placement + exit keys want refining; filed under L44 Signel.
-
-*** Signel: pick a contact and send a message
-What we're verifying: choosing a contact opens a chat buffer, =RET= at the prompt sends through =signel--send-input=, and the message arrives on the recipient's phone.
-- =C-; M m=, pick a contact you trust.
-- Type a short message at the prompt, press =RET=.
-- Check the recipient's phone.
-Expected: a =*Signel: +<number>*= buffer opens, the typed message renders with the =[HH:MM] <Me>= prefix on send, and arrives on the recipient's phone within a few seconds.
-
-*** Signel: Note-to-Self lands in the right Signal thread
-What we're verifying: =cj/signel-message-self= (=C-; M s=) resolves to =signel-account= and sending through it lands in the *Note to Self* thread on the phone, NOT a self-addressed display anomaly. This is the spec's medium-priority manual verify from D3.
-- Press =C-; M s=.
-- Type "test note to self" at the prompt, press =RET=.
-- Open Signal on your phone, scroll to the *Note to Self* thread.
-Expected: a =*Signel: +<your-number>*= buffer opens in Emacs, the message sends, and the message appears in the phone's *Note to Self* thread (not in any other conversation).
-
-*** Signel: Note-to-Self via the picker's pinned entry
-What we're verifying: picking the pinned "Note to Self" entry through =cj/signel-message= resolves the same way as the direct command.
-- =C-; M m=, choose "Note to Self".
-Expected: the same =*Signel: +<your-number>*= buffer opens. (No need to re-send; opening the right buffer proves the resolution.)
-
-*** Signel: typed input survives an incoming message
-What we're verifying: the clobber fix (fork commit 5ec56c0) preserves in-progress prompt input across =signel--insert-msg= when a message arrives mid-typing.
-- =C-; M m=, pick a contact.
-- Type a long unsent message at the prompt, do NOT press =RET=.
-- From a second device or by asking someone, send yourself a Signal message that lands in this chat (or any active chat).
-Expected: the incoming message renders above the prompt, the prompt redraws, and your typed text is still there at the prompt ready to send.
-
-*** Signel: dashboard opens
-What we're verifying: =signel-dashboard= (=C-; M d=) opens the active-chats dashboard.
-- Press =C-; M d=.
-Expected: a dashboard buffer opens listing active chats.
-
-*** Signel: stop tears down the daemon
-What we're verifying: =signel-stop= (=C-; M q=) deletes the process and clears the request-handler / buffer maps (the reconnect-invalidation contract from fork commit 4740d97).
-- Press =C-; M q=.
-- =M-x list-processes=.
-Expected: echo area shows "Signel service stopped.", and =list-processes= no longer lists =signal-rpc=.
-
-*** Signel: refresh forces a fresh contact fetch
-What we're verifying: =cj/signel-refresh-contacts= clears the cache and re-fetches via the new callback contract.
-- =C-; M SPC= to reconnect if you ran the stop test above.
-- =M-x cj/signel-refresh-contacts=.
-- Immediately =C-; M m=.
-Expected: the picker still opens cleanly with the same contact list (the refresh is silent; the picker is the visible check). If you added a contact on the phone, it now appears.
-
-*** Font setup reaches a GUI frame created after a TTY frame (daemon)
-What we're verifying: emoji glyphs + fonts apply in a GUI frame even when the first daemon frame was a TTY.
-- emacs --daemon
-- emacsclient -t (TTY frame first)
-- emacsclient -c (then a GUI frame)
-- in the GUI frame, open a buffer with an emoji and check it renders, and M-S-f / fonts look right
-Expected: emoji renders and fonts are applied in the GUI frame.
-
-*** ghostel migration: Claude Code TUI in a GUI frame
-What we're verifying: an agent runs in ghostel with good rendering (the reason for the engine swap).
-- restart Emacs (the migration changes load order + a use-package :config block)
-- in a GUI frame press F9, pick a project, let Claude stream a long response (big diff or file read)
-Expected: colors look right (not washed out), no flicker/strobing during the stream, box-drawing and the cursor render correctly.
-
-*** ghostel migration: Claude Code TUI in a TTY frame (replaces the old refuse test)
-What we're verifying: D4 dropped the GUI-only guard, so F9 now launches in a terminal frame too.
-- emacsclient -t (TTY frame, off the running daemon)
-- in the TTY frame press F9 and pick a project
-Expected: the agent launches and renders as text + color in the TTY (no echo-area refusal message); inline images are absent, which is expected.
-
-*** ghostel migration: F9 / C-F9 / M-F9 dispatch
-What we're verifying: the agent dispatch behaves as it did on vterm.
-- F9 toggles the agent window off/on; C-F9 always opens the project picker; M-F9 closes (kills the tmux session) after confirm
-- press F9 from inside an agent buffer (full-frame) — it should toggle, not get swallowed by the terminal
-Expected: each chord does its job from both normal and agent buffers.
-
-*** ghostel migration: tmux integration + C-; x menu
-What we're verifying: the tmux machinery ported intact.
-- launch an agent; M-x list it — runs in tmux session aiv-<project>
-- second F9 on the same project reattaches (no duplicate session)
-- C-; x h captures the tmux pane history into an Emacs buffer; C-; x c enters tmux copy-mode
-- C-; x l clears scrollback; C-; x n / p navigate prompts
-Expected: all menu commands work against the ghostel buffer; history capture + copy-mode behave as before.
-
-*** ghostel migration: copy-mode parity + mouse wheel
-What we're verifying: copy/selection and wheel scrolling survived the engine swap.
-- in a ghostel buffer enter copy-mode (C-; x c without tmux, or the tmux path with tmux); M-w copies and stays; q / C-g exit
-- mouse-wheel scroll inside tmux, inside Claude Code, and inside lazygit
-Expected: M-w copies without leaving; q/C-g exit; the wheel scrolls the program (this replaces the removed vterm wheel-forwarding — confirm ghostel's native SGR mouse covers it).
-
-*** ghostel migration: other TUIs + ssh
-What we're verifying: general terminal workloads render.
-- run lazygit, htop/btop, a heavy-output build, and ssh to a remote host in a ghostel terminal (F12)
-Expected: each renders and behaves correctly; ssh out works (if a remote lacks xterm-ghostty terminfo, note it — ghostel-ssh-install-terminfo / ghostel-term is the lever).
-
-*** ghostel migration: F12 general terminal + dashboard launcher
-What we're verifying: F12 manages non-agent terminals only, and the dashboard launcher uses ghostel.
-- F12 opens/toggles a general terminal; confirm it does NOT grab an agent buffer; resize it, toggle off and on — geometry is preserved
-- from the dashboard press t (Terminal) — opens a ghostel terminal (tooltip reads "Launch Terminal")
-Expected: F12 excludes agent buffers and keeps saved geometry; the dashboard launches ghostel.
-
-*** ghostel migration: crash recovery
-What we're verifying: the aiv- tmux session survives an Emacs crash and reattaches.
-- with a live agent, kill Emacs (not the tmux session); restart Emacs; F9 → project picker
-Expected: the project shows "[detached]" and reattaches to the surviving tmux session.
-
-** DOING [#B] Migrate all terminals from vterm to ghostel :terminal:ghostel:
+** DOING [#B] Migrate All Terminals From Vterm to Ghostel :terminal:ghostel:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-04
:END:
@@ -290,12 +209,6 @@ Folded the external review via spec-response. Craig accepted D1-D5; baked them p
*** 2026-06-04 Thu @ 23:30:18 -0500 External re-review: ready
Re-reviewed [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] after incorporation. Verdict: =Ready=. No further blocking review notes; implementation can start from the phase plan and acceptance criteria in the spec.
-** TODO [#C] Slack message buffers in a reused popup window :slack:ux:quick:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-05
-:END:
-Display slack.el message and thread buffers in a dedicated popup window (side or bottom) and reuse that one window instead of spawning a new window per buffer. Likely a =display-buffer-alist= rule (or popper integration) in =modules/slack-config.el=.
-
** PROJECT [#B] Implement ai-kb :feature:ai:kb:
Build v1 of the AI knowledge base per [[file:docs/design/ai-kb.org][docs/design/ai-kb.org]] (Ready; six reviews incorporated, all decisions resolved 2026-05-24). Step 1 splits into 1a (the safe write path — minimum usable) and 1b (retrieval, maintenance, push), since =remember= depends on =index=+=lint= and the adapter depends on =remember=. Step 2 is the Emacs layer: a full org-roam profile on switch, the human-edit safety model (same write path as the agent), and the browsing surface. Step 3 and the LLM-Wiki layer are vNext. Children are ordered by build sequence; the server bootstrap is the prerequisite.
@@ -704,7 +617,7 @@ Update the =dev-fkeys.el= header comment (L33) — TS/JS is no longer punted; th
** TODO [#B] Fix up test runner :bug:
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-06-06
:END:
*** 2026-05-16 Sat @ 11:15:51 -0500 Ideas
**** Current State
@@ -2545,7 +2458,23 @@ configuration (=text-config=, =diff-config=, =ledger-config=,
=games-config=, =mu4e-org-contacts-setup=, =telega-config=,
=httpd-config=, =org-agenda-config-debug=).
-** TODO [#C] M-F9 ai-vterm close removes the window split :quick:solo:
+** TODO [#B] Add Signal to the dashboard :quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-01
+:END:
+** TODO [#C] Consider consolidating/harmonizing the UI in all Message Clients
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-06
+:END:
+They should have the same UI paradigms and patters for consistency.
+** TODO [#C] Slack message buffers in a reused popup window :slack:ux:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-05
+:END:
+Display slack.el message and thread buffers in a dedicated popup window (side or bottom) and reuse that one window instead of spawning a new window per buffer. Likely a =display-buffer-alist= rule (or popper integration) in =modules/slack-config.el=.
+
+** DONE [#C] M-F9 ai-vterm close removes the window split :quick:solo:
+CLOSED: [2026-06-06 Sat]
:PROPERTIES:
:LAST_REVIEWED: 2026-06-02
:END:
@@ -2554,6 +2483,9 @@ Closing the ai-vterm with M-F9 while its window is in a split deletes the split
*** 2026-06-02 Tue @ 14:12:48 -0500 Audit: still a bug, distinct from the F9 collapse
The F9 toggle-off rework (38dad92) made F9 collapse the split by design, but that's the toggle path. This is M-F9 close (kills the agent process): close should leave the surrounding layout intact, not delete the sibling window. Craig confirmed it's still a bug. cj/--ai-vterm-close-buffer still calls delete-window.
+*** 2026-06-06 Sat @ 18:18:17 -0500 Fixed: close swaps the window to a non-agent buffer instead of deleting it
+=cj/--ai-term-close-buffer= no longer calls =delete-window=; it swaps the agent's window to the working buffer (=cj/--ai-term-most-recent-non-agent-buffer=), then kills the agent buffer, so the split survives. F9 hide still collapses the split by design; close no longer does. Regression test =test-ai-term--close-buffer-keeps-window-split=. Commit =1a097b7e=.
+
** TODO [#C] Implement EMMS-free music-config architecture :refactor:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-01
@@ -2679,7 +2611,7 @@ Depends on: command + Dired/Dirvish rewire.
** TODO [#C] music-config option-combination audit + tests :tests:harden:solo:
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-06-06
:END:
Two-part task surfaced 2026-05-28 during the Signel verify walk — generalized from the "are there combinations of options that we'd want to disallow together" question.
@@ -3855,18 +3787,6 @@ Write the README at the artifact: short prose entry point summarizing the tier m
*** TODO [#C] Commentary header in early-init.el :docs:
Add a Commentary-section header in =early-init.el= pointing at =.localrepo/README.org= for usage and =docs/design/localrepo.org= for architecture. Sits at the top of the localrepo block (around L130).
-** TODO [#D] Treesitter grammar offline cache :feature:offline:localrepo:
-Treesitter grammars are downloaded by =treesit-auto= on first use and live outside the localrepo. For true offline reproducibility, cache the grammars next to the localrepo (a =.localrepo/treesitter/= tier, or a separate mirror script). Cross-linked from =docs/design/localrepo.org=.
-
-** TODO [#D] Native-comp .eln cache strategy :feature:offline:localrepo:
-The native-comp =.eln= cache is Emacs-version-specific; an Emacs upgrade invalidates everything. Document the cache location, what an upgrade triggers, and whether a warm-the-cache script is worth shipping. Cross-linked from =docs/design/localrepo.org=.
-
-** TODO [#D] System-tool dependency install script :feature:offline:localrepo:
-=ripgrep=, =fd=, =pandoc=, =prettier=, =pyright=, and other binaries that =cj/executable-find-or-warn= flags at module load are not in =package.el='s reach. Document the required-tool set and ship a setup script (or =pacman=/=apt= invocation set). Cross-linked from =docs/design/localrepo.org=.
-
-** TODO [#D] Localrepo refresh / update script :feature:offline:localrepo:
-No dedicated update path today — refreshing a pinned package means ad-hoc =cp= from the local elpa mirrors. Document the current shape and decide whether a =scripts/refresh-localrepo.sh= is worth writing. Cross-linked from =docs/design/localrepo.org=.
-
** TODO [#C] TRAMP/dirvish "?" for remote dates — verify the fix per host :bug:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-02
@@ -3926,6 +3846,18 @@ Restart the daemon, open a GUI frame, trigger an encrypted decrypt, confirm =pin
*** TODO [#C] Archive the original L3813 task :chore:
After this work lands, mark the original "Finish terminal GPG pinentry configuration" task DONE with a =CLOSED:= stamp and a one-line note pointing at this parent task.
+** TODO [#D] Treesitter grammar offline cache :feature:offline:localrepo:
+Treesitter grammars are downloaded by =treesit-auto= on first use and live outside the localrepo. For true offline reproducibility, cache the grammars next to the localrepo (a =.localrepo/treesitter/= tier, or a separate mirror script). Cross-linked from =docs/design/localrepo.org=.
+
+** TODO [#D] Native-comp .eln cache strategy :feature:offline:localrepo:
+The native-comp =.eln= cache is Emacs-version-specific; an Emacs upgrade invalidates everything. Document the cache location, what an upgrade triggers, and whether a warm-the-cache script is worth shipping. Cross-linked from =docs/design/localrepo.org=.
+
+** TODO [#D] System-tool dependency install script :feature:offline:localrepo:
+=ripgrep=, =fd=, =pandoc=, =prettier=, =pyright=, and other binaries that =cj/executable-find-or-warn= flags at module load are not in =package.el='s reach. Document the required-tool set and ship a setup script (or =pacman=/=apt= invocation set). Cross-linked from =docs/design/localrepo.org=.
+
+** TODO [#D] Localrepo refresh / update script :feature:offline:localrepo:
+No dedicated update path today — refreshing a pinned package means ad-hoc =cp= from the local elpa mirrors. Document the current shape and decide whether a =scripts/refresh-localrepo.sh= is worth writing. Cross-linked from =docs/design/localrepo.org=.
+
** TODO [#D] Dashboard over-scroll: pin last line to window bottom :bug:
:PROPERTIES:
:LAST_REVIEWED: 2026-05-22
@@ -3962,18 +3894,16 @@ Three small reveal.js improvements; collected into one task because each on its
2. *Default font sizing for slide elements.* Configure reveal.js font sizes for headings, body text, code blocks, etc. — better defaults via =org-reveal-head-preamble= CSS or a custom theme.
3. *Custom dupre reveal.js theme.* CSS theme using the colors from =themes/dupre-palette.el=. Install into =reveal.js/css/theme/= for use with =#+REVEAL_THEME: dupre=.
-** TODO [#B] Add Signal to the dashboard :quick:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-01
-:END:
-** TODO Bookmarking Calibre books should have a better title
-When I hit m in calibre, I'm making my place in the book with a bookmark.
-While sometimes, the books look fine: "The A.B.C. Murders - Agatha Christie.epub"
-Sometimes they look not so good: Engines of Logic_ Mathematicians and the O - Martin Davis.pdf or Software Architecture_ The Hard Parts _ Mo - Neal Ford.pdf
+** DOING Project-aware bug capture via C-c c t :feature:capture:
+Relocated from the global capture inbox 2026-06-06. When inside a projectile project, C-c c t (Task) files into that project's root todo.org under the "<Project> Open Work" header. If the project has no todo.org, fall back to the global inbox-file and warn naming the project.
-What I would like to do is to have the bookmarks be saved in the following format:
+Implemented 2026-06-06 in =modules/org-capture-config.el=: a shared project-aware =function= capture target (=cj/--org-capture-project-location=) used by =C-c c t= (Task, =* TODO=) and a new =C-c c b= (Bug, =* TODO [#C]=). Matches an existing top-level "... Open Work" heading (so ~/.emacs.d hits "Emacs Open Work") and creates "<Capitalized project> Open Work" only when absent. Outside a project / no todo.org -> global inbox under "Inbox" (with a warning in the no-todo.org case). 15 ERT tests in =tests/test-org-capture-config-project-target.el=; daemon e2e confirmed a real capture lands "** TODO [#C] ..." prepended under Open Work. Awaiting Craig's interactive manual verify (see the Manual Testing task) before close. NOTE: the matching "<Project> Resolved Work" header for the wrap-up workflow is a separate concern, not handled here.
+
+** TODO "? = curated help menu" convention across modes :feature:ux:discoverability:
+From the calibredb keybindings work 2026-06-06. The pattern that worked: in a modal/major-mode buffer (calibredb), bind =?= to a curated transient of the frequent workflows, and move the package's own full dispatch to =H=. It fixes the "I can't discover the keys" problem that which-key can't help with (which-key only pops up after a prefix, not for top-level single keys in a mode-map).
+
+Task: survey the modes/modules Craig works in and identify where a =?= -> curated-help-menu (transient) makes sense. Candidates: any major-mode buffer with single-key bindings and no good discovery affordance -- calibredb (done), nov, dirvish, mu4e, ghostel/term, signel, pearl/linear, ELFeed, etc. For each, note whether =?= is free or already a help dispatch, and whether a curated menu (vs the package's own) adds value. Establish it as a convention (and maybe a small helper/macro to define a curated =?= menu consistently).
-Author, Title [no extension]. Underscores should be stripped.
* Emacs Resolved
** DONE [#B] Fix likely =elpa-mirror-location= path bug :bug:quick:
CLOSED: [2026-05-03 Sun]
@@ -6972,7 +6902,7 @@ Filed 2026-06-02 from a C-f8/C-f9 mix-up. Priority set [#C] (UX polish) — re-g
** TODO [#C] Color dashboard navigator independently of list items :feature:ux:
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-06-06
:END:
The dashboard navigator (icons + labels) and the recentf/project/bookmark list items are both painted by =dashboard-items-face=: the navigator gets a =dashboard-items-face= overlay, and overlays beat text properties, so the per-button =dashboard-navigator= face is inert. To color the navigator independently of the items, override where that overlay is applied — advise or redefine =dashboard-insert-navigator=, or strip/replace the overlay's face.
Triggered by: 2026-05-22 dashboard color work (L105).
@@ -6991,3 +6921,162 @@ Changes in progress (modules/auth-config.el):
- Use epa-pinentry-mode 'loopback in terminal
- Use external pinentry (pinentry-dmenu) in GUI
- Requires env-terminal-p from host-environment module
+** DONE [#B] Emacs Manual Testing and Validation :verify:
+CLOSED: [2026-06-06 Sat 13:59] SCHEDULED: <2026-05-29 Fri>
+:PROPERTIES:
+:LAST_REVIEWED: 2026-05-28
+:END:
+
+Hand-verify checklist Craig walks one item at a time after the relevant code lands. Each child names what is being verified, the exact steps to run, and the observable expected result. On pass, the child gets marked or deleted. On fail, the actual behavior gets logged under the step and the child is promoted to a top-level =TODO= bug per the verification.md handoff rule.
+
+Walk started 2026-05-28 (tests 1 + 2 verified — surfaced two Signel bugs along the way, both fixed before continuing). Deferred to 2026-05-29: test 3 onward needs sending an actual Signal message, too late at night to be polite about it. Picker → chat buffer opens cleanly; the send half is what remains to exercise.
+
+*** Project-aware capture: C-c c t files into the project's Open Work
+What we're verifying: inside a projectile project that has a root todo.org, C-c c t (Task) files the new entry under that project's "<Project> Open Work" heading.
+- Open a file inside a projectile project whose root has a todo.org (e.g. this one, ~/.emacs.d).
+- Press C-c c, then t.
+- Type a short task, finish with C-c C-c.
+Expected: the entry lands as a new "** TODO ..." at the top of that project's "... Open Work" heading (e.g. "Emacs Open Work"), not in the global inbox.
+
+*** Project-aware capture: C-c c b files a [#C] bug
+What we're verifying: C-c c b (Bug) behaves like the Task capture but stamps the entry [#C].
+- Inside the same project, press C-c c, then b.
+- Type a short bug description, finish with C-c C-c.
+Expected: a "** TODO [#C] ..." entry lands at the top of the project's "... Open Work" heading.
+
+*** Nov bookmark naming: "Author, Title" instead of the raw filename
+What we're verifying: bookmarking your place in an EPUB names the bookmark "Author, Title" parsed from the filename (Calibre's "<Title> - <Author>.epub"), reordered with the colon restored — not the raw filename.
+- Open an EPUB in nov (m is bound to bookmark-set there).
+- Press m to set a bookmark.
+- Look at the default name in the bookmark prompt.
+Expected: the default is "<Author>, <Title>" (e.g. "Agatha Christie, The A.B.C. Murders"; a colon where the filename had "_ "), no extension, no underscores — not the raw filename.
+
+*** Calibredb curated menu on ? and full dispatch on H
+What we're verifying: in the calibredb buffer, ? opens the curated workflow menu and H opens calibredb's full dispatch.
+- M-B to open calibredb.
+- Press ?.
+- Press a key for a workflow (e.g. o to open, f format filter), or q to quit the menu.
+- Press H.
+Expected: ? shows the curated transient (Library / Filter / Sort / Book columns with your workflows); the keys run the right calibredb commands; q quits. H shows calibredb's full menu.
+
+*** Calibredb description docks to the bottom 30%
+What we're verifying: viewing a book's description docks it to the bottom 30% and q dismisses it.
+- M-B, move to a book.
+- Press ? then d (or v).
+- Read the description.
+- Press q.
+Expected: the *calibredb-entry* detail buffer opens docked across the bottom ~30% of the frame (not full-window); q closes it and returns to the list.
+
+*** Project-aware capture: inbox fallback + warning
+What we're verifying: outside a project (or in a project with no todo.org) the capture falls back to the global inbox; the no-todo.org case also warns.
+- Open a scratch file not inside any projectile project, C-c c t, type a task, C-c C-c. Expect it under "Inbox" in the global inbox file.
+- (If easy) open a file in a projectile project that has NO todo.org, C-c c t. Expect it in the global inbox AND an echo-area message naming the project.
+
+*** 2026-05-28 Thu @ 02:13:55 -0500 Verified: connect starts the daemon (after fix)
+=C-; M SPC= → "Signel connected." in echo area; =M-x list-processes= shows =signal-rpc= running (PID 1775279, command =/usr/bin/signal-cli -a +1510...=). Two bugs surfaced and fixed during the verify:
+- The =with-eval-after-load 'keybindings= binding at =signal-config.el:280= didn't take effect on a fresh Emacs restart; a live-reload of =signal-config.el= activated the =C-; M= prefix. Logged as a separate top-level TODO for follow-up (load-order or use-package interaction).
+- =cj/signel--ensure-started= referenced =signel--process-name= before signel had been autoloaded — the bare forward-declared =(defvar signel--process-name)= didn't actually bind the variable. Fix: added =(require 'signel)= at the top of the function (=signal-config.el:170=) so the package loads before any of its private variables are read. New ERT test =test-signal-config-ensure-started-requires-signel= captures the bug.
+
+*** 2026-05-28 Thu @ 02:16:45 -0500 Verified: picker opens with contact names
+=C-; M m= → minibuffer opened within ~1s, "Note to Self" pinned at the top, the 94 Signal contacts followed labeled "Name (+number)". Picker behavior matches spec. Surfaced a follow-up on the chat buffer that opens after a pick — placement + exit keys want refining; filed under L44 Signel.
+
+*** Signel: pick a contact and send a message
+What we're verifying: choosing a contact opens a chat buffer, =RET= at the prompt sends through =signel--send-input=, and the message arrives on the recipient's phone.
+- =C-; M m=, pick a contact you trust.
+- Type a short message at the prompt, press =RET=.
+- Check the recipient's phone.
+Expected: a =*Signel: +<number>*= buffer opens, the typed message renders with the =[HH:MM] <Me>= prefix on send, and arrives on the recipient's phone within a few seconds.
+
+*** Signel: Note-to-Self lands in the right Signal thread
+What we're verifying: =cj/signel-message-self= (=C-; M s=) resolves to =signel-account= and sending through it lands in the *Note to Self* thread on the phone, NOT a self-addressed display anomaly. This is the spec's medium-priority manual verify from D3.
+- Press =C-; M s=.
+- Type "test note to self" at the prompt, press =RET=.
+- Open Signal on your phone, scroll to the *Note to Self* thread.
+Expected: a =*Signel: +<your-number>*= buffer opens in Emacs, the message sends, and the message appears in the phone's *Note to Self* thread (not in any other conversation).
+
+*** Signel: Note-to-Self via the picker's pinned entry
+What we're verifying: picking the pinned "Note to Self" entry through =cj/signel-message= resolves the same way as the direct command.
+- =C-; M m=, choose "Note to Self".
+Expected: the same =*Signel: +<your-number>*= buffer opens. (No need to re-send; opening the right buffer proves the resolution.)
+
+*** Signel: typed input survives an incoming message
+What we're verifying: the clobber fix (fork commit 5ec56c0) preserves in-progress prompt input across =signel--insert-msg= when a message arrives mid-typing.
+- =C-; M m=, pick a contact.
+- Type a long unsent message at the prompt, do NOT press =RET=.
+- From a second device or by asking someone, send yourself a Signal message that lands in this chat (or any active chat).
+Expected: the incoming message renders above the prompt, the prompt redraws, and your typed text is still there at the prompt ready to send.
+
+*** Signel: dashboard opens
+What we're verifying: =signel-dashboard= (=C-; M d=) opens the active-chats dashboard.
+- Press =C-; M d=.
+Expected: a dashboard buffer opens listing active chats.
+
+*** Signel: stop tears down the daemon
+What we're verifying: =signel-stop= (=C-; M q=) deletes the process and clears the request-handler / buffer maps (the reconnect-invalidation contract from fork commit 4740d97).
+- Press =C-; M q=.
+- =M-x list-processes=.
+Expected: echo area shows "Signel service stopped.", and =list-processes= no longer lists =signal-rpc=.
+
+*** Signel: refresh forces a fresh contact fetch
+What we're verifying: =cj/signel-refresh-contacts= clears the cache and re-fetches via the new callback contract.
+- =C-; M SPC= to reconnect if you ran the stop test above.
+- =M-x cj/signel-refresh-contacts=.
+- Immediately =C-; M m=.
+Expected: the picker still opens cleanly with the same contact list (the refresh is silent; the picker is the visible check). If you added a contact on the phone, it now appears.
+
+*** Font setup reaches a GUI frame created after a TTY frame (daemon)
+What we're verifying: emoji glyphs + fonts apply in a GUI frame even when the first daemon frame was a TTY.
+- emacs --daemon
+- emacsclient -t (TTY frame first)
+- emacsclient -c (then a GUI frame)
+- in the GUI frame, open a buffer with an emoji and check it renders, and M-S-f / fonts look right
+Expected: emoji renders and fonts are applied in the GUI frame.
+
+*** ghostel migration: Claude Code TUI in a GUI frame
+What we're verifying: an agent runs in ghostel with good rendering (the reason for the engine swap).
+- restart Emacs (the migration changes load order + a use-package :config block)
+- in a GUI frame press F9, pick a project, let Claude stream a long response (big diff or file read)
+Expected: colors look right (not washed out), no flicker/strobing during the stream, box-drawing and the cursor render correctly.
+
+*** ghostel migration: Claude Code TUI in a TTY frame (replaces the old refuse test)
+What we're verifying: D4 dropped the GUI-only guard, so F9 now launches in a terminal frame too.
+- emacsclient -t (TTY frame, off the running daemon)
+- in the TTY frame press F9 and pick a project
+Expected: the agent launches and renders as text + color in the TTY (no echo-area refusal message); inline images are absent, which is expected.
+
+*** ghostel migration: F9 / C-F9 / M-F9 dispatch
+What we're verifying: the agent dispatch behaves as it did on vterm.
+- F9 toggles the agent window off/on; C-F9 always opens the project picker; M-F9 closes (kills the tmux session) after confirm
+- press F9 from inside an agent buffer (full-frame) — it should toggle, not get swallowed by the terminal
+Expected: each chord does its job from both normal and agent buffers.
+
+*** ghostel migration: tmux integration + C-; x menu
+What we're verifying: the tmux machinery ported intact.
+- launch an agent; M-x list it — runs in tmux session aiv-<project>
+- second F9 on the same project reattaches (no duplicate session)
+- C-; x h captures the tmux pane history into an Emacs buffer; C-; x c enters tmux copy-mode
+- C-; x l clears scrollback; C-; x n / p navigate prompts
+Expected: all menu commands work against the ghostel buffer; history capture + copy-mode behave as before.
+
+*** ghostel migration: copy-mode parity + mouse wheel
+What we're verifying: copy/selection and wheel scrolling survived the engine swap.
+- in a ghostel buffer enter copy-mode (C-; x c without tmux, or the tmux path with tmux); M-w copies and stays; q / C-g exit
+- mouse-wheel scroll inside tmux, inside Claude Code, and inside lazygit
+Expected: M-w copies without leaving; q/C-g exit; the wheel scrolls the program (this replaces the removed vterm wheel-forwarding — confirm ghostel's native SGR mouse covers it).
+
+*** ghostel migration: other TUIs + ssh
+What we're verifying: general terminal workloads render.
+- run lazygit, htop/btop, a heavy-output build, and ssh to a remote host in a ghostel terminal (F12)
+Expected: each renders and behaves correctly; ssh out works (if a remote lacks xterm-ghostty terminfo, note it — ghostel-ssh-install-terminfo / ghostel-term is the lever).
+
+*** ghostel migration: F12 general terminal + dashboard launcher
+What we're verifying: F12 manages non-agent terminals only, and the dashboard launcher uses ghostel.
+- F12 opens/toggles a general terminal; confirm it does NOT grab an agent buffer; resize it, toggle off and on — geometry is preserved
+- from the dashboard press t (Terminal) — opens a ghostel terminal (tooltip reads "Launch Terminal")
+Expected: F12 excludes agent buffers and keeps saved geometry; the dashboard launches ghostel.
+
+*** ghostel migration: crash recovery
+What we're verifying: the aiv- tmux session survives an Emacs crash and reattaches.
+- with a live agent, kill Emacs (not the tmux session); restart Emacs; F9 → project picker
+Expected: the project shows "[detached]" and reattaches to the surviving tmux session.
+