Coordinator-routed proactive check-ins (2026-04-30)¶
Three scheduled scripts replace the old morning-brief / heartbeat / wellness-alarm trio. All push through the coordinator bot so a reply threads back into the coordinator's brain context.
The schedule¶
| Cron | Script | Bot title | What it does |
|---|---|---|---|
50 8 * * * |
forge_morning_report.py |
"Morning Report, <date>" | Opus synthesis of 6 parallel Sonnet fetches: wellness, today's calendar, due tasks across Life/JWVR/Nova, today's habits, business email last 24h + starred 7d, personal email same. Lands at 8:50 so it's done by 9 AM work start. ~30-40s end-to-end. |
*/15 8-21 * * * |
forge_heartbeat_random.py |
time-of-day, e.g. "3:42 PM" | Probabilistic ping. Each fire rolls 8.5% with a 90 min min-gap, averages ~4/day. Sonnet pass over wellness + remaining calendar + habits + due tasks + recent inbox. Direct, non-motivational, asks at most one open question. |
0 21 * * * |
forge_evening_winddown.py |
"Wind-down, <date>" | Day recap (today's calendar + recent inbox capture) + habits status + tomorrow's calendar. |
30 13 * * * (UTC) |
forge_garmin_poll.py |
n/a | Bumped earlier (8:30 AM CT) so the morning report has fresh Garmin data. The original */30 14-23,0-3 UTC line stays. |
Coordinator routing pattern¶
Shared helper: forge_coordinator_proactive.py exposes send_coordinator(slot, title, body) which:
- Calls
scripts/forge_telegram_push.sh coordinator info <title> <body>(uses~/.forge-secrets/telegram-lifeos-coordinator.env) - Appends a synthetic
{"role": "assistant", "reply": "[title]\n<body>", "actions_summary": "proactive:<slot>"}toforge/data/coordinator-context.jsonl
The append is what makes proactive messages reply-threadable: when Justin replies in the coordinator chat, the brain reads its coordinator-context.jsonl, sees the recent assistant entry, and treats the reply as a continuation. Critical detail.
Garmin freshness gate¶
Each Garmin metric in the context API carries a ts. Heartbeat and morning report check now - ts <= GARMIN_STALE_HOURS (3h heartbeat, 4h morning report). Stale metrics are dropped from heartbeat with a "(some metrics stale, omitted)" suffix; morning report keeps them but marks each "(stale)" so Opus can call out "Garmin hasn't synced this morning yet" honestly. Eight Sleep gets an 18 h freshness window since it's HA real-time and persists overnight.
Tone hard rules (in every prompt)¶
- No em dashes (Doctrine Section 9; also stripped at the boundary via
forge_text_sanitize.strip_em_dashes) - No upbeat / motivational filler ("you got this", "great job", exclamation points)
- Reference ONLY data shown in the prompt, do not invent events / people / tasks / habits
- If data is sparse, the message must be SHORTER, not padded with confabulation
- At most one open question per message, only if something genuinely warrants asking
Retired scripts (moved 2026-04-30)¶
forge/_archive/scripts-retired-2026-04-30/:
- morning-brief.py (was 12:00 cron) — replaced by morning report
- heartbeat.py (was 17:00 + 23:00 cron) — replaced by random heartbeat + wind-down
- forge_wellness_morning_check.sh (was 7:30 cron) — retired; threshold logic folded into morning report's "(stale)" call-outs
- cron-before.txt — pre-swap crontab snapshot
Reverting¶
Reinstall old crontab: crontab /home/justinwieb/forge/_archive/scripts-retired-2026-04-30/cron-before.txt and move the three scripts back into forge/scripts/.
[Claude Code, 2026-04-30]