Cadence Pulse — design spec
Status: Phase 0c + Phase 1 BUILT (pushed to the command-inbox Apps Script project 2026-05-26 ~3:31 PM), awaiting Sam-side go-live + 48h observe watch. Tested: not yet (manual pulseNow() is Sam's first run).
Owner: Sam Treitel · built by Claude Code session 2026-05-26
Source files: command-inbox/start-here.gs (all changes), bound to the Personal Command Inbox sheet (1U0-Ll_…).
Approved: Sam, 2026-05-26 ("go phase zero, phase one, A and B … without putting my life at risk of guessing anything").
The problem
Sam wants a self-firing full-circle loop: after a batch of back-and-forth with the bot (or after a time gap), the system re-reads everything (open queue + the new conversation + all his calendars + flagged mail), catches any loop he closed in conversation that the queue still shows open, sends back ONE updated plan, then resets and repeats. It must ask when unsure, act on his answer, and never jump ahead on a misread. Today the bot acts live per message (telegramBrainReply_) and assembles raw captures (autoAssembleInbox_), but nothing periodically reflects across the conversation to close loops + reshape the day.
What's true about the existing system (verified)
- Apps Script bound to the command-inbox sheet. Tabs:
INBOX,Action_Queue,Action_Events(immutable audit),Grocery_List,Transcript(every bot turn, IN/OUT),Context. callClaude_(Anthropic, live —ANTHROPIC_API_KEYset),buildMorningContext_(queue + calendar + flagged email),parseAIActions_/executeAIActions_(the<<<ACTIONS>>>protocol),sendTelegram_,logTranscript_/recentTranscriptTurns_.ACTIONS_PROTOCOL_already forbids auto-closing sensitive cards (HOA/legal/money/trading) — confirm instead.- Two findings that shaped the design: (1) personal mail (ztreitel@) does NOT fully forward into the business mailbox — a cloud-only read is blind to most personal mail until forwarding is on; (2) 7 calendars exist but the bot read only
getDefaultCalendar()(work) — blind to personal, Family/Skylight, Mildred; two duplicate Hebcal feeds.
Architecture (Approach 1 of the 1→hybrid roadmap)
Runs in the cloud on the bot so it works while Sam is offline. New code, all additive:
gatherTodayEvents_()— reads the curated calendar set (PULSE_CALENDARS_: work + personal + Family/Skylight + Mildred + one Hebcal), deduped + time-sorted.buildMorningContext_now calls it instead ofgetDefaultCalendar(). (Phase 0c.)pulseDigest_(force)— the pulse. Watermark =PULSE_LAST_ROW(Transcript row at last pulse) +PULSE_LAST_AT. Fires when≥ PULSE_TURN_THRESHOLDnew Transcript rows or≥ PULSE_MAX_GAP_HOURSelapsed with >0 new turns; gated by a min-gap (anti-spam), waking hours (7am–10pm NY), andisQuietForShabbos_(). On fire: assemblesbuildMorningContext_()+ the new conversation since the watermark →callClaude_with a close-the-loop + updated-plan + one-question prompt → sends to Telegram → logs the turn → advances the watermark past its own digest.isQuietForShabbos_()— conservative, fail-safe: Saturday always quiet; Friday ≥5pm quiet; major Yom Tov (Hebcal) quiet; Hebcal failure degrades to the day/hour heuristic. Better to skip a Friday pulse than text on Shabbos.- Installer
setupPulse()→ ONE dedup-guarded 30-min trigger; baselines the watermark; defaultsPULSE_MODE=observe.pulseNow()= forced manual test (looks back ~12 turns).setPulseMode('act'|'observe').
Autonomy boundary (Sam's locked decision)
Dynamic ask→answer→act→close. Confident + safe → close it. Unsure or money/legal/HOA/insurance/FAMILY/children → ask a one-line yes/no and wait. Never guess. Enforced three ways: (1) ACTIONS_PROTOCOL_'s existing sensitive-card rule; (2) the pulse prompt extends it to FAMILY + anything about the kids = ask-never-act, never surfaced to Mildred; (3) FAMILY added to SH_CFG.sensitiveLanes so family cards are auto-flagged sensitive + undelegatable to Mildred.
Safety / validation (Rules 9 + 10 + the children's-info flag)
- Observe mode first (default): the pulse SENDS the digest + lists what it WOULD do, but writes nothing. Flip to
actonly after ~48h of clean observe output. - Trigger-registry gate (Sam's rule):
auditTriggers_()must show the project clean beforesetupPulse()installs the one trigger. - Proof artifact: one
pulseNow()run that reads the queue + all calendars + recent conversation and returns a sane updated plan = the "tested" evidence.
Go-live sequence (Sam, in the Apps Script editor, in order)
auditTriggers_()— confirm no duplicate/unexpected triggers (the gate).pulseNow()— forces one observe-mode pulse to Telegram; confirm it reads right.setupPulse()— installs the single 30-min trigger (observe, baselined).- Watch ~48h. Then
setPulseMode("act")to let it start closing safe loops. - Phase 0b (Gmail, Sam-side): forward ztreitel@ → sam@ so the cloud pulse sees personal mail (Claude can fetch the confirm code from sam@). Until then, personal calendar is covered (added above) but personal mail is not.
Decisions locked
- A — personal into the loop via forwarding ztreitel@ → sam@ (Sam-side; revisit if he prefers fully separate → native read in Phase 3).
- B — BOS canonical = Monthly Obligations v1 (
1L_rxC…); v2 is a stale snapshot. The loop's money signals come from v1 (currently via CASH-lane queue cards; a direct v1 read is a later increment).
Out of scope (later phases — do not build until Phase 1 validates)
- Phase 2 — full two-inbox + calendar redundancy map; queue-card correctness audit; direct v1 obligations read.
- Phase 3 (hybrid) — scheduled Claude Code pass with native gmail-personal MCP + memory/doc updates + cross-system reconciliation, writing into the command center for the pulse to surface.