| name | assistant-loop |
| description | Personal assistant loop. Telegram I/O (tg-daemon), scheduled tasks (cron-daemon), and an interactive REPL — supervised by a background bash watchdog so the REPL doesn't need to stay open burning tokens. Consults CLAUDE.md for what to do; this skill handles how. |
Assistant Loop
(All bin/... paths below are relative to the repo root — see CLAUDE.md "Path Convention".)
The assistant has three long-running components plus an optional REPL:
| Component | Scope | Started by |
|---|
tg-daemon (bin/tg-daemon.ts) | Telegram I/O — receives Telegram messages and sends replies via a long-lived claude -p --input-format stream-json subprocess | nohup bun run bin/tg-daemon.ts & (or watchdog respawn) |
cron-daemon (bin/cron-daemon.ts) | Scheduled tasks from CRON.md (gmail digest, daily journal, morning brief). On fire, spawns a one-shot claude -p --permission-mode bypassPermissions to handle the prompt. Auto-reloads on CRON.md change. | nohup bun run bin/cron-daemon.ts & (or watchdog respawn) |
watchdog (bin/watchdog.sh) | Bash supervisor (no LLM). Loops every 60s; respawns dead daemon(s) + Telegram alert. | /assistant-loop first tick spawns it & disown if not already alive. |
| Main REPL (this Claude Code session) | Interactive session for ad-hoc work. NO heartbeat — the watchdog handles supervision. | User runs /assistant-loop (one-shot) when they want a status check. |
Cost note: The REPL no longer schedules wake-ups. Open it when you want to interact, close it when you're done — daemons stay supervised by the watchdog regardless.
Channel Discipline (information isolation)
Reply on the channel the request came in on. Don't cross-post.
- Telegram message arrives -> daemon's claude subprocess replies on Telegram (
bun run bin/tg-send.ts <chat_id> "<msg>"). Don't echo anything to the REPL.
- REPL command typed by your human in this Claude Code session -> reply only in REPL output. Do not send a Telegram message unless they explicitly ask ("send to telegram"/"tell me on telegram").
- Cron-fired tasks (gmail digest, daily journal, morning brief) - push results to Telegram per their CRON.md prompt. REPL gets only a brief status line.
- Watchdog respawns — the bash watchdog sends Telegram alerts directly when it restarts a daemon (
🦌 tg-daemon was down, restarting). REPL doesn't see this; the user gets the message on their phone.
When in doubt, prefer one channel - the one that originated the request.
Daemon — On Telegram Event
The tg-daemon feeds its inner claude subprocess one user turn per Telegram event (typing → log → lazy-load context + scratchpad → handle attachment/reply → route to skill → send → log). The daemon's own PRIMING enforces this at runtime; the /assistant-loop REPL never executes it. Full event-shape + step-by-step contract: read references/daemon-telegram-event.md.
Main REPL — /assistant-loop (one-shot)
This skill runs once per invocation. No ScheduleWakeup, no heartbeat.
0. Preflight — abort before spawning anything if config is incomplete
The watchdog will tight-loop respawning daemons that crash on missing env, so refuse to start the loop unless config is actually present. One bash:
{
if [ ! -f /.dockerenv ] && [ -f .env ]; then
set -a; . ./.env; set +a
fi
[ -n "${TELEGRAM_BOT_TOKEN:-}" ] || { echo "✗ TELEGRAM_BOT_TOKEN unset — run ./setup.sh"; exit 1; }
[ -n "${TELEGRAM_CHAT_ID:-}${TELEGRAM_CHAT_IDS:-}" ] || { echo "✗ TELEGRAM_CHAT_ID(S) unset"; exit 1; }
[ -d "${VAULT_PATH:-}/persona" ] || { echo "✗ VAULT_PATH=${VAULT_PATH:-(unset)}/persona missing — run ./setup.sh"; exit 1; }
echo "preflight ✓"; }
If any check fails: report the specific failure to the user, tell them to run ./setup.sh from the repo root, and stop. Do NOT spawn the watchdog, daemons, or session registration. A crash-looping watchdog spamming Telegram alerts is much worse than a clean abort.
If preflight passes, proceed to step 1.
1. Tag this session as loop in the registry
bun run bin/register-session.ts loop. Idempotent; enables /stats to bucket billing accurately.
2. Verify the watchdog is alive
pgrep -f bin/watchdog.sh. If not running, spawn it:
nohup bash bin/watchdog.sh > /dev/null 2>&1 & disown
(watchdog.sh self-wraps with a pseudoterminal at the top — no extra setsid/script needed here, regardless of whether the caller has a controlling TTY.)
3. Quick daemon status
pgrep tg-daemon and cron-daemon. If either is dead, the watchdog will catch them within 60s; for instant feedback you can run bash bin/watchdog.sh --once to do a single check now.
4. Re-arm pending reminders
Read <vault>/persona/tasks.md for incomplete tasks with dates ≤ today. Send overdue ones immediately via Telegram with a note. (Future-dated tasks are handled by the upcoming-1h-preview cron task, no need to re-arm.)
5. Report status to the REPL user
In 1–3 lines (e.g., "✓ watchdog up, both daemons healthy, 2 overdue tasks pinged"). Done — no scheduled wake-up.
The main REPL does not poll Telegram (tg-daemon owns that), does not schedule cron tasks (cron-daemon owns that), and does not supervise daemons in a loop (watchdog owns that).
Upcoming Preview (soft reminders)
The upcoming-1h-preview cron task (every 20 min) pings Telegram once per imminent event (~30 min lead) from Google Calendar + tasks.md, deduped via data/upcoming-notified.json, respecting quiet hours. It runs in the cron subprocess, not this REPL. Full procedure + tuning (window, lead overrides, dedup, quiet-hours gate): read references/upcoming-preview.md. The CRON.md prompt also points there.
How cron tasks run (background)
The REPL does not see cron fires — cron-daemon spawns an independent claude -p per task from CRON.md, stamps the Last run: line on exit, and hot-reloads CRON.md on change. Tasks can be inline prompts or deterministic bin/<task>.ts wrappers. Full mechanics + the wrapper pattern: read references/cron-mechanics.md.
Onboarding (first-time setup)
Triggered on the first Telegram message when USER.md has "not set" fields:
- Greet warmly. Ask them to introduce themselves: name, location, timezone, preferred language. Or let them write freely - extract from natural conversation.
- Update
USER.md with what you learn (name, location, timezone, language, telegram chat_id).
- Fill in
IDENTITY.md - pick a name, creature type, vibe, and emoji from conversation tone.
- Confirm naturally, then handle their original message (don't make them repeat).
One or two questions max. Learn the rest over time.
Conversation Log
All Telegram messages and responses log to data/conversations/YYYY-MM-DD.md:
[HH:MM] user: message text
[HH:MM] bot: response text
The daemon writes both halves. The main REPL does NOT write to this file (REPL conversations are separate - they live in this Claude Code session's transcript and don't get logged).
Telegram Commands (reference)
bun run bin/tg-send.ts <chat_id> "<message>"
bun run bin/tg-typing.ts <chat_id>
bun run bin/tg-daemon.ts
bun run bin/cron-daemon.ts
bin/tg-pull.ts and bin/tg-watch.ts are legacy from the pre-daemon architecture. Don't use.
bin/tg-send.ts reads the message body from stdin only — pass via echo ... | tg-send <id> or <<'EOF' heredoc. Passing the body as an argv argument exits with a usage error (bash interpolation of $, backticks, " would silently corrupt it).
Error Handling
- If tg-daemon's inner claude subprocess dies mid-turn, tg-daemon respawns it and re-sends the priming.
- If tg-daemon or cron-daemon themselves die, the bash watchdog (
bin/watchdog.sh) detects via pidfile + kill -0 within 60s and respawns the missing one(s) + Telegram alerts the user.
- If the watchdog itself dies, the next
/assistant-loop invocation respawns it. Until then, no supervision — in Docker the container restart policy still catches container-level failures.
- If a single cron fire fails (claude subprocess errors out), cron-daemon logs and continues with the schedule — one bad fire doesn't break future fires.