# GoalBuddy v2 state.yaml — BaoLife polish/hardening + continued improvement
version: 2

goal:
  title: "BaoLife — Polish/Hardening + Continued Improvement"
  slug: "baolife-polish-plus"
  kind: existing_plan
  tranche: "Fix the 2 named follow-ups (diamond clamp, offline-digest death line), then discover + execute the next highest-leverage player-felt improvements in verified waves until a Judge audit says the tranche delivered enough real value."
  status: done
  completion_note: "GOAL COMPLETE — T999 PM audit, full_outcome_complete: true. 2 follow-ups fixed+proven (FU-A diamonds never negative; FU-B offline death now mirrors canonical handleDeath + loop stops + reconnect routes). 4 player-felt improvement waves: W1 performActivity skill progression+varied outcomes, W2 stats now MATTER (intelligence->promotion + stat-gated payoff events), W3 childhood (0-9) content pack, W4 reward delivery verified on the online path. tsc clean; vitest 1627->1678. UNCOMMITTED on this goal. Deferred (not churn): wealth/late-game economy (#3) pending a real raise-curve check; reward cadence reads 0 in headless sim only (offline path; online verified)."
  oracle:
    signal: "2 follow-ups fixed+proven (diamonds NEVER negative, asserted by test; offline-death/digest root-caused + consistent — no phantom 'you died' while alive, real offline death applied+routed); each further wave verified green (tsc + vitest >=1627); final audit confirms real player-felt value (not churn), full_outcome_complete: true."
    cadence: "after each Worker package; phase/final audits at boundaries"
    final_proof: "Final Judge/PM receipt mapping every change to a verified outcome + value justification, full_outcome_complete: true."
  intake:
    original_request: "Hit the 2 follow-ups (diamond clamp; offline-digest stale death line), then proceed further — discover+execute next highest-leverage improvements, trust my judgment."
    interpreted_outcome: "2 follow-ups fixed + a sequence of high-leverage, verified, player-felt improvements delivered."
    input_shape: existing_plan
    audience: "BaoLife players + Craig"
    authority: approved
    proof_type: test
    completion_proof: "2 fixes proven by tests + further waves verified green + final value audit"
    likely_misfire: "Churning low-value 'improvements' instead of real player-felt wins (Judge must gate value); diamond fix breaking legit spending/IAP; treating the digest death-line as cosmetic when it may expose a real offline-death-not-applied bug; regressing the 1627 suite; touching the avatar WIP."
    blind_spots_considered:
      - "Offline death may not be fully applied (status/routing) — the digest line could expose a real correctness gap, not just a stale message."
      - "Diamonds are premium/IAP currency — clamp must not corrupt purchased balances or block affordable spends."
      - "'Proceed further' should favor player-felt value (content/feel/retention/UX) over internal churn."
    existing_plan_facts:
      - "FU-A DIAMOND CLAMP: event choices with diamond costs are applied even when unaffordable -> diamonds go negative. Likely sites: events/v2/engine effects.ts / EventResponder / applyEventEffects; diamondEconomy.ts; how diamonds persist on player.c.diamonds. Fix: never allow negative diamonds (clamp at 0) AND/OR gate diamond-cost choices by affordability. Don't break legit spends or IAP grants. Assert with a test."
      - "FU-B OFFLINE-DIGEST DEATH LINE: offline sim can roll a death during processOfflineTime; the welcome-back digest 'while you were away' notable-events line showed 'You have died!' while saved end-state was alive (observed T008 prior goal). Investigate: is offline death actually applied (status set, handleDeath called) and should it route to DeathView, or is it a phantom captured message? Root-cause then fix the inconsistency (LoopManager.processOfflineTime / GameEngine death / the digest notable-events capture)."
      - "Constraint: do NOT touch avatar WIP (avatar.ts, avatarLibrary.*, Player.ts DiceBear-migration). baseline vitest 1627; tsc clean. npm run metrics simulator available."

rules:
  pm_owns_state: true
  one_active_task: true
  max_write_workers: 1
  no_implementation_without_worker_or_pm_task: true
  no_completion_without_judge_or_pm_audit: true
  planning_is_not_completion: true
  queued_required_worker_blocks_completion: true
  continuous_until_full_outcome: true
  missing_input_or_credentials_do_not_stop_goal: true
  preserve_and_validate_existing_plan: true
  intake_misfire_must_be_audited: true
  goal_pressure_requires_oracle: true
  no_completion_on_weak_proof: true
  slice_policy:
    max_consecutive_tiny_tasks: 2
    prefer_vertical_slices: true
    judge_picks_largest_safe_slice: true
    worker_completes_whole_slice: true

agents:
  scout: installed
  worker: installed
  judge: installed

visual_board:
  selected: local
  local:
    status: live
    url: "http://127.0.0.1:41737/baolife-polish-plus/"
    command: "npx goalbuddy board /Users/craigvandergalien/Documents/GitHub/lichun/docs/goals/baolife-polish-plus"

active_task: null

tasks:
  - id: T001
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "FU-A diamond clamp. Find where event-choice diamond costs are applied (events/v2/engine effects.ts / EventResponder / applyEventEffects) and how diamonds persist (player.c.diamonds + diamondEconomy.ts). Ensure diamonds can NEVER go negative: clamp the balance at 0 on any spend AND prevent applying a diamond-cost choice the player can't afford (gate affordability / no-op the spend rather than going negative). Do NOT break legitimate affordable spends or IAP diamond grants. Add a test asserting: an unaffordable diamond-cost choice does not drive diamonds below 0; an affordable one still works; IAP/grant path unaffected. Mirror online+offline if the spend path differs."
    allowed_files:
      - "server/src/events/v2/engine/effects.ts"
      - "server/src/events/v2/engine/respond.ts (EventResponder, if the spend applies there)"
      - "server/src/monetization/diamondEconomy.ts (or services/.../diamondEconomy.ts — confirm path)"
      - "server/src/services/character/stats_manager.ts or wherever diamonds are mutated (only if the clamp belongs there)"
      - "server/tests/** (new diamond-clamp test)"
      - "docs/goals/baolife-polish-plus/notes/PROGRESS.md"
    verify:
      - "cd server && npx tsc --noEmit  (exit 0)"
      - "cd server && npx vitest run  (>=1627, no regression)"
      - "new test proves diamonds never go negative on an unaffordable choice; affordable spend + IAP grant still work"
    stop_if:
      - "Need files outside allowed_files (esp. avatar.ts/avatarLibrary/Player.ts DiceBear — forbidden)."
      - "The clamp would block legitimate affordable spends or corrupt IAP balances — escalate to Judge."
      - "Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/src/events/v2/engine/effects.ts (affordability gate + clamp at the diamond chokepoint)"
        - "server/tests/events-v2/engine.diamond-clamp.test.ts (new, 9 cases)"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "1636/1636 (135 files; +9)"
      summary: "Root cause: applyEventEffects applied resources.diamonds deltas directly with no gate/clamp; respond.ts feeds -choice.diamondCost in -> unaffordable choices went negative. Fix at effects.ts chokepoint: unaffordable spend no-ops the diamond portion (non-diamond effects still apply), all diamond mutations clamp >=0, positive grants always apply. diamondEconomy.ts/awardDiamonds/IAP untouched (already safe). Online+offline both resolve via this chokepoint. FU-A done."
  - id: T002
    type: scout
    assignee: Scout
    status: done
    reasoning_hint: medium
    objective: "FU-B root-cause: investigate the offline-digest 'You have died!' line shown while the saved end-state was alive. READ-ONLY. Trace: does processOfflineTime (LoopManager) actually apply death when the offline sim rolls one (handleDeath called? player.c.status set to 'dead'? does it stop further offline ticks)? How does the digest's notable-events capture pick up messages (does it grab a death message that then didn't 'stick' because offline ticks continued, or because status wasn't persisted)? Is there a real correctness gap (offline death not applied -> player keeps living) OR a phantom captured message (death rolled mid-sim but a later tick/clamp revived, or the message was captured without the state)? Determine the fix: (a) if offline death should be real -> ensure it's applied + the digest reflects death + the client routes to DeathView on reconnect; (b) if phantom -> stop capturing/clamp the spurious message. Cite file:line + recommend a concrete Worker fix."
    inputs:
      - "server/src/game/engine/LoopManager.ts processOfflineTime + GameEngine death path"
      - "the offline digest notable-events capture (from the prior feel-pacing T005 work)"
      - "health_manager handleDeath; the digest builder"
    constraints:
      - "Read-only."
      - "Distinguish real correctness gap from cosmetic phantom with evidence."
    expected_output:
      - "Root cause: real offline-death gap | phantom message, with file:line"
      - "Concrete Worker fix spec (files + change + verify)"
    receipt:
      result: done
      verdict: "(A) REAL CORRECTNESS GAP"
      evidence:
        - "LoopManager.ts:402-421 (inline status='dead', no handleDeath, no loop stop)"
        - "LoopManager.ts:710-712 (processOfflineTime never breaks on death)"
        - "WebSocketServer.ts:262-282 (reconnect only runs for alive; never routes dead-offline)"
        - "PlayerSession.ts:320-345 + health_manager.ts:555 (canonical online path to mirror)"
      summary: "Offline death is half-applied + diverges from the online single-source-of-truth. LoopManager.ts:402-421 sets c.status='dead' INLINE (no handleDeath -> no lifeSummary, no controller flip, no death message, no loop stop). processOfflineTime (LoopManager.ts:710-712) never breaks on death -> ticks continue past it. Reconnect (WebSocketServer.ts:262-282) only runs offline processing for status==='alive' and never routes a freshly-dead-offline player to DeathView/lifeSummaryEvent. So digest can show 'You have died!' (NOTABLE /died/ match) with no death flow + inconsistent persisted state. Online path is PlayerSession.ts:320-345 -> health_manager.handleDeath:555 (the canonical path to mirror)."
      fix_spec:
        - "LoopManager.ts:402-421 — replace inline c.status='dead' with shared handleDeath(player) (import from services/health/health_manager.js), mirroring PlayerSession.processDayTick."
        - "LoopManager.ts:710-712 processOfflineTime — break the per-minute loop as soon as c.status==='dead' (stop ticking/earning past death). applyProratedFinances already alive-guarded."
        - "WebSocketServer.ts:262-282 — after processOfflineTime, if c.status==='dead' (died offline), persist + emit lifeSummaryEvent so the client opens DeathView (consistent w/ PlayerSession.ts:335-340)."
        - "Test: offline player w/ forced death (deathChance=1 or ageYears>120) over multi-day gap -> status persists dead, loop stopped (money/age frozen post-death), lifeSummary built, reconnect emits lifeSummaryEvent; control deathChance=0 -> no death line, stays alive."
  - id: T003
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "FU-B fix (real offline-death gap, per T002). Route the offline death through the shared canonical path so it matches online: (1) In LoopManager.ts (~:402-421) replace the inline `player.c.status='dead'` death-check block with a call to the shared `handleDeath(player)` (import from services/health/health_manager.js) — same as PlayerSession.processDayTick — so it builds lifeSummary + flips controller + queues the death message consistently. (2) In processOfflineTime (~:710-712) BREAK the per-minute offline loop as soon as `player.c.status==='dead'` so no further ticks/earnings happen post-death. (3) In WebSocketServer.ts (~:262-282) after processOfflineTime, if the player died offline (c.status==='dead'), persist and emit `lifeSummaryEvent` so the client routes to DeathView on reconnect (consistent with the online emit at PlayerSession.ts:335-340). Add a test: forced offline death (deathChance=1 / ageYears>120) over a multi-day gap -> status persists dead, loop stopped (money/age frozen after death), lifeSummary built, reconnect emits lifeSummaryEvent; control (no death) -> stays alive, no spurious death line. Keep online+offline consistent."
    allowed_files:
      - "server/src/game/engine/LoopManager.ts"
      - "server/src/server/WebSocketServer.ts"
      - "server/src/services/health/health_manager.ts (only if handleDeath needs a tiny tweak to be callable from the offline path — prefer NOT to change it)"
      - "server/tests/** (new offline-death-consistency test)"
      - "docs/goals/baolife-polish-plus/notes/PROGRESS.md"
    verify:
      - "cd server && npx tsc --noEmit  (exit 0)"
      - "cd server && npx vitest run  (>=1636, no regression)"
      - "new test: offline forced death persists dead + loop stops + lifeSummary built + reconnect emits lifeSummaryEvent; control stays alive w/ no death line"
    stop_if:
      - "Need files outside allowed_files (esp. avatar.ts/avatarLibrary/Player.ts DiceBear — forbidden), ws/, ios/, android/."
      - "Routing offline death through handleDeath breaks an offline/loop test in a way needing design judgment (escalate)."
      - "Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/src/game/engine/LoopManager.ts (death check -> shared handleDeath; processOfflineTime breaks on death)"
        - "server/src/server/WebSocketServer.ts (reconnect detects offline death -> persist + emit lifeSummaryEvent)"
        - "server/tests/game/offlineDeath.consistency.test.ts (new, 4 cases)"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "1640/1640 (136 files; +4)"
      summary: "Offline death now mirrors the online canonical path: LoopManager calls shared handleDeath (status/controller/death-msg/lifeSummary), processOfflineTime breaks on death (age/finance frozen at death point), reconnect persists + emits lifeSummaryEvent -> client routes to DeathView. handleDeath untouched. No existing test changed. FU-B done; both follow-ups complete."
  - id: T004
    type: scout
    assignee: Scout
    status: done
    reasoning_hint: medium
    objective: "PROCEED FURTHER — discover the next highest-leverage PLAYER-FELT improvement candidates now that survival/economy/cadence/death are solid. Read-only survey: use npm run metrics + read the codebase for the biggest remaining gaps in fun/retention/clarity (e.g. reward cadence actually reaching the player, energy/activity loop depth, relationship/AI-conversation payoff, onboarding/first-session hook, content variety gaps by life stage, monetization fairness, UX clarity). Rank candidates by player-felt impact × confidence × reversibility. Produce 4-6 concrete candidates with evidence + a suggested first wave. Do NOT propose touching the avatar WIP."
    inputs:
      - "npm run metrics trajectories; docs/AI_CONVERSATION_P2_BACKLOG.md; the catalog + retention + economy systems"
    constraints:
      - "Read-only. Favor player-felt value over internal churn. Concrete, evidence-backed candidates."
    expected_output:
      - "4-6 ranked improvement candidates (impact/confidence/reversibility + evidence)"
      - "Suggested first wave"
    receipt:
      result: done
      evidence:
        - "job_manager.ts:847-854 (jobs gate only on education tier)"
        - "v2 catalog isEligible reads occupation/money/flags/dates/stress only — never intelligence/creativity/social/happiness"
        - "intradayActivity.ts:132-169 + performActivity.ts:81-93 (flat fixed deltas, no progression)"
        - "lifeGoals.ts:206 reach_millionaire vs ~$61k peak; metrics flat stat trajectories"
      summary: "TOP FINDING: character stats are WRITE-ONLY/cosmetic. Across ~70 v2 events ZERO isEligible gates read intelligence/happiness/looks/creativity/social; jobs gate ONLY on education tier (job_manager.ts:847-854); only health is ever read as a condition. metrics confirm flat stat trajectories. performActivity is flat fixed deltas, no progression (intradayActivity.ts:132-169, performActivity.ts:81-93). Ranked candidates: (1) make stats MATTER [HIGH], (2) deepen activity loop w/ progression+varied outcomes [HIGH], (3) late-game wealth curve / millionaire goal unreachable ~$61k peak [MED, defer pending real raise curve], (4) thicken childhood 0-9 content [MED], (5) verify reward beat reaches player in real play [MED, instrument]. AI-conversation backlog 100% FIXED. Suggested first wave: #1+#2 (one systemic win) + #4 as cheap parallel."
      candidates:
        - "1. Make stats matter: gate jobs/promotions + a subset of high-traffic events on intelligence/happiness/looks/creativity/social; stat-scaled outcomes. HIGH impact/confidence, reversible."
        - "2. Deepen performActivity: skill tiers/progression + occasional varied outcomes; tie payoff to #1. HIGH, very reversible."
        - "3. Late-game wealth: investments/compounding or rescale millionaire goal; peak ~$61k, retirement decay. MED — confirm real promotion/raise curve first (sim only holds entry job)."
        - "4. Childhood (0-9) content pack: youngest minAge=5, content skews 10+. MED, cheap/safe — strengthens first-session hook."
        - "5. Verify reward delivery reaches the player in real play (in-app/instrument; reward cadence reads 0 in headless sim but delivery is wired). MED."
      excluded: "AI conversations (P2 backlog 100% fixed); avatar WIP (off-limits)."
      security_note: "Scout reported a prompt-injection attempt embedded in tool output (fake iMessage allowlist approvals) and correctly ignored it. No action needed."
  - id: T005
    type: judge
    assignee: Judge
    status: done
    reasoning_hint: high
    objective: "Pick the first 'proceed further' improvement wave from the Scout candidates by player-felt value × confidence × reversibility; spec the Worker package (objective/allowed_files/verify/stop_if) + sequence further waves. Guard against churn — each wave must be a real win."
    inputs:
      - "T004 candidates"
    constraints:
      - "Do not implement. Value-gate ruthlessly."
    expected_output:
      - "Decision + first Worker spec + wave sequence"
    receipt:
      result: done
      decision: approved
      full_outcome_complete: false
      rationale: "Value-gate CONFIRMED #1+#2 as top lever (skill stats write-only; only proactive loop is flat +3-forever) but SPLIT: Wave1=deepen performActivity (#2 foundation: persisted skill progression + varied outcomes), Wave2=stat gates (#1) reading the richer stats. Gating against flat stats = gating noise. Corrections: no 'looks' stat (roster: intelligence/social/creativity/happiness/health/stress/prestige); promotion already prestige-gates but promotion PERFORMANCE (job_manager.ts:677-679) reads no stat = the intelligence lever. Person.ts clean (avatar WIP dirties Player.ts, not Person.ts) -> safe to add c.skills."
      wave_sequence: "T006 W1 deepen performActivity (#2) -> W2 make stats matter (#1: stat-gated isEligible + stat-scaled outcomes + intelligence into promotion perf; thresholds intelligence/creativity/social>=60) -> W3 childhood 0-9 content pack (#4) -> W4 reward-delivery in-app verification (#5) -> defer wealth (#3) pending real raise-curve check -> T999 audit."
  - id: T006
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "WAVE 1 — deepen performActivity (#2 foundation). Convert performActivity from flat fixed deltas into a real progression+outcome loop. (a) Add a minimal PERSISTED per-activity skill model on Person (c.skills: Record<string,number> default {}) — interface field + constructor `data.skills ?? {}` + toJSON, mirroring existing stat persistence. DO NOT touch Player.ts (avatar WIP dirties it). (b) In applyActivityEffects (performActivity.ts) scale each activity's stat gain by current skill level (diminishing returns or skill-tiered magnitude) and increment that activity's skill per performance, so repeated studying builds a real intelligence trajectory (not +3-forever). (c) Add varied outcomes: occasional better/worse/crit results via a SEEDED injectable RNG, with distinct feedback in the activityPerformed payload (message + outcomeTier + skillLevel + actual deltas). (d) Keep energy gate, free-slot logic, override mode, age gate, clampPlayerStats intact. Online handler path only; isEligible/offline read-only (skills) -> no divergence. Do NOT add stat GATES yet (Wave 2)."
    allowed_files:
      - "server/src/handlers/performActivity.ts"
      - "server/src/game/engine/intradayActivity.ts"
      - "server/src/models/Person.ts (ONLY add persisted c.skills: interface + constructor + toJSON; NOT Player.ts)"
      - "server/src/utils/statUtils.ts (only if a clamp helper is needed)"
      - "server/tests/** (new performActivity progression + varied-outcome + persistence test)"
      - "docs/goals/baolife-polish-plus/notes/PROGRESS.md"
    verify:
      - "cd server && npx tsc --noEmit  (exit 0)"
      - "cd server && npx vitest run  (>=1640, no regression)"
      - "new test: skill increments + persists across toJSON/fromJSON; repeated same activity yields a NON-FLAT (growing/tiered) trajectory; varied-outcome reachable w/ seeded RNG; energy/age/free-slot gates still hold"
    stop_if:
      - "Need files outside allowed_files (esp. Player.ts / avatar.ts / avatarLibrary.* — FORBIDDEN; ws/, ios/, android/)."
      - "Adding c.skills collides with the avatar Player.ts migration (escalate — should not; Person.ts is clean)."
      - "A progression/varied-outcome change breaks an existing performActivity/metrics test in a way needing design judgment (escalate)."
      - "Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/src/models/Person.ts (persisted skills: Record<string,number>)"
        - "server/src/game/engine/intradayActivity.ts (resolveActivityOutcome/skillMasteryMultiplier/rollOutcomeTier helpers)"
        - "server/src/handlers/performActivity.ts (skill-scaled + varied outcomes + richer activityPerformed payload)"
        - "server/tests/handlers/performActivity.progression.test.ts (new, 8 cases); performActivity.test.ts (2 stale flat-delta assertions updated)"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "1648/1648 (137 files; +8)"
      summary: "performActivity is now a real progression loop: persisted per-activity skills (+1/session), front-loaded mastery tiers (novice1.6x->expert1.0x) so study=5@novice tapering to +3 while skill climbs; varied outcomes via seeded injectable RNG (poor0.5x/normal1.0x/great1.75x+bonus); activityPerformed payload carries outcomeTier/skillLevel/skillTier/deltas/message. Gates (energy/age/free-slot/clamp) preserved; no stat gates yet (W2). Player.ts untouched."
  - id: T007
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: high
    objective: "WAVE 2 — make stats MATTER (#1), reading the richer stats from W1. Add stat-threshold isEligible gates + stat-scaled outcomes to a subset of high-traffic v2 events (career.ts, education.ts, social/hobbies) AND tie intelligence into promotion performance/eligibility in job_manager.handleJob (e.g. perf bonus when intelligence>=60, or an intelligence floor on a mid-tier promotion). Starting thresholds: intelligence>=60 (advanced project/mentorship career event), creativity>=60 (hobby-monetization event), social>=60 (networking event). So the stats players raise actually open doors. Tests prove a gate/outcome changes with the stat."
    allowed_files:
      - "server/src/services/jobs/job_manager.ts (handleJob promotion performance: intelligence raises promotion odds / floor on a mid-tier promotion; keep existing education+prestige gates)"
      - "server/src/events/v2/catalog/career.ts, education.ts, hobbies.ts, random.ts (add stat-threshold isEligible gates + stat-scaled outcomes to a subset of high-traffic events)"
      - "server/src/events/v2/types.ts (only if a helper/field is needed; isEligible(player) already supported)"
      - "server/src/events/v2/engine/selector.ts (read-only ref; edit only if a shared stat-gate helper belongs there)"
      - "server/tests/** (tests proving a gate/outcome changes with the stat)"
      - "docs/goals/baolife-polish-plus/notes/PROGRESS.md"
    verify:
      - "cd server && npx tsc --noEmit  (exit 0)"
      - "cd server && npx vitest run  (>=1648, no regression)"
      - "tests prove: a stat-gated event is ineligible below threshold + eligible at/above; intelligence measurably affects promotion outcome"
    stop_if:
      - "Need files outside the jobs/events surface, or Player.ts/avatar WIP, ws/, ios/, android/."
      - "A stat gate makes a milestone unreachable or breaks an existing eligibility test needing design judgment (escalate)."
      - "Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/src/services/jobs/job_manager.ts (intelligence->promotion: perf bonus >=60, top-tier floor >=75; injectable rng)"
        - "server/src/events/v2/catalog/career.ts (flagshipProject, intelligence>=60); random.ts (creative_breakthrough creativity>=60, networking_opportunity social>=60)"
        - "server/tests/events-v2/catalog.stat-gates.test.ts; tests/services/jobs/jobs.intelligencePromotion.test.ts (new, +17)"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "1665/1665 (139 files; +17)"
      summary: "Stats now matter. Intelligence->promotion (perf-roll +1 at >=60 bias; top elite rung also needs intelligence>=75, held-not-fired like prestige gate; education+prestige gates intact). 3 stat-gated payoff events (intelligence/creativity/social>=60) reward the W1 trajectories. isEligible already receives player -> no engine change. Milestone-safe (only bonus content gated; ordinary ladders verified ungated). No existing test changed."
  - id: T008
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "WAVE 3 — childhood (0-9) content pack (#4). The first in-game years are content-thin (youngest event minAge=5; content skews 10+), weakening the first-session hook. Add a new catalog file (e.g. catalog/childhood.ts) with several EARLY-LIFE events spanning ages ~2-9 (include some minAge<5): formative childhood beats (first words/steps milestone, first day of school, making a first friend, a childhood pet, a family trip, a playground triumph/scrape, a small early choice that nudges a stat) — warm, age-appropriate, some interactive choices + some passive milestones. Follow the established v2 catalog recipe (flatten + isEligible age gates + repeatable/cooldown where it recurs + date-seeded textVariation for repeatables). Register in catalog/index.ts (globally-unique ids). A few can lightly nudge stats (so early choices matter, building toward the now-meaningful stats) but keep it gentle. Tests: events registered + eligible at the intended young ages."
    allowed_files:
      - "server/src/events/v2/catalog/childhood.ts (new)"
      - "server/src/events/v2/catalog/index.ts (register)"
      - "server/src/events/v2/catalog/adolescence.ts (only if a gate needs widening to avoid overlap)"
      - "server/src/events/v2/types.ts (only if a field is needed; unlikely)"
      - "server/tests/** (new childhood catalog test)"
      - "docs/goals/baolife-polish-plus/notes/PROGRESS.md"
    verify:
      - "cd server && npx tsc --noEmit  (exit 0)"
      - "cd server && npx vitest run  (>=1665, no regression)"
      - "test: childhood events registered + eligible at intended young ages (incl <5); ids unique"
    stop_if:
      - "Need files outside allowed_files (Player.ts/avatar WIP forbidden), ws/, ios/, android/."
      - "An id collides in the registry (namespace your ids, e.g. 'childhood.firstFriend')."
      - "Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/src/events/v2/catalog/childhood.ts (new; 10 events, category 'family', namespaced childhood.*)"
        - "server/src/events/v2/catalog/index.ts (register); tests/events-v2/catalog.childhood.test.ts (new, 8 cases)"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "1673/1673 (140 files; +8)"
      summary: "Childhood pack: 10 early-life events ages ~2-9 (4 reachable <5: firstWords/firstSteps/shareTheToy/bedtimeStory), 6 passive milestones + 4 interactive (small |Δ|<=5 stat nudges), bedtimeStory repeatable cd120 date-seeded. Unique namespaced ids (registry build asserts), eligible young, milestone-safe. Fills the empty first-session window."
  - id: T009
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "WAVE 4 — verify the reward beat reaches the player in REAL play (#5; verify/instrument, do NOT rebuild). The reward-delivery code (dailyQuests/lifeGoals/achievements/diamonds via services/retention/integration.ts -> session.send + push) is wired but reads 0 in the headless sim (handler-driven, not the autonomous loop). Prove rewards actually fire + reach the client on the ONLINE path. Preferred (cheap+durable): add an INTEGRATION TEST that exercises the online reward path — e.g. via a PlayerSession (or the retention integration hooks with a fake session) trigger a reward (complete a daily quest / reach a life goal / unlock an achievement) and assert (a) diamonds are awarded, (b) a reward message is sent to the client session (session.sent contains the reward/lifeGoalsUpdate/quest message), (c) clamps from FU-A still hold. OPTIONALLY also a quick in-app spot-check (drive the app vs local server, claim a daily reward, see the balance update). If a REAL online-path delivery gap is found, STOP and escalate (PM widens allowed_files to fix it). Document the confirmation + evidence."
    allowed_files:
      - "server/tests/** (new online-reward-path integration test)"
      - "scripts/** (throwaway in-app spot-check driver, optional)"
      - "docs/goals/baolife-polish-plus/notes/ (confirmation notes)"
      - "(Read-only on product code. If a real gap needs a product fix, STOP + escalate to PM.)"
    verify:
      - "cd server && npx tsc --noEmit  (exit 0)"
      - "cd server && npx vitest run  (>=1673)"
      - "new test proves a reward fires + a reward message reaches the client session on the online path"
    stop_if:
      - "A real reward-delivery bug is found that needs a product-code fix (STOP, escalate to PM to widen scope)."
      - "Verification fails twice."
      - "Need product-code edits (do NOT; escalate)."
    receipt:
      result: done
      changed_files:
        - "server/tests/services/retention/rewardDelivery.online.integration.test.ts (new, 5 cases)"
        - "docs/goals/baolife-polish-plus/notes/T009-reward-verification.md"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "1678/1678 (141 files; +5)"
      summary: "Rewards DO reach the client on the online path — NO gap. Proven: daily quest (real handleClaimQuestReward -> diamonds + questRewardClaimed msg; double-claim rejected), achievement (onChildBorn -> diamonds + achievementUnlocked), life goal (updateLifeGoals -> diamonds + lifeGoalsUpdate justCompleted). FU-A clamp coexists (non-negative, affordable deduct, overspend refused, exact-drain->0). Headless 'reward=0' confirmed to be the offline GameEngine path only (online path now test-covered). No product fix needed."
  - id: T999
    type: pm
    assignee: PM
    status: done
    reasoning_hint: high
    objective: "FINAL AUDIT: confirm the 2 follow-ups fixed+proven AND the further-improvement waves delivered real verified player-felt value (not churn). Map every change to a verified outcome + the oracle."
    receipt:
      result: done
      decision: complete
      full_outcome_complete: true
      rationale: "Both named follow-ups fixed + proven, and 4 player-felt improvement waves delivered with verified value (not churn). Backend independently green: tsc clean, vitest 1678 (was 1627, +51 net). Every change maps to a verified outcome:"
      value_per_change:
        - "FU-A diamond clamp: diamonds can never go negative on unaffordable choices (real currency-integrity bug) — proven by engine.diamond-clamp.test.ts."
        - "FU-B offline death: was half-applied (bypassed handleDeath, loop never stopped, reconnect never routed) -> now mirrors the canonical online path; offlineDeath.consistency.test.ts."
        - "W1 performActivity depth: the only proactive agency loop went from flat +3-forever to persisted skill progression + varied outcomes — players now feel growth from activities."
        - "W2 stats matter: stats were write-only/cosmetic -> intelligence drives promotion + 3 stat-gated payoff events; raising stats now opens doors (the core life-sim fantasy)."
        - "W3 childhood content: the empty 0-9 first-session window now has 10 warm early-life beats — strengthens the most retention-critical window."
        - "W4 reward delivery: verified rewards reach the client online (quest/achievement/life-goal) — confidence the dopamine beats fire."
      verification: "tsc clean; vitest 1678/1678 (141 files). No avatar WIP touched (kept uncommitted). No production deploy (per constraint)."
      deferred_not_churn:
        - "Wealth/late-game economy (#3): deferred pending a real promotion/raise-curve check (sim only holds entry job, understates income) — avoided rescaling on bad data."
        - "Reward cadence reading 0 in headless sim is the offline-path artifact, not a bug (online verified in W4)."
      remaining_owner_action: "Implementation UNCOMMITTED on this goal. Recommend commit (+ deploy if desired). Optional future slice: the deferred wealth/economy curve after a raise-curve measurement."
    inputs:
      - "All done receipts + last verification + dirty diff"
    constraints:
      - "Do not implement."
      - "Reject churn/over-engineering; reject unverified fixes."
      - "Reject completion if a required Worker is still queued/active."
    expected_output:
      - "complete | not_complete"
      - "full_outcome_complete: true | false"
      - "value justification per change"
      - "next task if not complete"
    receipt: null

checks:
  dirty_fingerprint: unknown
  last_verification:
    result: green
    task: "prior goal (feel-pacing T999)"
    commands:
      - cmd: "cd server && npx tsc --noEmit"
        status: pass
      - cmd: "cd server && npx vitest run"
        status: pass
        detail: "1627/1627 (134 files) — baseline for this goal"
