# Kade — Phase v0.1 Architecture Spec (Meds-Only)

**Owner:** Riv
**Counterpart:** Atlas (API contract: `Team Inbox/Atlas/kade-api-contract.md`)
**Date:** 2026-05-22
**Target ship:** ~2026-05-27 (5 calendar days)

---

## Scope

**In:** Slack bot DMs Jimmie 3x/day (or more — one per active dose) for medication reminders. Reads 👍 reactions and natural-language acks ("done", "took it"). Logs everything to Atlas. ONE non-shaming follow-up if no ack within the window. Then move on.

**Out (saved for v0.2/v0.3):** Task capture, "Top 3" / "List From Hell" queries, DND windows, kick-in-the-pants deadline logic, voice-memo transcription.

---

## Architecture

```
                    Jimmie (Slack)
                          │
                          │ DM / 👍 reaction
                          ▼
              ┌──────────────────────┐
              │  Slack (Socket Mode) │
              └──────────────────────┘
                          │
                          │ events (message.im, reaction_added)
                          ▼
       ┌────────────────────────────────────────┐
       │   Kade bot (Python, slack-bolt-async)  │
       │   running on Jimmie's Windows PC       │
       │   - APScheduler for fire times         │
       │   - LLM call for voice composition     │
       │   - HTTP to Atlas for logging          │
       └────────────────────────────────────────┘
                  │                          │
                  │ HTTP POST                │ Anthropic API
                  │ Authorization: Bearer    │ (Sonnet for voice)
                  ▼                          ▼
       ┌───────────────────────┐    ┌─────────────────────┐
       │  Atlas Next.js API    │    │  Anthropic API      │
       │  localhost:3000       │    │  claude-sonnet-4-6  │
       │  /api/kade/meds/*     │    └─────────────────────┘
       └───────────────────────┘
                  │
                  │ writes
                  ▼
       ┌───────────────────────┐
       │  Postgres             │
       │  kade_meds_log table  │
       └───────────────────────┘
```

---

## Components

### `kade_slack/bot.py` — main entry point
- Loads `.env` (Slack tokens, Atlas ingest token, tz)
- Connects to Slack via Socket Mode (no public webhook URL)
- On startup: GET `/api/kade/meds/today` → seed today's fire schedule into APScheduler
- Daily at 00:01 local: refresh schedule from Atlas

### `kade_slack/scheduler.py` — APScheduler wrapper
- One job per dose per day
- On fire time:
  1. Compose message via `voice.py` (LLM call)
  2. Send Slack DM to Jimmie
  3. POST `/api/kade/meds/fire` with the Slack message ts as `external_id`
  4. Schedule a follow-up at `fire_time + follow_up_minutes` (default 30)
- Follow-up job:
  1. Check if ack landed (in-memory state OR query Atlas)
  2. If acked: cancel follow-up, no-op
  3. If not acked: send ONE follow-up DM (non-shaming, Kade voice), POST `/api/kade/meds/missed` after a final grace period

### `kade_slack/handlers.py` — event handlers
- `@app.event("message")` — only fires for IMs because of `message.im` scope
  - LLM parses Jimmie's reply: ack? skip? other?
  - If ack: POST `/api/kade/meds/ack` linked to the latest pending fire
  - If skip: POST `/api/kade/meds/skip`
  - If other: log it, optionally respond (Kade voice) — v0.1 minimal, mostly silent for non-med chatter
- `@app.event("reaction_added")` — 👍 on a med fire = ack
  - Look up the fire by the parent message ts
  - POST `/api/kade/meds/ack`
- `@app.event("reaction_added")` for ❌ or 🚫 = skip
  - POST `/api/kade/meds/skip`

### `kade_slack/voice.py` — LLM voice composition
- System prompt = `C:\PKA\Team\Kade\profile.md` (loaded at startup, cached)
- One call per outbound message
- Model: `claude-sonnet-4-6` (good voice fidelity at reasonable cost)
- Cache the profile.md as a cached prompt prefix via prompt caching (Anthropic SDK — 5-min TTL, refreshed automatically by hot path)
- **Voice contract (non-negotiable):**
  - NEVER "Hello Jimmie! Just a friendly reminder..."
  - NEVER lecture, moralize, or "I understand this might be difficult"
  - Lead with the action ("Adderall — morning dose 💊")
  - Brief. Emojis OK. Sass OK.
  - Stoic quotes occasionally on missed/skipped, not every fire

**Example v0.1 fire prompts (LLM-generated, will vary):**
> 💊 Adderall — morning. Take it now.

> 11:45 hit. Adderall midday. Pop it before you forget 🧠

**Example v0.1 follow-up (30 min after no ack):**
> Adderall morning still showing as not taken. Did you take it and forget to confirm, or skip?

**Example v0.1 missed-after-followup:**
> (silent — logged to DB, no further DM)

### `kade_slack/atlas_client.py` — Atlas API client
- Thin HTTP wrapper around the 6 endpoints in the contract
- Retry with exponential backoff (1s/2s/4s/8s/16s, 5 attempts)
- If all 5 attempts fail: DM Jimmie in Kade voice ("Atlas DB unreachable — meds still scheduled, can't log them. Bug me if it stays broken.")

### `kade_slack/.env` — secrets (Jimmie populates this — see install checklist)
- `SLACK_BOT_TOKEN`
- `SLACK_SIGNING_SECRET`
- `SLACK_APP_TOKEN`
- `ATLAS_INGEST_TOKEN`
- `KADE_SLACK_USER_ID` (Jimmie's Slack user ID — bot fills in on first run)
- `TZ=America/Chicago`

---

## Dependencies

```
slack-bolt>=1.21        # Slack SDK with Socket Mode
slack-sdk>=3.27
apscheduler>=3.10       # Cron-like scheduling
httpx>=0.27             # Async HTTP to Atlas
anthropic>=0.40         # Claude API for voice
python-dotenv>=1.0
pydantic>=2.6           # Request/response validation
```

Install pattern matches Riv's existing `j2_qbo_mcp`: pinned `requirements.txt`, Python 3.12+.

---

## Run loop

### Foreground (dev/testing)
```
cd C:\PKA\Team\Riv\kade_slack
python bot.py
```

Logs to `kade_slack/bot.log`.

### Production (background — Task Scheduler)
Same pattern as the QBO worker — once stable, add to Windows Task Scheduler with:
- Trigger: At system startup
- Action: `python C:\PKA\Team\Riv\kade_slack\bot.py`
- Run with highest privileges, in the background
- Restart on failure (3 retries, 1 min apart)

This piggybacks on the same Task Scheduler work Riv has queued for the QBO worker. One install pass for both.

---

## Idempotency strategy

**The non-obvious risk:** bot crashes mid-fire (after Slack send, before Atlas POST). On restart, no record of the fire exists in Atlas, but the message is in Slack and the follow-up timer is lost.

**Mitigation:**
1. The bot writes its own local journal (`kade_slack/state.json`) BEFORE sending the Slack message — records `(med_id, dose_label, scheduled_for)`
2. After Slack send succeeds, journal entry gets `slack_ts` added
3. After Atlas POST succeeds, journal entry gets `atlas_log_id` added
4. On startup, bot scans journal for entries with `slack_ts` but no `atlas_log_id` → re-POST to Atlas (idempotent, dedupes on `external_id`)
5. Journal entries older than 48h get pruned

This trades a 50-line state file for robust crash recovery. Worth it for meds — missed Atlas writes mean missed adherence data.

---

## Open questions for Jimmie

These get asked **after the .env is saved**, in Kade's first Slack message:

1. **Med times** — confirm or correct the defaults:
   - Adderall morning — 8:00am?
   - Adderall midday — 11:45am ✅
   - Adderall afternoon — 3:00pm?
   - Buspar — 8:00am?
   - Metformin — 5:30pm ✅
2. **Follow-up window** — 30 min is the default. Change per-dose if Jimmie wants tighter on some, looser on others.
3. **Voice calibration** — first 24h, Jimmie reviews ~3-5 actual Kade messages. If anything sounds off (corporate, sycophantic, drift), we tune the system prompt.

---

## Risks + mitigations

| Risk | Likelihood | Mitigation |
|---|---|---|
| LLM voice drifts toward corporate over time | Medium | Profile.md is the system prompt; voice anti-patterns are explicit. Periodic spot-check by Jimmie. |
| Slack rate limit on burst | Low | 5 doses/day × 1 user is far below limit. |
| Atlas DB down at fire time | Low-medium | Bot still fires the Slack reminder (med safety is priority); journal queues the Atlas write for retry. |
| Anthropic API down at fire time | Low | Fall back to a pre-cached "minimal Kade-voice" template per dose (stored in `voice.py`). Functional, less varied. |
| Jimmie acks on a different day's fire by mistake | Low | Ack ts → look up most recent fire of that med_name+dose_label within 4h window. |
| Bot eats memory / leaks over weeks | Medium | APScheduler is well-behaved but watch this — Riv should monitor for first 2 weeks. |

---

## Ship checklist (v0.1 done = these all true)

- [ ] Atlas migration `006_kade_tables` + seed `007_seed_kade_meds_and_habits` applied to prod DB
- [ ] Atlas endpoints (6 of them) live at `localhost:3000/api/kade/meds/*`, contract acked
- [ ] Riv: bot scaffold built, `.env` populated by Jimmie
- [ ] Bot connects to Slack, opens DM with Jimmie, asks for med-time confirmation
- [ ] Jimmie confirms / corrects times — Riv updates kade_meds rows directly
- [ ] Next day: all 5 doses fire on schedule, Jimmie acks at least 3, missed acks get exactly one follow-up
- [ ] Adherence query returns sensible numbers
- [ ] Bot survives a manual restart with no duplicate fires/acks
- [ ] Task Scheduler entry created so bot auto-starts on boot

When all 9 are green → v0.1 ships, v0.2 (capture + queries) kickoff begins.
