Skip to content

v3.12 — Identity-Anchored Goal Journey

Three optional short-text fields — an identity prompt, a past-attempt note, and a completion reflection — thread the user's own words through the goal lifecycle and back into Coach's personalization pipeline.

Summary

Before this milestone, Coach personalized its messaging from structural signals only: streaks, progress percentages, categories, recent journal entries. It had no way to know why a goal mattered to the user, whether they'd tried and failed before, or what they learned when they finally finished. This milestone bundles three previously-separate proposals — OBJ-1065 (identity prompt at goal creation) and roadmap items #23 (Reflection on Completion) and #27 (Past Goal Attempt Context) — into one three-phase delivery, because all three share the same shape: an optional short-text field, threaded into the same Coach prompt surfaces, under the same fidelity contract.

All three phases shipped: identity_prompt (Phase 113), past_attempt_context (Phase 114), and completion_reflection (Phase 115). Each is a single nullable Goal column, captured through an existing form (GoalForm.vue for the first two, a new GoalCompletionSheet.vue for the third), and folded into Coach's existing prompt templates — no new prompt module was created. Every field is opt-in, free, never required, and never gates a feature; instrumentation records only whether each field is present, never its text.

The milestone also surfaced (and closed) the same code-review finding three times in a row: an updateGoal field that supports explicit-clear needs both a resolver allowlist and an interaction-level guard, or a client can never clear a saved value. See Tech Debt and the Nullable Field: Clear vs. Omit Gotcha.

Goal

Ship three optional Goal fields (identity_prompt, past_attempt_context, completion_reflection) end-to-end — migration → model → interaction → GraphQL → frontend → Coach prompt threading → tests — reusing the existing Ai::CoachService personalization pipeline with no new prompt module, no required-field promotion, and no engagement nudges on empty fields.

(Quoted from the milestone tracking issue description — the original .planning/milestones/v3.12-identity-anchored-goal-journey-ROADMAP.md was never committed to the repo; see Known gaps.)

Scope — What Shipped

  • identity_prompt (Phase 113, PR #1243) — free-text identity framing at goal creation, max 240 chars. GoalForm.vue: "Who you're becoming" field, always visible.
  • past_attempt_context (Phase 114, PR #1260) — free-text prior-attempt context at goal creation, max 200 chars. GoalForm.vue: "Tried this before?" disclosure, collapsed by default, auto-expands if a saved value exists.
  • completion_reflection (Phase 115, PR #1289) — free-text reflection captured at goal completion, max 200 chars. New GoalCompletionSheet.vue: "What did you take away?", with Save/Skip actions; always sends the field explicitly (trimmed text or null), never omits it.
  • Three additive, reversible migrations, no backfill: 20260703120000_add_identity_prompt_to_goals.rb, 20260705000000_add_past_attempt_context_to_goals.rb, 20260706000000_add_completion_reflection_to_goals.rb. Pre-v3.12 goals read nil for all three.
  • Coach prompt threading — all three fields fold into existing prompt templates (Ai::Prompts::Coaching, Ai::Prompts::Synopsis, Ai::Prompts::CheckIns, Ai::Prompts::WelcomeBack); each template's VERSION bumped. No new prompt module.
    • identity_prompt / past_attempt_context: CoachService#get_advice(), #generate_synopsis(), #generate_check_in_prompt().
    • completion_reflection: CoachService#generate_welcome_back()'s streak-lapse pathway only, sourced from the user's most recently completed goal (not the lapsed goal itself) via WelcomeBackOfferResolver#last_completed_goal_reflection.
  • Prompt-injection fence — the single-turn get_advice() path and Ai::Prompts::Synopsis wrap user-authored text in an explicit <user_input>...</user_input> fence; the multi-turn chat path is intentionally left unfenced per-turn (already role-separated + MULTI_TURN_INJECTION_GUARD).
  • Presence-only instrumentation — every LLM call records identity_prompt_present / past_attempt_context_present / completion_reflection_present (booleans) on call metadata; raw text is never logged.
  • CLEARABLE_NULLABLE_FIELDS allowlist — all three fields support explicit-null-clears vs. omit-preserves on updateGoal, closing the same gap three separate times across the three phases (see Tech Debt).
  • Full-stack docsdocs/features/goals.md § Coach Personalization, docs/features/coach-surfaces.md (added the previously-undocumented generate_synopsis()/Ai::Prompts::Synopsis surface), docs/architecture/data-models.md, Nullable Field: Clear vs. Omit Gotcha.

Phases

PhaseNameStatusPRHighlights
113Identity Prompt at Goal CreationShipped#1243identity_prompt end-to-end; established the <user_input> fence pattern on the single-turn get_advice()/Synopsis path
114Past Goal Attempt Context at Goal CreationShipped#1260past_attempt_context, reusing the Phase 113 delivery spine
115Reflection on CompletionShipped#1289completion_reflection at goal completion; threaded into generate_welcome_back's streak-lapse pathway; scope collapsed from "completion/archive" to completion-only (no archive state exists on Goal)

Key Decisions

  • Bundle three proposals into one milestoneidentity_prompt, past_attempt_context, and completion_reflection share the same shape (optional short text, same Coach threading, same fidelity contract), so they shipped as three phases of one milestone rather than three independent efforts.
  • No new prompt module — all three fields reuse Ai::Prompts::Coaching / CheckIns / Synopsis / WelcomeBack, bumping each VERSION rather than introducing a new template.
  • Single completion_reflection column, not a separate model — consistent with identity_prompt/past_attempt_context; a dedicated GoalReflection model was considered and rejected as unnecessary for a single optional field.
  • Completion scope collapsed to completion-only — the original Phase 115 breakdown asked whether an "archive" touch point should also capture a reflection. Goal has no archive state (only completed:boolean + completed_at), so this collapsed to completion-only, resolved against the actual code before Phase 115 was dispatched rather than left as an open question for the planner.
  • Reflection capture extends GoalTracking::UpdateGoal; no new CompleteGoal interaction — completion already flows through the existing interaction plus the model's goal_completed? hook.
  • completion_reflection is explicit-null-on-skip, not omittedGoalCompletionSheet.vue always sends the completionReflection key on completion (trimmed text or null), so re-completing a goal without new text explicitly clears a stale reflection from a prior completion, rather than silently leaving it. This supersedes the original UI-SPEC wording ("the mutation carries no completionReflection arg on skip").
  • Prompt-fence scoping: single-turn only — the <user_input> fence around identity-bearing context applies to the single-turn get_advice()/Synopsis path only. The multi-turn chat path is left unfenced per-turn by design (already role-separated via chat roles plus MULTI_TURN_INJECTION_GUARD); re-fencing every turn would be redundant, not safer.
  • GoalMotivationSnapshotCard.vue gating fix rides along — Phase 113 also fixed a Roy MAJOR finding where the edit field wasn't gated on an active goalId, causing a false-success save with no goal to attach to. Not scoped to identity/past-attempt/reflection directly, but shipped in the same phase.

Requirements Coverage

No formal REQUIREMENTS.md exists for this milestone (see Known gaps). Informally, 3 / 3 fields shipped end-to-end (migration → model → interaction → GraphQL → frontend → Coach threading → tests), matching the milestone's own acceptance bar.

FieldStatus
identity_prompt✅ Shipped (Phase 113)
past_attempt_context✅ Shipped (Phase 114)
completion_reflection✅ Shipped (Phase 115)

Outcomes

  • Users can optionally tell Coach who they're becoming and what tripped them up last time, right when they create a goal — and Coach's advice, daily synopsis, and check-in prompts can reference it.
  • Users can optionally reflect on what they took away from a finished goal — and if they lapse on a different goal later, Coach's welcome-back message can acknowledge that reflection.
  • None of the three fields are required, gate any feature, or trigger an engagement nudge when left blank — consistent with the anti-social-app philosophy (opt-in, no dark patterns).
  • The <user_input> fencing pattern established here is the reusable template for any future free-text field threaded into a single-turn Coach prompt.

Tech Debt

  • (Phases 113–115) The same "GraphQL nullable field can't distinguish omit from explicit-clear" gap was found and fixed three separate times — once per field — because each phase's RSpec coverage only exercised the "omitted" path, not "explicit clear." Phase 115's fix folded all three fields into one CLEARABLE_NULLABLE_FIELDS allowlist and one given?-guarded loop; see Nullable Field: Clear vs. Omit Gotcha for the pattern to reuse (allowlist + guard + mutation-level spec) on any future clearable field, rather than repeating this a fourth time.
  • (Phase 113) create-goal-with-identity.cy.ts Cypress spec exists but was never wired into the CI cypress-e2e --spec filter. Carried forward as an open follow-up (Tess).
  • (Phase 114) A post-ship staging smoke failure was traced to an unrelated broken Playwright regression guard (from OBJ-1140, PR #1264) rather than Phase 114 code; fixed in test-only PR #1266. Surfaced a systemic deploy-concurrency bug in .github/workflows/staging.yml (two deploys racing on completion order, not just overlap) — filed as OBJ-1144 and fixed separately (see Staging deploy race in CLAUDE.md's gotchas).
  • (Phase 115) A pre-existing mobile-safari/WebKit-only Playwright smoke failure (unrelated to this milestone, recurring across ≥5 prior master pushes) was observed at merge time; filed as OBJ-1167 (backlog), not a rollback trigger.

Known gaps

  • The kickoff ROADMAP.md was lost, then reconstructed at close — the original was authored in an ephemeral workspace and never committed to any branch (flagged from Phase 113 onward), so the phase-completion record that would normally live there was captured across three milestone-update comments on the tracking issue (OBJ-1080) instead — those comments are the primary source for this page's Phases/Key Decisions/Tech Debt sections. Both the reconstructed ROADMAP.md and the formal MILESTONE-AUDIT.md now live under .planning/milestones/, having landed with this narrative page in the v3.12 milestone-close PR. No REQUIREMENTS.md exists — consistent with other bundled milestones, which don't carry one.
  • No dedicated v3.12 git tag yet — all three phases are merged to master and live on staging; production release is gated to the weekly Wednesday release train and hadn't cut a tag as of this page's last update.
  • e26a46605 — [Codi] feat(goals): add optional identity prompt at goal creation (Phase 113) (#1243)
  • 756782d63 — [Codi] feat(goals): add optional past-attempt context at goal creation (Phase 114) (#1260)
  • 7110d5a6a — [Codi] feat(goals): add optional completion reflection on goal completion (Phase 115) (#1289)
  • 993621717 — fix(smoke): journey.spec.ts hasError guard fails on every passing run (OBJ-1082/OBJ-1140) (#1266)

Last updated: 2026-07-07

Loading…