aboutsummaryrefslogtreecommitdiff
path: root/docs/specs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-20 12:16:33 -0500
committerCraig Jennings <c@cjennings.net>2026-07-20 12:16:33 -0500
commit986d6caeeaba6e4ab2f1e6f5e760ce841c922259 (patch)
tree4798f838cd231f5e6a7c345e86a2f5ef05860a13 /docs/specs
parent492514dce5feaa0b0a5a6ce61c121f91b0f28821 (diff)
downloadrulesets-986d6caeeaba6e4ab2f1e6f5e760ce841c922259.tar.gz
rulesets-986d6caeeaba6e4ab2f1e6f5e760ce841c922259.zip
docs: spec the silent-until-signal monitor policy
An in-session monitor loop (sentry, auto triage-intake, auto inbox-zero) fires the model every interval and narrates a full turn even when nothing changed, so a session fills with quiet-fire noise. The spec settles the fix as a policy, not a watcher: detect first, and on an empty check collapse to one labelled heartbeat line, spending a full turn only on a real item. Keeping detection in-session dissolves the MCP-auth split (triage's sources need the session's auth, which a detached watcher can't reach), so it applies uniformly across all three loops. DRAFT, decisions resolved live. Links a [#B] build task.
Diffstat (limited to 'docs/specs')
-rw-r--r--docs/specs/2026-07-20-silent-until-signal-monitors-spec.org122
1 files changed, 122 insertions, 0 deletions
diff --git a/docs/specs/2026-07-20-silent-until-signal-monitors-spec.org b/docs/specs/2026-07-20-silent-until-signal-monitors-spec.org
new file mode 100644
index 0000000..275bcfa
--- /dev/null
+++ b/docs/specs/2026-07-20-silent-until-signal-monitors-spec.org
@@ -0,0 +1,122 @@
+#+TITLE: Silent-Until-Signal Monitor Loops — Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-20
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT silent-until-signal monitor loops
+:PROPERTIES:
+:ID: af592bd6-d3e6-47e2-8804-2a287b4d9303
+:END:
+- 2026-07-20 Mon @ 12:14:20 -0500 — drafted during the morning session. Design decisions resolved live with Craig off the .emacs.d proposal and the sentry live-trial evidence. DRAFT pending his deep read.
+
+* Metadata
+| Status | draft |
+|----------+--------------------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+--------------------------------------------------------------|
+| Reviewer | (spec-review, next session) |
+|----------+--------------------------------------------------------------|
+| Related | [[file:../../todo.org::*Silent-until-signal for in-session monitor loops][todo.org — silent-until-signal task]] |
+|----------+--------------------------------------------------------------|
+
+* Summary
+
+An in-session monitor loop (sentry, auto triage-intake, auto inbox-zero) fires the model once per interval. Today each fire narrates a full turn even when nothing changed, so a long session fills with walls of "quiet fire" / "no new items" output. The fix is a policy, not a mechanism: every monitor fire does cheap detection first, and on an empty check collapses to a single labelled heartbeat line ("sentry at 03:20: nothing") and stops. Only a fire that finds a genuinely new item spends a full surface-and-judge turn. Detection stays in-session, so the policy applies uniformly to file-based and MCP-auth loops alike.
+
+* Problem / Context
+
+In-session cron/loop monitors surface a visible model turn on every fire. When nothing changed, that turn is pure noise — a per-pass sentry digest full of SKIP lines, or a triage/inbox "nothing new" report. Over a night or a long session the signal (the one fire that found something) drowns in the empty ticks. The rulesets sentry live trial (2026-07-20) demonstrated it directly: fires 1-2 did real work, fires 3-8 were near-identical walls of no-op lines.
+
+Origin: a Craig-approved proposal from .emacs.d (2026-07-20), captured off an auto inbox-zero session filling with empty-check noise.
+
+** Why not an external watcher (the rejected shape)
+
+The proposal's first instinct was a shell-level watcher (systemd timer or backgrounded loop) doing detection outside the model, producing zero output on an empty check and dropping a handoff into inbox/ only on a real item — so the model runs zero turns when nothing changed. That is truly-zero-idle, but it has a disqualifying cost: it moves detection out of the session, and auto triage-intake's sources (Gmail, Slack, Linear) are reachable only through the session's inherited MCP auth. triage-intake.org is explicit that it runs in the live session precisely because "the headless-auth wall that blocks a detached cron run does not apply." A detached watcher cannot scan those sources at all. The external-watcher shape would therefore split the three targets into two incompatible cases (file-detectable vs MCP-auth) and still leave triage unsolved.
+
+The reframing (Craig, 2026-07-20): the noise is a *policy* problem — when to spend a full model turn — not a missing piece of infrastructure. Keeping detection in-session and making the empty fire cheap solves the actual complaint without the watcher, without per-machine daemon setup, and without breaking MCP auth. It applies to all three loops uniformly.
+
+* Goals and Non-Goals
+
+** Goals
+- An empty monitor fire produces exactly one labelled heartbeat line, not a full narrated turn: =<workflow> at HH:MM: nothing=.
+- A fire that detects a genuinely new item does the full surface-and-judge turn unchanged.
+- One uniform policy across sentry, auto triage-intake, and auto inbox-zero — no per-loop special-casing.
+- Detection stays in-session so MCP-auth loops (triage) get the same treatment as file-based loops.
+- Each loop reuses the seen-state it already keeps; no new watcher-owned seen-list.
+
+** Non-Goals
+- No external watcher, systemd timer, or Monitor-tool daemon. Detection is the loop body's own cheap check.
+- No truly-zero-idle (no model turn at all on empty). That needs an external watcher and is the shape rejected above; if it is ever wanted for the file-based loops only, it is a separate vNext, logged not built.
+- No change to what a loop does when it *does* find something — the surface/judge/act behaviour is untouched.
+- No change to the one-shot (non-loop) invocations of these workflows.
+
+* Design
+
+** The policy
+
+Every in-session monitor fire runs in two steps:
+
+1. *Detect (cheap, silent).* Run the loop's existing detection against its seen-state — sentry's pass probes and branch/digest state, triage's sentinel scan, the inbox monitor's disposition check. This is Bash/tool work, not narration.
+2. *Branch on the result.*
+ - *Nothing new* → emit one line, =<workflow> at HH:MM: nothing=, and end the fire. No digest, no per-pass lines, no report.
+ - *Something new* → the full existing turn: surface, judge, act/queue, and its normal richer output.
+
+The heartbeat is the whole output of an empty fire. Its format is fixed: the workflow's short name, =at=, =HH:MM= (local, from =date=), then =: = and the result word (=nothing= for an empty check). Examples: =sentry at 03:20: nothing=, =triage intake at 03:30: nothing=, =inbox zero at 03:30: nothing=.
+
+** Per-workflow application
+
+- *Sentry.* A fire whose passes all probe-skip or no-op collapses to =sentry at HH:MM: nothing=. No per-pass digest block is written for a quiet fire. A fire that runs or queues anything writes its full digest as today. (This supersedes the trial's behaviour, where fires 3-8 each wrote a full no-op digest.)
+- *Auto triage-intake.* An auto-mode sweep that finds nothing across its enabled sources collapses to =triage intake at HH:MM: nothing=. Detection stays in-session, so the MCP sources are scanned normally; only the output on empty changes.
+- *Auto inbox-zero.* A roam-mode cycle that finds no new inbox items collapses to =inbox zero at HH:MM: nothing=.
+
+** Seen-state (no new artifact)
+
+Because detection stays in-session, each loop keeps using the state it already maintains to know what is "new": triage's =.ai/last-triage-intake= sentinel, sentry's branch + digest, the inbox monitor's per-cycle disposition. The proposal's "watcher-owned seen-list replacing the in-anchor Dispositioned list" is dropped — it was an artifact of the external-watcher shape, which is not being built.
+
+** The accepted consequence
+
+An in-session loop still fires the model once per interval; the harness invokes it each time. So "silent" means the empty fire is *cheap* (one detection pass plus one heartbeat line), not *absent*. This is the deliberate trade for uniformity and for keeping the MCP auth that makes triage possible. The heartbeat also doubles as a liveness pulse: a visible "still running, nothing to do" beats silence that is indistinguishable from a stalled loop.
+
+* Decisions
+
+** DONE Policy, not mechanism — detection stays in-session
+CLOSED: [2026-07-20 Mon]
+Craig reframed the proposal: silent-until-signal is a policy about when to spend a full model turn, not a new watcher. Keeping detection in-session dissolves the MCP-auth split (triage's sources need session auth) and needs no per-machine daemon.
+
+** DONE Empty fire → one labelled heartbeat line (not fully silent)
+CLOSED: [2026-07-20 Mon]
+Format =<workflow> at HH:MM: nothing=. A visible pulse is worth one line so a running loop is distinguishable from a stalled one; full silence was the rejected alternative.
+
+** DONE Applies to sentry, auto triage-intake, and auto inbox-zero uniformly
+CLOSED: [2026-07-20 Mon]
+The two MCP-auth loops are first-class targets, not just sentry, precisely because detection stays in-session.
+
+** DONE No external watcher; no truly-zero-idle in v1
+CLOSED: [2026-07-20 Mon]
+The systemd-timer / Monitor-tool / backgrounded-shell shapes are out. Truly-zero-idle (no turn on empty) is the only thing they'd buy, it only works for file-based loops, and it breaks triage. Logged as a possible file-only vNext, not built.
+
+** DONE Reuse each loop's existing seen-state
+CLOSED: [2026-07-20 Mon]
+No new watcher-owned seen-list; the sentinel / branch-digest / disposition each loop already keeps is the detection state.
+
+* Implementation Phases
+
+** Phase 1 — sentry quiet-fire heartbeat
+Edit =.ai/workflows/sentry.org= (canonical + mirror): a fire whose passes all probe-skip or no-op writes =sentry at HH:MM: nothing= instead of a full per-pass digest; a fire that runs or queues anything writes the full digest unchanged. Update the digest section and the Common Mistakes "silent skip" note to draw the quiet-fire-vs-working-fire line. Run sync-check. (Separable and the highest-value piece — do first.)
+
+** Phase 2 — auto triage-intake heartbeat
+Edit =.ai/workflows/triage-intake.org= Auto mode: an empty sweep collapses to =triage intake at HH:MM: nothing=. Preserve in-session detection and the accumulate-don't-mutate contract. Run sync-check.
+
+** Phase 3 — auto inbox-zero heartbeat
+Edit =.ai/workflows/inbox.org= Auto inbox zero mode: an empty cycle collapses to =inbox zero at HH:MM: nothing=. Run sync-check.
+
+** Phase 4 — state the shared policy once
+Factor the policy statement into one place both loops and sentry point at (a short section in =inbox.org= monitor-mode core, or a =claude-rules/= note if it reads as cross-cutting), so the three workflows reference one definition rather than restating it. Decide the home during build.
+
+** Phase 5 — verification
+Workflow prose, no bats surface. Verify by exercising: a sentry quiet fire prints one heartbeat line and writes no digest; a working fire still writes its full digest and commits. For the two MCP loops, confirm an empty sweep prints the heartbeat and a sweep with a planted item still does the full turn. Add a manual-testing checklist entry per =verification.md= where a live check is the only proof.
+
+* Prototype / UI
+
+Not applicable — no UI surface; the deliverable is the one-line heartbeat format.