Phase 3: coaching preferences + weekly check-in engine

Add coaching preferences (auto-derived from the profile, user-overridable) and
a periodic check-in engine that quotes the person's own words and asks whether
their direction still feels valid — mirror, not compass.

- Preferences are deterministic: a documented triad mapping (gut → direct/
  higher-friction, heart → warm/drift-sensitive, head → reflective/question-led)
  produces defaults for the six fields (coaching_frequency, coaching_style,
  misalignment_threshold, friction_tolerance, prefer_questions_over_directives,
  time_of_day_preference). PUT overrides; regenerate re-derives.
- CheckinCoach (app/services/coaching.py): Anthropic-backed; writes a check-in
  that quotes the person's goals back and asks if the direction still holds.
- Endpoints (app/routers/coaching.py): GET/PUT/regenerate preferences;
  GET/POST checkins; respond (records still_valid); admin POST /run is the
  weekly batch (due = cadence elapsed + locked profile), intended for a cron.
- Models + migration 005: coaching_preferences (per user) and coaching_checkin.
- Frontend: coaching.html (preferences form + check-in feed); linked from
  profile.html.

Tests: 68 passing (added deterministic-preference unit tests and coaching
endpoint/batch tests; run in-container). README updated for Phase 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joel Salmon
2026-06-16 21:10:54 -05:00
parent b4d8d17aed
commit 50453901b3
13 changed files with 1317 additions and 1 deletions
+92
View File
@@ -42,6 +42,10 @@ If you need to explain this app in detail, use this mental model:
profile through an AI-coach reflection loop (`POST /discovery/profile/me/reflect`)
— the coach mirrors the profile back and applies the person's own
corrections — before affirming with the same confirm/lock.
13. After affirming (Phase 3) the user opens `/static/coaching.html` to tune
coaching preferences (auto-derived from their profile) and receive periodic
check-ins that quote their own words and ask if their direction still holds.
A weekly cron calls `POST /discovery/coaching/run` to generate due check-ins.
Machine-to-machine callers (e.g. the MCP server) skip the OAuth dance and
authenticate with `X-API-Key: $IMPACTFLOW_API_KEY` instead. That header
@@ -142,16 +146,22 @@ Important files:
| `app/static/discovery.html` | Browser-based seven-prompt flow |
| `app/static/profile.html` | Browser-based profile display, edit, and confirm actions; links to reflection |
| `app/static/reflect.html` | Phase 2 AI-coach reflection chat (mirror loop, applies revisions, affirm) |
| `app/static/coaching.html` | Phase 3 coaching preferences form + check-in feed |
| `app/static/auth.js` | Shared `authedFetch` helper: sends session cookies, silently refreshes on `401`, redirects to login |
| `app/static/style.css` | Shared UI styling |
| `app/services/reflector.py` | `ReflectionCoach`: Anthropic-backed mirror loop, JSON parsing, revision filtering |
| `app/services/coaching.py` | Deterministic preference generator + `CheckinCoach` (Anthropic check-in text) |
| `app/routers/coaching.py` | Phase 3 coaching routes: preferences, check-ins, weekly batch `/run` |
| `alembic/versions/001_initial.py` | Initial database schema migration |
| `alembic/versions/002_add_auth.py` | Adds `users`, `refresh_tokens`, and `activity_log` tables |
| `alembic/versions/003_add_goals.py` | Adds the goal columns to `discovery_conversation` and `discovery_profile` |
| `alembic/versions/004_add_reflection.py` | Adds `reflection_message` and `profile_revision` tables (Phase 2) |
| `alembic/versions/005_add_coaching.py` | Adds `coaching_preferences` and `coaching_checkin` tables (Phase 3) |
| `tests/conftest.py` | Shared `app_client` fixture (isolated app + temp DB) |
| `tests/test_extractor.py` | Unit tests for extraction plumbing, goals, and retry behavior |
| `tests/test_reflector.py` | Unit tests for `ReflectionCoach` (mirror, revision filtering, retry) |
| `tests/test_coaching_prefs.py` | Unit tests for the deterministic coaching-preference generator |
| `tests/test_coaching.py` | Tests for coaching endpoints (preferences, check-ins, due-logic batch) |
| `tests/test_auth.py` | Tests for the dual-auth dependency (JWT + cookie + API key), token refresh/logout, admin enforcement, and domain allow-list |
| `tests/test_profile_edit.py` | Tests for `PATCH /discovery/profile/me` (edit, partial update, lock/`409`) |
| `tests/test_reflection.py` | Tests for the reflection endpoints (turns, applied revisions, lock/`409`, history) |
@@ -386,6 +396,37 @@ still the `/confirm` lock above; the loop is what happens before it.
history, newest first. Every change is snapshotted in `profile_revision`
with a `source` of `extraction` (initial), `reflection`, or `manual_edit`.
### 9. Coaching Preferences & Check-ins (Phase 3)
How the person wants to be coached, plus a periodic check-in engine.
Coaching **preferences** are auto-generated from the profile's Enneagram centre
(a deterministic mapping — gut → direct/higher-friction, heart → warm/sensitive
to drift, head → reflective/question-led) and are fully overridable. Fields:
`coaching_frequency`, `coaching_style`, `misalignment_threshold`,
`friction_tolerance`, `prefer_questions_over_directives`,
`time_of_day_preference`.
- `GET /discovery/coaching/preferences` returns the preferences, deriving
defaults from the latest profile on first access (`404` if no profile yet).
- `PUT /discovery/coaching/preferences` overrides any field (validated against
the allowed value sets) and marks them user-customized.
- `POST /discovery/coaching/preferences/regenerate` re-derives the defaults
from the latest profile, discarding overrides.
A **check-in** quotes the person's own words and asks whether their stated
direction still feels valid — mirror, not compass: it asks, it never judges or
prescribes. The person's answer is recorded in `still_valid`.
- `POST /discovery/coaching/checkins` generates a check-in now (on demand).
- `GET /discovery/coaching/checkins` lists them, newest first.
- `PUT /discovery/coaching/checkins/{id}/respond` records the self-assessment
(`still_valid` + optional note).
- `POST /discovery/coaching/run` is the **weekly batch job** (admin-only,
intended for a cron): it generates a check-in for every eligible user whose
cadence is due. Eligible = coaching cadence not `off` and an affirmed
(locked) profile; due = no prior check-in or the cadence interval has elapsed.
## API Reference
All `/discovery/*`, `/api/me*`, `/api/activity*`, and `/api/admin/*` routes
@@ -421,6 +462,13 @@ clients.) `/api/auth/login`, `/api/auth/callback`, `/health`, `/`, and
| `GET` | `/discovery/profile/me/reflection` | yes | The reflection dialogue for the latest profile |
| `GET` | `/discovery/profile/me/revisions` | yes | Profile edit/iteration history (newest first) |
| `GET` | `/discovery/conversation/{conversation_id}` | yes | Fetch stored conversation responses (owner only) |
| `GET` | `/discovery/coaching/preferences` | yes | Coaching preferences (auto-derived on first access) |
| `PUT` | `/discovery/coaching/preferences` | yes | Override coaching preferences |
| `POST` | `/discovery/coaching/preferences/regenerate` | yes | Re-derive preference defaults from the profile |
| `GET` | `/discovery/coaching/checkins` | yes | List the user's check-ins (newest first) |
| `POST` | `/discovery/coaching/checkins` | yes | Generate a check-in now |
| `PUT` | `/discovery/coaching/checkins/{id}/respond` | yes | Record "is your direction still valid?" |
| `POST` | `/discovery/coaching/run` | admin | Weekly batch: generate due check-ins for eligible users |
## Data Model
@@ -541,6 +589,39 @@ iterations are captured rather than overwritten (migration `004`).
| `note` | text nullable | What changed (e.g. the coach's revision note) |
| `created_at` | datetime | UTC |
### `coaching_preferences`
How the person wants to be coached; one row per user (migration `005`).
| Column | Type | Notes |
| --- | --- | --- |
| `id` | string | UUID primary key |
| `user_id` | string | FK to `users.id`, unique, indexed |
| `profile_id` | string nullable | The profile the defaults were derived from |
| `coaching_frequency` | string | `weekly`, `biweekly`, `monthly`, or `off` |
| `coaching_style` | string | `direct`, `warm`, or `reflective` |
| `misalignment_threshold` | string | `low`, `medium`, or `high` |
| `friction_tolerance` | string | `low`, `medium`, or `high` |
| `prefer_questions_over_directives` | boolean | Lead with questions vs statements |
| `time_of_day_preference` | string | `morning`, `afternoon`, or `evening` |
| `auto_generated` | boolean | True until the user edits a field |
| `created_at` / `updated_at` | datetime | UTC |
### `coaching_checkin`
A periodic coaching check-in and the person's response (migration `005`).
| Column | Type | Notes |
| --- | --- | --- |
| `id` | string | UUID primary key |
| `user_id` | string | FK to `users.id`, indexed |
| `profile_id` | string | FK to `discovery_profile.id` |
| `body` | text | The check-in text (quotes the person's own words) |
| `created_at` | datetime | UTC, indexed |
| `still_valid` | boolean nullable | The person's answer: is their direction still valid? |
| `response_note` | text nullable | Optional note with their response |
| `acknowledged_at` | datetime nullable | When they responded |
## Extraction Details
`DiscoveryExtractor` is intentionally responsible for plumbing, not business
@@ -626,6 +707,17 @@ callback), so the pages hold no tokens of their own.
true it updates the summary and notes what changed
- "This is me — affirm" locks the profile via the same confirm endpoint
`coaching.html` (Phase 3 coaching):
- loads preferences via `GET /discovery/coaching/preferences` (auto-derived on
first visit) and renders the six fields as selects + a toggle; Save
(`PUT`) marks them customized, "Reset to suggested" re-derives from the
profile
- lists check-ins and can generate one on demand (`POST .../checkins`); each
unanswered check-in offers "still feels true" / "it's shifted" which posts to
`.../respond`
- linked from `profile.html` ("Coaching preferences & check-ins")
## Configuration
Populate `.env` with at minimum the Anthropic key and the auth-related