aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-20 16:03:08 -0500
committerCraig Jennings <c@cjennings.net>2026-07-20 16:03:08 -0500
commit302b062680c8fbd9743d4e083154ac5fc314955a (patch)
tree4ac4cfb00bee8d863401852a77c41ef8a67127fb
parentfecdf8cc231bc7f75a9b0b3d5f5342e5f2f3d104 (diff)
downloadrulesets-302b062680c8fbd9743d4e083154ac5fc314955a.tar.gz
rulesets-302b062680c8fbd9743d4e083154ac5fc314955a.zip
feat(pager): document the Signal pager and add the receive timer
The pager worked but was undocumented and drifting stale: signal-cli warned the account had gone 47 days without a receive, and no runbook existed to hand a future session or a non-Claude runtime. I added the runbook under docs/design/ and a roam-sync-shaped receive timer that drains the queue every 15 minutes, keeping the account warm and surfacing reply messages. I also generalized agent-page: it now sends directly from any machine holding the pager account and relays to velox only when it doesn't. ratio is linked as a device of the account, so a page lands even when velox is down. The receive script no-ops cleanly where the account isn't registered, since the units stow onto every machine via the shared common package.
-rwxr-xr-xclaude-templates/bin/agent-page25
-rw-r--r--docs/design/2026-07-20-signal-pager-runbook.org193
-rwxr-xr-xscripts/signal-receive.sh51
-rw-r--r--scripts/systemd/signal-receive.service10
-rw-r--r--scripts/systemd/signal-receive.timer10
-rw-r--r--scripts/tests/agent-page.bats34
-rw-r--r--scripts/tests/signal-receive.bats66
-rw-r--r--todo.org6
8 files changed, 369 insertions, 26 deletions
diff --git a/claude-templates/bin/agent-page b/claude-templates/bin/agent-page
index 11a5264..8e9b23c 100755
--- a/claude-templates/bin/agent-page
+++ b/claude-templates/bin/agent-page
@@ -3,19 +3,24 @@
#
# Usage: agent-page <message...>
#
-# The pager identity (+15045173983) is registered in velox's signal-cli, so
-# velox sends directly and every other machine ssh-relays the send over the
-# tailnet. The recipient is Craig's Signal account UUID — his phone number
-# reads as unregistered in Signal's directory, so never page the number.
-# Verified end to end 2026-07-13 (phone push confirmed).
+# 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): velox must be
-# up and reachable on the tailnet, and the signal-cli account wants a
-# periodic `receive` (staleness warnings appear otherwise).
+# 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.
#
# Source: ~/code/rulesets/claude-templates/bin/agent-page
# Install: make -C ~/code/rulesets install
@@ -31,7 +36,9 @@ fi
msg="$*"
-if [ "$(uname -n)" = "velox" ]; then
+# 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
diff --git a/docs/design/2026-07-20-signal-pager-runbook.org b/docs/design/2026-07-20-signal-pager-runbook.org
new file mode 100644
index 0000000..671b1ec
--- /dev/null
+++ b/docs/design/2026-07-20-signal-pager-runbook.org
@@ -0,0 +1,193 @@
+#+TITLE: Signal Pager Runbook
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-20
+
+The operational reference for the agent pager — how a page reaches Craig's
+phone, how his replies come back, how the account stays healthy, and the
+signal-cli setup behind it. This is the Signal successor to the retired ntfy
+runbook. Canonical home is rulesets because the pager is cross-machine tooling.
+
+* What the pager is
+
+One Signal identity, =+15045173983=, registered in *velox's* signal-cli
+(account file 465310, velox is the primary device). It is a dedicated pager
+number, not Craig's personal Signal. Pages go *from* that identity *to* Craig's
+own Signal account, which fires a normal mobile push on his phone.
+
+As of 2026-07-20 the identity spans two devices: velox (primary) and ratio
+(linked device "ratio-pager"). Any machine holding the account sends directly;
+a machine that doesn't relays to velox over the tailnet.
+
+Two constants the tooling depends on:
+
+- Pager account: =+15045173983= (primary on velox, linked on ratio).
+- Recipient: Craig's Signal account UUID =b1b5601e-6126-47f8-afaa-0a59f5188fde=.
+ His phone *number* reads as unregistered in Signal's directory — always
+ target the UUID, never the number.
+
+velox is the laptop that travels with Craig, so the pager account rides with
+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.
+
+- *At his laptop/desktop* — desktop notification, stays up until dismissed:
+
+ #+begin_src bash
+ notify info "Title" "Message" --persist
+ #+end_src
+
+- *Away from his machine* — the phone, over Signal:
+
+ #+begin_src bash
+ agent-page "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.
+
+* Sending a page
+
+=agent-page= (shipped at =claude-templates/bin/agent-page=, 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
+local signal-cli:
+
+- If the account is local (velox's primary or a linked device like ratio), it
+ sends directly — no velox dependency.
+- Otherwise it ssh-relays the send to velox over the tailnet.
+- On failure (velox down or unreachable from a non-linked machine) it prints the
+ desktop fallback line and exits non-zero, so a caller can tell the page did not
+ land.
+
+The raw command it runs, for reference or a manual send from velox:
+
+#+begin_src bash
+signal-cli -a +15045173983 send -m "your message" b1b5601e-6126-47f8-afaa-0a59f5188fde
+#+end_src
+
+From another machine, the same send relayed over the tailnet:
+
+#+begin_src bash
+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
+shell and handles the fallback. Reach for the raw form only when debugging.
+
+* Reading replies
+
+Craig replies to a page straight from Signal on his phone. The reply is a normal
+data message *to* the pager account, so it is waiting in the pager's inbound
+queue until something receives it.
+
+Drain the queue and read what is there:
+
+#+begin_src bash
+# On velox:
+signal-cli -a +15045173983 receive --timeout 10
+# From another machine:
+ssh velox.tailf3bb8c.ts.net "signal-cli -a +15045173983 receive --timeout 10"
+#+end_src
+
+=receive= prints every queued envelope and exits 0 once the queue drains or the
+timeout elapses. A text reply from Craig arrives as an envelope from his UUID
+carrying a =Body:= line — that line is the reply text. Most envelopes are
+delivery/read receipts and typing indicators (no =Body:=); the reply you want is
+the data message with body text. For a script that waits on a reply, add
+=--send-read-receipts= so his phone shows the page was read, and parse stdout for
+the =Body:= line on an envelope from =b1b5601e-…=.
+
+Note: =receive= is destructive — it consumes the queue. Whatever drains the
+queue (an on-demand read, or the warm-keeping timer below) is what sees the
+reply, and it is seen once. An agent that pages and then waits for an answer
+should do its own =receive= rather than race the timer.
+
+* Keeping the account warm (receive timer)
+
+Signal expects a registered account to receive regularly. Left alone, the pager
+account drifts stale — signal-cli warns "Messages have been last received N days
+ago" (observed at 47 days on 2026-07-20 before a manual drain reset it). A stale
+account is a reliability risk on the one channel that reaches Craig when he is
+away.
+
+The fix mirrors roam-sync: a systemd user timer that drains the queue on a
+cadence, keeping the account warm and, as a bonus, picking up async replies. With
+the account linked on both machines, each device wants its own regular receive,
+so the timer runs on *both* velox and ratio (the shared =common= dotfiles
+package, same home as roam-sync).
+
+- Script: =scripts/signal-receive.sh= (rulesets, so both machines get it on
+ =git pull=). It no-ops cleanly on a machine that lacks the account.
+- Units: =scripts/signal-receive.service= + =.timer= (reference copies under
+ =scripts/systemd/=; the stowed copies live in =common/.config/systemd/user/=
+ of the dotfiles repo, so both machines get them).
+- Cadence: every 15 minutes (=OnUnitActiveSec=15min=), matching roam-sync.
+
+Enable on each machine (one-time, per daily-drivers.md's one-time-setup class):
+
+#+begin_src bash
+# After the dotfiles + rulesets pull, on each daily driver:
+systemctl --user daemon-reload
+systemctl --user enable --now signal-receive.timer
+systemctl --user status signal-receive.service # confirm a clean receive
+#+end_src
+
+* signal-cli setup notes
+
+- *Version:* signal-cli 0.14.5 on velox (2026-07-20).
+- *Accounts:* velox's signal-cli holds the pager identity =+15045173983= as the
+ registered primary (account file 465310). ratio's signal-cli holds two
+ accounts: Craig's personal number =+15103169357= (its own primary,
+ note-to-self only — no phone push) and the pager identity as a *linked device*
+ (Device 2, "ratio-pager", linked 2026-07-20). Both accounts coexist; target
+ the pager with =-a +15045173983=. A future daily driver joins the same way.
+- *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
+ =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
+ approve the link from the account holder:
+
+ #+begin_src bash
+ # On the new machine — prints a tsdevice:/ URI (render as QR to approve):
+ signal-cli link -n "ratio-pager"
+ # Approve from velox (the primary device):
+ signal-cli -a +15045173983 addDevice --uri "tsdevice:/?uuid=…"
+ #+end_src
+
+* Decision — linked device (2026-07-20)
+
+The topology question — ssh-relay only vs. registering daily drivers as linked
+devices — was decided in favor of linked devices. ssh-relay only was simpler
+(one identity, one receive point) but had a single point of failure: a page
+failed when velox was down or off the tailnet.
+
+Registering ratio as a linked device removes that: ratio sends directly, so a
+page lands even when velox is down. The costs, both paid: linked-device
+provisioning per machine (the =link= / =addDevice= handshake above), and each
+device wanting its own regular =receive= — so the warm-keeping timer moved from a
+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
+account."
+
+* History
+
+- 2026-07-04 — home retired ntfy (self-hosted on ratio) and tore it down,
+ switching agent paging to Signal. Handoff to rulesets to document and own.
+- 2026-07-13 — reconciled to one pager identity on velox; =agent-page= shipped
+ (direct on velox, ssh-relay elsewhere, desktop fallback); protocols.org "Paging
+ 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.
diff --git a/scripts/signal-receive.sh b/scripts/signal-receive.sh
new file mode 100755
index 0000000..8c3ef01
--- /dev/null
+++ b/scripts/signal-receive.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# signal-receive.sh — drain the Signal pager account's inbound queue.
+#
+# The pager identity (+15045173983) lives on velox (primary) and any linked
+# device (ratio). The Signal protocol expects a registered account to receive
+# regularly; when it goes quiet, signal-cli prints a staleness warning
+# ("Messages have been last received N days ago") and the account drifts toward
+# an unhealthy state. This script pulls anything queued and exits, keeping the
+# account warm. Run by the signal-receive systemd user timer (scripts/systemd/)
+# on every machine holding the account — the roam-sync-shaped fix for the
+# receive-staleness caveat on the pager channel.
+#
+# Draining the queue is also what surfaces Craig's replies: a reply he types in
+# Signal arrives as a data message here, so a warm account is a prerequisite for
+# the read-replies half of the pager (see docs/design/…-signal-pager-runbook.org).
+#
+# The units stow via the shared dotfiles `common` package, so the timer may land
+# on a machine that does not hold the account. That is fine: the script no-ops
+# cleanly (exit 0) when the account is not registered locally, rather than
+# erroring every cadence.
+#
+# Usage: signal-receive.sh [account] [timeout-seconds]
+# account defaults to the pager identity below
+# timeout-seconds seconds to wait for new messages (default 10)
+#
+# Exit: 0 on a clean receive (including "nothing queued" and "account not on this
+# machine"), non-zero only if signal-cli itself errors, so a real failure is
+# visible in `systemctl --user status signal-receive`.
+
+set -euo pipefail
+
+account="${1:-+15045173983}"
+timeout="${2:-10}"
+
+if ! command -v signal-cli >/dev/null 2>&1; then
+ echo "signal-receive: signal-cli not on PATH — nothing to do" >&2
+ exit 0
+fi
+
+# No-op cleanly where the account isn't registered (a machine that stows the
+# common units but isn't a pager device). Only a genuine receive error should
+# surface as a failure.
+if ! signal-cli listAccounts 2>/dev/null | grep -q "$account"; then
+ echo "signal-receive: $account not registered on this machine — nothing to do" >&2
+ exit 0
+fi
+
+# `receive` prints envelopes to stdout and returns 0 once the queue drains or
+# the timeout elapses. --send-read-receipts tells Craig's phone his replies were
+# read by the pager, matching normal Signal behavior.
+exec signal-cli -a "$account" receive --timeout "$timeout" --send-read-receipts
diff --git a/scripts/systemd/signal-receive.service b/scripts/systemd/signal-receive.service
new file mode 100644
index 0000000..d63a5c8
--- /dev/null
+++ b/scripts/systemd/signal-receive.service
@@ -0,0 +1,10 @@
+# Signal pager receive — keep the pager account warm on every machine holding it.
+# Stowed via the dotfiles `common` package, so it lands on both daily drivers;
+# the script no-ops cleanly where the account isn't registered. Enable per machine:
+# systemctl --user daemon-reload && systemctl --user enable --now signal-receive.timer
+[Unit]
+Description=Drain the Signal pager account inbound queue (keeps it warm)
+
+[Service]
+Type=oneshot
+ExecStart=%h/code/rulesets/scripts/signal-receive.sh
diff --git a/scripts/systemd/signal-receive.timer b/scripts/systemd/signal-receive.timer
new file mode 100644
index 0000000..3fdaee0
--- /dev/null
+++ b/scripts/systemd/signal-receive.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Keep the Signal pager account warm every 15 minutes
+
+[Timer]
+OnBootSec=3min
+OnUnitActiveSec=15min
+RandomizedDelaySec=60
+
+[Install]
+WantedBy=timers.target
diff --git a/scripts/tests/agent-page.bats b/scripts/tests/agent-page.bats
index 071e4b9..7317ac7 100644
--- a/scripts/tests/agent-page.bats
+++ b/scripts/tests/agent-page.bats
@@ -1,9 +1,11 @@
#!/usr/bin/env bats
# agent-page — the runtime-neutral phone pager. Pages Craig over Signal from
-# any machine on the tailnet: runs signal-cli directly on velox (where the
-# pager identity lives), ssh-relays to velox from everywhere else. These tests
-# stub ssh/uname/signal-cli on PATH to verify command construction without a
-# network or a phone.
+# 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.
setup() {
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
@@ -15,8 +17,14 @@ setup() {
echo "ssh \$*" >> "$LOG"
exit 0
EOF
+ # HAS_ACCOUNT controls the listAccounts answer: "1" → the pager account is
+ # local (send direct), unset/"0" → not local (relay).
cat > "$STUBS/signal-cli" <<EOF
#!/bin/bash
+if [ "\$1" = "listAccounts" ]; then
+ [ "\${HAS_ACCOUNT:-0}" = "1" ] && echo "Number: +15045173983"
+ exit 0
+fi
echo "signal-cli \$*" >> "$LOG"
exit 0
EOF
@@ -33,8 +41,8 @@ teardown() {
[[ "$output" == *"usage"* ]]
}
-@test "relays through ssh to velox with the pager account and Craig's UUID" {
- PATH="$STUBS:$PATH" run bash "$PAGE" build finished
+@test "relays through ssh to velox when the account is not local" {
+ HAS_ACCOUNT=0 PATH="$STUBS:$PATH" run bash "$PAGE" build finished
[ "$status" -eq 0 ]
grep -q "^ssh .*velox" "$LOG"
grep -q "15045173983" "$LOG"
@@ -43,16 +51,10 @@ teardown() {
grep -qF 'build\ finished' "$LOG"
}
-@test "on velox itself, calls signal-cli directly (no ssh)" {
- cat > "$STUBS/uname" <<'EOF'
-#!/bin/bash
-[ "$1" = "-n" ] && { echo velox; exit 0; }
-exec /usr/bin/uname "$@"
-EOF
- chmod +x "$STUBS/uname"
- PATH="$STUBS:$PATH" run bash "$PAGE" hello
+@test "sends directly (no ssh) when the pager account is registered locally" {
+ HAS_ACCOUNT=1 PATH="$STUBS:$PATH" run bash "$PAGE" hello
[ "$status" -eq 0 ]
- grep -q "^signal-cli " "$LOG"
+ grep -q "^signal-cli -a +15045173983 send " "$LOG"
! grep -q "^ssh " "$LOG"
}
@@ -62,7 +64,7 @@ EOF
exit 255
EOF
chmod +x "$STUBS/ssh"
- PATH="$STUBS:$PATH" run bash "$PAGE" urgent thing
+ HAS_ACCOUNT=0 PATH="$STUBS:$PATH" run bash "$PAGE" urgent thing
[ "$status" -ne 0 ]
[[ "$output" == *"notify"* ]]
}
diff --git a/scripts/tests/signal-receive.bats b/scripts/tests/signal-receive.bats
new file mode 100644
index 0000000..8fe4d67
--- /dev/null
+++ b/scripts/tests/signal-receive.bats
@@ -0,0 +1,66 @@
+#!/usr/bin/env bats
+# signal-receive.sh — drains the Signal pager account's inbound queue to keep it
+# warm (the roam-sync-shaped fix for receive-staleness) and to surface Craig's
+# replies. Run on velox by the signal-receive systemd timer. These tests stub
+# signal-cli on PATH to verify command construction without a network or the
+# real account.
+
+setup() {
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+ RECV="$REPO_ROOT/scripts/signal-receive.sh"
+ STUBS="$(mktemp -d)"
+ LOG="$STUBS/calls.log"
+ # HAS_ACCOUNT (default 1) controls the listAccounts answer so the local-account
+ # guard can be exercised: "1" lists both test accounts as present, "0" lists
+ # none (so the guard no-ops).
+ cat > "$STUBS/signal-cli" <<EOF
+#!/bin/bash
+if [ "\$1" = "listAccounts" ]; then
+ if [ "\${HAS_ACCOUNT:-1}" = "1" ]; then
+ echo "Number: +15045173983"
+ echo "Number: +19995550000"
+ fi
+ exit 0
+fi
+echo "signal-cli \$*" >> "$LOG"
+exit 0
+EOF
+ chmod +x "$STUBS/signal-cli"
+}
+
+teardown() {
+ rm -rf "$STUBS"
+}
+
+@test "defaults to the pager account, a 10s timeout, and read receipts" {
+ PATH="$STUBS:$PATH" run bash "$RECV"
+ [ "$status" -eq 0 ]
+ grep -q -- "-a +15045173983 receive --timeout 10 --send-read-receipts" "$LOG"
+}
+
+@test "honors an explicit account and timeout" {
+ PATH="$STUBS:$PATH" run bash "$RECV" +19995550000 25
+ [ "$status" -eq 0 ]
+ grep -q -- "-a +19995550000 receive --timeout 25" "$LOG"
+}
+
+@test "no-ops (exit 0) when the account is not registered on this machine" {
+ # Guards the runbook claim that the timer no-ops on a common-package machine
+ # that stows the units but holds no pager account.
+ HAS_ACCOUNT=0 PATH="$STUBS:$PATH" run bash "$RECV"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"not registered"* ]]
+ # The receive must not have run.
+ ! grep -q "receive" "$LOG"
+}
+
+@test "no-ops (exit 0) when signal-cli is not on PATH" {
+ # Empty stub dir with no signal-cli; echo/exit/command are bash builtins, so
+ # the script still runs and hits its signal-cli-absent branch, which no-ops.
+ rm -f "$STUBS/signal-cli"
+ # Invoke bash by absolute path so `run` finds it regardless of the empty
+ # PATH the script itself sees.
+ PATH="$STUBS" run "$(command -v bash)" "$RECV"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"signal-cli"* ]]
+}
diff --git a/todo.org b/todo.org
index 5200406..25eccdc 100644
--- a/todo.org
+++ b/todo.org
@@ -260,7 +260,8 @@ Open question from the roam inbox (2026-07-11): could the startup sequence for .
:END:
From the roam inbox (2026-07-11): work in progress in one project shouldn't stop the sync gate. Idea: keep all diffs/changes in a =working/= directory and exclude it (and its subdirectories) from the sync gate. Many projects run at once, so their WIP files need to be grouped. Also add a per-project count of when the gate tripped, tracked as a metric to investigate. Distinct from the 2026-07-02 policy (untracked/gitignored changes already pass — this is about *tracked* WIP under =working/=). Verify how the gate detects dirtiness today before designing.
-** TODO [#B] Document (and own) the Signal pager :feature:spec:
+** DONE [#B] Document (and own) the Signal pager :feature:spec:
+CLOSED: [2026-07-20 Mon]
:PROPERTIES:
:CREATED: [2026-07-11 Sat]
:LAST_REVIEWED: 2026-07-13
@@ -281,6 +282,9 @@ Craig's call: make paging universal and rename it the agent pager. NEW =claude-t
*** 2026-07-13 Mon @ 18:13:07 -0500 RECONCILED — one channel, on velox; live page verified end to end
The two-paths question is answered: there is ONE pager identity, +15045173983, registered in velox's signal-cli (account file 465310) — and signal-mcp is a locally-configured MCP server in velox's global ~/.claude.json (not claude.ai-side as the 14:40 entry inferred; it's just invisible from ratio, which is why home and this session couldn't find it). ratio's signal-cli holds only Craig's personal number (note-to-self, no push). Verified live today: =ssh velox 'signal-cli -a +15045173983 send -m … b1b5601e-6126-47f8-afaa-0a59f5188fde'= buzzed Craig's phone — his remembered CLI page was this same account on velox. Reliability findings for the runbook: both accounts throw receive-staleness warnings (velox 40 days, ratio 26; the signal protocol wants regular receives — a systemd receive timer on velox is the roam-sync-shaped fix), and the channel requires velox to be up. Remaining deliverables sharpened: the runbook (send + read-replies + receive timer + account notes); decide ssh-over-tailnet-only vs registering ratio as a linked device of the pager account; update protocols.org "Paging Craig" (it names signal-mcp as the only supported path — true only on velox; the ssh recipe is the cross-machine path) — shared-asset edit, own review pass. Interim recipe sent to home so it's unblocked today.
+*** 2026-07-20 Mon @ 15:56:56 -0500 Runbook shipped; ratio linked as a device; receive timer on both machines
+All four remaining deliverables landed. (1) Runbook: [[file:docs/design/2026-07-20-signal-pager-runbook.org]] — send, read-replies, receive timer, signal-cli account/setup notes, the resolved topology decision. (2) Receive timer: =scripts/signal-receive.sh= + =scripts/systemd/signal-receive.{service,timer}= (roam-sync-shaped, 15-min cadence, 3 bats), stowed via dotfiles =common=, enabled + verified on ratio; a manual drain also cleared the 47-day staleness live. (3) Topology decision (Craig, option 2): register daily drivers as linked devices rather than ssh-relay-only — ratio linked as Device 2 "ratio-pager", direct send verified, =agent-page= generalized from a velox-only check to "any machine holding the account sends directly, else relay" (bats updated). (4) protocols.org "Paging Craig": verified accurate, no edit — already describes both channels and the caveats, and its generic runbook pointer is correct to leave un-pathed since it syncs into every project. Remaining one-time step: enable the timer on velox after it pulls dotfiles.
+
** TODO [#C] KB orphan-node review pass :chore:
:PROPERTIES:
:CREATED: [2026-07-01 Wed]