Skip to content

Coaching System

Vita's coaching system generates contextual, identity-grounded coaching messages for active goals. It maps each goal to relevant identity data (observations, quotes), determines the coaching context (progress, stagnation, setback, etc.), and delivers nudges via Telegram. The system uses bio-zack's own words and philosophy rather than generic motivation.

A second, separate module under scripts/coaching/ handles communication-pattern coaching β€” it parses processed meeting transcripts, computes behavioral metrics, and queues a qualitative debrief. That module is documented in its own section near the end.

Status (dormant): The proactive goal-coaching loop is currently not running. The coaching-nudge sentinel that drove it is disabled ("enabled": false in data/sentinel/sentinels.json); its state shows it auto-disabled after 5 consecutive errors, last successful trigger 2026-04-22, trigger_count 130. The goal-coaching data files (data/coaching/cooldowns.json, effectiveness.jsonl, goal-identity-map.json) have been frozen since ~March 2026. Treat the goal-coaching subsystem below as reference architecture β€” the engine is intact and re-enableable, but it is not delivering nudges today. The /coach slash command still works on demand, and the communication-coaching module (a different pipeline) is still active.

Why This Design?

  1. Identity-grounded, not generic - Coaching messages draw from actual observations and quotes captured during identity interviews. When the system references a stated belief, it's quoting a real statement, not inventing motivation.

  2. Cooldown-protected - A 12-hour cooldown per goal prevents nagging. If a goal was coached at 10am, it won't be coached again until at least 10pm.

  3. Batched delivery - Instead of spamming one message per stagnating goal, the system consolidates multiple goals into a single Telegram message composed in natural voice.

  4. LLM with template fallback - The primary path uses Claude to generate Socratic, direct coaching messages. If the API fails, template-based messages fill in seamlessly.

Architecture

+--------------------+     +--------------------+     +--------------------+
| Identity Data      |     | Goal Files         |     | Occurrence Data    |
| observations.jsonl |     | goals/active/*.json|     | occurrences-*.jsonl|
| quotes.jsonl       |     |                    |     |                    |
+--------+-----------+     +--------+-----------+     +--------+-----------+
         |                          |                          |
         v                          v                          v
+--------+-----------+     +--------+-----------+     +--------+-----------+
| identity_loader.py |     | determine_context()|     | _load_occurrence_  |
| load observations  |     | classify goal state|     | stats()            |
| load quotes        |     |                    |     | last_occurrence    |
+--------+-----------+     +--------+-----------+     | occurrences_7d     |
         |                          |                 +--------+-----------+
         v                          |                          |
+--------+-----------+              |                          |
| mapper.py          |              |                          |
| keyword extraction |<-------------+--------------------------+
| relevance scoring  |
| cached mapping     |
+--------+-----------+
         |
         v
+--------+-----------+
| generator.py       |
| LLM generation     |
| template fallback  |
+--------+-----------+
         |
         v
+--------+-----------+     +--------------------+
| proactive.py       |---->| Telegram Queue     |
| check_stagnating   |     | (proactive system) |
| queue_coaching_batch|    | coaching.composer  |
+--------+-----------+     +--------+-----------+
         |                          |
         v                          v
+--------+-----------+     +--------+-----------+
| effectiveness.py   |     | Bio-zack receives  |
| log events         |     | composed message   |
| manage cooldowns   |     | via Telegram       |
+--------+-----------+     +--------------------+

Context Types

The system classifies each goal into one of five coaching contexts based on progress data:

Context Trigger Message Style
progress Recent occurrences within expected frequency Reinforcement, acknowledge momentum
stagnation No progress for 2x expected interval Socratic challenge using own words
setback Declining trend in occurrences Reframe difficulty as growth
celebration Goal completed or target hit Acknowledge achievement
new Goal created within last 7 days, no occurrences Ask what success looks like

Context Determination Logic

def determine_context(goal, progress_data):
    # 1. Completed? -> celebration
    # 2. No occurrences + new goal (<7 days) -> new
    # 3. No occurrences + old goal -> stagnation
    # 4. Days since last > 2x expected gap -> stagnation
    # 5. Trend == declining -> setback
    # 6. Default -> progress

Expected gap is derived from goal frequency (in generator.py): - Weekly goals: 7 // times_per_week (e.g., 2x/week = 3-day expected gap) - Interval goals: interval_days from frequency config - Default: 7 days

Message Templates

Templates are organized by whether they incorporate a quote from identity data. All templates below match generator.py verbatim.

With Quote (50% chance when quotes available)

The template path rolls rng.random() < 0.5 to decide whether to use a quote-bearing template (when relevant quotes exist).

Stagnation: - You said: "{quote}" - {days} days without progress on {goal_title}. What would regret-minimizing Zack do?

Progress: - Consistent with {goal_title}. The "{quote}" trajectory. - {occurrences} this week on {goal_title}. You mentioned: "{quote}"

Without Quote

Stagnation: - The hard middle - where you said the most joy lives. Still in it with {goal_title}? - {days} days. You've said failure is a choice. Is {goal_title} still chosen?

Progress: - Progress on {goal_title}. Eyes wide open - that's the mode.

Setback: - Setback on {goal_title}. You said difficulty means growth. Where's the growth here? - {goal_title} slipped. Failure is a choice, not an event - worth reclaiming?

Celebration: - Hit your {goal_title} target. The direction you wanted. - {goal_title} complete. What you said about progress over destination - this is that.

New: - New goal: {goal_title}. What does success look like?

LLM Generation

When available, the system uses Claude (default claude-sonnet-4-6) to generate messages that are more contextual than templates. See Model Guidance for why sonnet-4-6 is the balanced default for proactive composers even though the frontier models are claude-opus-4-8 and claude-fable-5. The LLM prompt includes:

  • Goal title, purpose, and hypothesis
  • Current context (stagnation, progress, etc.)
  • Up to 3 relevant identity observations
  • Up to 3 relevant quotes

LLM rules (quoted verbatim from the prompt in generate_coaching_message_llm()): - Use their own words/philosophy, don't invent new motivation - Be direct, not flowery - One Socratic question is good - No exclamation points or "you can do it" energy - Sound like them talking to themselves

The smart generator (generate_coaching_message_smart) tries LLM first, falls back to templates on failure. LLM can be disabled with COACHING_DISABLE_LLM=1 (accepts 1/true/yes).

Goal-Identity Mapping

The mapper (scripts/coaching/mapper.py) connects each goal to relevant identity data through keyword matching and caching.

How Mapping Works

  1. Extract keywords from goal metadata (title, domain, tags, purpose)
  2. Score observations by counting keyword matches in observation text (top 5 kept)
  3. Score quotes with weighted matching: tag match (2x), theme match (1x), quote text match (0.5x) (top 5 kept)
  4. Fallback quotes from universal themes (regret, progress, discipline, growth) when no goal-specific matches found
  5. Cache result in data/coaching/goal-identity-map.json with a 24-hour TTL (CACHE_TTL_HOURS = 24)

Identity Coverage Checking

The mapper also detects gaps in identity data relevant to a goal. DOMAIN_GOAL_MAP maps a goal-keyword to the identity domains that goal draws on:

DOMAIN_GOAL_MAP = {
    "training": ["values_beliefs", "self_knowledge"],
    "skateboard": ["creativity_passions", "self_knowledge"],
    "body": ["self_knowledge", "values_beliefs"],
    "date": ["relationships"],
    "<family-member>": ["relationships", "legacy_mortality"],  # real key is a name (PII-scrubbed here)
    "family": ["relationships", "childhood_family"],
    "work": ["work_purpose"],
    "claude": ["work_purpose", "creativity_passions"],
}

Note: One real key in the source code is a family member's first name and is redacted above as <family-member>. If you regenerate this page from source, scrub that key β€” do not copy the raw value.

If a goal's relevant identity domains have coverage below the threshold (LOW_THRESHOLD = 0.4), suggest_interview_domain() recommends which domain to explore next in an identity interview.

Identity Data Sources

The identity loader (scripts/coaching/identity_loader.py) reads from: - data/identity/observations.jsonl - Personality, value, and preference observations - data/identity/quotes.jsonl - Memorable quotes with tags and themes

Only observations of type value, personality, or preference are used for coaching (filtered by get_coaching_observations()). See Identity for how this data is captured.

Effectiveness Tracking

Every coaching message is logged to data/coaching/effectiveness.jsonl. Example (sanitized placeholders):

{
  "event_id": "goal-id-20260110180000",
  "ts": "2026-01-10T18:00:00.029319+00:00",
  "goal_id": "<goal-uuid>",
  "message": "Batched nudge with 2 goals",
  "context": "stagnation",
  "source": "proactive_batch",
  "template_used": null,
  "quote_used": null
}

Fields

Field Purpose
event_id {goal_id}-{timestamp} for dedup
ts UTC timestamp
goal_id Which goal was coached
message Truncated message text (max 200 chars, via _safe_truncate)
context One of: progress, stagnation, setback, celebration, new
source One of: manual, checkin, proactive, proactive_batch, test
template_used Which template was selected (if template path)
quote_used Which quote was included (truncated to 50 chars)

Stats

from scripts.coaching.effectiveness import get_effectiveness_stats

stats = get_effectiveness_stats(days=30)
# {
#   "total": 42,
#   "by_context": {"stagnation": 38, "progress": 4},
#   "by_source": {"proactive_batch": 40, "manual": 2}
# }

Cooldown Mechanism

Each goal has an independent 12-hour cooldown to prevent over-messaging.

How it works: 1. When a coaching message is sent, log_coaching_event() writes the current timestamp to data/coaching/cooldowns.json 2. Before coaching a goal, is_on_cooldown(goal_id) checks if fewer than 12 hours have passed 3. Goals on cooldown are skipped during proactive coaching runs

Cooldowns file (data/coaching/cooldowns.json), sanitized:

{
  "<goal-uuid-1>": "2026-02-24T04:20:19.204171+00:00",
  "<goal-uuid-2>": "2026-02-24T16:02:18.724346+00:00"
}

Manual reset: clear_cooldown(goal_id) removes a goal's cooldown entry.

The cooldown constant is COOLDOWN_HOURS = 12, defined in scripts/coaching/effectiveness.py.

Proactive Coaching Integration

Reminder: This loop is currently dormant (see the status banner at the top). The mechanism below is accurate but not firing.

Scheduling

Proactive coaching was migrated from a direct scheduler loop to a sentinel detector/responder pair, driven by the coaching-nudge sentinel (now disabled):

  • Detector: scripts/sentinel/checks/stagnating_goals.py - Runs check_stagnating_goals(stagnation_days=7) and outputs sorted goal IDs for hash comparison
  • Responder: scripts/sentinel/checks/run_coaching.py - Calls run_proactive_coaching() (from scripts/coaching/proactive.py) to queue nudges

The legacy scheduler loop (_proactive_coaching_loop, designed to run at COACHING_HOUR, default 10am PT) still exists as a method in api/src/scheduler.py, but it is not registered in the scheduler's task list (self._tasks), so it never starts. (Note: that dormant loop's manual trigger run_proactive_coaching_now() routes through a separate followup implementation in api/src/services/scheduler_followups.py, not the scripts/coaching/ path.)

The supported invocation paths today are: - The sentinel responder scripts/sentinel/checks/run_coaching.py (when the coaching-nudge sentinel is enabled) - Running python scripts/coaching/proactive.py directly (CLI, for manual/testing use) - from scripts.coaching.proactive import run_proactive_coaching in Python

See Sentinel for the detector/responder pattern and the auto-disable-at-5-errors behavior that took this sentinel offline.

Proactive Run Flow

run_proactive_coaching()
    -> check_stagnating_goals(stagnation_days=7)
        -> load all goals from goals/active/
        -> load occurrence stats from occurrences-*.jsonl
        -> for each goal: skip if on cooldown
        -> determine_context() for each
        -> collect goals where context == "stagnation" and days >= 7
    -> sort by staleness (most stagnant first)
    -> take top MAX_NUDGES_PER_RUN (default 2, env COACHING_MAX_NUDGES)
    -> queue_coaching_batch()
        -> get identity mappings for each goal
        -> enqueue single message with builder_key="coaching_batch"
        -> log coaching event for each goal (updates cooldown)

Return shape:

{"checked": 9, "stagnating": 2, "nudged": 2, "nudged_goals": ["Goal A", "Goal B"]}

Telegram Delivery

The queued message flows through the proactive outreach pipeline (see Proactive):

  1. Builder (build_coaching_batch in scripts/proactive/builders.py): Registered as an ATO source in the reflection category. Returns a composable dict with goal data (or None if nothing to send).

  2. Composer (scripts/proactive/composer.py): Unlike most message types (which use the lightweight simple agent), coaching_batch is composed by a dedicated tool-enabled agent named coaching.composer. Because stagnation detection keys on specific sub-components (e.g. a sub-component going stale even when the parent goal has activity), the composer is instructed to verify the stagnation claims against primary source before composing β€” it reads data/scheduling/occurrences-*.jsonl (and goals/active/*.json if needed) to confirm days_stagnant matches reality, then weaves the goals into a single conversational nudge ending in one reflective question. This avoids sending a "you've stalled" nudge that the occurrence data contradicts.

  3. Telegram: Delivered as a single message to bio-zack's Telegram chat.

ATO Integration

The coaching batch builder is registered with Adaptive Telegram Outreach via @register_message_source in scripts/proactive/builders.py:

Property Value
Source name coaching_batch
Category reflection
Priority 0.7
Max per week 7
Min hours between 20

This means coaching nudges respect quiet hours, category limits, and backoff if bio-zack doesn't respond.

Dead-Trigger Suppression

Beyond ATO category limits, coaching_batch is also listed in ENGAGEMENT_SOURCES in scripts/proactive/trigger_suppression.py β€” the set of response-expecting sources eligible for dead-trigger suppression. If engagement (the response rate) drops to zero, the source is suppressed even when ATO would otherwise allow it. coaching_batch was at one point promoted to the never-suppress ALWAYS_SEND set, but was moved back to normal suppression (historical note: CNR=7) during an engagement-collapse tuning pass. See Proactive for the suppression and CNR (consecutive-no-response) mechanics.

Python API

Quick Reference

# Generate a coaching message for a goal
from scripts.coaching import (
    generate_coaching_message_smart,
    determine_context,
    get_or_create_mapping,
    is_on_cooldown,
    log_coaching_event,
    get_effectiveness_stats,
    clear_cooldown,
    find_relevant_observations,
    find_relevant_quotes,
    invalidate_cache,
    check_identity_coverage,
    suggest_interview_domain,
)
# proactive.py is NOT re-exported from the package (circular-import guard):
from scripts.coaching.proactive import run_proactive_coaching

# Check context
context, ctx_vars = determine_context(goal, progress_data)

# Get identity mapping (cached 24h)
mapping = get_or_create_mapping(goal_id, goal)

# Generate message (LLM first, template fallback)
message = generate_coaching_message_smart(
    goal, context, ctx_vars,
    mapping["observations"], mapping["quotes"]
)

# Run proactive coaching manually
result = run_proactive_coaching()
# {"checked": 9, "stagnating": 2, "nudged": 2, "nudged_goals": ["Goal A", "Goal B"]}

CLI / Slash Command

/coach [goal-name]   # coach one goal by name (resolves via scripts.scheduling.goals.resolve_goal)
/coach               # coach all goals with drift, skipping any on cooldown

The /coach skill loads goal progress from memory/views/goal-progress.json, checks the 12h cooldown, calls the coaching engine, and logs the event. This on-demand path works regardless of whether the proactive loop is running.

Environment Variables

Variable Default Purpose Defined in
COACHING_LLM_MODEL claude-sonnet-4-6 Model for LLM-generated messages scripts/coaching/generator.py
COACHING_DISABLE_LLM (unset) Set to 1/true/yes to force template-only scripts/coaching/generator.py
COACHING_MAX_NUDGES 2 Max goals per proactive run (MAX_NUDGES_PER_RUN) scripts/coaching/proactive.py
COACHING_HOUR 10 Hour (PT) for the dormant legacy scheduler loop only api/src/scheduler.py

Note: COACHING_HOUR still exists in code but only configures _proactive_coaching_loop, which is not started β€” so it currently has no effect.

Communication-Pattern Coaching (separate module)

A distinct pipeline under scripts/coaching/ coaches how bio-zack communicates in meetings, independent of the goal-coaching engine above. Unlike goal coaching, this module is still active (data/coaching/communication_metrics.jsonl updates with each processed meeting).

It runs automatically after a meeting transcript is ingested and processed:

  1. Analyze (scripts/coaching/communication.py) β€” parses the transcript and computes behavioral metrics for bio-zack's turns versus baselines from prior good meetings:
  2. Talk ratio (share of total words)
  3. Monologue count β€” segments longer than 150 words (~2 minutes of speech), unless they're interactive (β‰₯2 questions per 150 words exempts them)
  4. Framework-lecture count β€” monologues containing structural language (framework / structure / approach / process / pipeline)
  5. Question rate β€” questions per 1000 words, excluding rhetorical
  6. Max listening run β€” longest stretch where bio-zack doesn't speak
  7. Log (log_meeting_metrics) β€” appends the metrics to data/coaching/communication_metrics.jsonl for trend tracking. Baselines live in data/coaching/communication_baselines.json.
  8. Debrief (scripts/coaching/post_meeting_debrief.py, queue_communication_debrief) β€” compares the meeting against baselines, collects the flagged segments, and queues a Claude deferred prompt (enqueue_prompt) that reads the transcript and writes a qualitative, identity-grounded communication debrief, deduped per meeting. The debrief is delivered to the Meetings Telegram topic (or, as a fallback, via enqueue_message with trigger='communication_debrief').

Scope: The metrics and baselines are derived from real meetings. This page documents the mechanism only β€” meeting content is never surfaced here. The communication_debrief trigger is itself an ENGAGEMENT_SOURCE subject to dead-trigger suppression.

Data Structures and File Locations

File Purpose
scripts/coaching/__init__.py Module exports (goal coaching)
scripts/coaching/generator.py Template and LLM message generation
scripts/coaching/mapper.py Goal-to-identity keyword mapping with cache
scripts/coaching/effectiveness.py Event logging, cooldowns, stats
scripts/coaching/proactive.py Proactive coaching run logic, batching
scripts/coaching/identity_loader.py Load and filter observations/quotes
scripts/coaching/communication.py Meeting communication-metric analysis
scripts/coaching/post_meeting_debrief.py Post-meeting debrief, deferred-prompt queueing
scripts/sentinel/checks/stagnating_goals.py Sentinel detector for stagnating goals
scripts/sentinel/checks/run_coaching.py Sentinel responder that runs coaching
scripts/proactive/builders.py build_coaching_batch ATO builder
scripts/proactive/composer.py coaching.composer voice composition agent
scripts/proactive/trigger_suppression.py Dead-trigger suppression eligibility
data/coaching/cooldowns.json Per-goal cooldown timestamps (frozen)
data/coaching/effectiveness.jsonl Coaching event log (frozen)
data/coaching/goal-identity-map.json Cached goal-to-identity mappings (frozen)
data/coaching/communication_baselines.json Communication-metric baselines
data/coaching/communication_metrics.jsonl Communication-metric trend log (active)
data/identity/observations.jsonl Source identity observations
data/identity/quotes.jsonl Source identity quotes
data/sentinel/sentinels.json coaching-nudge sentinel config (enabled: false)
api/src/scheduler.py Dormant _proactive_coaching_loop + COACHING_HOUR

Known Limitations

  1. Currently dormant - The proactive goal-coaching loop is offline because its sentinel auto-disabled after repeated errors. Re-enabling requires fixing the responder error and flipping enabled back to true in data/sentinel/sentinels.json. The /coach command and communication-coaching module remain functional.

  2. No response-based effectiveness - The system logs that messages were sent but doesn't track whether bio-zack responded or took action. Effectiveness stats are volume-only, not outcome-based.

  3. Stagnation dominates - In practice, most coaching messages were stagnation nudges for the same set of goals. Progress and celebration contexts rarely trigger because goals either have occurrences (no coaching needed) or don't (stagnation).

  4. Mapping is keyword-only - The goal-to-identity mapping uses simple keyword matching. A goal won't match a semantically related observation unless overlapping words appear.

  5. Universal themes are static - The fallback quote themes (regret, progress, discipline, growth) are hardcoded. As identity data grows, these may become less representative.

  6. Cooldown is per-goal, not per-message - If two different coaching pathways (manual + proactive) try to coach the same goal, the first one blocks the second via cooldown. There's no global "don't send more than N coaching messages today" limit beyond ATO category limits and dead-trigger suppression.

Where to go next

  • Sentinel β€” the detector/responder framework and auto-disable behavior that gates proactive coaching
  • Proactive β€” the outreach queue, ATO categories, voice composition, and dead-trigger suppression
  • Goals β€” goal files, frequency config, and occurrence tracking that feed determine_context()
  • Identity β€” how observations and quotes are captured
  • Model Guidance β€” model routing for composers and why sonnet-4-6 is the proactive default