From e27aea2e473433d782da518bf03739b40f5d34f7 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 20 Jul 2026 23:35:39 -0500 Subject: feat(triage): push signal sweeps to phone via agent-text in auto mode Auto mode is the away-from-desk mode. A sweep worth seeing should reach Craig's phone, not just print into a session he isn't watching. I folded a Phone delivery subsection into canonical triage-intake.org: a full-three-section sweep now pushes to the phone over Signal via agent-text. A pointer from End-of-sweep output and a Living Document note tie it in. Signal-only, by Craig's ruling: a quiet sweep's "nothing" heartbeat never reaches the phone. Silent-until-signal governs the phone channel too, so it only fires on real signal. The in-session heartbeat stays as proof the loop ran. Only the send half ships. Reply polling (the retired ntfy design's phone-recv) waits on the reply-correlation follow-up: a Signal reply fans out to every linked device and neither knows which page it answers. That spec owns the recv wiring. I re-pointed the transport off dead ntfy onto agent-text (the renamed pager). The mirror is synced and the DONE task records the deferral. --- .ai/workflows/triage-intake.org | 21 ++++++++++++++++++++- claude-templates/.ai/workflows/triage-intake.org | 21 ++++++++++++++++++++- todo.org | 9 +++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) 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/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/todo.org b/todo.org index 25eccdc..3e9189c 100644 --- a/todo.org +++ b/todo.org @@ -515,12 +515,17 @@ 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: +** 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: -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. +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] -- cgit v1.2.3