diff options
22 files changed, 583 insertions, 273 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/sessions/2026-07-20-23-36-signal-pager-and-triage-phone-push.org b/.ai/sessions/2026-07-20-23-36-signal-pager-and-triage-phone-push.org new file mode 100644 index 0000000..fe43f61 --- /dev/null +++ b/.ai/sessions/2026-07-20-23-36-signal-pager-and-triage-phone-push.org @@ -0,0 +1,185 @@ +#+TITLE: Session Context — Sentry live trial (ratio, 2026-07-19) +#+AUTHOR: Craig Jennings + +* Summary + +** Active Goal + +Post-flush session. Two items shipped: (1) the archsetup voice-#46 (comma-budget) handoff, reviewed and committed rulesets-side (2ea5d9a); (2) the triage-intake auto-mode phone-push, built onto agent-text, signal-only per Craig's ruling (e27aea2). Items 2 (reply-correlation spec) and 3 (token-rotation discussion) from the prior next-steps list remain untouched, carried forward. + +** Decisions (this session, all shipped) + +- Voice pattern #46 (comma budget, max two per sentence): personal mode only, in the attestation high-recurrence set. Craig's 2026-07-20 direction via archsetup; committed rulesets-side 2ea5d9a. +- Triage auto-mode phone-push is signal-only (Craig's option 1): a quiet sweep's "nothing" heartbeat never pushes to the phone — silent-until-signal governs the phone channel too. Send half shipped (e27aea2); reply-polling deferred to the reply-correlation spec. +- Sentry live trial: ran on ratio, 8 hourly fires, stopped on "sentry off", branch fast-forwarded to main and deleted. +- working/ is tracked-from-creation + gitignored temp/ in both modes (Shape A). Committed. +- Triage source activation: general (synced) plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins active by presence; gate in triage-intake Phase 0 (interactive + unattended). Spec IMPLEMENTED. Migrations done (home + work declared their sources). +- Silent-until-signal: a POLICY not a mechanism — an in-session monitor fire heartbeats =<workflow> at HH:MM: nothing= on an empty check; detection stays in-session (MCP-safe). Applied to sentry, auto triage-intake, auto inbox-zero. Spec IMPLEMENTED. +- Suspend detach change applied to canonical. Sentry cluster consolidated (merged /schedule tasks, added cross-host-coordination task). Task audit stamped 2026-07-20. +- Polyglot: case-by-case, no option-2 machinery (bundles already compose; only coverage-makefile.txt collides, and it's a manual paste). Subprojects: don't promote (N=1). Both closed. + +** Data Collected / Findings — Signal pager (what the resume needs) + +RECONCILED (2026-07-13, in the task's dated log): there is ONE pager identity, =+15045173983=, registered in *velox's* signal-cli (account file 465310). =signal-mcp= is a velox-local MCP server (invisible from ratio). ratio's signal-cli holds only Craig's personal number (note-to-self, no push). =agent-page= already shipped (=claude-templates/bin/agent-page=): runs signal-cli directly on velox, ssh-relays from anywhere else, desktop-fallback on failure, 4 bats, live-verified. protocols.org "Paging Craig" was already rewritten around the two channels (notify desktop + agent-page phone). Craig's Signal UUID: =b1b5601e-6126-47f8-afaa-0a59f5188fde=. Reliability finding: both signal-cli accounts throw receive-staleness warnings (velox ~40 days, ratio ~26) — the Signal protocol wants regular receives, and a systemd receive timer on velox is the roam-sync-shaped fix. + +REMAINING deliverables (this is the work to do): (1) the runbook proper — send + read-replies + receive-timer + signal-cli account/setup notes, the Signal equivalent of the retired ntfy runbook, canonical home in rulesets docs/; (2) a systemd receive timer on velox (roam-sync-shaped) so receives don't go stale; (3) the ssh-over-tailnet-only vs register-ratio-as-a-linked-device decision (a design call for Craig); (4) confirm protocols.org "Paging Craig" is accurate (agent-page did most of it — verify, don't redo). Task body has full history. Source: home handoff 2026-07-04. + +** Files Modified (this session — all committed + pushed to main) + +Post-flush commits: 2ea5d9a (voice #46 comma budget — SKILL.md + voice-profile.org), 9721a49 (chore: mark archsetup handoff PROCESSED), e27aea2 (feat: triage phone-push via agent-text — canonical + mirror triage-intake.org, todo.org task closed). Left unstaged: .claude/settings.json model change (opus→fable, not this session's work — deferred). + +Earlier same-session (pre-flush): f625cf5 (working/temp feat), b02eade + 4d87f35 (triage source activation spec + build), 986d6ca + 0767af8 + 0e9958a (silent-until-signal spec + Phases), af565ba (suspend detach), 70fbe01 (link fixes), 93a2e6d (working-dir filing), c82b625 (sentry cluster), up to fecdf8c, plus the pager work (302b062, 6145489) and overnight sentry commits. + +** Next Steps + +Item 1 (triage phone-push) shipped this session. Two remain from the prior list: + +1. *Spec the reply-correlation follow-up.* The two-way gap: with the account linked on velox + ratio, a Signal reply reaches BOTH devices (one account, Signal fans out per-device, independent queues) and neither knows which page/session it answers. Only bites when two sessions page-and-wait at once (fire-and-forget is fine). Options laid out to Craig: (1) correlation tag stamped on the page, echoed in reply; (2) quote-reply matching (needs verifying signal-cli surfaces quotes); (3) single reply-owner (velox-only). Overlaps the helper-instance work (same shared-channel problem). This spec also owns the deferred triage-intake reply-polling (phone-recv) half. Also fix the runbook "Reading replies" section, which oversells it. Write as a spec in docs/specs/ (spec-create spine). + +2. *Discuss the token-rotation helper* (todo.org =[#C] Token-rotation helper for @a-bonus/google-docs-mcp OAuth refresh=, :feature:quick:) — a discussion first, not a build. Read the task body. + +Publish flow (review + voice + suite) for any commits. + +KB: promoted 0 / consulted no + +* Session Log + +** 2026-07-19 23:52 CDT — Sentry armed (entry) + +Startup ran clean: rulesets pull no-op, make install nothing new, project repo up to date on main (0/0). Session-context was absent (prior session 2026-07-19-21-15 wrapped cleanly). + +Sentry entry gates, all passed with Craig present: +- Autonomy ticket: =:COMMIT_AUTONOMY: yes= in notes.org Workflow State. +- Host: ratio (intended live-trial machine). +- Dirty-tree gate: tracked tree clean; only two untracked inbox files (=2026-07-19-2141-from-.emacs.d-version-working-directories.org=, =2026-07-19-2350-from-home-craig-approved-the-colloquialisms-the.org=) — do not block; sentry's inbox-zero pass (pass 2) handles them. +- Green-suite gate: =make test= exit 0, 377 bats ok, all pytest + ERT green. +- Prior sentry branch: none. +- Reconcile: main 0 behind / 0 ahead of upstream. +- =agent-lock= helper present. + +Created daily branch =sentry/2026-07-19-ratio= from HEAD (main @ f76bf40). Working tree now sits on this branch overnight. Arming the hourly loop next. + +Two inbox items pending at entry, both to be handled by pass 2 (inbox zero): +- .emacs.d working/ version-control ruling — a shared-asset change (working-files convention + install .gitignore behavior across projects). Parks for morning approval; does not fire unattended. +- home colloquialisms wiring approval — already shipped on the rulesets side in the prior session (protocols.org Colloquialisms section + wrap-it-up Before-Close Queue step, commits 8fd9e39/f6a2701). Inbox zero replies-and-files, confirming it's already wired. + +** 2026-07-20 00:06 CDT — Fire 1 digest (manual first fire, Craig present) + +Ran end to end on branch =sentry/2026-07-19-ratio=. Single-runner lock =sentry-rulesets= acquired at fire start, on the sentry branch, tree clean. Pass-by-pass: + +- P1 roam pull — SKIPPED: =~/org/roam= working tree dirty; roam-sync owns that case (pass is read-only ff-only otherwise). No write. +- P2 inbox zero — RAN. Two project handoffs processed. (a) home's colloquialisms + "the list" before-close-queue: already wired canonically before it arrived — replied to home confirming, marked PROCESSED. (b) .emacs.d's working/ tracked-from-creation ruling: shared-asset + convention change with a temp/-placement design decision → PARKED (see approval queue). Staged proposal at =working/sentry-2026-07-19-working-files-ruling/proposed.org=, filed a [#B] VERIFY, replied to .emacs.d with two findings. Committed 727a900. +- P3 triage intake — SKIPPED (probe-too-loose; queued as a finding). The template-synced general plugins (personal Gmail/calendar/cmail/Telegram/GitHub-PRs) are present in *every* project, so the "plugins present" probe self-activates triage everywhere — including rulesets, which is not a triage target. Running it would file Craig's personal action items into rulesets' todo.org and run trash/mark-read/star hygiene on his real accounts unattended. Skipped for scope + safety. No write. +- P4 todo cleanup — RAN: hygiene 0 fixes, --convert-subtasks 0, --archive-done 0. todo.org already clean. No write. +- P5 task audit — RAN (mechanical subset). No unambiguous autonomous staleness fixes: all open tasks carry a priority cookie + type tag except the intentional "Manual testing and validation" container; spec-lifecycle clean (P7); no dead file: links in todo.org. The full reconciliation (per-task fact-check, consolidation, parent-retirement, judgment flags) is interactive → queued. Did NOT bump :LAST_AUDIT: (only the mechanical subset ran). No write. +- P6 working-files hygiene — RAN: flagged =working/inbox-zero-phase-e/= (tracked, backs the now-IMPLEMENTED autonomous-batch-execution spec) as a filing candidate. Filing is a judgment move (3 inbound spec links need updating) → queued. No write. +- P7 spec status board — RAN: clean. All specs READY or IMPLEMENTED; the sentry spec is IMPLEMENTED; no DOING spec with a closed/missing bound parent. No write. +- P8 link integrity — RAN (report-only): ~4 candidate broken file: links in live docs — the autonomous-batch spec's =../../.ai/workflows/inbox-zero.org= (renamed to inbox.org), two folded-in spec-review siblings (wrapup-routing, pattern-catalog), and =subprojects-log.org=. Some may be intentional (review docs folded in and deleted). No unattended rewrites. No write. +- P9 git health — RAN: clean. main = origin/main (0/0), no unpushed commits on other branches, no stale merged branches, sentry branch is the only local extra. No write. +- P10 prep + symlink freshness — SKIPPED: no prep dir (work/home only). No write. + +Fire-end: only org/spine files touched this fire (todo.org, inbox renames, working/ proposal — all org) → conditional suite skipped per contract. Session-context spine is untracked in rulesets, so no digest commit needed; tracked tree is clean. Single-runner lock released. + +Branch state after fire: one commit ahead of main (727a900). Next scheduled fire at :17. + +** 2026-07-20 00:21 CDT — Fire 2 digest (scheduled, :17 cron) + +Lock =sentry-rulesets= acquired, on the sentry branch, tree clean. Little changed in the 14 min since fire 1 — one new inbox handoff, everything else steady. + +- P1 roam pull — RAN: =~/org/roam= clean this fire (was dirty in fire 1), ff-only pull → already up to date. No write. +- P2 inbox zero — RAN: one new handoff. .emacs.d routed three sentry-workflow design considerations from its own hand-run trial (codebase-gated bug/enhancement logging pass; system-health pass; sibling-machine freshness pass). Filed as =** TODO [#C] Sentry vNext passes — from live-trial design input= rather than applied (design input for the Living Document; two hinge on an unresolved cross-driver coordination question). Replied to .emacs.d, marked PROCESSED. Committed 067ed55. +- P3 triage intake — SKIPPED: same probe-too-loose reason as fire 1; the finding is already in the approval queue, not re-queued. +- P4 todo cleanup — RAN: 0 fixes. No write. +- P5 task audit — no change since fire 1; the full-audit-due item stays in the queue, not re-added. +- P6 working-files hygiene — RAN: =working/inbox-zero-phase-e/= still a filing candidate (already queued fire 1, not re-queued). =working/sentry-2026-07-19-working-files-ruling/= is an *active* working dir backing the open VERIFY task — correctly not a candidate. No new queue item. +- P7 spec status board — RAN: clean, no DOING specs. No write. +- P8 link integrity — not re-scanned: no doc changes since fire 1's scan except the new backlog task, whose one internal =file:todo.org::*...= link resolves (KB lesson-detection heading exists). Fire 1's report stands. No write. +- P9 git health — RAN: clean, main = origin/main (0/0), branch now 2 ahead. No write. +- P10 prep freshness — SKIPPED: no prep dir. No write. + +Fire-end: only org files touched → conditional suite skipped. Spine untracked → no digest commit; tracked tree clean. Lock released. Branch 2 commits ahead of main (727a900, 067ed55). No new approval-queue items this fire. + +** 2026-07-20 01:20 CDT — Fire 3 digest (scheduled, :17 cron) + +Quiet read-only fire — nothing changed in the hour since fire 2. All passes no-op or skip; no writes, no commit, no new approval-queue items. + +- P1 roam pull — RAN: clean, ff-only → already up to date. +- P2 inbox zero — RAN: 0 new items. +- P3 triage intake — SKIPPED: same probe-too-loose reason (finding already queued fire 1). +- P4 todo cleanup — RAN: 0 fixes. +- P5 task audit — no change; full-audit-due item stays queued. +- P6 working-files hygiene — RAN: same two dirs (inbox-zero-phase-e already queued; sentry-ruling active for the open VERIFY). No new item. +- P7 spec status board — RAN: clean, no DOING specs. +- P8 link integrity — not re-scanned: no doc changes since fire 1. +- P9 git health — RAN: clean, main = origin/main, branch 2 ahead. +- P10 prep freshness — SKIPPED: no prep dir. + +Branch unchanged at 2 commits ahead. Lock released. + +** 2026-07-20 02:20 CDT — Fire 4 digest (scheduled, :17 cron) + +Quiet read-only fire, same as fire 3 — nothing changed in the hour. All passes no-op or skip; no writes, no commit, no new queue items. P1 roam up to date; P2 inbox 0 new; P3 triage skipped (finding already queued); P4 0 fixes; P5 no change (full audit still queued); P6 same two working dirs (inbox-zero-phase-e queued, sentry-ruling active); P7 clean, no DOING specs; P8 not re-scanned (no doc changes); P9 clean, branch 2 ahead; P10 skipped (no prep dir). Lock released. + +** 2026-07-20 03:20 CDT — Fire 5 digest (scheduled, :17 cron) + +Quiet read-only fire, same as fires 3-4. No changes in the hour; all passes no-op or skip; no writes, no commit, no new queue items. P1 roam up to date; P2 inbox 0 new; P3 triage skipped (queued); P4 0 fixes; P5 no change; P6 same two working dirs; P7 clean; P8 not re-scanned; P9 clean, branch 2 ahead; P10 skipped. Lock released. + +** 2026-07-20 04:20 CDT — Fire 6 digest (scheduled, :17 cron) + +Quiet read-only fire, same as fires 3-5. No changes in the hour; all passes no-op or skip; no writes, no commit, no new queue items. P1 roam up to date; P2 inbox 0 new; P3 triage skipped (queued); P4 0 fixes; P5 no change; P6 same two working dirs; P7 clean; P8 not re-scanned; P9 clean, branch 2 ahead; P10 skipped. Lock released. + +** 2026-07-20 05:20 CDT — Fire 7 digest (scheduled, :17 cron) + +Quiet read-only fire, same as fires 3-6. No changes in the hour; all passes no-op or skip; no writes, no commit, no new queue items. P1 roam up to date; P2 inbox 0 new; P3 triage skipped (queued); P4 0 fixes; P5 no change; P6 same two working dirs; P7 clean; P8 not re-scanned; P9 clean, branch 2 ahead; P10 skipped. Lock released. + +** 2026-07-20 06:20 CDT — Fire 8 digest (scheduled, :17 cron) + +Quiet read-only fire, same as fires 3-7. No changes in the hour; all passes no-op or skip; no writes, no commit, no new queue items. P1 roam up to date; P2 inbox 0 new; P3 triage skipped (queued); P4 0 fixes; P5 no change; P6 same two working dirs; P7 clean; P8 not re-scanned; P9 clean, branch 2 ahead; P10 skipped. Lock released. + +* Sentry approval queue (2026-07-19) + +Judgment/parked items from the overnight fires. Review top to bottom; run or discard each. + +** [Fire 1 · P2] Apply the .emacs.d working/ tracked-from-creation ruling +- *What:* update the canonical working-files convention + add a gitignored temp/ pattern across projects. +- *Why:* Craig's ruling relayed from .emacs.d (2026-07-19). Parked because it's a shared-asset + convention change and carries a design decision (temp/ must be ignored in both track and gitignore modes — it's orthogonal to the personal-tooling set). +- *Prepared:* =working/sentry-2026-07-19-working-files-ruling/proposed.org= (exact edits + the two findings). Filed as the =** VERIFY [#B] working/ tracked-from-creation + gitignored temp/= task in todo.org. +- *Fires on approval:* edit =claude-rules/working-files.md= (tracked-from-creation + temp/ subsections), =scripts/install-ai.sh= + =scripts/sweep-gitignore-tooling.sh= (emit temp/ ignore in both modes — decide shape (a) vs (b) in the proposal), =.ai/protocols.org= Working-Files Convention (one-line mirror); then =scripts/sync-check.sh --fix= for the .ai mirror, run =make test=, commit. + +** [Fire 1 · P3] Tighten the sentry triage-intake pass probe — SPECCED during morning review +- *What:* the narrow "fix the probe" framing grew, on Craig's flexibility question, into a per-project source-activation model for triage-intake. +- *Resolution:* written up as a spec for review — =docs/specs/2026-07-20-triage-source-activation-spec.org= (DRAFT), linked from the =** TODO [#B] Triage source activation= task. General plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins stay active by presence; the activation layer lives in triage-intake Phase 0 so it fixes the interactive over-pull too; sentry's pass-3 probe reads the same signal. +- *Next:* Craig's deep read → DRAFT → READY → spec-response decomposes the 5 phases. Two decisions still open (declaration format, whether interactive adopts the same gate). No code landed — this item is now tracked by the spec + task, not the queue. + +** [Fire 1 · P5] A full interactive task-audit is due +- *What:* run task-audit.org interactively (its consolidation / parent-retirement / judgment-flag phases need Craig). +- *Why:* :LAST_AUDIT: is 2026-07-04 (~16 days); task-review-staleness flags 2 top-level tasks unreviewed >7 days. Fire 1's mechanical subset found nothing unambiguous to auto-fix, so the marker was deliberately not bumped. +- *Fires on approval:* "let's do a task audit" (or task-review for the lighter pass). + +** [Fire 1 · P6] File working/inbox-zero-phase-e/ to a permanent home +- *What:* file the three artifacts under =working/inbox-zero-phase-e/= per working-files.md and update inbound links. +- *Why:* the backing work (autonomous-batch-execution spec) is IMPLEMENTED, so the working dir is a filing candidate. Filing is a judgment move — 3 inbound =file:= links in =docs/specs/2026-06-16-autonomous-batch-execution-spec.org= point at it and need updating in the same move. +- *Fires on approval:* rename+move the 3 files flat into their permanent home, update the spec's 3 links, delete the empty working subdir. + +** [Fire 1 · P8] Resolve ~4 candidate broken file: links in live docs (report-only) +- *What:* fix or confirm-intentional the broken links P8 flagged. +- *Why:* report-only pass; no unattended rewrites. The clearest real one: =docs/specs/2026-06-16-autonomous-batch-execution-spec.org= links =../../.ai/workflows/inbox-zero.org= (renamed to inbox.org). Others (=wrapup-routing-spec-review.org=, a pattern-catalog inbox source, =subprojects-log.org=) may be folded-in/deleted review docs — Craig's call. +- *Fires on approval:* update the inbox-zero.org→inbox.org link; decide the rest. + +** 2026-07-20 Mon @ 15:28:41 -0500 — flushed (auto) +Auto-flush checkpoint mid-session. Session's shipped work is all committed + pushed (main == origin == velox). In flight: resuming into finishing the Signal pager task ([#B] in todo.org) — the runbook, velox receive timer, and the ssh-vs-linked-device decision. See Summary → Next Steps and Data Collected for the reconciled facts needed to resume blind. + +** 2026-07-20 Mon @ 16:48 CDT — Notification vocabulary split (page/text) + agent-page → agent-text rename +Craig's follow-on from the reply-ambiguity discussion: reserve trigger words by channel. "page me" = desktop notify, "text me" = Signal, "text and page me" = both. Renamed the tool agent-page → agent-text to match (deprecated agent-page shim delegates to it, removable later). Rewrote protocols.org "Paging Craig" → "Reaching Craig", page-me.org, work-the-backlog's away-run logic, INDEX, and the runbook; updated install-ai doc comments; bats renamed agent-page.bats → agent-text.bats (5 pass incl. shim delegation). /review-code + /voice ran; make test exit 0, shellcheck clean, no new lint. Commit 6145489, pushed; make install re-run on both ratio + velox, agent-text + shim resolve on both. Vocabulary decision arc (this session): first "page=Signal, notify=desktop" (rejected, inverted current default), then "page=desktop, message=Signal" (Craig's), then settled on "text me" for Signal (agent-text). NOTE excluded from staging: .claude/settings.json shows model opus→fable (not mine — left unstaged). + +** 2026-07-20 Mon @ 23:36 CDT — Post-flush resume: voice #46 handoff + triage phone-push (item 1) +Resumed from the auto-flush anchor. Two things landed. + +Inbox handoff (archsetup, voice pattern #46 comma budget): archsetup edited voice/SKILL.md + voice-profile.org through the shared ~/.claude/skills/voice symlink (points into rulesets) at Craig's 2026-07-20 direction — "no more than two commas per sentence" in the /voice personal pass. Reviewed both files (count 45→46 everywhere, personal-only set four→five, #46 in the attestation high-recurrence set, before/after from the Hyprland draft). /review-code approved, /voice personal on the message, make test green. Committed 2ea5d9a; replied to archsetup; marked PROCESSED (chore 9721a49). The .claude/settings.json model change (opus→fable) was left unstaged — not this session's work. + +Item 1 — triage-intake auto-mode phone-push, built. Folded a "Phone delivery" subsection into canonical triage-intake.org: a full-three-section sweep pushes to Craig's phone over Signal via agent-text, with a pointer from End-of-sweep output and a Living Document note. Craig's design ruling (option 1, signal-only): a quiet sweep's "nothing" heartbeat never reaches the phone — silent-until-signal governs the phone channel too — while the in-session heartbeat stays as proof-of-life. Send half only; reply-polling (old phone-recv) deferred to the reply-correlation follow-up (item 2), which owns the recv wiring. sync-check --fix synced the mirror; make test green; /review-code approved; /voice personal. Committed e27aea2. todo.org task closed DONE (send half shipped, recv deferred). + +Next: items 2 (reply-correlation spec) and 3 (token-rotation discussion) remain untouched — carried to the next session. + +** 2026-07-20 Mon @ 16:14 CDT — Signal pager DONE (all four deliverables + topology decision) +Finished the task end to end. (1) Runbook: docs/design/2026-07-20-signal-pager-runbook.org — send, read-replies, receive timer, signal-cli account/setup notes, the resolved decision. (2) Receive timer: scripts/signal-receive.sh + scripts/systemd/signal-receive.{service,timer} (roam-sync-shaped, 15-min, no-ops where the account isn't local, 4 bats); stowed via dotfiles common; enabled + verified on both ratio and velox; a manual drain cleared the 47-day staleness live. (3) Topology decision (Craig, option 2 — linked device over 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. Publish flow: /review-code caught a runbook/script contradiction (runbook claimed no-op-clean; script errored) → added the account-presence guard + a bats case. make test green, shellcheck clean. Commits: rulesets 302b062 (feat(pager)), dotfiles d8b0462 (feat(systemd)); both pushed; velox synced. todo.org task closed DONE. Craig's follow-up worry addressed: the pager only fires on an explicit page-me or an away-run's end-of-set page; the receive timer is silent (receive-only, no push). 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/triage-intake.org b/.ai/workflows/triage-intake.org index 7451fed..55cc939 100644 --- a/.ai/workflows/triage-intake.org +++ b/.ai/workflows/triage-intake.org @@ -209,6 +209,22 @@ Auto mode runs as a =/loop= in the *live session*, not a detached cron job: Running in the live session means MCP auth (Slack, Gmail, Linear) is inherited from the session — the headless-auth wall that blocks a detached cron run does not apply. A durable cross-session schedule is out of scope here; that belongs to the morning-ops orchestrator, which can later invoke auto mode's accumulate behavior as its triage limb. The close/stop commands below require a live session by design. +*** Phone delivery — push each signal sweep via =agent-text= + +Auto mode exists for when Craig is away from the desk, so a sweep that surfaces something worth seeing is delivered to his phone, not just printed into a session he isn't watching. After a sweep that renders the full three sections — one with real deltas or an unacked-list change (see "End-of-sweep output" below) — send that same output to his phone over Signal with =agent-text=: + +#+begin_src bash +agent-text "$SWEEP_SUMMARY" +#+end_src + +The pushed text is the *fuller* three-section shape, not a terse one-liner: the per-source deltas, the responses-awaiting-acknowledgment list, and the timestamp, led by a ⚠ SCAN FAILED banner if any source failed. + +*Signal-only — never on a quiet sweep.* An empty sweep (the =triage intake at HH:MM: nothing= heartbeat) does *not* push to the phone. Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=) governs the phone channel too, so the phone stays silent until a sweep has real signal. The in-session heartbeat still prints as proof the loop ran; the phone is reserved for something that actually needs Craig. (Craig's ruling, 2026-07-20: the higher-cost channel doesn't buzz with "nothing.") + +If =agent-text= isn't on =PATH=, fall back to inline delivery and say so once. + +*Reply polling is deferred.* The send half ships here; polling the phone for Craig's replies (the =phone-recv= half of the retired ntfy design) waits on the reply-correlation follow-up. With the Signal account linked on more than one device, a reply fans out to every device and neither knows which page it answers — that has to be resolved before auto mode reads replies back. Until then auto mode pushes but does not poll, and Craig acts on a pushed summary from wherever he picks it up. + ** Preconditions and Close-out Auto mode borrows the inbox monitor-mode gates (=inbox.org= monitor mode): do not start on a dirty worktree or a red test suite — a close's batch commit would otherwise sweep up unrelated changes — and leave the tree clean and green when the loop stops. Surface a blocker with inline numbered options per =interaction.md= and wait. @@ -226,7 +242,7 @@ Each sweep runs Phase 0 (load *both* plugin dirs — the loud requirement still ** End-of-sweep output — three sections, or one heartbeat -*Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=).* An *empty sweep* — no deltas since the previous sweep and no change to the awaiting-acknowledgment list — collapses to a single heartbeat line and nothing else: =triage intake at HH:MM: nothing= (HH:MM local, from =date=). Detection still runs in full (Phase 0 plus the A-D scan, against the session's inherited MCP auth); only the output collapses, so a long unattended run stops filling the session with identical "no changes" blocks. A sweep with real deltas or an unacked-list change prints the full three sections below. +*Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=).* An *empty sweep* — no deltas since the previous sweep and no change to the awaiting-acknowledgment list — collapses to a single heartbeat line and nothing else: =triage intake at HH:MM: nothing= (HH:MM local, from =date=). Detection still runs in full (Phase 0 plus the A-D scan, against the session's inherited MCP auth); only the output collapses, so a long unattended run stops filling the session with identical "no changes" blocks. A sweep with real deltas or an unacked-list change prints the full three sections below, and — when away — pushes them to Craig's phone via =agent-text= (see "Phone delivery" above). The empty-sweep heartbeat is never pushed. 1. *Deltas* — what changed since the *previous sweep* (the standard Phase C summary scoped to the inter-sweep delta). 2. *Responses awaiting your acknowledgment* — every Slack reply, email, or message directed at Craig that he hasn't acknowledged or had the agent answer. A *running list carried forward across sweeps* until Craig acks each item or closes the triage. An away user's first need is "who's waiting to hear back from me," which a delta-only sweep loses the moment it scrolls past. @@ -424,6 +440,9 @@ Update the engine as the orchestration pattern evolves; update a plugin as its s *** Updates and Learnings +**** 2026-07-20: Phone delivery for signal sweeps (=agent-text=, send half) +Auto mode now pushes a full-three-section sweep to Craig's phone over Signal via =agent-text=, the away-from-desk delivery the retired ntfy design carried before ntfy was torn down (2026-07-04). Transport is =agent-text= (the renamed Signal pager), not ntfy. Signal-only by Craig's 2026-07-20 ruling: a quiet sweep's =nothing= heartbeat never reaches the phone — silent-until-signal governs the phone channel too, so the higher-cost channel only fires when a sweep has real signal, while the in-session heartbeat stays as proof the loop ran. Falls back to inline when =agent-text= is absent. Only the send half ships; reply polling (the old =phone-recv=) waits on the reply-correlation follow-up, because a Signal reply fans out to every linked device and neither knows which page it answers. + **** 2026-07-18: Three-section digest + close-by-default (Phase C/D rewrite) Craig's ruling after a 42h-gap sweep where the long-form report (top signals + per-source breakdown + 7-option action menu) was followed by "summarize the notable items" — and the digest that answered it was the report he wanted first. Phase C now renders ==TASKS== (work items needing Craig, solo-executable first, priority order) / ==FYI== (work context, no action owed) / ==MISC== (everything outside the project, actions stated inline), then exactly two options (close-and-file / close-and-execute-solo, the latter only when solo items exist), timestamp last. Per-source blocks and the itemized action menu are gone from the default surface (long form on request). Phase D became the close: it runs as the next action no matter what Craig replies (unless he explicitly holds), includes the mail hygiene on every scanned account without itemized confirmation, files the TASKS, clears resolved unacked items, advances the sentinel, and tears down started services. Solo = mechanical + standing-approved + no prose under Craig's name; prose sends and destructive non-mail actions stay gated. The stay-open-until-confirmed exit loop is retired. Same-day addendum: the "and reroute" modifier ("1 and reroute") — MISC items are surfaced-only by default (never filed to this project's todo.org); appending the modifier delivers each outside-project item to its owner's inbox via inbox-send per the cross-project rule. 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/triage-intake.org b/claude-templates/.ai/workflows/triage-intake.org index 7451fed..55cc939 100644 --- a/claude-templates/.ai/workflows/triage-intake.org +++ b/claude-templates/.ai/workflows/triage-intake.org @@ -209,6 +209,22 @@ Auto mode runs as a =/loop= in the *live session*, not a detached cron job: Running in the live session means MCP auth (Slack, Gmail, Linear) is inherited from the session — the headless-auth wall that blocks a detached cron run does not apply. A durable cross-session schedule is out of scope here; that belongs to the morning-ops orchestrator, which can later invoke auto mode's accumulate behavior as its triage limb. The close/stop commands below require a live session by design. +*** Phone delivery — push each signal sweep via =agent-text= + +Auto mode exists for when Craig is away from the desk, so a sweep that surfaces something worth seeing is delivered to his phone, not just printed into a session he isn't watching. After a sweep that renders the full three sections — one with real deltas or an unacked-list change (see "End-of-sweep output" below) — send that same output to his phone over Signal with =agent-text=: + +#+begin_src bash +agent-text "$SWEEP_SUMMARY" +#+end_src + +The pushed text is the *fuller* three-section shape, not a terse one-liner: the per-source deltas, the responses-awaiting-acknowledgment list, and the timestamp, led by a ⚠ SCAN FAILED banner if any source failed. + +*Signal-only — never on a quiet sweep.* An empty sweep (the =triage intake at HH:MM: nothing= heartbeat) does *not* push to the phone. Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=) governs the phone channel too, so the phone stays silent until a sweep has real signal. The in-session heartbeat still prints as proof the loop ran; the phone is reserved for something that actually needs Craig. (Craig's ruling, 2026-07-20: the higher-cost channel doesn't buzz with "nothing.") + +If =agent-text= isn't on =PATH=, fall back to inline delivery and say so once. + +*Reply polling is deferred.* The send half ships here; polling the phone for Craig's replies (the =phone-recv= half of the retired ntfy design) waits on the reply-correlation follow-up. With the Signal account linked on more than one device, a reply fans out to every device and neither knows which page it answers — that has to be resolved before auto mode reads replies back. Until then auto mode pushes but does not poll, and Craig acts on a pushed summary from wherever he picks it up. + ** Preconditions and Close-out Auto mode borrows the inbox monitor-mode gates (=inbox.org= monitor mode): do not start on a dirty worktree or a red test suite — a close's batch commit would otherwise sweep up unrelated changes — and leave the tree clean and green when the loop stops. Surface a blocker with inline numbered options per =interaction.md= and wait. @@ -226,7 +242,7 @@ Each sweep runs Phase 0 (load *both* plugin dirs — the loud requirement still ** End-of-sweep output — three sections, or one heartbeat -*Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=).* An *empty sweep* — no deltas since the previous sweep and no change to the awaiting-acknowledgment list — collapses to a single heartbeat line and nothing else: =triage intake at HH:MM: nothing= (HH:MM local, from =date=). Detection still runs in full (Phase 0 plus the A-D scan, against the session's inherited MCP auth); only the output collapses, so a long unattended run stops filling the session with identical "no changes" blocks. A sweep with real deltas or an unacked-list change prints the full three sections below. +*Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=).* An *empty sweep* — no deltas since the previous sweep and no change to the awaiting-acknowledgment list — collapses to a single heartbeat line and nothing else: =triage intake at HH:MM: nothing= (HH:MM local, from =date=). Detection still runs in full (Phase 0 plus the A-D scan, against the session's inherited MCP auth); only the output collapses, so a long unattended run stops filling the session with identical "no changes" blocks. A sweep with real deltas or an unacked-list change prints the full three sections below, and — when away — pushes them to Craig's phone via =agent-text= (see "Phone delivery" above). The empty-sweep heartbeat is never pushed. 1. *Deltas* — what changed since the *previous sweep* (the standard Phase C summary scoped to the inter-sweep delta). 2. *Responses awaiting your acknowledgment* — every Slack reply, email, or message directed at Craig that he hasn't acknowledged or had the agent answer. A *running list carried forward across sweeps* until Craig acks each item or closes the triage. An away user's first need is "who's waiting to hear back from me," which a delta-only sweep loses the moment it scrolls past. @@ -424,6 +440,9 @@ Update the engine as the orchestration pattern evolves; update a plugin as its s *** Updates and Learnings +**** 2026-07-20: Phone delivery for signal sweeps (=agent-text=, send half) +Auto mode now pushes a full-three-section sweep to Craig's phone over Signal via =agent-text=, the away-from-desk delivery the retired ntfy design carried before ntfy was torn down (2026-07-04). Transport is =agent-text= (the renamed Signal pager), not ntfy. Signal-only by Craig's 2026-07-20 ruling: a quiet sweep's =nothing= heartbeat never reaches the phone — silent-until-signal governs the phone channel too, so the higher-cost channel only fires when a sweep has real signal, while the in-session heartbeat stays as proof the loop ran. Falls back to inline when =agent-text= is absent. Only the send half ships; reply polling (the old =phone-recv=) waits on the reply-correlation follow-up, because a Signal reply fans out to every linked device and neither knows which page it answers. + **** 2026-07-18: Three-section digest + close-by-default (Phase C/D rewrite) Craig's ruling after a 42h-gap sweep where the long-form report (top signals + per-source breakdown + 7-option action menu) was followed by "summarize the notable items" — and the digest that answered it was the report he wanted first. Phase C now renders ==TASKS== (work items needing Craig, solo-executable first, priority order) / ==FYI== (work context, no action owed) / ==MISC== (everything outside the project, actions stated inline), then exactly two options (close-and-file / close-and-execute-solo, the latter only when solo items exist), timestamp last. Per-source blocks and the itemized action menu are gone from the default surface (long form on request). Phase D became the close: it runs as the next action no matter what Craig replies (unless he explicitly holds), includes the mail hygiene on every scanned account without itemized confirmation, files the TASKS, clears resolved unacked items, advances the sentinel, and tears down started services. Solo = mechanical + standing-approved + no prose under Craig's name; prose sends and destructive non-mail actions stay gated. The stay-open-until-confirmed exit loop is retired. Same-day addendum: the "and reroute" modifier ("1 and reroute") — MISC items are surfaced-only by default (never filed to this project's todo.org); appending the modifier delivers each outside-project item to its owner's inbox via inbox-send per the cross-project rule. 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/inbox/PROCESSED-2026-07-20-1714-from-archsetup-voice-skill-new-pattern-46-comma-budget.org b/inbox/PROCESSED-2026-07-20-1714-from-archsetup-voice-skill-new-pattern-46-comma-budget.org new file mode 100644 index 0000000..e999567 --- /dev/null +++ b/inbox/PROCESSED-2026-07-20-1714-from-archsetup-voice-skill-new-pattern-46-comma-budget.org @@ -0,0 +1,5 @@ +#+TITLE: Voice skill: new pattern #46 (comma budget, personal mode) a +#+SOURCE: from archsetup +#+DATE: 2026-07-20 17:14:02 -0500 + +Voice skill: new pattern #46 (comma budget, personal mode) added at Craig's direction, 2026-07-20 archsetup session. His words: 'no more than two commas per sentence. we should add that to the /voice personal pass.' Paired edit already made in your working tree via the ~/.claude/skills/voice symlink: voice/SKILL.md (frontmatter count 45->46, Modes, Your Task, Craig's Voice intro, new #46 section, Process steps 2+8, Reference) and voice/references/voice-profile.org (new §46 with basis + before/after from the Hyprland issue draft that prompted it). #46 joined the attestation high-recurrence set at birth. Nothing else touched; review and commit rulesets-side. diff --git a/inbox/lint-followups.org b/inbox/lint-followups.org new file mode 100644 index 0000000..52905dd --- /dev/null +++ b/inbox/lint-followups.org @@ -0,0 +1,2 @@ + +* 2026-07-20 Mon — Task-review health: 1 top-level [#A]/[#B]/[#C] tasks unreviewed for >30 days (daily review may have slipped) 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. @@ -82,103 +82,6 @@ What we're verifying: triage-intake pulls only the sources a project declares (s - Under sentry, confirm pass 3 (triage) probe-skips in a project with no active source, and runs where one is declared. Expected: undeclared general plugins never scan; declared ones and project-specific ones do; sentry's triage pass activates only where a source is active. -** DONE [#B] working/ tracked-from-creation + gitignored temp/ :feature: -CLOSED: [2026-07-20 Mon] -Craig's ruling relayed from .emacs.d (2026-07-19): working/ is version-controlled from creation (not excluded until graduation); ephemeral artifacts go in a gitignored temp/ or /tmp; graduation reorganizes durable artifacts into permanent homes rather than marking when they become durable. - -Implemented (Shape A) during the morning sentry review, 2026-07-20: -- =claude-rules/working-files.md=: added "working/ Is Version-Controlled From Creation" section (tracked-from-creation, graduation-is-a-move, temp/ for ephemeral). -- =.ai/protocols.org= (canonical + mirror): one-paragraph mirror in the Working-Files Convention section. -- =scripts/install-ai.sh=: emits a =temp/= ignore block in BOTH track and gitignore modes; working/ never ignored. Idempotent. -- =scripts/sweep-gitignore-tooling.sh=: separate mode-independent temp/ backfill pass (a distinct loop, not an IGNORE_SET member — that would skip track-mode projects, the set that most needs it). -- Tests: 3 new install-ai bats + 4 new sweep bats (temp/ in both modes, never working/, idempotent). Suite green, 384 bats ok. - -Finding confirmed at implementation: the canonical machinery already never ignored working/ (tooling set is only =.ai/ .claude/ CLAUDE.md AGENTS.md=), so .emacs.d's =/working/= ignore was a purely local deviation. The staging proposal dir was removed after shipping; its content lives in the feat commit and this body. - -** DONE [#C] Polyglot projects — supported, or refused? :spec: -CLOSED: [2026-07-20 Mon] -DECISION (2026-07-20, scouting with Craig): *case-by-case, and it already composes — no option-2 machinery.* The evidence: bundle contents split into namespaced/additive files (rules =<lang>.md=, =validate-<lang>.sh= hooks, =coverage-summary.<ext>= scripts, appended =gitignore-add.txt=) that compose cleanly, and exactly three colliding files — =claude/settings.json= and =githooks/pre-commit= (full bundles only: bash/elisp/go) and =coverage-makefile.txt= (elisp/go/python/typescript). Of the three, only =coverage-makefile.txt= occurs in the fleet, and clock-panel (the one real polyglot, python+typescript) proves it's benign: both bundles installed, no breakage, because that fragment is a hand-pasted Makefile block nobody pasted twice. So: keep the install-lang collision guard (it blocks the destructive full+full settings/githooks clobber, which no project actually hits), and document the =coverage-<lang>:= + =coverage:= aggregate namespacing as the one manual step when going polyglot (filed below). No two-full-co-equal-bundle project exists, so the settings.json/githooks merge is unwarranted. Follow-up doc: [[file:todo.org::*Document polyglot coverage-makefile namespacing][Document polyglot coverage-makefile namespacing]]. - -Do we support more than one language bundle per project? The honest answer today -is "partly, by accident." The collision guard added 2026-07-16 refuses a -*colliding* second bundle rather than silently replacing the first's config, but -a non-overlapping pair still installs fine: bash ships =settings.json= + -githooks and no coverage fragment, python ships only a coverage fragment, so -=bash= + =python= composes cleanly today and yields a real polyglot project with -both rule sets. So the line isn't polyglot-vs-not, it's overlap-vs-not — and -nobody chose that line, it fell out of which bundle happens to ship what. Origin: -home's report after scaffolding clock-panel with python + typescript, -[[file:docs/design/2026-07-16-polyglot-bundle-collision.txt][docs/design/2026-07-16-polyglot-bundle-collision.txt]]. - -Pair this with the subproject scouting below — it's the same question in a -different costume ("which projects would actually be polyglot, and why"), so -they should be one conversation. - -The three options, in the order they'd be weighed: - -1. *Unsupported, explicitly.* Keep the guard as the answer. Cheapest, and - matches how little polyglot exists (one project, clock-panel). -2. *Supported.* Needs per-bundle filenames, a merged =settings.json= (the hooks - arrays compose rather than clobber), composed githooks, and namespaced - Makefile targets with a =coverage= aggregate. This is the real work. -3. *Case-by-case.* Support the pairs that come up, refuse the rest. - -What the decision needs to know: - -- *The target-name collision is the deeper half* (home's point, and it's right). - Every bundle's fragment defines =coverage:= and =coverage-summary:=, so even - with both files present a polyglot project can't paste both into one Makefile. - Renaming files doesn't fix it. -- *Only three of five shared filenames actually collide.* =gitignore-add.txt= - (5 bundles) appends deduped and composes. =CLAUDE.md= (3) is seed-only, and - its fallback comment shows multi-bundle was already considered there. - =claude/settings.json= (3), =githooks/*= (3), and =coverage-makefile.txt= (4) - are the real ones. -- *=FORCE=1= is a poor escape hatch* (home's catch): it also re-seeds - =CLAUDE.md=, which is destructive on a customized project. If polyglot - becomes supported, the override wants to be its own flag. - -** DONE [#C] Subproject pattern — promote to claude-rules? :spec: -CLOSED: [2026-07-20 Mon] -DECISION (2026-07-20, scouting with Craig): *don't promote — keep it local at home.* The scouting confirmed N=1: across all 27 =.ai= scopes, home (9 subprojects, all from the single 2026-06-11 fold) is the only real user. The nearest neighbors aren't the pattern — rulesets folds claude-templates in as a git *subtree* (different mechanism, not shared-=.ai/=-scope), archsetup's dotfiles/ likewise; every other project is a focused single package with no subproject structure or need. Promoting a 282-line convention into the always-on claude-rules layer for one project fails the thin-always-on principle (the precedent is patterns.md at 29 lines + docs-lifecycle.md's depth-in-a-spec). Keep the convention as home's local instance. Revisit only if a genuine second case appears, and then as a thin pointer + a spec, never 282 lines always-on. - -home proposes promoting its subproject pattern (a former standalone project -folded into a parent, living as a self-contained subdir sharing the parent's -=.ai/= scope) into the rules layer: vocabulary, the read-first -=<subproject>/<subproject>-brief.org= convention, the parent-vs-subproject -content criterion ("one fact, one home"), and create/archive criteria. -Proposal + home's full instance: -[[file:docs/design/2026-07-15-subproject-pattern-proposal.org][proposal]], -[[file:docs/design/2026-07-15-subprojects-convention-home-instance.org][home's convention doc]]. - -Deferred 2026-07-16 rather than promoted. *Craig's framing:* he wants to scout -which projects would actually get subprojects, and why, before we shape a rule. -If he hasn't done that scouting by the time this comes up, offer to do it -together — brainstorm the candidates, then explore the reasons behind each. That -evidence decides it: either we drop the pattern, or we know enough to adjust it -so it's effective. Don't shape the rule before the scouting. - -*Review findings from the 2026-07-16 pass* (the inputs the decision needs): - -- *N=1.* home is the only project with subprojects, across all 27 =.ai= scopes; - its nine all came from the single 2026-06-11 fold. This is the fact the - scouting tests. -- *Placement contradicts the proposal's own principle.* =claude-rules/*.md= - loads into every session of every project. home's doc argues the always-on - layer is "a tax paid whether or not it's relevant today" and depth belongs - "one open away". At 282 lines the doc would be the third-largest rule and add - ~11% to the always-on layer, so every .emacs.d / takuzu / chime session would - carry a one-project convention. -- *Precedent for the shape:* =patterns.md= (29 lines, explicit "don't carry the - catalog in context") and =docs-lifecycle.md= (75 lines, depth in a spec). - Thin rule + on-demand depth is the established answer. -- *Dangling reference:* the doc cites =claude-rules/git-hosting-privacy-model= - as authority for its shared-scope-safety criterion. No such file exists — the - real content is the gitignore-vs-track and public-reachability decision in - =protocols.org=. Fix before any promotion. -- *Instance vs rule:* the metrics, self-improvement log, kill criteria, rollout - dates, and adoption table are home's instance, not rule content. - ** TODO [#D] Document polyglot coverage-makefile namespacing :chore: The one manual step the polyglot decision (2026-07-20) left: when a project installs two bundles that both ship =coverage-makefile.txt= (elisp/go/python/typescript), their =coverage:= / =coverage-summary:= Makefile targets duplicate. Document the fix — rename to per-language =coverage-<lang>:= targets and add a =coverage:= aggregate that depends on them — where a polyglot user would meet it (install-lang docs, or a short note in the bundle README / languages/ overview). Low urgency: clock-panel is polyglot today and unaffected because it has no assembled Makefile. Do it when the coverage-makefile fragments are next actually pasted, or opportunistically. @@ -209,18 +112,6 @@ Two consumers, one contract: Design the one contract; both features consume it. Merged 2026-07-20 from the separate "Sentry unattended /schedule variant" and "Fully-unattended scheduled inbox check" tasks — same no-session design problem. -** DONE [#B] Triage source activation — per-project source declaration :feature:spec: -CLOSED: [2026-07-20 Mon] -Spec: [[file:docs/specs/2026-07-20-triage-source-activation-spec.org][docs/specs/2026-07-20-triage-source-activation-spec.org]] (IMPLEMENTED, ID af73ef0b-cd1d-46f1-9e1d-62695733a4de). Craig approved after his read, both open decisions resolved via cj comments; built same session. - -From the sentry live trial (2026-07-20): triage-intake self-activated in every project because the general (personal-account) plugins are template-synced everywhere, so sentry's pass-3 probe misfired and would pull Craig's personal inboxes into whatever project the fire ran in. Fixed with an activation gate in triage-intake Phase 0 — general plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins stay active by presence. Applies to interactive and unattended alike. sentry pass-3 probe reads the same signal. Migration handoffs sent to home + work. Supersedes Fire 1's narrower probe-only approval-queue item. - -** DONE [#B] Silent-until-signal for in-session monitor loops :feature:spec: -CLOSED: [2026-07-20 Mon] -Spec: [[file:docs/specs/2026-07-20-silent-until-signal-monitors-spec.org][docs/specs/2026-07-20-silent-until-signal-monitors-spec.org]] (IMPLEMENTED, ID af592bd6-d3e6-47e2-8804-2a287b4d9303). Craig approved without changes 2026-07-20; all 5 phases shipped that day. Sentry, auto triage-intake, and auto inbox-zero all collapse an empty fire to =<workflow> at HH:MM: nothing=; a manual-testing entry covers the live-loop verification. - -Craig-approved proposal from .emacs.d (2026-07-20), demonstrated by the sentry live trial (fires 3-8 were walls of no-op lines). Reframed live from a watcher *mechanism* to a *policy*: an in-session monitor fire detects first, and on an empty check collapses to one labelled heartbeat line (=<workflow> at HH:MM: nothing=) instead of a full turn; only a real item earns the full surface-and-judge turn. Keeping detection in-session dissolves the MCP-auth split (triage's Gmail/Slack/Linear need session auth), so it applies uniformly to sentry, auto triage-intake, and auto inbox-zero. No external watcher, no new seen-list. Five build phases in the spec. Phase 1 (sentry quiet-fire heartbeat) shipped 2026-07-20 ahead of the full READY gate at Craig's direction — a quiet fire now collapses to =sentry at HH:MM: nothing=. Phases 2-4 (auto triage-intake, auto inbox-zero, shared-policy home) + Phase 5 verification pending Craig's spec read. Overlaps the sentry cluster ([[file:todo.org::*Sentry vNext passes][Sentry vNext passes]], [[file:todo.org::*Triage source activation][Triage source activation]]). - ** TODO [#D] Research: MCP for device locations shared with you :feature: From .emacs.d (2026-07-20, rulesets-owned research). Is there a Google Maps MCP (or similar) that reports the locations of devices sharing their location with you? If none exists, research how hard it would be to build one. (Google's location-sharing has no official public API; likely needs investigation of unofficial routes or a different provider.) @@ -233,12 +124,6 @@ Three considerations captured by .emacs.d's own hand-run sentry trial (routed vi Take up with the sentry Living Document refinements once the trial has quiet nights behind it. Passes #2 and #3 are blocked on [[file:todo.org::*Cross-host agent coordination][Cross-host agent coordination]] (the missing ratio↔velox lock). Related: [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract]] (the no-session variant). -** DONE [#C] Apply suspend.org detach-on-suspend change to canonical :feature: -CLOSED: [2026-07-20 Mon] -Craig's change relayed from archsetup (2026-07-20): add Step 6 to suspend.org — detach the tmux client (=tmux detach-client -s "$sess"=) as the final action of every suspend, so a suspended session parks in the re-attachable set instead of cluttering the alt-space rotation. Also reworded the neighbors bullet and the "does NOT do" teardown bullet to draw the detach-vs-teardown line. - -Applied to canonical 2026-07-20 (verified the diff was exactly the intended change, canonical + mirror synced, lint clean bar one pre-existing flush/SKILL.md link). Replied to archsetup that its local stopgap is now canonical. - ** TODO [#B] Extend ui-prototyping rule with build-to-prototype :feature: :PROPERTIES: :CREATED: [2026-07-11 Sat] @@ -260,31 +145,6 @@ 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. -** 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 -:END: -home retired the ntfy phone-notification channel (phone-notify/phone-recv, self-hosted ntfy on ratio) on 2026-07-04 in favor of paging over Signal, and tore ntfy down. The Signal pager it replaced ntfy with is undocumented: no pager script in =~/.local/bin=, and =notify= doesn't reference Signal. What exists on ratio: signal-cli 0.14.5, account 404211. Deliverable: a documented Signal pager (send + read-replies), the signal-cli setup/account notes, and the sync path — the Signal equivalent of the retired ntfy runbook. Cross-machine tooling, so canonical home + docs belong in rulesets. - -RECONCILE FIRST: =protocols.org= "Paging Craig" already documents an agent-paging path via the *signal-mcp* tool (=send_message_to_user=, pager account +15045173983, Craig's UUID =b1b5601e-…=, verified 2026-06-30). home's handoff is about a *different* mechanism (signal-cli / account 404211, home's own paging). Decide whether these are one channel or two, and whether the signal-cli side needs its own runbook or should route through signal-mcp. Source: home handoff 2026-07-04 (=inbox/2026-07-04-1302-from-home-task-for-rulesets-document-and-decide.org=). Successor to the 2026-06-17 two-way-comms proposal. - -*** 2026-07-13 Mon @ 05:16:37 -0500 Folded home's ownership ack + current-state report into the reconcile scope -home confirmed (2026-07-11 reply) rulesets owns this task and the two-paths reconciliation is the right first step. New facts for the reconcile: from a home session on 2026-07-09, signal-mcp was NOT connected, and the local signal-cli is registered as Craig's own number — =send --note-to-self= returns a message id but produces no phone push. So home currently has no working ad-hoc page channel at all; whatever the runbook lands on must give home a live path. - -*** 2026-07-13 Mon @ 14:40:00 -0500 Added runtime-portability as a second motivation (Craig approved) -The MCP portability inventory ([[file:docs/design/2026-07-13-runtime-portability-inventories.org]]) found signal-mcp exists only claude.ai-side — no local config anywhere — so a non-Claude agent (Codex-style or local LLM) has no paging path at all. The signal-cli runbook this task produces is therefore also the runtime-neutral page channel, not just home's replacement for ntfy. - -*** 2026-07-13 Mon @ 18:30:19 -0500 agent-page shipped — every project now knows both channels -Craig's call: make paging universal and rename it the agent pager. NEW =claude-templates/bin/agent-page= (runs signal-cli directly on velox, ssh-relays from anywhere else, desktop-fallback hint on failure; 4 bats tests; live-verified from ratio through the real script). protocols.org "Paging Craig" rewritten around the two channels (notify desktop + agent-page phone; signal-mcp demoted to a velox-local nicety); page-me.org gained the phone section + fire-both guidance; work-the-backlog's end-of-set page names both surfaces; INDEX updated. Every project inherits via the startup sync + make install. Remaining here: the runbook proper, the receive timer, ssh-only vs linked-device. - -*** 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] @@ -515,17 +375,6 @@ Once specs carry lifecycle TODO keywords under =docs/specs/=, add a custom org-a :END: From Craig via the roam inbox (2026-07-02, routed by archsetup). Teardown-by-default already shipped (bare "wrap it up" closes the window; "with summary" keeps it). Craig's follow-on: "maybe we cut the summary altogether. help me think through when I'd want a summary and how I would recognize it before confirming and then having it close." Run that think-through with him (brainstorm-shaped, not solo), then adjust wrap-it-up.org's Step 6 + trigger phrases to the outcome. -** TODO [#C] triage-intake.org auto mode — push each sweep to phone (ntfy) :feature:solo: -:PROPERTIES: -:CREATED: [2026-06-20 Sat] -:LAST_REVIEWED: 2026-07-13 -:END: -The work project (2026-06-18) added a "Push each sweep to Craig's phone (ntfy) — the primary delivery" subsection under "Trigger and delivery" in triage-intake.org auto mode, and asks to fold it into the canonical engine plus re-sync. Preserved bundle: [[file:docs/design/2026-06-18-triage-intake-phone-push-note.org][note]] + [[file:docs/design/2026-06-18-triage-intake-phone-push-workflow.org][edited workflow]]. Auto mode is the away-from-desk / vacation mode, so phone-notify becomes the primary delivery each sweep (fuller end-of-sweep output: per-source deltas, open-PR/Linear state, awaiting-ack list, one-line verdict, timestamp; SCAN FAILED banner on any source failure), plus phone-recv polling each sweep for Craig's replies. Falls back to inline when phone-notify is absent. Transport re-pointed 2026-07-13: ntfy is retired; agent-page (shipped today) is the send channel, so the push half is buildable now — substitute agent-page for phone-notify throughout. The reply-polling half (phone-recv) waits on the Signal-pager runbook's read-replies deliverable. Shared template-workflow change, so review-gated. - -** CANCELLED [#D] Fully-unattended scheduled inbox check (/schedule cron pass) :feature: -CLOSED: [2026-07-20 Mon] -Merged 2026-07-20 into [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract — no-session variant]]. Same no-session design problem as the sentry /schedule variant (mutation rights, async surfacing, cross-run dedup, cron auth context); its inbox-specific context was absorbed there. - ** TODO [#D] Warn-only pre-commit hook for tooling-path enumeration :feature: :PROPERTIES: :CREATED: [2026-06-22 Mon] @@ -1614,3 +1463,150 @@ section vs a new shipped reference file); where the queue itself lives (home use insertion point (before the teardown/valediction, alongside the roam-inbox sub-step). Then it's a synced-file change (canonical-then-mirror) + a test that the wrap step drains the queue. +** DONE [#B] working/ tracked-from-creation + gitignored temp/ :feature: +CLOSED: [2026-07-20 Mon] +Craig's ruling relayed from .emacs.d (2026-07-19): working/ is version-controlled from creation (not excluded until graduation); ephemeral artifacts go in a gitignored temp/ or /tmp; graduation reorganizes durable artifacts into permanent homes rather than marking when they become durable. + +Implemented (Shape A) during the morning sentry review, 2026-07-20: +- =claude-rules/working-files.md=: added "working/ Is Version-Controlled From Creation" section (tracked-from-creation, graduation-is-a-move, temp/ for ephemeral). +- =.ai/protocols.org= (canonical + mirror): one-paragraph mirror in the Working-Files Convention section. +- =scripts/install-ai.sh=: emits a =temp/= ignore block in BOTH track and gitignore modes; working/ never ignored. Idempotent. +- =scripts/sweep-gitignore-tooling.sh=: separate mode-independent temp/ backfill pass (a distinct loop, not an IGNORE_SET member — that would skip track-mode projects, the set that most needs it). +- Tests: 3 new install-ai bats + 4 new sweep bats (temp/ in both modes, never working/, idempotent). Suite green, 384 bats ok. + +Finding confirmed at implementation: the canonical machinery already never ignored working/ (tooling set is only =.ai/ .claude/ CLAUDE.md AGENTS.md=), so .emacs.d's =/working/= ignore was a purely local deviation. The staging proposal dir was removed after shipping; its content lives in the feat commit and this body. +** DONE [#C] Polyglot projects — supported, or refused? :spec: +CLOSED: [2026-07-20 Mon] +DECISION (2026-07-20, scouting with Craig): *case-by-case, and it already composes — no option-2 machinery.* The evidence: bundle contents split into namespaced/additive files (rules =<lang>.md=, =validate-<lang>.sh= hooks, =coverage-summary.<ext>= scripts, appended =gitignore-add.txt=) that compose cleanly, and exactly three colliding files — =claude/settings.json= and =githooks/pre-commit= (full bundles only: bash/elisp/go) and =coverage-makefile.txt= (elisp/go/python/typescript). Of the three, only =coverage-makefile.txt= occurs in the fleet, and clock-panel (the one real polyglot, python+typescript) proves it's benign: both bundles installed, no breakage, because that fragment is a hand-pasted Makefile block nobody pasted twice. So: keep the install-lang collision guard (it blocks the destructive full+full settings/githooks clobber, which no project actually hits), and document the =coverage-<lang>:= + =coverage:= aggregate namespacing as the one manual step when going polyglot (filed below). No two-full-co-equal-bundle project exists, so the settings.json/githooks merge is unwarranted. Follow-up doc: [[file:todo.org::*Document polyglot coverage-makefile namespacing][Document polyglot coverage-makefile namespacing]]. + +Do we support more than one language bundle per project? The honest answer today +is "partly, by accident." The collision guard added 2026-07-16 refuses a +*colliding* second bundle rather than silently replacing the first's config, but +a non-overlapping pair still installs fine: bash ships =settings.json= + +githooks and no coverage fragment, python ships only a coverage fragment, so +=bash= + =python= composes cleanly today and yields a real polyglot project with +both rule sets. So the line isn't polyglot-vs-not, it's overlap-vs-not — and +nobody chose that line, it fell out of which bundle happens to ship what. Origin: +home's report after scaffolding clock-panel with python + typescript, +[[file:docs/design/2026-07-16-polyglot-bundle-collision.txt][docs/design/2026-07-16-polyglot-bundle-collision.txt]]. + +Pair this with the subproject scouting below — it's the same question in a +different costume ("which projects would actually be polyglot, and why"), so +they should be one conversation. + +The three options, in the order they'd be weighed: + +1. *Unsupported, explicitly.* Keep the guard as the answer. Cheapest, and + matches how little polyglot exists (one project, clock-panel). +2. *Supported.* Needs per-bundle filenames, a merged =settings.json= (the hooks + arrays compose rather than clobber), composed githooks, and namespaced + Makefile targets with a =coverage= aggregate. This is the real work. +3. *Case-by-case.* Support the pairs that come up, refuse the rest. + +What the decision needs to know: + +- *The target-name collision is the deeper half* (home's point, and it's right). + Every bundle's fragment defines =coverage:= and =coverage-summary:=, so even + with both files present a polyglot project can't paste both into one Makefile. + Renaming files doesn't fix it. +- *Only three of five shared filenames actually collide.* =gitignore-add.txt= + (5 bundles) appends deduped and composes. =CLAUDE.md= (3) is seed-only, and + its fallback comment shows multi-bundle was already considered there. + =claude/settings.json= (3), =githooks/*= (3), and =coverage-makefile.txt= (4) + are the real ones. +- *=FORCE=1= is a poor escape hatch* (home's catch): it also re-seeds + =CLAUDE.md=, which is destructive on a customized project. If polyglot + becomes supported, the override wants to be its own flag. +** DONE [#C] Subproject pattern — promote to claude-rules? :spec: +CLOSED: [2026-07-20 Mon] +DECISION (2026-07-20, scouting with Craig): *don't promote — keep it local at home.* The scouting confirmed N=1: across all 27 =.ai= scopes, home (9 subprojects, all from the single 2026-06-11 fold) is the only real user. The nearest neighbors aren't the pattern — rulesets folds claude-templates in as a git *subtree* (different mechanism, not shared-=.ai/=-scope), archsetup's dotfiles/ likewise; every other project is a focused single package with no subproject structure or need. Promoting a 282-line convention into the always-on claude-rules layer for one project fails the thin-always-on principle (the precedent is patterns.md at 29 lines + docs-lifecycle.md's depth-in-a-spec). Keep the convention as home's local instance. Revisit only if a genuine second case appears, and then as a thin pointer + a spec, never 282 lines always-on. + +home proposes promoting its subproject pattern (a former standalone project +folded into a parent, living as a self-contained subdir sharing the parent's +=.ai/= scope) into the rules layer: vocabulary, the read-first +=<subproject>/<subproject>-brief.org= convention, the parent-vs-subproject +content criterion ("one fact, one home"), and create/archive criteria. +Proposal + home's full instance: +[[file:docs/design/2026-07-15-subproject-pattern-proposal.org][proposal]], +[[file:docs/design/2026-07-15-subprojects-convention-home-instance.org][home's convention doc]]. + +Deferred 2026-07-16 rather than promoted. *Craig's framing:* he wants to scout +which projects would actually get subprojects, and why, before we shape a rule. +If he hasn't done that scouting by the time this comes up, offer to do it +together — brainstorm the candidates, then explore the reasons behind each. That +evidence decides it: either we drop the pattern, or we know enough to adjust it +so it's effective. Don't shape the rule before the scouting. + +*Review findings from the 2026-07-16 pass* (the inputs the decision needs): + +- *N=1.* home is the only project with subprojects, across all 27 =.ai= scopes; + its nine all came from the single 2026-06-11 fold. This is the fact the + scouting tests. +- *Placement contradicts the proposal's own principle.* =claude-rules/*.md= + loads into every session of every project. home's doc argues the always-on + layer is "a tax paid whether or not it's relevant today" and depth belongs + "one open away". At 282 lines the doc would be the third-largest rule and add + ~11% to the always-on layer, so every .emacs.d / takuzu / chime session would + carry a one-project convention. +- *Precedent for the shape:* =patterns.md= (29 lines, explicit "don't carry the + catalog in context") and =docs-lifecycle.md= (75 lines, depth in a spec). + Thin rule + on-demand depth is the established answer. +- *Dangling reference:* the doc cites =claude-rules/git-hosting-privacy-model= + as authority for its shared-scope-safety criterion. No such file exists — the + real content is the gitignore-vs-track and public-reachability decision in + =protocols.org=. Fix before any promotion. +- *Instance vs rule:* the metrics, self-improvement log, kill criteria, rollout + dates, and adoption table are home's instance, not rule content. +** DONE [#B] Triage source activation — per-project source declaration :feature:spec: +CLOSED: [2026-07-20 Mon] +Spec: [[file:docs/specs/2026-07-20-triage-source-activation-spec.org][docs/specs/2026-07-20-triage-source-activation-spec.org]] (IMPLEMENTED, ID af73ef0b-cd1d-46f1-9e1d-62695733a4de). Craig approved after his read, both open decisions resolved via cj comments; built same session. + +From the sentry live trial (2026-07-20): triage-intake self-activated in every project because the general (personal-account) plugins are template-synced everywhere, so sentry's pass-3 probe misfired and would pull Craig's personal inboxes into whatever project the fire ran in. Fixed with an activation gate in triage-intake Phase 0 — general plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins stay active by presence. Applies to interactive and unattended alike. sentry pass-3 probe reads the same signal. Migration handoffs sent to home + work. Supersedes Fire 1's narrower probe-only approval-queue item. +** DONE [#B] Silent-until-signal for in-session monitor loops :feature:spec: +CLOSED: [2026-07-20 Mon] +Spec: [[file:docs/specs/2026-07-20-silent-until-signal-monitors-spec.org][docs/specs/2026-07-20-silent-until-signal-monitors-spec.org]] (IMPLEMENTED, ID af592bd6-d3e6-47e2-8804-2a287b4d9303). Craig approved without changes 2026-07-20; all 5 phases shipped that day. Sentry, auto triage-intake, and auto inbox-zero all collapse an empty fire to =<workflow> at HH:MM: nothing=; a manual-testing entry covers the live-loop verification. + +Craig-approved proposal from .emacs.d (2026-07-20), demonstrated by the sentry live trial (fires 3-8 were walls of no-op lines). Reframed live from a watcher *mechanism* to a *policy*: an in-session monitor fire detects first, and on an empty check collapses to one labelled heartbeat line (=<workflow> at HH:MM: nothing=) instead of a full turn; only a real item earns the full surface-and-judge turn. Keeping detection in-session dissolves the MCP-auth split (triage's Gmail/Slack/Linear need session auth), so it applies uniformly to sentry, auto triage-intake, and auto inbox-zero. No external watcher, no new seen-list. Five build phases in the spec. Phase 1 (sentry quiet-fire heartbeat) shipped 2026-07-20 ahead of the full READY gate at Craig's direction — a quiet fire now collapses to =sentry at HH:MM: nothing=. Phases 2-4 (auto triage-intake, auto inbox-zero, shared-policy home) + Phase 5 verification pending Craig's spec read. Overlaps the sentry cluster ([[file:todo.org::*Sentry vNext passes][Sentry vNext passes]], [[file:todo.org::*Triage source activation][Triage source activation]]). +** DONE [#C] Apply suspend.org detach-on-suspend change to canonical :feature: +CLOSED: [2026-07-20 Mon] +Craig's change relayed from archsetup (2026-07-20): add Step 6 to suspend.org — detach the tmux client (=tmux detach-client -s "$sess"=) as the final action of every suspend, so a suspended session parks in the re-attachable set instead of cluttering the alt-space rotation. Also reworded the neighbors bullet and the "does NOT do" teardown bullet to draw the detach-vs-teardown line. + +Applied to canonical 2026-07-20 (verified the diff was exactly the intended change, canonical + mirror synced, lint clean bar one pre-existing flush/SKILL.md link). Replied to archsetup that its local stopgap is now canonical. +** 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 +:END: +home retired the ntfy phone-notification channel (phone-notify/phone-recv, self-hosted ntfy on ratio) on 2026-07-04 in favor of paging over Signal, and tore ntfy down. The Signal pager it replaced ntfy with is undocumented: no pager script in =~/.local/bin=, and =notify= doesn't reference Signal. What exists on ratio: signal-cli 0.14.5, account 404211. Deliverable: a documented Signal pager (send + read-replies), the signal-cli setup/account notes, and the sync path — the Signal equivalent of the retired ntfy runbook. Cross-machine tooling, so canonical home + docs belong in rulesets. + +RECONCILE FIRST: =protocols.org= "Paging Craig" already documents an agent-paging path via the *signal-mcp* tool (=send_message_to_user=, pager account +15045173983, Craig's UUID =b1b5601e-…=, verified 2026-06-30). home's handoff is about a *different* mechanism (signal-cli / account 404211, home's own paging). Decide whether these are one channel or two, and whether the signal-cli side needs its own runbook or should route through signal-mcp. Source: home handoff 2026-07-04 (=inbox/2026-07-04-1302-from-home-task-for-rulesets-document-and-decide.org=). Successor to the 2026-06-17 two-way-comms proposal. + +*** 2026-07-13 Mon @ 05:16:37 -0500 Folded home's ownership ack + current-state report into the reconcile scope +home confirmed (2026-07-11 reply) rulesets owns this task and the two-paths reconciliation is the right first step. New facts for the reconcile: from a home session on 2026-07-09, signal-mcp was NOT connected, and the local signal-cli is registered as Craig's own number — =send --note-to-self= returns a message id but produces no phone push. So home currently has no working ad-hoc page channel at all; whatever the runbook lands on must give home a live path. + +*** 2026-07-13 Mon @ 14:40:00 -0500 Added runtime-portability as a second motivation (Craig approved) +The MCP portability inventory ([[file:docs/design/2026-07-13-runtime-portability-inventories.org]]) found signal-mcp exists only claude.ai-side — no local config anywhere — so a non-Claude agent (Codex-style or local LLM) has no paging path at all. The signal-cli runbook this task produces is therefore also the runtime-neutral page channel, not just home's replacement for ntfy. + +*** 2026-07-13 Mon @ 18:30:19 -0500 agent-page shipped — every project now knows both channels +Craig's call: make paging universal and rename it the agent pager. NEW =claude-templates/bin/agent-page= (runs signal-cli directly on velox, ssh-relays from anywhere else, desktop-fallback hint on failure; 4 bats tests; live-verified from ratio through the real script). protocols.org "Paging Craig" rewritten around the two channels (notify desktop + agent-page phone; signal-mcp demoted to a velox-local nicety); page-me.org gained the phone section + fire-both guidance; work-the-backlog's end-of-set page names both surfaces; INDEX updated. Every project inherits via the startup sync + make install. Remaining here: the runbook proper, the receive timer, ssh-only vs linked-device. + +*** 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. +** DONE [#C] triage-intake auto mode — push signal sweeps to phone via agent-text :feature:solo: +CLOSED: [2026-07-20 Mon] +:PROPERTIES: +:CREATED: [2026-06-20 Sat] +:LAST_REVIEWED: 2026-07-13 +:END: +Send half shipped 2026-07-20. Folded a "Phone delivery" subsection into canonical =triage-intake.org= auto mode: a full-three-section sweep pushes to Craig's phone over Signal via =agent-text=, with a pointer from "End-of-sweep output" and a Living Document note. Signal-only by Craig's 2026-07-20 ruling — a quiet sweep's =nothing= heartbeat never reaches the phone, so silent-until-signal governs the phone channel too and the in-session heartbeat stays as proof-of-life. Falls back to inline when =agent-text= is absent. + +Reply-polling half (the old =phone-recv=) deferred to the reply-correlation follow-up: with the Signal account linked on more than one device a reply fans out to every device and neither knows which page it answers, so auto mode pushes but does not poll until that's resolved. The recv wiring is owned by that spec, not this task. + +Origin: the work project's 2026-06-18 fold-in request; preserved bundle [[file:docs/design/2026-06-18-triage-intake-phone-push-note.org][note]] + [[file:docs/design/2026-06-18-triage-intake-phone-push-workflow.org][edited workflow]]. Transport re-pointed off retired ntfy → agent-text (renamed from agent-page 2026-07-20). +** CANCELLED [#D] Fully-unattended scheduled inbox check (/schedule cron pass) :feature: +CLOSED: [2026-07-20 Mon] +Merged 2026-07-20 into [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract — no-session variant]]. Same no-session design problem as the sentry /schedule variant (mutation rights, async surfacing, cross-run dedup, cron auth context); its inbox-specific context was absorbed there. diff --git a/voice/SKILL.md b/voice/SKILL.md index 19eb38b..fe5d48a 100644 --- a/voice/SKILL.md +++ b/voice/SKILL.md @@ -1,7 +1,7 @@ --- name: voice description: | - Multi-pass prose editor with three modes. General mode (default) edits arbitrary writing — research notes, essays, anyone's README prose — by walking 31 patterns: Wikipedia's Signs of AI Writing patterns plus universal good-writing rules (long-word → short-word, active-over-passive, comma splices, cliché flag, jargon-fragment-in-prose rewrite, corporate-speak nominalizations). Prose mode adds Craig's writing-voice patterns for prose he authors or sends — emails, documents, notes — on top of general (em-dash zero-tolerance, no-emphasis-formatting, contractions, semicolons → periods, sentence-split, felt-experience cut, sentence-fragment rewrite, terse-cut, single-sentence cadence, parenthetical asides, declarative-register marker). Personal mode is for publish artifacts only (commits, PR titles + bodies, PR review comments) and adds the artifact-mechanics patterns on top of prose (first-person rewrite, public-artifact scope flag, praise/correction asymmetry, finding stems) plus per-artifact terseness budgets. Total 45 patterns; one editorial review covers all relevant ones for the chosen mode, with an attestation receipt for the high-recurrence set. Replaces the standalone humanizer skill. Use when editing prose. Do NOT use for code, structured data, or plain bullet lists where fragments are valid. + Multi-pass prose editor with three modes. General mode (default) edits arbitrary writing — research notes, essays, anyone's README prose — by walking 31 patterns: Wikipedia's Signs of AI Writing patterns plus universal good-writing rules (long-word → short-word, active-over-passive, comma splices, cliché flag, jargon-fragment-in-prose rewrite, corporate-speak nominalizations). Prose mode adds Craig's writing-voice patterns for prose he authors or sends — emails, documents, notes — on top of general (em-dash zero-tolerance, no-emphasis-formatting, contractions, semicolons → periods, sentence-split, felt-experience cut, sentence-fragment rewrite, terse-cut, single-sentence cadence, parenthetical asides, declarative-register marker). Personal mode is for publish artifacts only (commits, PR titles + bodies, PR review comments) and adds the artifact-mechanics patterns on top of prose (first-person rewrite, public-artifact scope flag, praise/correction asymmetry, finding stems, comma budget) plus per-artifact terseness budgets. Total 46 patterns; one editorial review covers all relevant ones for the chosen mode, with an attestation receipt for the high-recurrence set. Replaces the standalone humanizer skill. Use when editing prose. Do NOT use for code, structured data, or plain bullet lists where fragments are valid. allowed-tools: - Read - Write @@ -32,7 +32,7 @@ Three modes determine which patterns to walk. They nest: prose is general plus C - **General** (default) — apply patterns **#1-31**. Use for writing whose author isn't Craig and that isn't a publish artifact: research notes you're editing for someone else, a quoted passage, README prose for a shared project, any third-party text. Output is well-edited human-sounding prose, but does not impose Craig's voice (first-person, contractions, em-dash elimination) — those conflict with academic, literary, or formal registers that aren't his. - **Prose** — apply **#1-31** plus the patterns tagged **(prose + personal)**: em-dash zero-tolerance (#13), contractions (#34), semicolons → periods (#33), sentence-split (#35), felt-experience cut (#36), sentence-fragment rewrite (#37), terse-cut (#38), no-emphasis-formatting (#41), single-sentence cadence (#43), parenthetical asides (#44), and the declarative-register marker (#45). Use for prose Craig authors or sends in his own voice that isn't a publish artifact: emails, documents he writes or hands to someone, working notes, journal entries. This is the mode that finally applies his actual writing voice to the documents he most wants it on. It skips the artifact-mechanics patterns (#32, #39, #40, #42) — those assume a commit or PR and misfire on free prose (a document is legitimately third-person; a journal has no public-scope concern; praise/correction asymmetry and finding stems are PR-review rules). -- **Personal** — apply all **#1-45**. Use only for publish artifacts: commits, PR titles + bodies, and PR review comments. Adds the artifact-mechanics patterns (#32 first-person rewrite, #39 public-artifact scope flag, #40 praise/correction asymmetry, #42 finding stems) on top of everything prose mode walks. +- **Personal** — apply all **#1-46**. Use only for publish artifacts: commits, PR titles + bodies, and PR review comments. Adds the artifact-mechanics patterns (#32 first-person rewrite, #39 public-artifact scope flag, #40 praise/correction asymmetry, #42 finding stems, #46 comma budget) on top of everything prose mode walks. If invoked without a mode argument, default to general. Prose mode is invoked explicitly with `/voice prose` (emails, authored documents). Personal-context callers (`commits.md` publish flow, `respond-to-cj-comments.md`) invoke `/voice personal`. @@ -53,7 +53,7 @@ Terse is a budget, not an adjective. Each publish-artifact type has a target sha When given text to edit: -1. **Identify which patterns apply** — Scan for the patterns numbered below. General mode walks #1-31 only. Prose mode adds the patterns tagged **(prose + personal)**. Personal mode adds those *and* the ones tagged **(personal only)** — i.e. all 45. +1. **Identify which patterns apply** — Scan for the patterns numbered below. General mode walks #1-31 only. Prose mode adds the patterns tagged **(prose + personal)**. Personal mode adds those *and* the ones tagged **(personal only)** — i.e. all 46. 2. **Rewrite problematic sections** — Replace each detected pattern with its rewrite. 3. **Preserve meaning** — Keep the core message intact. 4. **Maintain voice** — Match the intended tone (formal, casual, technical, academic, literary). @@ -296,7 +296,7 @@ See `voice/references/voice-profile.org` §31 for problem, basis, examples, and ## Craig's Voice (prose + personal modes) -These patterns carry Craig's writing voice. Most apply in **both** prose mode (emails, documents, notes he authors) and personal mode (commits, PRs, PR comments) — tagged **(prose + personal)**. Four are publish-artifact-specific — tagged **(personal only)** — because they assume a commit or PR and misfire on free prose: #32 (first-person rewrite) wrongly imposes "I did X" voice on a document that's legitimately third-person, #39 (public-artifact scope flag) has nothing to guard in a private journal, and #40 (praise/correction asymmetry) and #42 (finding stems) are PR-review rules. General mode skips all of them — it edits text that isn't Craig's, where contractions, em-dash elimination, and first-person would conflict with academic, literary, or formal registers. +These patterns carry Craig's writing voice. Most apply in **both** prose mode (emails, documents, notes he authors) and personal mode (commits, PRs, PR comments) — tagged **(prose + personal)**. Five are publish-artifact-specific — tagged **(personal only)** — because they assume a commit or PR and misfire on free prose: #32 (first-person rewrite) wrongly imposes "I did X" voice on a document that's legitimately third-person, #39 (public-artifact scope flag) has nothing to guard in a private journal, #40 (praise/correction asymmetry) and #42 (finding stems) are PR-review rules, and #46 (comma budget) is scoped to publish artifacts by Craig's 2026-07-20 directive. General mode skips all of them — it edits text that isn't Craig's, where contractions, em-dash elimination, and first-person would conflict with academic, literary, or formal registers. ### 32. First-Person Voice Rewrite [personal] @@ -382,10 +382,16 @@ See `voice/references/voice-profile.org` §44 for problem, basis, examples, and See `voice/references/voice-profile.org` §45 for problem, basis, examples, and history. +### 46. Comma Budget — Max Two Per Sentence [personal] + +**Rule.** No sentence carries more than two commas. Rewrite the third comma away: split the sentence, move a clause into a parenthetical (#44) or behind a colon, or break an inline serial list into bullets or its own sentence. Count prose commas only — commas inside code spans, quoted log lines, and literal strings don't count toward the budget. + +See `voice/references/voice-profile.org` §46 for problem, basis, examples, and history. + ## Process 1. Read the input text carefully. Confirm the mode (general, prose, or personal) — invocation argument or context. If a file path was given, that file is the deliverable: the final text gets written back to it in step 7. -2. Walk patterns 1-31 in general mode; add the (prose + personal) patterns in prose mode; walk all 45 patterns in personal mode. +2. Walk patterns 1-31 in general mode; add the (prose + personal) patterns in prose mode; walk all 46 patterns in personal mode. 3. For each pattern, scan the text. If a match is found, rewrite it according to the pattern's rule. Patterns #39 and #45 emit flags without rewriting. 4. After walking all patterns, ensure the revised text: - Sounds natural when read aloud @@ -395,7 +401,7 @@ See `voice/references/voice-profile.org` §45 for problem, basis, examples, and 5. **Terse pass — mandatory, last rewrite pass (prose + personal modes).** Walk pattern #38 again as a standalone action: read each sentence and try to cut it in half, keeping only the words that change meaning. Run it on its own here, not folded into step 3's walk — it is the most-skipped pattern and the bloat it catches is the first thing a reader notices. General mode skips this step — academic and third-party registers keep their transition markers. 6. **Anti-AI audit — on the final text.** Prompt: "What makes the below so obviously AI generated?" Answer briefly with remaining tells, then revise. This runs *after* the terse pass so the audited text is the text that ships. If the audit triggers rewrites, re-apply the #38 per-sentence test to every changed sentence before proceeding. 7. **Write-back.** If the invocation supplied a file path, write the final text to that file now and say so. The publish flow posts from the file (`git commit -F`, `gh pr create --body-file`), so a final text that lives only in chat is a drift bug waiting to post the un-voiced version. -8. **Attestation block (prose + personal modes).** The high-recurrence patterns — the ones with a documented failure history — each get one explicit line: pattern, checked, match or no match, action taken. Current high-recurrence set: **#13 (em-dash), #37 (fragments), #38 (terse), #40 (praise asymmetry), #42 (finding stems)**. This is a receipt, not a summary: a pattern with no match still gets its line. When a pattern in this set fails in the wild despite the receipt, escalate it the way #38 was escalated; when one holds clean for a long stretch, it can rotate out. +8. **Attestation block (prose + personal modes).** The high-recurrence patterns — the ones with a documented failure history — each get one explicit line: pattern, checked, match or no match, action taken. Current high-recurrence set: **#13 (em-dash), #37 (fragments), #38 (terse), #40 (praise asymmetry), #42 (finding stems), #46 (comma budget)**. This is a receipt, not a summary: a pattern with no match still gets its line. When a pattern in this set fails in the wild despite the receipt, escalate it the way #38 was escalated; when one holds clean for a long stretch, it can rotate out. 9. Present the final version per the Output Format below. ## Output Format @@ -481,6 +487,7 @@ This skill draws from: - Plain English Campaign — pattern #26 (Plain English wordlist). - Garner, *Modern English Usage* — pattern #26 (word-pair preferences). - Craig's voice rules from `claude-rules/commits.md` (Voice and Focus section) — patterns #32-42, split across prose mode (his authored prose and email) and personal mode (publish artifacts). +- Craig's directive, 2026-07-20 (archsetup session) — pattern #46 (comma budget), personal mode. - Corpus measurement (2026-05-29 phases 1-2, documented in the profile) — patterns #43-45 and the calibration notes on #7, #13, #33. Key insight (Wikipedia, paraphrased): LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely text that applies to the widest variety of cases. Patterns #1-25 detect that signature. diff --git a/voice/references/voice-profile.org b/voice/references/voice-profile.org index 765d513..3907edf 100644 --- a/voice/references/voice-profile.org +++ b/voice/references/voice-profile.org @@ -1532,3 +1532,33 @@ A question mark in a draft in Craig's voice. Flag it; keep genuine questions to *** History - 2026-05-29: surfaced by the Phase 1-2 corpus measurement as a register marker; filed as suggested delta 6. - 2026-06-10: promoted from the suggested-deltas list into a numbered advisory pattern. Craig's call, from the work-project session. + +** §46 Comma Budget — Max Two Per Sentence + +*** Modes +Personal mode only. Prose and general modes skip. + +*** Rule +No sentence carries more than two commas. Rewrite the third comma away: split the sentence, move a clause into a parenthetical (§44) or behind a colon, or break an inline serial list into bullets or its own sentence. Count prose commas only — commas inside code spans, quoted log lines, and literal strings (version numbers, paths) don't count toward the budget. + +*** Problem +Three or more commas in one sentence almost always mark stacked clauses or an inline list doing a paragraph's work. The sentence reads fine to its author and lands as a pileup on the reader. The comma count is a mechanical proxy the walk can enforce, where "don't stack clauses" is prose advice that gets skipped. + +*** Basis +Craig's directive, 2026-07-20 (archsetup session, while gating a Hyprland issue draft): "no more than two commas per sentence. we should add that to the /voice personal pass." + +*** Before (spec-sheet line with three commas, from the draft that prompted the rule) +#+begin_example +System: Arch Linux, kernel 6.18.25-lts, AMD Strix Halo (Radeon 8060S), no plugins loaded. +#+end_example + +*** After +#+begin_example +System: Arch Linux, kernel 6.18.25-lts. GPU: AMD Strix Halo (Radeon 8060S). No plugins loaded. +#+end_example + +*** Detection +Count commas per sentence on the final text. A sentence at three or more gets restructured, not trimmed to exactly the budget — the third comma is the symptom, the stacked structure is the target. + +*** History +- 2026-07-20: added at Craig's direction from the archsetup session. Scoped to personal mode; broaden to prose only if he asks. Added to the attestation high-recurrence set at birth — a mechanical count is cheap to receipt, and new discipline fails silently without one. |
