Skip to content

Coach Features — Feature & Testing Guide

Overview

Objectuve integrates Coach features throughout the app via three capabilities: Insight Cards (contextual cards on every major page), Goal Coach (interactive chat for personalized advice), and Milestone Generation (auto-suggest sub-goals). The backend uses an LLM service (CoachService) that routes all calls through Ai::ServiceClient, which proxies to LiteLLM.

This page covers what Coach does at runtime. For how users configure Coach (persona, tone, depth, focus, frequency), see Meet Coach. For a comprehensive architectural reference mapping all Coach surfaces (frontend, backend services, prompt templates) and fallback rungs, see Coach Surfaces Map.

Related docs:

Four AI surfaces now touch goal creation — worth disambiguating up front, since they were built in four separate phases and are easy to conflate:

SurfaceMethodWhat it doesDocs
Coach-Drafted Goalsgenerate_goal_draftIdea sentence → a full drafted goal (title, category, kind, target date, milestones)This page, below
Goal-discovery quizsuggest_goalsHelps a user who isn't sure what to set pick a goalAI Coaching Expansion
Refine Descriptionrefine_descriptionRewrites the goal's description prose (≤240 chars)This page, GraphQL API Reference
Goal Quality Checkrefine_goalCritiques a goal the user has already formed for specificity, realism, and right-sizing, before they save itThis page, below

Insight Cards

What They Are

Insight cards are contextual tips, actions, insights, or celebrations that appear on key pages. Each card has a type, title, message, and optional CTA button.

Insight Types

TypeIconWhen Used
tipSparkles (purple)General advice or best practices
actionZap (yellow)Something the user should do now
insightLightbulbPattern or trend observation
celebrationTrophyRecognizing an achievement or milestone

Where Insights Appear

PageBackend-SupportedContext ProvidedExample Insight
Dashboard✅ YesActive/completed counts, streak, urgent goal, weekly check-ins"Time to Check In" if urgent goal inactive >3 days
Goals List✅ YesAverage progress, needs-attention count, categories"Goals Need Attention" if any are behind
Goal Detail✅ YesGoal name, progress %, days left, milestones, events, status"Almost There!" if progress ≥75%
Achievements✅ YesUnlocked/total badge count, level, nearest badge"Badges Within Reach" if partially progressed
Communities✅ YesJoined count, posts this week"Stronger Together" if no communities joined
Admin❌ NoUser/goal counts, growth ratesClient template only (not part of daily batch)

Note on backend support: The 5 backend-supported pages (dashboard, goals, goal-detail, achievements, communities) receive insights from the daily batch job. The admin page uses the client-side generateMockInsight template only and is not wired to the backend batch.

Two more insights ride this same composable but sit outside the daily batch pack: a mood-goal correlation insight (Dashboard) and a pace-suggestion card with one-tap accept (Goals), both flag-gated and owner-scoped. See AI Insight Intelligence for details.

Context Building

The useAiInsights(page) composable builds page-specific context for each insight request:

Dashboard context:

  • activeGoalCount, completedGoalCount, streak, urgentGoalName, urgentGoalDaysSince, weeklyCheckIns

Goals context:

  • averageProgress, needsAttentionCount, goalCategories[]

Goal Detail context:

  • goalName, goalProgress, goalDaysLeft, hasMilestones, eventCount, goalStatus

Achievements context:

  • unlockedBadgeCount, totalBadgeCount, userLevel, nearestBadgeKey

How Insights Are Sourced

Insights follow a three-rung fallback ladder:

  1. Rung 1: Backend daily batch — Backend job (AiCoaching::GenerateDailyInsightPackJob) runs once per day and populates Rails.cache with LLM-generated insights for each user per page. Frontend retrieves via the me.currentInsights GraphQL query. Cached for 24 hours per (user, page) key.

  2. Rung 2: Persona-aware library — If the backend pack is unavailable (AI disabled, rate-limited, or job backlog), fallback copy from Ai::Fallbacks::Insights (6 personas × 4 surface types) is returned instead. Persona-aware, brand-voice clean, never returns nil.

  3. Rung 3: Client-side template — If the backend never returns a pack (e.g., network error), frontend generateMockInsight() rule template runs locally based on page context. Provides fallback copy without LLM context. Kept through v1.15 soak window, then deleted in v1.15.

Eligibility: Backend daily batch only generates insights for users with active (non-completed) goals. Inactive users receive no batch pack, falling through to Rung 2 (persona-aware library) or Rung 3 (client template).

Caching & Session Behavior

  • Apollo cache: Frontend me.currentInsights result cached by Apollo for the session.
  • In-session re-fetch: Frontend cacheTimestamps Map tracks when each page's insight was last fetched. If ≥15 minutes have elapsed since the last fetch, the query re-runs on next navigation to that page.
  • Dismiss state: Dismissed insights stored in localStorage under 'ai-insights-dismissed' key with stable IDs and timestamps. Dismissed insights stay hidden for 24 hours. After expiry, the insight can re-appear on the page.

Card UI

  • Indigo-themed card with left gradient accent bar
  • Icon in gradient circle (indigo-to-purple)
  • Title (bold) + message text
  • Optional CTA button with ChevronRight icon
  • Dismiss button (X) when dismissible prop is true
  • Compact variant available via compact prop
  • Loading skeleton with pulsing animation

Goal Coach

What It Is

An interactive chat modal where users can ask for personalized goal advice. The Coach responds with motivational, strategic, or tactical guidance.

Opening the Coach

  • Available from the Goal Detail page
  • Click the Coach button to open the modal

Chat Flow

  1. Modal opens with a welcome message contextually generated from the current goal
  2. Suggested prompts appear as chips (e.g., "How can I stay motivated?", "What milestones should I set?")
  3. User types a question or taps a prompt
  4. AI generates a response, structured into short paragraphs (with a bullet list only when it's genuinely enumerating steps)
  5. Chat history persists within the modal session

Chat UI

  • Mobile: 80% height bottom sheet
  • Desktop: 600x500px centered modal
  • Welcome message with Coach avatar (gradient sparkles icon)
  • Suggested prompt chips (indigo borders)
  • User messages: blue background, right-aligned, plain text
  • Coach messages: indigo background, left-aligned, rendered as sanitized structured HTML — see Structured reply rendering below
  • Loading: three animated bouncing dots
  • Input field with send button (disabled when empty or loading)

Structured reply rendering

Coach replies render as short paragraphs separated by whitespace, with a real bullet list when the reply enumerates steps — not as one dense block of text. Implemented in CoachChatPanel.vue's formatAssistantContent():

  1. The reply text is split on blank lines into paragraphs and - /* line groups into list items
  2. Paragraphs become <p> tags, list groups become <ul><li> tags; **bold** becomes <strong> and *italic*/_italic_ becomes <em>
  3. The result is sanitized via DOMPurify with an allow-list of exactly ['p', 'br', 'ul', 'ol', 'li', 'strong', 'em'] and ALLOWED_ATTR: [] — no links, attributes, or scripts can reach the DOM
  4. The assistant bubble's v-html host is a <div> (not a <p>, which can't contain block children like <ul>); block/list spacing (12px between blocks, 4px between list items, coach/70-tinted disc markers) is applied via Tailwind classes on that wrapper only, never on the generated inner HTML
  5. A single-paragraph reply renders with no extra margin — visually identical to the old unstructured output

The user's own messages are unaffected — they still render as plain text via a <p> in the sibling branch.

Backend

The CoachService.get_advice(user_context) method:

  1. Builds a prompt with the user's context (goal details, progress, history)
  2. Calls the configured LLM provider
  3. Returns a text response instructed to be structured into short paragraphs, with a bullet list reserved for genuine step enumerations — via a core rule in Ai::Prompts::Base.build_personality_preamble (Ai::Prompts::Coaching::VERSION = 6)
  4. Routes the request through Ai::ServiceClient to LiteLLM

Milestone Generation

What It Does

Automatically suggests 3–5 milestones based on a goal's name and description. Available during goal creation when the goal name has 3+ characters.

How to Use

  1. On the GoalCreate page, enter a goal name (minimum 3 characters)
  2. Tap Auto-Generate button (appears next to the milestones section)
  3. Button shows "Thinking..." spinner during generation
  4. 3–5 milestone suggestions appear in the milestone list
  5. User can keep, edit, or remove any suggestion

Backend

The CoachService.generate_milestones(goal_name, goal_context) method:

  1. Builds a prompt asking for 3–5 specific, actionable milestones
  2. Calls the configured LLM provider
  3. Parses the response as a JSON array of strings
  4. Returns the milestone text suggestions

Coach-Drafted Goals

What It Does

Turns a single freeform sentence into a complete, form-ready goal: title, category, kind, suggested target date, a short "why", and 3–5 first milestones. Available on the GoalCreate screen when the page opens normally (not via deeplink or template pre-fill).

How to Use

  1. Open the New Goal screen — the Coach hero appears by default
  2. Type what you want to achieve (e.g. "I want to run a 5K by spring")
  3. Tap Build with Coach or press Enter
  4. The hero shows a loading skeleton while the draft generates
  5. Form fields hydrate automatically: title, category, kind, target date, and milestones are pre-filled
  6. Edit any field before saving

Backend

The CoachService.generate_goal_draft(idea, categories:, kinds:, user_id:) method:

  1. Builds a prompt from the user's idea, passing the full list of available GoalCategories and GoalTypes as allowed values
  2. Calls the coaching/milestones LLM model via Ai::ServiceClient
  3. Parses the JSON response and resolves category/kind names to reference-table integer ids
  4. Returns a hash: { title, category_id, category_name, kind_id, kind_name, target_date, why, milestones }
  5. Results cached for 1 hour — same idea returns the same draft until the cache expires

Fallback

If the service is unavailable (AI disabled, rate-limited, or nil response), the mutation returns { draft: null }. GoalCreate stays in form phase and the user proceeds with manual entry — no error toast. A blank idea raises INVALID_INPUT before the AI call.


Goal Quality Check

What It Does

A pre-commit, opt-in critique of a goal the user has already formed — not a draft generator like Coach-Drafted Goals above. Reads the goal-as-typed (title, description, target date, category, kind, milestones) and returns up to 3 one-tap-acceptable suggestions to make it more specific, realistically dated, or right-sized (e.g. add a milestone, tighten a vague title, set a target date). Non-blocking: Save is always available whether or not the user checks. Ships dark behind the goal_refinement_enabled flag (0% rollout).

How to Use

  1. On the GoalCreate screen, fill in a goal (title required)
  2. Tap Check my goal — a card appears: "Give it a gut check"
  3. Coach returns 0–3 suggestions, each with a one-sentence issue, a coaching suggestion, and an Accept chip showing the concrete replacement value, plus a Dismiss (×)
  4. Tap Accept on a suggestion to apply its suggested_value to the matching field (title, description, target date, or a new milestone) — the field flashes a brief coach-ring pulse and focus moves to the next remaining suggestion's Accept button (or to Create goal if none remain)
  5. Tap Dismiss to discard a suggestion without applying it
  6. If the goal is already solid, the card shows "Looks solid — nothing to tighten."
  7. If Coach is unavailable (flag off, AI disabled, or rate-limited), the card shows "Coach is resting — save whenever you're ready." — Save is never blocked

Backend

The CoachService.refine_goal(name, description:, target_date:, category_name:, kind_name:, milestones:, user_id:) method:

  1. Builds a prompt (Ai::Prompts::GoalQualityCheck, VERSION = 1) asking the model to gut-check the goal-as-formed for a vague title, missing/unrealistic target date, or no milestones — instructed not to invent issues with an already-solid goal
  2. Calls the coaching/milestones LLM model via Ai::ServiceClient, on the goal_quality_check rate-limit category (5/min per user) and gated by Ai::SettingsGuard.enabled?('coaching_enabled', ...) (same backend kill-switch as Coach-Drafted Goals and Refine Description)
  3. Parses the JSON response into an array of up to 3 { field, issue, suggestion, suggested_value } hashes (field is one of title, description, target_date, milestone), or an empty array if the goal is already solid
  4. Results cached for 1 hour by prompt hash — resubmitting the same goal-as-formed returns the same critique until the cache expires
  5. Returns nil on any failure (AI disabled, rate-limited, parse error) — fail-open; the refineGoal mutation catches this and returns { suggestions: nil } rather than raising, so the frontend hides the surface instead of erroring

Operates entirely on unsaved form data — no persisted model, no migration, no new table.

Frontend

useGoalRefinement.ts composable (models on useGoalDraft.ts, never throws) drives a GoalQualityCheck.vue card mounted in GoalCreate.vue's form phase. State machine: idleloadingsuggestions (≤3, client-clamped) | clear (all-solid) | unavailable (fail-open or rate-limited). Gated by the goal_refinement_enabled PostHog flag (ionic_frontend/src/lib/featureFlags.ts, fail-closed — surface never renders when the flag is off); a dev override key (objectuve_goal_refinement_enabled_override) exists for local/preview testing.

Rate limiting & flags

Distinct from the frontend PostHog flag: the refineGoal mutation also checks the backend coaching_enabled Settings kill-switch and its own goal_quality_check rate-limit category (5 requests/min/user, Ai::RateLimiter). Hitting the rate limit surfaces as the throttle copy above, not an error toast.


LLM Backend Service

CoachService (rails_api/app/services/ai/coach_service.rb)

Routes all LLM calls through Ai::ServiceClient (rails_api/app/services/ai/service_client.rb), which proxies to a LiteLLM gateway. Two environment variables are required:

VariablePurpose
LITELLM_URLLiteLLM gateway URL (default: http://localhost:4000)
LITELLM_MASTER_KEYAuthentication key for the LiteLLM gateway

Provider selection (OpenAI, Anthropic, Ollama, etc.) is configured inside LiteLLM, not via Rails environment variables. No direct provider API keys are read by Rails.

Methods

MethodInputOutput
generate_milestones(goal_name, goal_context)Goal name + optional descriptionArray of 3–5 milestone strings
generate_goal_draft(idea, categories:, kinds:)Freeform idea sentence + allowed category/kind lists{ title, categoryId, categoryName, kindId, kindName, targetDate, why, milestones } or null
refine_goal(name, description:, target_date:, category_name:, kind_name:, milestones:)Goal-as-formed (unsaved)Array of ≤3 { field, issue, suggestion, suggested_value }, [] if solid, or null
get_advice(user_context)User context string (question + goal data)Structured motivational text (short paragraphs, optional bullet list)
generate_insight(page_context)Page name + context object{ type, title, message, cta_label }

UI Components

AiInsightCard

  • Props: type, title, message, ctaLabel, dismissible, loading, compact
  • Emits: dismiss, cta-click
  • Indigo gradient accent, responsive icon sizing

AiCoachModal

  • Props: none (uses useAiCoach() composable internally)
  • Responsive sizing (bottom sheet on mobile, centered on desktop)
  • Smooth enter/leave animations
  • Auto-scrolls to latest message

GoalQualityCheck

  • Emits: check, accept, dismiss, dismiss-all, all-resolved
  • States: idle/prompt · loading · suggestions (≤3) · accepted (field flash + focus move) · all-clear · unavailable/coach-resting
  • 44×44 tap targets, coach focus rings, reduced-motion → opacity-only; light + dark

Testing the Feature

Manual Testing Checklist

1. Dashboard Insight

  • [ ] Navigate to Dashboard with active goals
  • [ ] Verify an Insight card appears
  • [ ] Verify the insight is contextually relevant (e.g., mentions streak or urgent goal)
  • [ ] Tap dismiss (X) → verify card disappears
  • [ ] Reload page within 24 hours → verify dismissed insight stays hidden
  • [ ] Wait 24 hours (or clear localStorage) → verify insight returns

2. Goals Page Insight

  • [ ] Navigate to Goals page
  • [ ] Verify insight appears with goals-specific context
  • [ ] If goals need attention: verify "action" type insight
  • [ ] Tap CTA button → verify navigation or action triggers

3. Goal Detail Insight

  • [ ] Navigate to a goal detail page
  • [ ] Verify insight reflects goal status (at-risk → "Get Back on Track", almost done → "Almost There!")
  • [ ] Navigate to a different goal → verify insight updates for new context

4. Achievements Insight

  • [ ] Navigate to Achievements page
  • [ ] Verify insight mentions badge progress
  • [ ] Tap CTA → verify it applies the relevant filter

5. Goal Coach

  • [ ] Navigate to a goal detail page
  • [ ] Open Coach modal
  • [ ] Verify welcome message appears with goal context
  • [ ] Verify suggested prompts display as chips
  • [ ] Tap a suggested prompt → verify Coach response appears
  • [ ] Type a custom question → verify Coach responds
  • [ ] Ask something that prompts a multi-part answer → verify the reply renders as separated paragraphs and/or a bullet list, not one dense block
  • [ ] Verify chat history persists within the session
  • [ ] Close and reopen → verify fresh session starts

6. Coach-Drafted Goals

  • [ ] Open the New Goal screen (no deeplink, no template pre-fill)
  • [ ] Verify the Coach hero appears (not the form)
  • [ ] Type "I want to run a 5K" → tap Build with Coach
  • [ ] Verify loading skeleton shows during generation
  • [ ] Verify form fields hydrate: title, category, kind, target date, and milestones pre-filled
  • [ ] Verify milestones are relevant to the idea
  • [ ] Edit one field → verify edit persists after submission
  • [ ] Clear the idea field and tap Build → verify the button is disabled or an error appears
  • [ ] Deeplink to create (e.g. with goalKindId param) → verify hero is bypassed and form shows directly
  • [ ] Template select → verify hero is bypassed

7. Goal Quality Check

  • [ ] On GoalCreate, enter a vague goal title (no description, no target date, no milestones)
  • [ ] Tap Check my goal → verify a loading skeleton, then ≤3 suggestions appear
  • [ ] Tap Accept on a suggestion → verify the matching field updates and flashes, and focus moves to the next Accept button (or Create goal, if none remain)
  • [ ] Tap Dismiss on a suggestion → verify it's removed without changing the field
  • [ ] Enter a specific, dated, milestone-having goal → tap Check → verify "Looks solid — nothing to tighten."
  • [ ] With goal_refinement_enabled off (or via dev override), verify the card never renders and Save is unaffected
  • [ ] Verify Save is always available, before and after tapping Check

8. AI Milestone Generation

  • [ ] Start creating a new goal
  • [ ] Enter name "Learn to play guitar" (3+ chars)
  • [ ] Tap Auto-Generate
  • [ ] Verify loading spinner appears
  • [ ] Verify 3–5 milestones are suggested
  • [ ] Verify milestones are relevant to "Learn to play guitar"
  • [ ] Edit one milestone → verify edit persists
  • [ ] Remove one milestone → verify it's deleted
  • [ ] Try with < 3 character name → verify button is disabled/hidden

9. Insight Caching

  • [ ] Load Dashboard → note the insight content
  • [ ] Navigate away and return within 15 minutes
  • [ ] Verify same insight displays (cached)
  • [ ] Wait 15+ minutes or clear cache → verify new insight generates

10. Loading States

  • [ ] On slow connection, verify insight card shows skeleton animation
  • [ ] Verify Coach modal shows bouncing dots while waiting for response
  • [ ] Verify Generate Milestones shows "Thinking..." text

Backend Unit Tests

bash
cd rails_api

# Service specs
bundle exec rspec spec/services/ai/coach_service_spec.rb

# GraphQL specs
bundle exec rspec spec/requests/mutations/ai/generate_milestones_spec.rb
bundle exec rspec spec/requests/mutations/ai/generate_goal_draft_spec.rb
bundle exec rspec spec/requests/mutations/ai/refine_goal_spec.rb
bundle exec rspec spec/requests/mutations/ai/get_insight_spec.rb
bundle exec rspec spec/requests/mutations/ai/get_advice_spec.rb

Frontend Unit Tests

bash
cd ionic_frontend

npm run test:unit -- --run -t "AiInsightCard"
npm run test:unit -- --run -t "AiCoachModal"
npm run test:unit -- --run -t "useAiInsights"
npm run test:unit -- --run -t "useAiCoach"
npm run test:unit -- --run -t "CoachGoalHero"
npm run test:unit -- --run -t "useGoalDraft"
npm run test:unit -- --run -t "GoalQualityCheck"
npm run test:unit -- --run -t "useGoalRefinement"

GraphQL API Reference

Queries

me.currentInsights

Fetches the daily insight pack for the logged-in user:

graphql
query GetCurrentInsights {
  me {
    currentInsights {
      page          # "dashboard" | "goals" | "goal-detail" | "achievements" | "communities"
      id            # Stable insight ID (e.g., "dashboard-tip-consistency")
      type          # "tip" | "action" | "insight" | "celebration"
      title
      message
      ctaLabel      # Optional CTA button text
    }
  }
}

Behavior:

  • Returns [] (empty array) when Settings.ai.batch_enabled = false (intentional asymmetry — see below)
  • Returns one insight per page in the pack (max 5, one per supported page)
  • Uses fallback logic if backend pack unavailable: Rung 2 (persona-aware) or Rung 3 (client template)
  • Does NOT raise GraphQL::ExecutionError — returns [] silently

Intentional asymmetry with mutations: The getInsight and generateMilestones mutations raise AI_DISABLED error when the feature is disabled. The currentInsights query returns [] instead. Why? The query is called on every page mount across the app. A hard error would surface as a toast on every navigation, creating a poor UX. Returning [] allows the frontend to render the fallback rung silently.

Mutations

graphql
mutation GetInsight($pageContext: JSON!) {
  getInsight(pageContext: $pageContext) {
    insightId
    insightType     # "tip" | "action" | "insight" | "celebration"
    title
    message
    ctaLabel        # Optional CTA button text
  }
}

mutation GetAdvice($goalId: String, $page: String!, $userMessage: String!) {
  getAdvice(goalId: $goalId, page: $page, userMessage: $userMessage) {
    advice          # structured motivational text (short paragraphs, optional bullet list)
  }
}

mutation GenerateMilestones($goalName: String!, $goalContext: String) {
  generateMilestones(goalName: $goalName, goalContext: $goalContext) {
    milestones      # Array of milestone text strings
  }
}

mutation GenerateGoalDraft($idea: String!) {
  generateGoalDraft(idea: $idea) {
    draft {
      title
      why
      categoryId    # Integer reference-table id (GoalCategory)
      categoryName
      kindId        # Integer reference-table id (GoalType)
      kindName
      targetDate    # ISO8601 date or null for habits/ongoing goals
      milestones    # Array of milestone text strings
    }
  }
}

mutation RefineGoal(
  $name: String!
  $description: String
  $targetDate: String
  $categoryName: String
  $kindName: String
  $milestones: [String!]
) {
  refineGoal(
    name: $name
    description: $description
    targetDate: $targetDate
    categoryName: $categoryName
    kindName: $kindName
    milestones: $milestones
  ) {
    suggestions {
      field           # "title" | "description" | "target_date" | "milestone"
      issue
      suggestion
      suggestedValue
    }
  }
}

refineGoal behavior: raises INVALID_INPUT if name is blank (before the AI call); raises AI_DISABLED if the backend coaching_enabled Settings flag is off; raises AI_RATE_LIMITED at 5 requests/min/user; returns { suggestions: null } (not an error) on any other AI failure, so the frontend fails open and hides the surface rather than showing a toast.


Known Behaviors & Edge Cases

  1. LiteLLM gateway: All LLM calls route through Ai::ServiceClient, which reads LITELLM_URL and LITELLM_MASTER_KEY. Provider selection and model routing are configured inside LiteLLM, not in Rails. If the gateway is unreachable, the service raises an error that surfaces as a toast notification.

  2. Insight caching prevents flicker: Insights are cached for 15 minutes to prevent regeneration on every page navigation. Dismiss state is separate (24-hour localStorage persistence).

  3. Coach context assembly: Context (goal details, page, user message, personality profile, motivation profile) is assembled server-side by Ai::BuildCoachContext. The system prompt also includes ## Personality profile (enneagram framing, when available) and ## Goal motivation profile (motivation framing, when available to personalize tone and recommendations). This means the AI has context even for short questions like "help".

  4. Milestone generation minimum: The goal name must be at least 3 characters. With shorter names, the Auto-Generate button is hidden/disabled.

  5. Rate limiting: There is no client-side rate limiting on AI requests. Excessive use would hit the LLM provider's rate limits, returning errors that display as toast notifications.

  6. Insight types are hints: The LLM is asked to return an insight type (tip/action/insight/celebration), but it may not always match the expected pattern. The frontend handles any type gracefully.

  7. Coach history is session-scoped: Chat history is stored in the composable's reactive state. Closing the modal and reopening starts a fresh session — there is no persistence across sessions.

  8. Admin insights: The admin page uses the same insight infrastructure with admin-specific context (user counts, growth rates). Only users with admin: true see the admin page and its insights.

  9. Goal Quality Check is dark by default: gated by the goal_refinement_enabled PostHog flag at 0% rollout. It also depends on the backend coaching_enabled Settings flag, same as Coach-Drafted Goals and Refine Description — either flag being off hides/disables the surface, fail-open, with no error toast.

  10. Goal Quality Check field scope: the prompt and GoalRefinementSuggestionType only ever return field: title | description | target_date | milestone — category and recurrence are not covered by this slice.

Last updated: 2026-07-27 (OBJ-1815: Coach chat replies now render as structured short paragraphs + an optional bullet list instead of one dense block, PR #1826)

Loading…