diff --git a/hooks/phase-b-rem.sh b/hooks/phase-b-rem.sh index 143694a..28d5abe 100755 --- a/hooks/phase-b-rem.sh +++ b/hooks/phase-b-rem.sh @@ -27,7 +27,20 @@ IFS=$'\n\t' [ -f ~/.claude/secrets/.env ] && set -a && source ~/.claude/secrets/.env && set +a REPO_PATH="${KEI_MEMORY_REPO_PATH:-$HOME/.claude/memory/sync-repo}" -KEI_MEMORY_BIN="${KEI_MEMORY_BIN:-$HOME/Projects/KeiSeiKit/_primitives/_rust/target/release/kei-memory}" +# kei-memory binary resolution order (first hit wins): +# 1. Explicit env override KEI_MEMORY_BIN=/some/path +# 2. PATH lookup ~/.cargo/bin/kei-memory typically +# 3. KeiSeiKit-public local-build target dir +# 4. Legacy KeiSeiKit (pre-public-rebrand) target dir +if [ -z "${KEI_MEMORY_BIN:-}" ]; then + if command -v kei-memory >/dev/null 2>&1; then + KEI_MEMORY_BIN="$(command -v kei-memory)" + elif [ -x "$HOME/Projects/KeiSeiKit-public/_primitives/_rust/target/release/kei-memory" ]; then + KEI_MEMORY_BIN="$HOME/Projects/KeiSeiKit-public/_primitives/_rust/target/release/kei-memory" + else + KEI_MEMORY_BIN="$HOME/Projects/KeiSeiKit/_primitives/_rust/target/release/kei-memory" + fi +fi TODAY=$(date +%Y-%m-%d) NOW_TS=$(date +%s) WALL_BUDGET_S=3600 @@ -219,3 +232,12 @@ git commit -m "REM: consolidation $TODAY (${#NEW_TRACES[@]} new traces)" 2>&1 | git push 2>&1 | tail -3 || { log "WARN: push failed"; exit 1; } log "DONE — $REPORT pushed" + +# Step 7 — Telegram delivery (defensive: never fails Phase B) +log "Step 7/7: tg delivery" +TG_HOOK="$HOME/.claude/hooks/sleep-report-tg.sh" +if [ -x "$TG_HOOK" ]; then + "$TG_HOOK" "$REPORT" "$TODAY" 2>&1 | tail -3 || true +else + log "WARN: $TG_HOOK not present, skipping tg" +fi diff --git a/hooks/sleep-report-tg.sh b/hooks/sleep-report-tg.sh new file mode 100755 index 0000000..97f19a5 --- /dev/null +++ b/hooks/sleep-report-tg.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# sleep-report-tg.sh — Phase B → Telegram delivery hook. +# +# Runs as the FINAL step of phase-b-rem.sh after the report is committed + +# pushed to the memory-repo. Spawns a cloud Claude agent to read the report +# + tracking digests + cross-session analyze, distil 3-5 actionable +# findings, then sends to the whitelisted chat_id via @KeiSeiBot. +# +# Defensive: never blocks Phase B exit code. Failures land in +# ~/.claude/memory/sleep-report-tg-errors.log and exit 0. +# +# Required env (RULE 0.8 — secrets in ~/.claude/secrets/.env): +# TELEGRAM_BOT_TOKEN — @KeiSeiBot +# TELEGRAM_ALLOWED_CHAT_ID — 86059912 (Parfionovich, single-user whitelist) +# ANTHROPIC_API_KEY — for the reasoning step +# +# Bypass: SLEEP_REPORT_TG_BYPASS=1 ... +# +# Usage (called from phase-b-rem.sh after `git push`): +# ~/.claude/hooks/sleep-report-tg.sh "$REPORT" "$TODAY" +# $1 = path to reports/sleep-YYYY-MM-DD.md (required) +# $2 = TODAY date string (required, YYYY-MM-DD) + +set -u + +ERR_LOG="${HOME}/.claude/memory/sleep-report-tg-errors.log" + +log_err() { + mkdir -p "$(dirname "$ERR_LOG")" 2>/dev/null || return 0 + printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$ERR_LOG" 2>/dev/null || true +} + +[ "${SLEEP_REPORT_TG_BYPASS:-0}" = "1" ] && exit 0 + +REPORT="${1:-}" +TODAY="${2:-$(date +%Y-%m-%d)}" +[ -f "$REPORT" ] || { log_err "report not found: $REPORT"; exit 0; } + +# Source secrets if env vars not already set. +SECRETS_FILE="${HOME}/.claude/secrets/.env" +if [ -f "$SECRETS_FILE" ] && [ -z "${TELEGRAM_BOT_TOKEN:-}" ]; then + set -a + # shellcheck disable=SC1090 + . "$SECRETS_FILE" 2>/dev/null || true + set +a +fi + +[ -n "${TELEGRAM_BOT_TOKEN:-}" ] || { log_err "TELEGRAM_BOT_TOKEN unset"; exit 0; } +CHAT_ID="${TELEGRAM_ALLOWED_CHAT_ID:-86059912}" + +command -v curl >/dev/null 2>&1 || { log_err "curl missing"; exit 0; } +command -v jq >/dev/null 2>&1 || { log_err "jq missing"; exit 0; } + +# ---- Cloud-agent reasoning step ------------------------------------------- +# Send the report to Claude API for distillation. Keep it cheap with Sonnet +# 4.6 — the task is summary, not generation. Caps + max_tokens prevent +# runaway cost on a malformed report. +TG_AGENT_LOG="${HOME}/.claude/memory/sleep-report-tg-agent.log" + +if [ -z "${ANTHROPIC_API_KEY:-}" ]; then + log_err "ANTHROPIC_API_KEY unset — sending raw report excerpt" + SUMMARY=$(head -c 3500 "$REPORT") +else + REPORT_BODY=$(cat "$REPORT") + PROMPT_BODY=$(jq -n --arg r "$REPORT_BODY" ' +{ + "model": "claude-sonnet-4-6", + "max_tokens": 1500, + "system": "You are the KeiSei sleep-report distiller. The user wakes up and reads ONE Telegram message summarizing what happened in their dev environment overnight. Be ruthlessly concise.\n\nMandatory output structure (max 3500 chars total, plain text suitable for Telegram, no markdown beyond *bold* and `code`):\n\n1. ONE-LINE TL;DR — what is the single most important thing the user should know.\n2. NUMBERS — agent outcomes (functional/partial/scaffolding/fail counts), cost burned, top tool-call categories. Cite raw values from the digests, no interpretation.\n3. ACTIONABLE FINDINGS — 3-5 bullet points. Each must be: a concrete observation FROM THE DATA + suggested action. Examples: \"Opus burned $X with 0 functional outcomes — investigate or accept as legacy noise\", \"Skill X has Y% success rate over N runs — candidate for archive\". DO NOT invent findings without data backing.\n4. RULE-CANDIDATES — if any cross-session pattern in the analyze section appeared >=3 times, name it + suggest a /escalate-recurrence command. Skip section if nothing qualifies.\n\nNO emoji except a single 💤 prefix. NO chat-style preamble (\"Доброе утро!\"). NO closing pleasantries. Direct technical brief.", + "messages": [{"role":"user","content":[{"type":"text","text":$r}]}] +}') + + RESP=$(curl -sS -X POST "https://api.anthropic.com/v1/messages" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + --max-time 120 \ + -d "$PROMPT_BODY" 2>/dev/null) + + printf '[%s]\n%s\n---\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$RESP" >> "$TG_AGENT_LOG" + + SUMMARY=$(printf '%s' "$RESP" | jq -r '.content[0].text // empty' 2>/dev/null) + if [ -z "$SUMMARY" ]; then + ERR=$(printf '%s' "$RESP" | jq -r '.error.message // "unknown"' 2>/dev/null) + log_err "claude api returned no text (error: $ERR) — falling back to raw excerpt" + SUMMARY=$(head -c 3500 "$REPORT") + fi +fi + +# ---- Telegram send -------------------------------------------------------- +# Telegram message limit is 4096 chars. We cap at 3900 to leave room for +# the prefix + headers. If the cloud agent went over budget, hard-truncate +# rather than splitting (one cohesive message > two fragmented). +HEADER="💤 *Sleep report* — ${TODAY}"$'\n\n' +BODY="${HEADER}${SUMMARY}" +BODY=$(printf '%s' "$BODY" | head -c 3900) + +# Use --data-urlencode for safe transport of newlines / specials. +HTTP_RESP=$(curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + --max-time 30 \ + --data-urlencode "chat_id=${CHAT_ID}" \ + --data-urlencode "parse_mode=Markdown" \ + --data-urlencode "text=${BODY}" 2>/dev/null) + +OK=$(printf '%s' "$HTTP_RESP" | jq -r '.ok' 2>/dev/null) +if [ "$OK" = "true" ]; then + MSG_ID=$(printf '%s' "$HTTP_RESP" | jq -r '.result.message_id' 2>/dev/null) + log_err "INFO sent to chat=${CHAT_ID} msg_id=${MSG_ID} report=${TODAY}" +else + DESC=$(printf '%s' "$HTTP_RESP" | jq -r '.description' 2>/dev/null) + log_err "send failed: $DESC" + + # Markdown parse errors are a known failure mode (orphan * / [) — retry + # without parse_mode so the user at least sees the report verbatim. + if printf '%s' "$DESC" | grep -qi "parse"; then + curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + --max-time 30 \ + --data-urlencode "chat_id=${CHAT_ID}" \ + --data-urlencode "text=${BODY}" >/dev/null 2>&1 || true + fi +fi + +exit 0