diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-20 16:47:39 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-20 16:47:39 -0500 |
| commit | 61454894349e75b593ede5901a7b348ad580d3ce (patch) | |
| tree | ca88583c053049e53299612bbad632b39f466a9c | |
| parent | 302b062680c8fbd9743d4e083154ac5fc314955a (diff) | |
| download | rulesets-61454894349e75b593ede5901a7b348ad580d3ce.tar.gz rulesets-61454894349e75b593ede5901a7b348ad580d3ce.zip | |
feat(notify): reserve "page me" for desktop, "text me" for Signal
"page me" and "text me" now name distinct channels: page is the desktop notification, text is the Signal phone push, and "text and page me" fires both. Before, "page me" defaulted to desktop and the phone was an unnamed "on my phone" variant, so you couldn't name the phone without naming the machine.
I renamed the Signal tool agent-page to agent-text to match, with a deprecated agent-page shim delegating to it so callers and other machines don't break before their next install. The dispatch and tests are otherwise unchanged. I rewrote protocols.org "Reaching Craig", page-me.org, and work-the-backlog's away-run logic around the three phrasings, and updated the runbook.
| -rw-r--r-- | .ai/protocols.org | 18 | ||||
| -rw-r--r-- | .ai/workflows/INDEX.org | 2 | ||||
| -rw-r--r-- | .ai/workflows/page-me.org | 24 | ||||
| -rw-r--r-- | .ai/workflows/work-the-backlog.org | 2 | ||||
| -rw-r--r-- | claude-templates/.ai/protocols.org | 18 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/INDEX.org | 2 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/page-me.org | 24 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/work-the-backlog.org | 2 | ||||
| -rwxr-xr-x | claude-templates/bin/agent-page | 57 | ||||
| -rwxr-xr-x | claude-templates/bin/agent-text | 57 | ||||
| -rwxr-xr-x | claude-templates/bin/install-ai | 2 | ||||
| -rw-r--r-- | docs/design/2026-07-20-signal-pager-runbook.org | 39 | ||||
| -rw-r--r-- | scripts/tests/agent-text.bats (renamed from scripts/tests/agent-page.bats) | 26 | ||||
| -rw-r--r-- | scripts/tests/install-ai.bats | 2 |
14 files changed, 161 insertions, 114 deletions
diff --git a/.ai/protocols.org b/.ai/protocols.org index d89e345..3cd02ef 100644 --- a/.ai/protocols.org +++ b/.ai/protocols.org @@ -430,27 +430,29 @@ 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 — the agent pager +** Reaching Craig — the notification vocabulary -"Page me" has two channels; pick by where Craig is. Both work from any agent runtime — nothing here is Claude-specific. +Two channels, two trigger words. "page me" is the desktop, "text me" is the phone, "text and page me" is both. Pick by where Craig is, and default to both when a run can't tell. Both work from any agent runtime (nothing here is Claude-specific). The words are what Craig says; a run deciding on its own maps the same way (away run texts, at-desk run pages, unsure does both). -- *At his laptop/desktop* — desktop =notify ... --persist= (above). It reaches him on the machine and stays up until dismissed. +- *"page me" — at his laptop/desktop.* A desktop =notify ... --persist= that reaches him on the machine and stays up until dismissed. #+begin_src bash notify info "Title" "Message" --persist #+end_src -- *Away from his laptop/desktop* — page his phone over Signal with the *agent pager*: +- *"text me" — away from his machine.* A Signal push to his phone via =agent-text=: #+begin_src bash - agent-page "Message for Craig's phone" + agent-text "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. + =agent-text= (in =~/.local/bin= via the rulesets install) sends from the dedicated Signal identity (+15045173983) to Craig's Signal account UUID, firing a normal mobile push. The account is registered on velox (primary) and ratio (linked device), so either sends directly; a machine without it ssh-relays to velox. Verified end to end 2026-07-13 (velox) and 2026-07-20 (ratio). Never target Craig's phone *number* (it reads as unregistered in Signal's directory); the script 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. + Caveats: a relay from a non-linked machine needs velox up on the tailnet, and each device holding the account wants a periodic =receive= (the signal-receive timer handles that). The full runbook lives in rulesets =docs/design/=. -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). +- *"text and page me" — both.* Fire =agent-text= and =notify= together. The phone reaches him now, the desktop note waits for his return. This is the default when a run can't tell whether he's away. + +On velox, Claude sessions may also have the *signal-mcp* tool (=send_message_to_user=, same identity), fine to use there, but it exists only in velox's local MCP config, so =agent-text= is the portable habit. The tool was named =agent-page= before 2026-07-20; a deprecated =agent-page= shim still delegates to =agent-text=. Do *not* use the old =page-signal= shell script (removed 2026-06-12). * Session Protocols diff --git a/.ai/workflows/INDEX.org b/.ai/workflows/INDEX.org index 02c4264..f18d953 100644 --- a/.ai/workflows/INDEX.org +++ b/.ai/workflows/INDEX.org @@ -113,7 +113,7 @@ 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 (desktop =notify=; phone via =agent-page= when Craig is away). +- =page-me.org= — set a timed notification. "page me" desktop =notify=, "text me" phone via =agent-text=, "text and page me" both. - 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. diff --git a/.ai/workflows/page-me.org b/.ai/workflows/page-me.org index bfa92c6..7a3b792 100644 --- a/.ai/workflows/page-me.org +++ b/.ai/workflows/page-me.org @@ -13,9 +13,17 @@ Uses the =notify= command (info type) for consistent notifications across all AI Craig says *"page me"* (or variations like "page me in 10 minutes", "page me at 3pm"). -The word "page" is the trigger for this workflow. It means: set a timed notification. +The word "page" is the trigger for this workflow. It means: set a timed notification on the *desktop* channel (=notify=). -Previously called "set-alarm" -- renamed to "page-me" for a distinctive, short trigger phrase that won't collide with common words like "remind" or "alert." +Two sibling triggers pick a different channel; the timed =at= machinery below is identical for all three, only the fired command changes: + +- *"page me"* — desktop =notify= (this workflow's default). +- *"text me"* — a Signal push to Craig's phone via =agent-text= (the away channel). +- *"text and page me"* — both, for when he might be either place. + +Scope the triggers to the reflexive "me": "page me" and "text me", not a bare "page" or "text" in prose. The full channel vocabulary lives in protocols.org "Reaching Craig". + +"page" was chosen (renamed from the old "set-alarm") for a distinctive, short trigger that won't collide with common words like "remind" or "alert". * Problem We're Solving @@ -113,18 +121,18 @@ 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) +** Texting Craig's phone (the "text me" channel) -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: +The timed =notify= alarm above is the desktop channel. When Craig says "text me" (or a run expects him away from the machine), use =agent-text= instead, a Signal push to his phone from any machine or agent runtime: #+begin_src bash -agent-page "Build finished — ready for your eyes" +agent-text "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 +# Timed phone message: same at-daemon pattern, different channel +echo "agent-text '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. +Channel selection and the mechanics live in protocols.org "Reaching Craig". On "text and page me", fire both: the desktop notification persists for whenever he returns, the phone push reaches him now. ** Managing Alarms diff --git a/.ai/workflows/work-the-backlog.org b/.ai/workflows/work-the-backlog.org index ab24d3b..3933749 100644 --- a/.ai/workflows/work-the-backlog.org +++ b/.ai/workflows/work-the-backlog.org @@ -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. =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"). +=--persist= keeps it on screen until dismissed, and =info= is the notification urgency convention (persistent but never crash-scary). The notification 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 channel (the "page me" surface); a run that expects Craig to be away also fires =agent-text= with the same message (the Signal phone channel, "text me"). See protocols.org "Reaching Craig". * Metrics diff --git a/claude-templates/.ai/protocols.org b/claude-templates/.ai/protocols.org index d89e345..3cd02ef 100644 --- a/claude-templates/.ai/protocols.org +++ b/claude-templates/.ai/protocols.org @@ -430,27 +430,29 @@ 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 — the agent pager +** Reaching Craig — the notification vocabulary -"Page me" has two channels; pick by where Craig is. Both work from any agent runtime — nothing here is Claude-specific. +Two channels, two trigger words. "page me" is the desktop, "text me" is the phone, "text and page me" is both. Pick by where Craig is, and default to both when a run can't tell. Both work from any agent runtime (nothing here is Claude-specific). The words are what Craig says; a run deciding on its own maps the same way (away run texts, at-desk run pages, unsure does both). -- *At his laptop/desktop* — desktop =notify ... --persist= (above). It reaches him on the machine and stays up until dismissed. +- *"page me" — at his laptop/desktop.* A desktop =notify ... --persist= that reaches him on the machine and stays up until dismissed. #+begin_src bash notify info "Title" "Message" --persist #+end_src -- *Away from his laptop/desktop* — page his phone over Signal with the *agent pager*: +- *"text me" — away from his machine.* A Signal push to his phone via =agent-text=: #+begin_src bash - agent-page "Message for Craig's phone" + agent-text "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. + =agent-text= (in =~/.local/bin= via the rulesets install) sends from the dedicated Signal identity (+15045173983) to Craig's Signal account UUID, firing a normal mobile push. The account is registered on velox (primary) and ratio (linked device), so either sends directly; a machine without it ssh-relays to velox. Verified end to end 2026-07-13 (velox) and 2026-07-20 (ratio). Never target Craig's phone *number* (it reads as unregistered in Signal's directory); the script 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. + Caveats: a relay from a non-linked machine needs velox up on the tailnet, and each device holding the account wants a periodic =receive= (the signal-receive timer handles that). The full runbook lives in rulesets =docs/design/=. -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). +- *"text and page me" — both.* Fire =agent-text= and =notify= together. The phone reaches him now, the desktop note waits for his return. This is the default when a run can't tell whether he's away. + +On velox, Claude sessions may also have the *signal-mcp* tool (=send_message_to_user=, same identity), fine to use there, but it exists only in velox's local MCP config, so =agent-text= is the portable habit. The tool was named =agent-page= before 2026-07-20; a deprecated =agent-page= shim still delegates to =agent-text=. Do *not* use the old =page-signal= shell script (removed 2026-06-12). * Session Protocols diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org index 02c4264..f18d953 100644 --- a/claude-templates/.ai/workflows/INDEX.org +++ b/claude-templates/.ai/workflows/INDEX.org @@ -113,7 +113,7 @@ 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 (desktop =notify=; phone via =agent-page= when Craig is away). +- =page-me.org= — set a timed notification. "page me" desktop =notify=, "text me" phone via =agent-text=, "text and page me" both. - 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. diff --git a/claude-templates/.ai/workflows/page-me.org b/claude-templates/.ai/workflows/page-me.org index bfa92c6..7a3b792 100644 --- a/claude-templates/.ai/workflows/page-me.org +++ b/claude-templates/.ai/workflows/page-me.org @@ -13,9 +13,17 @@ Uses the =notify= command (info type) for consistent notifications across all AI Craig says *"page me"* (or variations like "page me in 10 minutes", "page me at 3pm"). -The word "page" is the trigger for this workflow. It means: set a timed notification. +The word "page" is the trigger for this workflow. It means: set a timed notification on the *desktop* channel (=notify=). -Previously called "set-alarm" -- renamed to "page-me" for a distinctive, short trigger phrase that won't collide with common words like "remind" or "alert." +Two sibling triggers pick a different channel; the timed =at= machinery below is identical for all three, only the fired command changes: + +- *"page me"* — desktop =notify= (this workflow's default). +- *"text me"* — a Signal push to Craig's phone via =agent-text= (the away channel). +- *"text and page me"* — both, for when he might be either place. + +Scope the triggers to the reflexive "me": "page me" and "text me", not a bare "page" or "text" in prose. The full channel vocabulary lives in protocols.org "Reaching Craig". + +"page" was chosen (renamed from the old "set-alarm") for a distinctive, short trigger that won't collide with common words like "remind" or "alert". * Problem We're Solving @@ -113,18 +121,18 @@ 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) +** Texting Craig's phone (the "text me" channel) -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: +The timed =notify= alarm above is the desktop channel. When Craig says "text me" (or a run expects him away from the machine), use =agent-text= instead, a Signal push to his phone from any machine or agent runtime: #+begin_src bash -agent-page "Build finished — ready for your eyes" +agent-text "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 +# Timed phone message: same at-daemon pattern, different channel +echo "agent-text '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. +Channel selection and the mechanics live in protocols.org "Reaching Craig". On "text and page me", fire both: the desktop notification persists for whenever he returns, the phone push reaches him now. ** Managing Alarms diff --git a/claude-templates/.ai/workflows/work-the-backlog.org b/claude-templates/.ai/workflows/work-the-backlog.org index ab24d3b..3933749 100644 --- a/claude-templates/.ai/workflows/work-the-backlog.org +++ b/claude-templates/.ai/workflows/work-the-backlog.org @@ -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. =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"). +=--persist= keeps it on screen until dismissed, and =info= is the notification urgency convention (persistent but never crash-scary). The notification 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 channel (the "page me" surface); a run that expects Craig to be away also fires =agent-text= with the same message (the Signal phone channel, "text me"). See protocols.org "Reaching Craig". * Metrics diff --git a/claude-templates/bin/agent-page b/claude-templates/bin/agent-page index 8e9b23c..728ee78 100755 --- a/claude-templates/bin/agent-page +++ b/claude-templates/bin/agent-page @@ -1,55 +1,12 @@ #!/bin/bash -# agent-page — page Craig's phone over Signal, from any machine or agent runtime. +# agent-page — deprecated alias for agent-text. # -# Usage: agent-page <message...> -# -# The pager identity (+15045173983) is registered in velox's signal-cli, and -# any daily driver linked as a device of that account (ratio, 2026-07-20) can -# send directly too. So the dispatch is: if the pager account is registered in -# the local signal-cli, send directly; otherwise ssh-relay the send to velox -# over the tailnet. A direct send from a linked device still lands when velox -# is down — the reason ratio was linked. 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 (velox) and -# 2026-07-20 (ratio, direct). -# -# 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, full runbook in -# rulesets docs/design/): a relay from a non-linked machine needs velox up on -# the tailnet, and each device holding the account wants a periodic `receive` -# (staleness warnings appear otherwise) — the signal-receive timer handles that. +# The Signal phone tool was renamed agent-text on 2026-07-20, when the +# notification vocabulary split into "text me" (Signal) and "page me" (desktop). +# This shim keeps old callers and other machines working until they re-install +# and pick up agent-text directly. Remove it in a later cleanup once nothing +# references agent-page. # # 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="$*" - -# The pager account is local if this machine's signal-cli holds it — the -# registered primary (velox) or any linked device. Those send directly. -if signal-cli listAccounts 2>/dev/null | grep -q "$PAGER_ACCOUNT"; 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" +exec "$(dirname "$(readlink -f "$0")")/agent-text" "$@" diff --git a/claude-templates/bin/agent-text b/claude-templates/bin/agent-text new file mode 100755 index 0000000..86aa933 --- /dev/null +++ b/claude-templates/bin/agent-text @@ -0,0 +1,57 @@ +#!/bin/bash +# agent-text — text Craig's phone over Signal, from any machine or agent runtime. +# The Signal half of the notification vocabulary: "text me" reaches the phone, +# "page me" is the desktop channel (notify). See protocols.org "Reaching Craig". +# +# Usage: agent-text <message...> +# +# The Signal identity (+15045173983) is registered in velox's signal-cli, and +# any daily driver linked as a device of that account (ratio, 2026-07-20) can +# send directly too. So the dispatch is: if the account is registered in the +# local signal-cli, send directly; otherwise ssh-relay the send to velox over +# the tailnet. A direct send from a linked device still lands when velox is +# down (the reason ratio was linked). The recipient is Craig's Signal account +# UUID; his phone number reads as unregistered in Signal's directory, so never +# target the number. Verified end to end 2026-07-13 (velox) and 2026-07-20 +# (ratio, direct). +# +# This is the AWAY channel. At his desk, use the desktop channel instead: +# notify info "Title" "Message" --persist +# See protocols.org "Reaching Craig" for choosing between them. +# +# Known caveats (full runbook in rulesets docs/design/): a relay from a +# non-linked machine needs velox up on the tailnet, and each device holding the +# account wants a periodic `receive` (staleness warnings appear otherwise); the +# signal-receive timer handles that. +# +# Source: ~/code/rulesets/claude-templates/bin/agent-text +# Install: make -C ~/code/rulesets install + +SIGNAL_ACCOUNT="+15045173983" +CRAIG_UUID="b1b5601e-6126-47f8-afaa-0a59f5188fde" +VELOX_HOST="velox.tailf3bb8c.ts.net" + +if [ $# -eq 0 ]; then + echo "usage: agent-text <message...>" >&2 + exit 2 +fi + +msg="$*" + +# The account is local if this machine's signal-cli holds it: the registered +# primary (velox) or any linked device. Those send directly. +if signal-cli listAccounts 2>/dev/null | grep -q "$SIGNAL_ACCOUNT"; then + signal-cli -a "$SIGNAL_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 $SIGNAL_ACCOUNT send -m $(printf '%q' "$msg") $CRAIG_UUID" + rc=$? +fi + +if [ "$rc" -ne 0 ]; then + echo "agent-text: phone message failed (velox down or unreachable?); fall back to the desktop channel: notify info 'Message' '<message>' --persist" >&2 +fi +exit "$rc" diff --git a/claude-templates/bin/install-ai b/claude-templates/bin/install-ai index 88cb8e5..3283c4a 100755 --- a/claude-templates/bin/install-ai +++ b/claude-templates/bin/install-ai @@ -2,7 +2,7 @@ # install-ai — PATH-facing launcher for the fresh-project bootstrapper. # # make install symlinks this into ~/.local/bin/install-ai (same bin loop that -# links `ai` and `agent-page`), so `install-ai [--track|--gitignore] [PROJECT]` +# links `ai` and `agent-text`), so `install-ai [--track|--gitignore] [PROJECT]` # runs from anywhere. The real logic lives in scripts/install-ai.sh; this # resolves its own location through the ~/.local/bin symlink and execs that # script by its true repo path, so the script's own repo-root computation diff --git a/docs/design/2026-07-20-signal-pager-runbook.org b/docs/design/2026-07-20-signal-pager-runbook.org index 671b1ec..f31ed18 100644 --- a/docs/design/2026-07-20-signal-pager-runbook.org +++ b/docs/design/2026-07-20-signal-pager-runbook.org @@ -30,30 +30,30 @@ him; ratio holds it too, so a page still lands when velox is down. * Choosing a channel -"Page me" has two channels; the choice is where Craig is, and both work from any -agent runtime (nothing here is Claude-specific). protocols.org "Paging Craig" -is the short version pointed at every project; this runbook is the full one. +Two trigger words, two channels, and both work from any agent runtime (nothing +here is Claude-specific). protocols.org "Reaching Craig" is the short version +pointed at every project; this runbook is the full one for the Signal side. -- *At his laptop/desktop* — desktop notification, stays up until dismissed: +- *"page me"* — desktop notification, stays up until dismissed: #+begin_src bash notify info "Title" "Message" --persist #+end_src -- *Away from his machine* — the phone, over Signal: +- *"text me"* — the phone, over Signal: #+begin_src bash - agent-page "Message for Craig's phone" + agent-text "Message for Craig's phone" #+end_src -When in doubt, fire both — the desktop one is free and the phone one reaches him -if he's away. +- *"text and page me"* — both. The default when a run can't tell whether he's + away: the desktop one is free and the phone one reaches him if he is. -* Sending a page +* Sending a text -=agent-page= (shipped at =claude-templates/bin/agent-page=, installed to +=agent-text= (shipped at =claude-templates/bin/agent-text=, installed to =~/.local/bin= by =make -C ~/code/rulesets install=) is the interface. It hides -the machine topology by checking whether the pager account is registered in the +the machine topology by checking whether the account is registered in the local signal-cli: - If the account is local (velox's primary or a linked device like ratio), it @@ -76,7 +76,7 @@ ssh velox.tailf3bb8c.ts.net \ "signal-cli -a +15045173983 send -m 'your message' b1b5601e-6126-47f8-afaa-0a59f5188fde" #+end_src -Prefer =agent-page= over the raw command — it hardens the message for the remote +Prefer =agent-text= over the raw command — it hardens the message for the remote shell and handles the fallback. Reach for the raw form only when debugging. * Reading replies @@ -149,7 +149,7 @@ systemctl --user status signal-receive.service # confirm a clean receive - *signal-mcp:* on velox, Claude sessions may also expose a =signal-mcp= tool (=send_message_to_user=, same pager identity) configured in velox's global =~/.claude.json=. It works there but is invisible from any other machine and - from non-Claude runtimes, so =agent-page= is the portable habit. The old + from non-Claude runtimes, so =agent-text= is the portable habit. The old =page-signal= shell script was removed 2026-06-12 — do not resurrect it. - *Linking a device:* to add a second signal-cli as a linked device of the pager account (see the open decision below), provision it from the new machine and @@ -176,7 +176,7 @@ device wanting its own regular =receive= — so the warm-keeping timer moved fro velox-only home to the shared =common= package, running on both. Adding another daily driver later is the same handshake plus a dotfiles stow; -the timer and =agent-page= already generalize to "any machine holding the +the timer and =agent-text= already generalize to "any machine holding the account." * History @@ -188,6 +188,11 @@ account." Craig" rewritten around the two channels. - 2026-07-20 — this runbook; receive-timer script + units added; a manual drain cleared the 47-day staleness live; ratio linked as a device of the pager - account and its direct send verified; =agent-page= generalized to send directly - from any machine holding the account; receive timer moved to the shared - =common= package and enabled on both machines. + account and its direct send verified; the tool (still named =agent-page= that + morning) generalized to send directly from any machine holding the account; + receive timer moved to the shared =common= package and enabled on both machines. +- 2026-07-20 (later) — notification vocabulary split: "page me" is the desktop + channel, "text me" is Signal, "text and page me" is both. The tool was renamed + =agent-page= → =agent-text= to match, with a deprecated =agent-page= shim + delegating to it. protocols.org section renamed "Paging Craig" → "Reaching + Craig". diff --git a/scripts/tests/agent-page.bats b/scripts/tests/agent-text.bats index 7317ac7..e5d80fc 100644 --- a/scripts/tests/agent-page.bats +++ b/scripts/tests/agent-text.bats @@ -1,15 +1,16 @@ #!/usr/bin/env bats -# agent-page — the runtime-neutral phone pager. Pages Craig over Signal from -# any machine on the tailnet: sends directly wherever the pager account is -# registered locally (velox, or any linked device), and ssh-relays to velox -# from a machine that doesn't hold the account. These tests stub -# ssh/signal-cli on PATH to verify command construction without a network or -# a phone. The signal-cli stub answers `listAccounts` to control which branch -# the dispatch takes. +# agent-text — the runtime-neutral Signal phone messenger ("text me"). Reaches +# Craig over Signal from any machine on the tailnet: sends directly wherever the +# account is registered locally (velox, or any linked device), and ssh-relays to +# velox from a machine that doesn't hold the account. These tests stub +# ssh/signal-cli on PATH to verify command construction without a network or a +# phone. The signal-cli stub answers `listAccounts` to control which branch the +# dispatch takes. The final test covers the deprecated agent-page shim. setup() { REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" - PAGE="$REPO_ROOT/claude-templates/bin/agent-page" + PAGE="$REPO_ROOT/claude-templates/bin/agent-text" + SHIM="$REPO_ROOT/claude-templates/bin/agent-page" STUBS="$(mktemp -d)" LOG="$STUBS/calls.log" cat > "$STUBS/ssh" <<EOF @@ -17,7 +18,7 @@ setup() { echo "ssh \$*" >> "$LOG" exit 0 EOF - # HAS_ACCOUNT controls the listAccounts answer: "1" → the pager account is + # HAS_ACCOUNT controls the listAccounts answer: "1" → the account is # local (send direct), unset/"0" → not local (relay). cat > "$STUBS/signal-cli" <<EOF #!/bin/bash @@ -68,3 +69,10 @@ EOF [ "$status" -ne 0 ] [[ "$output" == *"notify"* ]] } + +@test "the deprecated agent-page shim delegates to agent-text" { + HAS_ACCOUNT=1 PATH="$STUBS:$PATH" run bash "$SHIM" via shim + [ "$status" -eq 0 ] + # Reaches the same direct-send path as agent-text. + grep -q "^signal-cli -a +15045173983 send " "$LOG" +} diff --git a/scripts/tests/install-ai.bats b/scripts/tests/install-ai.bats index 14f1152..1549184 100644 --- a/scripts/tests/install-ai.bats +++ b/scripts/tests/install-ai.bats @@ -176,7 +176,7 @@ EOF # --- claude-templates/bin/install-ai launcher -------------------------------- # # The launcher is the PATH-facing front door (make install symlinks it into -# ~/.local/bin/install-ai, same loop as `ai` and `agent-page`). It resolves its +# ~/.local/bin/install-ai, same loop as `ai` and `agent-text`). It resolves its # own real path through the symlink and execs scripts/install-ai.sh, so the # repo-root computation survives being invoked as a symlink from anywhere. |
