Skip to content

PRD: Richer, More Personable Coach Responses

Product: Objectuve (codename: Enkidu) Feature: Depth-aware reply length, deeper personalization, persisted conversation memory, and rich response rendering for the consumer Coach Status: Approved / Ready for planning Date: June 2026 Author: Josh Lockhart


1. Context — why this change

Relationship to prior work

This PRD builds on the Coach UDG PRD (docs/product/coach-udg-prd.md, status: Proposed), which established server-side context assembly — goals, habits, and recent activity — as the foundation for richer coaching. The UDG PRD's architecture (upfront injection via BuildCoachContext, token-budgeted context block) is the platform this work extends.

This PRD deliberately revisits three decisions the UDG PRD deferred or left in place:

  1. The 2-sentence cap — UDG explicitly preserves it ("No change to the 2-sentence response cap"). We remove it, replacing it with a depth-gated length budget tied to the user's coach_depth slider (see Workstream A).
  2. Mood logs — UDG excludes them pending a privacy decision. We make that decision here: include, scoped to the user's own coach (see §6 and Workstream B).
  3. Gamification — UDG deems badge/rank details "not relevant to coaching advice unless the user asks." We include them as a light, occasional acknowledgement — not a focus area — when the coach naturally references the user's progress (see §6 and Workstream B).

Once this PRD is greenlit, the UDG PRD's "Excluded (Phase 1)" list should be updated: mood logs and gamification move from Excluded to Included under this PRD's scope.


Coach is Objectuve's signature surface, but live replies feel bare compared to the copy users are shown during onboarding. The onboarding wizard (MeetCoach.vue:598-625) and coachPersonaDetails.ts:12-54 set a high bar — persona-true, references specific streak numbers, action-forward, textured:

Captain: "Twelve days of practice — that's a real signal, not a coincidence. Two days off changes the count, not the capacity. Pick today's session and we move."

Live Coach can't reach that bar today for three structural reasons:

  1. A hard 2 sentences max cap appears in both coaching.rb:12 and base.rb:94, flattening every reply and silently overriding the user's own Brief→Detailed depth slider (collected in onboarding, stored as coach_depth 1–5, never used in prompts).
  2. No conversation memory. Each getAdvice call sends only the current message as a single user-role turn (coach_service.rb:80-92). The coach can't follow a thread or remember the user across days. Frontend chat history is local-only and cleared on context change.
  3. Underused user data. BuildCoachContext already loads GoalEvents but only counts them (build_coach_context.rb:33,64) — the journal text is never read. Mood (MoodLog), gamification (User.xp/level/longest_streak, ranks, badges), and goal category are never passed. The frontend renders replies as plain <p> text only (CoachChatPanel.vue:41).

The machinery is actually sophisticated (per-persona "soul" .md files, tone/warmth/focus instruction maps, enneagram framing) — the gap is what data we select, how long we let replies be, whether the coach remembers, and how richly the UI renders the reply.

Intended outcome

Live Coach replies match the onboarding gold standard: persona-true, grounded in the user's real streaks/mood/journal/progress, length-appropriate to their depth setting, conversational across turns, and rendered with light emphasis plus tappable next-step actions.

Decisions locked in this session

  • Length: Honor the depth slider — terse default, but the Brief→Detailed setting unlocks longer multi-part coaching. Remove the hard 2-sentence cap.
  • Personalization: Weave in all four underused sources — mood logs, gamification, journal-entry text, goal categories.
  • Memory: Persist across sessions — a lightweight message store so the coach recalls past chats and prior days.

2. Goals / Non-goals

Goals

  • Replies match the onboarding persona-copy quality bar, grounded in real user data.
  • Reply length scales to the user's coach_depth setting (no more flat 2-sentence cap).
  • Coach follows multi-turn threads and remembers across sessions.
  • Replies render with light markdown emphasis + 0–2 tappable action chips (log progress, update mood, view goal, etc.).

Non-goals

  • Not changing the persona set, enneagram assessment, or the tone/warmth/focus controls.
  • Not building a general-purpose chatbot — Coach stays goal/habit-scoped and brief-by-default (anti-social brand: ~10 min/day).
  • Not LLM-emitted JSON for actions (brittle/costly) — action chips are derived deterministically in Rails from existing context signals.
  • Not the agent_runner / AI Workforce path — consumer Coach is the Rails Ai::CoachService path.

3. Current-state reference (files this PRD touches)

ConcernFile
Response generationrails_api/app/services/ai/coach_service.rb (#get_advice, ~80–92)
Prompt template + hard caprails_api/app/services/ai/prompts/coaching.rb (cap at line 12)
Persona preamble + hard cap + instruction mapsrails_api/app/services/ai/prompts/base.rb (cap at line 94; BREVITY/WARMTH/FOCUS maps; build_personality_preamble)
Persona "soul" exemplarsrails_api/app/services/ai/prompts/souls/{captain,spark,mirror,rival,sage,analyst}.md
Context assemblyrails_api/app/interactions/ai/build_coach_context.rb
GraphQL entryrails_api/app/graphql/mutations/ai/get_advice.rb (returns advice: String)
Coach prefs (depth/tone)rails_api/app/models/user_detail.rb; rails_api/app/models/user.rb (~218–230)
Audit/cost lograils_api/app/models/coach_interaction.rb (tracks prompt/completion tokens)
FE chat + plain renderionic_frontend/src/components/coach/CoachChatPanel.vue
FE mutationionic_frontend/src/constants/graphql/ai.js (GET_ADVICE_MUTATION)
FE stateionic_frontend/src/composables/useAiCoach.ts
Gold-standard copy (design reference)ionic_frontend/src/views/MeetCoach.vue:598-625; src/constants/coachPersonaDetails.ts:12-54

4. Workstreams

Workstream A — Depth-aware length + persona-true quality

Prompt/context only. No schema, no FE structural change. Highest impact / lowest risk.

  • Remove the hard cap in coaching.rb:12 and base.rb:94.
  • Add a DEPTH_INSTRUCTIONS map in base.rb (parallel to BREVITY_INSTRUCTIONS), keyed by coach_depth 1–5, mapping to a length budget:
    • 1 Brief → 1–2 sentences
    • 2 → 2–3 sentences
    • 3 (default) → a short paragraph (3–4 sentences)
    • 4 → two short paragraphs
    • 5 Detailed → multi-part, may include a short ordered list of next steps
  • Clarify depth vs. brevity: depth governs how much content/length; tone_brevity governs terseness of style. Wire coach_depth through build_personality_preamble (it's already stored, just unused). Document the interplay in the prompt.
  • Add few-shot exemplars to the soul files so live output matches the onboarding bar. Each souls/*.md gains 1–2 example outputs mirroring its streakMessage / wizard-preview copy. The onboarding TS copy stays the design reference; the Ruby soul files are the single source for the LLM.

Workstream B — Deeper personalization from user data (all four sources)

Enrich BuildCoachContext. Reuse existing models; respect a token budget with prioritized ordering.

  • Moodbuild_mood_summary: recent MoodLog trend (last 7–14 days) + most recent mood; lets the coach connect emotional state to progress (the reflection focus already hints at this).
  • Gamificationbuild_progress_summary: rank name, current_streak, longest_streak, recently earned badges, plus XP signal. Reuse, don't rebuild (capability-first): a server-side rank service already exists — rails_api/app/services/achievements/compute_user_rank_service.rb (Achievements::ComputeUserRankService) derives current_rank_name from user.xp (the canonical XP column; it deliberately ignores the drift-prone user.level integer) and is already exposed over GraphQL via Types::AchievementStatsType — reachable at me { achievementStats { currentRankName } } (a sub-field of UserType.achievement_stats, not a standalone query). Workstream B should call that service directly from BuildCoachContext rather than replicating any ladder. There is no dedicated Badge or Rank ActiveRecord model and no stored rank field. Pre-existing risk to surface: the frontend ionic_frontend/src/components/SideMenu.vue:366-384 computes rank names from a different source — a badge-count proxy (unlockedCount * 100) — and its ladder diverges from the server's (e.g. rank 3 is "Foundation" in the UI vs "Path Maker" in ComputeUserRankService). Once the coach cites rank names, a user could see two different ranks; planning must decide whether to align the UI ladder to the server before Workstream B ships. Badges are tracked as UserAction enum actions (~49 distinct actions such as complete_first_goal) in rails_api/app/models/user_action.rb; "recently earned badges" means recent relevant UserAction records. Challenge badges (badge_name/badge_icon on CommunityChallenge) are a separate domain and out of scope here.
  • Journal entries → read recent GoalEvent.content text (last N), include verbatim snippets in the focused-goal detail and a global "recent reflections" block. The events are already eager-loaded (includes(:events)) — select content, not just created_at.
  • Goal categories → include each goal's category name in the goals summary so advice respects life-area context (Health/Career/etc.).
  • Budgeting: cap total context tokens; order by relevance (focused goal > recent activity > mood > gamification). CoachInteraction already records token counts for monitoring.

Workstream C — Persisted conversation memory

Biggest scope: new models + migration + retrieval + FE history load. Do last.

  • New models (PublicRecord, acts_as_paranoid):
    • CoachConversation — one rolling thread per user (optionally scoped per goal_id), columns: user_id, goal_id (nullable), last_message_at.
    • CoachMessageconversation_id, role (user/assistant), content, created_at.
    • Migration + regenerate schema.rb in the same CI cycle (per CLAUDE.md gotcha).
  • Retrieval: in CoachService#get_advice, load the last K turns (e.g. 10, token-budgeted) and pass them as a real multi-message array to ServiceClient.chat instead of a single user-role message. Persist both the user message and the assistant reply per turn.
  • Frontend: on Coach open, load prior history via a new query (e.g. coachConversation(goalId, page)) so reopening shows past messages instead of a blank slate. useAiCoach.ts history becomes hydrated from the server.
  • Retention & privacy: define a retention policy (e.g. 90 days), honor acts_as_paranoid deletion, and include coach messages in user data export/delete. Mood + journal text are sensitive — document that this is the user's own data fed to their own coach, already transiting LiteLLM. (See §6.)
  • Coach opt-out interaction: the Coach opt-out PRD (docs/product/coach-opt-out-prd.md) gates GetAdvice behind coach_modal_enabled? and explicitly left chat history out of scope for its v1. Since this workstream is what introduces persisted history, planning must define behavior when a user has Coach disabled: do not persist new CoachMessage records while disabled, and retain (but never read or surface) any prior history rather than hard-deleting it. Re-enabling restores the thread.

Important — data availability at launch: CoachConversation and CoachMessage are net-new tables introduced by this workstream. Conversation memory begins collecting at the moment this feature ships — there is no historical chat history to backfill, so the "coach remembers past chats" capability applies only to conversations that occur after this ships. Users will experience a one-time fresh start when Workstream C launches.

Workstream D — Rich response rendering

Extend the GraphQL surface + FE render. Prose stays short; richness comes from emphasis + chips.

  • Extend getAdvice response from advice: String to:
    • advice: String — prose, may include light markdown (bold/italic only).
    • suggestedActions: [CoachAction!] — each { label, kind, targetId } where kindlog_event | log_mood | view_goal | create_milestone | open_meet_coach.
  • Derive suggestedActions deterministically in Rails from the same BuildCoachContext signals (e.g. goal stalled → "Log progress"; no mood today → "Update mood"). No extra LLM call, reliable, cheap. Avoids brittle LLM JSON.
  • Frontend (CoachChatPanel.vue):
    • Render markdown safely. dompurify ^3.4.0 is already installed in ionic_frontend/package.json and already used for sanitization across the app: ChangelogModal.vue pairs it with a bold/italic regex (/\*\*(.+?)\*\*/g) — the exact pattern to reuse here — while BadgeImage.vue uses it for SVG sanitization. marked and markdown-it are not installed. Reuse the existing DOMPurify + bold/italic pattern — no new dependency needed.
    • Render suggestedActions as tappable chips reusing the existing suggestion-chip pattern (CoachChatPanel.vue:16-25), deep-linking via the router / useAiCoach.
    • Add Storybook stories for the rich message states.

  1. Phase 1 — A + B (prompt + context). Immediate richness and personability, no schema, no FE structural change. Ship first.
  2. Phase 2 — D (markdown + deterministic action chips). FE + additive GraphQL fields.
  3. Phase 3 — C (persisted memory). New models/migration, retrieval, FE history hydration.

6. Privacy, cost & guardrails

  • Token/cost: more context + history + longer replies all raise cost — across three new vectors (richer context, K-turn history, depth-gated longer prose). The Coach UDG PRD pinned a ~400-token context baseline; this work will exceed it, so planning must set a concrete envelope (suggested starting target: context ≤ ~700 tokens, history ≤ 10 turns / ~600 tokens, depth-5 replies capped to a hard output ceiling). Monitor against UDG's baseline via CoachInteraction token columns; keep the existing 10 req/min rate limit.
  • Mood & journal text inclusion — deliberate decision: The Coach UDG PRD excluded mood logs pending a privacy decision. That decision is made here: include them. Mood logs and journal entry text are the user's own data, already collected with their agency, and already transiting LiteLLM (no new third-party exposure). They are scoped solely to that user's own coach — never shared with other users, never used for training. Those properties make inclusion appropriate despite UDG's earlier deferral. Add retention policy, ensure deletion/export coverage, and keep all sensitive data (mood, journal text, persisted chat history) scoped to the user's own coach.
  • Gamification — deliberate decision: The Coach UDG PRD deemed badge/rank details "not relevant to coaching advice unless the user asks." We include gamification as a light, occasional acknowledgement: the coach may naturally reference a level-up, rank name, or longest streak when relevant to the user's progress narrative. Gamification is not a focus area or a prompt topic — this scoping directly addresses UDG's "not relevant unless asked" concern by limiting it to opportunistic acknowledgement, not a dedicated coaching thread.
  • Brand guardrail: longer replies cut against the anti-social/~10-min ethos — mitigated by depth defaulting to brief; detail is opt-in via the slider.

7. Success metrics

  • Qualitative: live replies match the onboarding gold standard (persona-true, cite specific user data) — spot-check across all 6 personas × depth levels.
  • Quantitative (PostHog — coach_chat_opened / coach_advice_sent already exist):
    • Messages per session (multi-turn depth) ↑
    • Return-to-Coach rate ↑
    • New coach_action_chip_tapped event rate
    • Depth-slider adoption
  • Guardrails: reply latency, tokens/reply (CoachInteraction), rate-limit headroom.

8. Verification / testing (for the implementation phases)

  • Backend RSpec:
    • spec/services/ai/coach_service_spec.rb — depth→length budget, history passed as multi-message array, no hard cap.
    • spec/interactions/ai/build_coach_context_spec.rb — mood / gamification / journal-text / category inclusion + budgeting.
    • New specs: CoachConversation/CoachMessage models, persistence + retrieval; get_advice request spec for new suggestedActions field.
  • Frontend Vitest + Storybook: CoachChatPanel markdown render, action-chip render + tap deep-link, server history hydration.
  • Manual / preview: drive Coach across personas and depth settings; confirm replies cite real streaks/mood/journal and that chips route correctly.
  • Migration discipline: run bin/rails db:migrate and commit schema.rb in the same cycle (per CLAUDE.md gotcha).

9. Open questions / risks to resolve in planning

  • Depth vs. brevity interplay — finalize the exact interplay so the two controls don't contradict (depth governs content length; brevity governs stylistic terseness — document this clearly in the prompt).
  • Memory threading — one rolling thread per user vs. per-goal threads (PRD assumes one rolling thread, optionally goal-scoped).
  • Retention window — confirm the exact policy with stakeholder before Phase 3.
  • UserAction badge query/caching — confirm the query/caching approach for surfacing recent UserAction badge records within the context token budget (sources confirmed; approach TBD in planning).
  • Rank-name ladder divergence — the frontend SideMenu.vue badge-count ladder and the server Achievements::ComputeUserRankService XP ladder disagree (e.g. "Foundation" vs "Path Maker" at rank 3). Decide whether to align the UI to the server before the coach starts citing rank names. (The coach should reuse the server currentRankName via me { achievementStats { currentRankName } }, not rebuild a ladder.)
  • Update the Coach UDG PRD's exclusion list after greenlight: mood logs and gamification move from the UDG "Excluded (Phase 1)" list to Included under this PRD's scope.

10. Next step

Route to planning via the GSD workflow (/gsd-new-milestone, then /gsd-plan-phase). Implement in the §5 phase order (A+B → D → C), each phase gated by the §8 tests. Phase 1 (Workstreams A+B) is the recommended first slice — highest impact, no schema or FE structural change.


Last updated: 2026-06-15

Loading…