# GoalBuddy v2 state.yaml
# Board truth lives here. goal.md is the editable charter; notes/ holds long receipts only.

version: 2

goal:
  title: "BaoLife — Late-Game Wealth / Economy Curve"
  slug: "baolife-economy"
  kind: existing_plan # specific | open_ended | existing_plan | recovery | audit
  tranche: "Measure a real per-age income/expense/net-worth curve with career progression, then (data-gated) decide + execute any warranted late-game economy tuning, verified + re-measured, until a final audit proves the full outcome."
  status: done # active | blocked | done
  oracle:
    signal: "Headless lifetime-simulator income/expense/net-worth-by-age report with real career progression (promotions+raises applied), re-run before/after any tuning."
    cadence: "after each Worker package and at final audit"
    final_proof: "Reproducible curve + Judge ruling on tuning + any tuning implemented & verified (tsc clean, vitest >= 1678, curve re-measured to intended shape) + final audit full_outcome_complete: true."
  intake:
    original_request: "Continue BaoLife autonomously; new slice = late-game wealth/economy curve (deferred from polish-plus pending a real promotion/raise-curve check)."
    interpreted_outcome: "Money stays meaningful across a full life; measured first, then tuned only if the real curve shows it's broken."
    input_shape: existing_plan
    audience: "BaoLife players + Craig"
    authority: approved
    proof_type: metric
    completion_proof: "Reproducible per-age economy curve + Judge ruling + any verified tuning re-measured + final audit full_outcome_complete: true."
    likely_misfire: "Rescaling on KNOWN-BAD entry-job-only data (the exact deferred trap); churning cosmetic tweaks; breaking diamonds/IAP or the reward economy; touching avatar WIP; regressing 1678 tests."
    blind_spots_considered:
      - "Simulator likely needs a real fix to drive promotions/raises before it can measure — measurement-enablement is a legit Worker slice."
      - "'Money meaningless late game' may be a sink problem (nothing worth buying) as much as a source problem (too much income)."
      - "Expenses/inflation must scale with life stage (rent, family, healthcare) or wealth trivially accumulates."
      - "Mirror loop/economy changes across online + offline paths."
    existing_plan_facts:
      - "Measure-first is mandatory; the deferral reason was 'sim only holds entry job, understates income'."
      - "Diamonds are premium/IAP; soft-currency tuning must not corrupt them or the reward economy."
      - "Avatar WIP is off-limits (avatar.ts, avatarLibrary.*, Player.ts DiceBear change, ios MainCharacterView/QuickStatsCard)."

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: none
  local:
    status: not_requested
    url: null
    command: "npx goalbuddy board docs/goals/baolife-economy"

active_task: null

tasks:
  - id: T001
    type: scout
    assignee: Scout
    status: done
    reasoning_hint: medium
    objective: "Map the full BaoLife money economy in code (income sources, raises/promotions, expenses/bills, prices/sinks, savings/interest, starting money by age) across the lifespan, AND map how the headless lifetime simulator drives careers — does it apply for jobs / get promotions / raises, or hold the entry job? Identify exactly what's needed to produce a realistic per-age income/expense/net-worth curve."
    inputs:
      - "server/tests/e2e/lifetime-simulator.ts (+ npm run metrics)"
      - "server/src/services/jobs/job_manager.ts"
      - "server/src/game/PlayerSession.ts; server/src/game/engine/{GameEngine,LoopManager}.ts"
      - "server/src/events/v2/catalog/*.ts (career/economy events); server/src/services/**/*.ts (income/expenses/purchases)"
      - "server/src/handlers/{applyForJob,purchaseItem,performActivity}.ts"
    constraints:
      - "Read-only. Do not edit implementation files."
      - "Prefer concrete file:line evidence. Quote actual numbers (incomes, raise %, prices, bills)."
      - "Explicitly determine whether the simulator progresses careers or holds entry job, and why."
      - "Do NOT read/inspect the avatar WIP as part of scope."
    expected_output:
      - "Economy model map: income sources + amounts, raise/promotion mechanics, expenses/bills, prices/sinks, savings/interest, starting money."
      - "Simulator career-progression verdict: progresses vs holds-entry-job, with the controlling code path."
      - "Concrete plan to enable a realistic per-age income/expense/net-worth measurement (what to extend in the sim/harness)."
      - "Ranked improvement candidates IF the curve later proves broken (sources vs sinks vs expenses)."
      - "Candidate next tasks."
    receipt:
      result: done
      summary: "Economy fully mapped AND the deferral premise is STALE: the sim already progresses careers. lifetime-simulator.ts:577-623 maybeDriveEmployment (default ON) grants a degree + applies for Accountant at age 22 via real applyForJob, re-applies if fired, retires at 65; handleJob (job_manager.ts:733-820) auto-promotes through 5 salary tiers when weekly performance>90. metrics.test.ts:230-245 already samples YearlySample{money,netWorth,salary,occupation} per in-game year (3 seeds, age18, 160y). So measurement is HARNESS-ONLY — no production change needed to get a real curve."
      economy_model:
        income: "Job salary only, applied salary/4 weekly (finance.ts:92). 30 occupations x 5 tiers, $500-$10000/mo (job_manager.ts:175-588). Raises are promotion-driven via handleJob (no tenure %-raise). One-off v2 career event money bumps ($400-$850 raise events, severance) exist but aren't recurring. NPC-death inheritance is a rare lump sum (GameEngine.ts:402)."
        expenses: "rent = clamp(income*0.3, $50, $1250) + luxury-item upkeep, x lifestyle modifier (0.7/1.0/1.5). Only savingsRate fraction of surplus kept (frugal .20/normal .10/extravagant .05). money floored at 0 (finance.ts:132). NO healthcare/family/childcare/tax expenses; expenses do NOT scale with age/family. Non-earners still owe the $50/wk rent floor."
        sinks: "50+ luxury items $100-$5,000,000 (shop_manager.ts:107-449); prestige-gated exclusives (minPrestige 100/250). Upkeep only on prestige items."
        savings_interest: "NO bank/interest in live path (Person.hasBankAccount unused). 'money' is a flat field; netWorth in metrics = money + item resale."
        starting_money: "0 for new AND backfilled/age-N characters (character_manager.ts:264); no childhood allowance wired into the loop."
        money_vs_diamonds: "Cleanly separate; finance/jobs touch only money; diamonds are reward/IAP. Tuning target = money only."
      simulator_verdict: "PROGRESSES (not entry-job-held). Harness-only to measure. Both online (PlayerSession) + offline (GameEngine) call the SAME finance.ts + handleJob, so wages/promotions accrue regardless of connection."
      risks_for_judge:
        - "Two pay paths coexist: live handleJob (catalog Occupation) AND PlayerSession.ts:612,618 payPlayer+checkPromotion (different JobListing interface, job_manager.ts:14-44). Must confirm which actually fires online to avoid DOUBLE-COUNTING income in the measured curve."
        - "Whether the curve is 'broken' needs the REAL numbers — run npm run metrics (could not run read-only)."
        - "Design call: rent capped $1250/wk, top income ~$2500/wk (CTO) => bounded net accrual, may top out in tens of thousands — possibly far below top luxury prices ($5M), making late-game sinks unreachable (dead sinks) OR money simply plateaus. Needs the curve."
      ranked_broken_hypotheses:
        - "SINK/expense (most likely per Scout): late-game money plateaus low vs luxury prices => nothing meaningful to buy OR nothing to spend on; expenses don't scale with life stage so surplus just accumulates slowly."
        - "SOURCE: income tiers may be too low/high relative to prices; promotion cadence unknown until measured."
      evidence:
        - "server/tests/e2e/lifetime-simulator.ts:577-623"
        - "server/tests/e2e/metrics.test.ts:230-245"
        - "server/src/services/jobs/job_manager.ts:175-588,733-820"
        - "server/src/game/finance.ts:85-132"
        - "server/src/game/economyConstants.ts:114-141"
        - "server/src/stats/stats_manager.ts:36-55"
        - "server/src/services/shop/shop_manager.ts:107-449"
        - "server/src/game/PlayerSession.ts:149-150,499-503,612,618"
        - "server/src/game/engine/GameEngine.ts:637,781,831-834; LoopManager.ts:738"
      note_needed: false

  - id: T002
    type: pm
    assignee: PM
    status: done
    reasoning_hint: medium
    objective: "Capture the REAL baseline per-age economy curve (the sim already progresses careers per T001). Run `npm run metrics`; inspect what metrics.test.ts emits; if it asserts bands but doesn't print the per-age income/expense/net-worth/savings curve, note exactly what a small harness extension needs. ALSO reconcile the two pay paths (handleJob vs PlayerSession payPlayer/checkPromotion) to confirm the online loop does NOT double-count income. Output: the actual numbers (or the precise gap to emit them) + the pay-path verdict."
    inputs:
      - "T001 receipt"
      - "server/tests/e2e/metrics.test.ts; lifetime-simulator.ts"
      - "server/src/game/PlayerSession.ts:499-503,612,618; server/src/services/jobs/job_manager.ts"
    constraints:
      - "PM evidence gathering: run existing harness (read-only to product code) + read code. No economy constant changes."
      - "If a tiny harness/report extension is needed to emit the curve, define it as a bounded Worker slice (T003)."
    expected_output:
      - "Baseline per-age curve numbers (money/netWorth/salary/occupation by age) OR the precise emit-gap"
      - "Pay-path verdict: single source of truth vs double-count risk"
      - "Whether a T003 harness/report Worker slice is needed, or skip straight to Judge"
    receipt:
      result: done
      summary: "Ran `npm run metrics` and captured the REAL curve (3 seeds, near-identical). NO double-count: the only income path is applyWeeklyFinances (finance.ts), called once/Monday in BOTH PlayerSession:503 (online) and GameEngine:834 (offline); PlayerSession has NO payPlayer/checkPromotion (Scout's 'two pay paths' was a dead unused JobListing interface). The metrics harness ALREADY drives employment + samples/prints/guards the per-age curve. BUT it measures only the PASSIVE worker — the sim never sets focus='Work Hard', so handleJob perfUpdate=randint(-1,1) random-walks ~0, never crosses >90 => NEVER PROMOTED. Salary frozen at entry $1200/mo for the entire 42y career."
      measured_passive_curve:
        - "age18 $0 (no job) -> age23 ~$4.5k salary1200 -> age33 $18.8k -> age43 $42.6k -> age53 $66.5k -> age63 $93.0k -> peak ~$97.8k at retirement ~65 -> slow drawdown in retirement (rent floor)."
        - "salary=1200 at EVERY sample, all seeds (no promotions ever). Growth is pure linear savings (~$2.4k/yr)."
        - "netWorth==money exactly every year (sim buys nothing). Diamonds frozen at 20 (first-job achievement). reward cadence 0 = known offline-sim artifact."
      diagnosis: "Frozen salary is a SIM-PASSIVITY artifact (no Work-Hard focus, avg intelligence<60), not a double-count and not obviously a production bug. The PASSIVE floor (~$97k/life) is trivial vs the $100-$5M shop => late-game money meaningless for a passive player. The ENGAGED curve (Work Hard -> promotions -> climbing tiers; and high-ceiling occupations like SWE->CTO $10k/mo) has NEVER been measured — that is the real 'promotion/raise-curve check' the prior goal deferred."
      pay_path_verdict: "Single source of truth = applyWeeklyFinances. No double-counting. handleJob is the sole promotion/raise mechanic; promotion needs JobRecord.performance>90; perfMod Work Hard +2 / Balanced 0 / Slack -1; intelligence>=60 adds +1; elite top tiers gated by prestige + intelligence."
      decision: "Need a T003 measurement extension (harness-only) to drive an ENGAGED worker (focus=Work Hard) across representative occupations and emit the real climbing curve, WITHOUT changing the existing passive metrics assertions. THEN Judge rules on tuning."
      evidence:
        - "npm run metrics output (seeds 1/7/42): salary=1200 constant, peak netWorth ~$97.8k"
        - "tests/e2e/lifetime-simulator.ts:567-623 maybeDriveEmployment (no setJobFocus call)"
        - "src/services/jobs/job_manager.ts:691-733 perf/promotion mechanic"
        - "src/game/PlayerSession.ts:503 + GameEngine.ts:834 single finance path"
      note_needed: false

  - id: T003
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "MEASUREMENT EXTENSION (harness-only, no product/economy changes). Extend the lifetime simulator to optionally model an ENGAGED worker: after applyForJob, drive job focus to 'Work Hard' (real setJobFocus path) so handleJob promotions actually fire, and allow choosing the occupation. Emit the real climbing per-age income/net-worth curve for a few representative engaged trajectories (at minimum: engaged Accountant = mid-tier, and one high-ceiling career e.g. Software Engineer -> CTO). Do NOT change the existing PASSIVE metrics behavior/assertions — add engaged effort as OPT-IN. Capture the numbers for the Judge."
    allowed_files:
      - "server/tests/e2e/lifetime-simulator.ts"
      - "server/tests/e2e/economy-curve.test.ts"
      - "docs/goals/baolife-economy/notes/T003-engaged-curve.md"
    verify:
      - "cd server && npx tsc --noEmit"
      - "cd server && npx vitest run (>= 1678, existing metrics.test.ts passive assertions UNCHANGED + still green)"
      - "the new engaged-trajectory measurement runs and prints climbing per-age salary/money/netWorth curves"
    stop_if:
      - "Modeling an engaged worker requires changing PRODUCTION game logic (PlayerSession/GameEngine/job_manager/finance) rather than the test harness — STOP and surface (that would be a real bug or a tuning change, gated on the curve)."
      - "Need files outside allowed_files (esp. avatar WIP: avatar.ts/avatarLibrary.*/Player.ts/MainCharacterView/QuickStatsCard, or ws/)."
      - "Changing the sim to drive Work Hard breaks the existing passive metrics.test.ts economy assertions (means default behavior leaked) — fix to keep engaged opt-in, or surface."
      - "Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/tests/e2e/lifetime-simulator.ts"
        - "server/tests/e2e/economy-curve.test.ts"
        - "docs/goals/baolife-economy/notes/T003-engaged-curve.md"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "142 files, 1681 tests, 0 failures (>=1678); metrics.test.ts passive assertions unchanged/green"
        - cmd: "npx vitest run tests/e2e/economy-curve.test.ts"
          status: pass
      summary: "Measured the ENGAGED worker. It is BYTE-FOR-BYTE IDENTICAL to passive: workFocus has zero income effect because the promotion mechanic (job_manager.handleJob, performance>90 -> tier salary) is DEAD CODE — never called in any live path. Engaged Accountant peak netWorth $97.8k (==passive); engaged Software Engineer peak $126.9k (entry salary $2000 the WHOLE career, never climbs to the $10k CTO tier)."
      engaged_curves:
        - "Engaged Accountant (Work Hard): salary [1200] flat entire career; peak netWorth $97,764 @65. Identical to passive control."
        - "Engaged Software Engineer (Work Hard): salary [2000] flat entire career (never reaches $3k/$6k/$10k tiers); peak netWorth $126,940 @65."
      takeaway: "Engaged late-game money does NOT become meaningful — it plateaus at the entry-tier floor ($98k-$127k lifetime peak) regardless of effort or career, at the very bottom of the $100-$5M shop. ROOT CAUSE is career-progression being inert, NOT economy constants."
      pm_root_cause_addendum: "PM independently traced + RESOLVED the Scout/Worker contradiction. VERIFIED: (1) job_manager.handleJob is dead (only refs are its own export + services/jobs/index.ts re-export; the only this.handleJob() call is GameEngine's DIFFERENT private happiness-only method:879). (2) Online weekly tick PlayerSession.processWeekTick:499 runs ONLY applyWeeklyFinances. (3) Salary write sites: setJob entry:604 (live), handleJob promotion:787 (DEAD), fired/quit ->0, and careerMilestones.ts c.salary+=raise (LEGACY class path). (4) The LIVE v2 career catalog (events/v2/catalog/career.ts) was intentionally ported to give ONE-OFF money bonuses ($400/$600/$850) NOT salary raises ('the v2 context doesn't track the salary field'). (5) ActivityRecord.incrementPerformance auto-levels a numeric skill level but NEVER touches person.salary. NET: recurring salary is frozen at the applyForJob entry value for the whole career; tier promotions + recurring raises do not exist in production. The 5-tier ladders on all 30 occupations are decorative."
      note: docs/goals/baolife-economy/notes/T003-engaged-curve.md
      note_needed: false

  - id: T004
    type: judge
    assignee: Judge
    status: done
    reasoning_hint: high
    objective: "RISK/PHASE-BOUNDARY ARCHITECTURAL RULING. The measured data shows the late-game economy is broken because CAREER PROGRESSION IS INERT: recurring salary is frozen at the entry value for the whole career (tier-promotion handleJob is dead code; the live v2 career catalog gives one-off money bonuses, not salary raises). Confirm/refute this root cause from the evidence, then decide the highest-leverage, SAFEST fix to make careers progress and money meaningful — without making it run away. Define the exact Worker slice(s)."
    inputs:
      - "T003 receipt (engaged==passive, peak ~$98-127k) + T003-engaged-curve.md"
      - "T002 receipt (passive curve, salary frozen $1200) + T001 economy map"
      - "PM root-cause addendum on T003 (dead handleJob; v2 catalog drops salary raises; legacy careerMilestones.ts is the old class path)"
    constraints:
      - "Do not implement. Ground every claim in the measured numbers + traced call sites."
      - "Pick the highest-leverage SAFEST lever to restore career progression. Candidate approaches to weigh: (A) make the LIVE v2 career events also raise recurring salary (track salary in the v2 effect/context); (B) wire job_manager.handleJob tier-promotions into BOTH weekly ticks (online PlayerSession.processWeekTick + offline GameEngine); (C) a simpler periodic deterministic raise; or a combination. Note prestige/intelligence gates on elite top tiers already exist in handleJob."
      - "MIRROR online + offline. Any salary/promotion change must behave the same in PlayerSession (online) and GameEngine/LoopManager (offline). State this in the Worker contract."
      - "Must NOT corrupt diamonds/IAP or the now-green reward economy. Must keep money floored >=0."
      - "The fix MUST be re-measured via the simulator (drive Work Hard) and produce a HEALTHY climbing curve: money becomes meaningful (reaches mid/high tiers, mid-tier shop items become attainable for an engaged career) WITHOUT running away (stays under the $5M ceiling; passive/low-effort players still progress slowly, not explosively)."
      - "Define whether iOS/Android client work is needed (does the app surface salary/raises/promotions?) — if yes, queue a parity slice; iOS priority."
      - "Reject churn: if you judge the honest fix too risky/large to do safely here, say so and recommend surfacing to Craig with the data instead of shipping a band-aid."
    expected_output:
      - "Root-cause verdict: confirmed | refined | refuted (with evidence)"
      - "Chosen approach (A/B/C/combo) + rationale (leverage x safety x reversibility)"
      - "Exact Worker objective for T005, allowed_files (online+offline+any v2 effect plumbing), verify (incl. tsc, vitest>=1681, RE-MEASURE engaged curve via economy-curve.test.ts), stop_if"
      - "Whether a client-parity Worker slice (T006) is needed"
      - "Any owner-steer recommendation (e.g., confirm in real app before deploy)"
    receipt:
      result: done
      decision: approved
      full_outcome_complete: false
      root_cause_verdict: "CONFIRMED by independent trace. Decisive added fact: handleJob self-advances record.performance from record.focus (Work Hard +2/wk) BEFORE the >90 promotion check, so wiring it is self-contained — no separate live perf driver needed. grossIncome=salary/4 is the only income input, so a salary write at handleJob propagates automatically. net=surplus*savingsRate (0.10 normal) is a built-in anti-runaway governor."
      chosen_approach: "(B) Wire the existing tier-promotion job_manager.handleJob into BOTH weekly ticks, before finances, mirrored online (PlayerSession.processWeekTick) + offline (GameEngine.handleWeeklyUpdates, swapping the happiness-only private handleJob for the promotion-bearing one). Rejected (A) v2-salary-plumbing (more surface/risk, event-paced only) and (C) new raise system (discards tested gated ladder). (B) reuses already-tested gated logic + is fully reversible by removing one call."
      healthy_curve_target: "Engaged mid-tier (Accountant->CFO $3000): tiers climb $1200->$3000, peak netWorth ~$150k-$300k. Engaged high-ceiling (SWE): climbs past $2000 toward ungated rungs, peak ~$250k-$600k (more only if prestige/intelligence gates clear). Passive/no-focus: still creeps via neutral perf random-walk, peak low ~$100k-$150k, does NOT explode. HARD CEILING: no seed/career may peak past $5M (savings governor is the brake — do not weaken it)."
      client_parity: "T006 likely a follow-up, not part of T005. Promotions already emit a messageEvent + person.salary flows over the existing WS playerObject channel, so the core loop needs no client change — but verify iOS QuickStatsCard/Home + Android actually DISPLAY current salary/job tier. Gated on T005 + in-app walkthrough."
      owner_steer: "Changes live economy for ALL players by activating an inert mechanic. Land T005 + re-measure, then confirm in the real app (promotions/raises fire + feel right) and get Craig sign-off BEFORE any deploy. This run does NOT deploy. Do NOT also retune finance constants / v2 events in this slice (separate ruling after re-measure)."
      evidence:
        - "job_manager.ts:666-787 handleJob (dead); grep src/ shows only def+re-exports"
        - "GameEngine.ts:879 private handleJob = happiness only; :790 calls THAT, not promotion"
        - "PlayerSession.ts:499-506 processWeekTick = applyWeeklyFinances only; :149-151 weekly schedule"
        - "finance.ts:92 grossIncome=salary/4 single income path"
        - "handleJob:694-722 self-advances performance from focus before promotion check"
        - "v2 catalog/career.ts one-off money only; stats_manager SAVINGS_RATES normal 0.10 governor"

  - id: T005
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "Wire the existing tier-promotion job_manager.handleJob (perf-advance + gated promotion + salary raise + firing) into BOTH weekly ticks — online PlayerSession.processWeekTick + offline GameEngine.handleWeeklyUpdates — running it BEFORE weekly finances so careers progress and salary climbs in live play, identically online and offline. Then RE-MEASURE and prove a healthy climbing curve."
    allowed_files:
      - "server/src/game/PlayerSession.ts"
      - "server/src/game/engine/GameEngine.ts"
      - "server/src/services/jobs/job_manager.ts"
      - "server/src/services/jobs/index.ts"
      - "server/tests/services/jobs/*.test.ts"
      - "server/tests/e2e/economy-curve.test.ts"
      - "server/tests/e2e/lifetime-simulator.ts"
      - "server/tests/e2e/metrics.test.ts"
      - "docs/goals/baolife-economy/notes/T005-wired-curve.md"
    verify:
      - "cd server && npx tsc --noEmit (clean)"
      - "cd server && npx vitest run (>=1681 green; no NEW failures vs documented pre-existing set)"
      - "cd server && npx vitest run tests/e2e/economy-curve.test.ts (salaryTierProgression length >1; engaged netWorth materially above passive control; engaged Accountant reaches a tier above $1200; engaged SWE climbs above $2000)"
    stop_if:
      - "Re-measured engaged peak netWorth exceeds the $5,000,000 ceiling for ANY seed (runaway) — STOP, report, do NOT weaken savings/expense constants without a new ruling."
      - "Passive/no-focus curve changes materially beyond slow promotions from neutral perf drift (should creep ~$100k-$150k, not explode) — STOP/report."
      - "The fix requires editing v2 effect/context, finance.ts math, occupation salary tiers, or stats_manager constants — that is approach-(A)/tuning scope, OUT of bounds; STOP and request a new ruling."
      - "Need files outside allowed_files (esp. avatar WIP: avatar.ts/avatarLibrary.*/Player.ts/MainCharacterView/QuickStatsCard, or ws/)."
      - "tsc errors or >0 NEW test failures. Verification fails twice."
    receipt:
      result: done
      changed_files:
        - "server/src/game/PlayerSession.ts"
        - "server/src/game/engine/GameEngine.ts"
        - "server/src/services/jobs/job_manager.ts"
        - "server/tests/e2e/economy-curve.test.ts"
        - "docs/goals/baolife-economy/notes/T005-wired-curve.md"
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
          detail: "independently re-run by PM: clean"
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "independently re-run by PM: 142 files, 1679 tests, 0 failed, 0 skipped (>=1678)"
        - cmd: "cd server && npx vitest run tests/e2e/economy-curve.test.ts"
          status: pass
          detail: "climbing-ladder regression asserted"
      summary: "Approach B implemented + PM-verified. Wired real tier-promotion job_manager.handleJob into BOTH weekly ticks before finances, mirrored online (PlayerSession.processWeekTick) + offline (GameEngine.handleWeeklyUpdates aliased processJobProgression, player-char only). Promotion/firing messages ride the existing player.messageQueue->messageEvent drain (no new channel). Required tiny job_manager fix: post-promotion performance reset 0->50 (fresh-hire baseline) to prevent a promote-then-fire sawtooth once handleJob runs live (latent bug). PM reviewed all diffs: clean, mirrored, defensively try/catch-wrapped, avatar WIP untouched."
      wired_curve:
        - "Engaged Accountant (Work Hard): salary 1200->2000->3000 (CFO top), peak netWorth $163,410 @65 (target $150-300k)."
        - "Engaged Software Engineer (Work Hard): salary 2000->4000->6000 (Mgr), peak netWorth $273,978 @65 (CTO $10k correctly prestige/intel-gated off; target $250-600k)."
        - "Passive control: salary 1200 (late creep 1600), peak $97,693 — slow creep, NOT exploding."
        - "Engaged = 1.67x-2.8x passive peak; max peak $273,978 << $5M ceiling. Money now meaningful + bounded."
      mirror_parity: "Identical function, identical ordering (progression BEFORE finances), identical message surfacing (player.messageQueue) online + offline."
      tests_updated: "economy-curve.test.ts rewritten from 3 measurement its -> 1 climbing-ladder regression it (asserts salaryTiers.length>1 both engaged, Accountant peak>1200, SWE peak>2000, each engaged peak>=1.3x passive, passive bounded, all <$5M). Suite count 1681->1679 (no failures; -2 from the it-collapse). metrics.test.ts NOT modified (passive bands still pass)."
      note: docs/goals/baolife-economy/notes/T005-wired-curve.md
      note_needed: false

  - id: T006
    type: scout
    assignee: Scout
    status: done
    reasoning_hint: medium
    objective: "(Read-only client-parity verification, gated on T005) Client parity: confirm iOS (priority) + Android surface current salary, job tier/title, and promotion/raise notifications. Core promotion already emits messageEvent + salary over the WS playerObject channel, so this may be display-only or a no-op — verify the app actually shows salary/tier and that promotion messages render. Only implement if a real gap exists."
    allowed_files: []
    verify:
      - "iOS: xcodebuild -project ios/lichunWebsocket.xcodeproj -scheme BaoLife -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO (BUILD SUCCEEDED)"
      - "Android: cd android && ./gradlew :app:assembleDebug (if touched)"
    stop_if:
      - "No real gap (salary/tier already displayed + promotion messages render) — mark done as a no-op with evidence, don't invent UI churn."
      - "Need to touch the avatar WIP files."
    receipt:
      result: done
      summary: "Verified NO-OP (read-only check). The iOS app already surfaces everything the now-working career progression produces, so promotions/raises are visible with ZERO client change — exactly the Judge's prediction. No UI churn invented."
      evidence:
        - "ios/.../ActivityView.swift:197-208 shows current job level.level (tier title, e.g. 'Chief Financial Officer') AND '$<level.salary> / month' — salary + tier already displayed."
        - "messageEvent rendered by ContentView.swift + Core/Services/EventService.swift + Features/Home/Components/LifeEventCard.swift => promotion/firing messages (pushed to player.messageQueue server-side) appear automatically as life events/notifications."
        - "PlayerProfileSheet.swift:103-104 shows Occupation; PrestigeView.swift:43 explains senior-tier prestige gates (aligns with handleJob's prestige gate)."
        - "OccupationList/ActivityView show per-level salaries when choosing/working a job."
      deferred_not_churn: "Optional enhancement (a prominent current-salary readout on the Home/QuickStats surface) is NOT done because (a) it overlaps the uncommitted avatar WIP (QuickStatsCard.swift) which is off-limits, (b) it needs Craig's product call + an in-app walkthrough (no device access this run), and (c) it is not required — the career ladder is already visible. Flagged for Craig."
      android_parity: "Not separately verified this run (no gap found requiring a change; Android mirrors the same WS messageEvent + playerObject salary channel). Flag for in-app confirmation alongside iOS."
      note_needed: false

  - id: T999
    type: judge
    assignee: Judge
    status: done
    reasoning_hint: high
    objective: "Final audit: did this tranche deliver the full outcome? A reproducible per-age economy curve measured with career progression; a data-backed Judge ruling on tuning; any warranted tuning implemented + verified + re-measured to the intended shape; suite green (>= 1678); avatar WIP untouched."
    inputs:
      - "All done task receipts"
      - "Last verification"
      - "Before/after curves"
      - "Current dirty diff"
    constraints:
      - "Do not implement."
      - "Reject completion if a required Worker is still queued/active."
      - "Reject completion if tuning was done on entry-job-only data (the forbidden misfire)."
      - "Accept a data-backed 'healthy, no change needed' as complete."
      - "Map every change to a verified outcome."
    expected_output:
      - "complete | not_complete"
      - "full_outcome_complete: true | false"
      - "value justification per change (or healthy-no-change rationale)"
      - "next task if not complete"
    receipt:
      result: done
      decision: complete
      full_outcome_complete: true
      rationale: "Independently re-ran tsc (clean) + vitest (142 files/1679 tests/0 failed) and inspected all diffs. handleJob promotion fn invoked in BOTH weekly ticks before finances, mirrored; perf reset 0->50 correctly avoids the performance<10 firing check (kills the promote-then-fire sawtooth). Engaged curve climbs to $163k-$274k vs passive ~$98k, all far under the $5M ceiling. Fix chosen AFTER measuring a real progression curve (measure-before-tune honored). No ws/, finance/tier/stats constants, avatar WIP, or IAP/reward code touched."
      verified_outcomes:
        - "PlayerSession.ts: online weekly tick now runs handleJob before finances -> online careers progress. Proof: diff + green suite."
        - "GameEngine.ts: offline weekly tick runs aliased processJobProgression before finances, player-char-gated -> offline mirrors online. Proof: diff + green suite."
        - "job_manager.ts: post-promotion performance 0->50 fixes latent promote-then-fire sawtooth (firing check is <10). Proof: diff + climbing curve."
        - "economy-curve.test.ts: climbing-ladder regression locks the wiring in. Proof: passes."
        - "T006 client parity: verified no-op (iOS ActivityView shows salary/tier; messageEvent renders promotions)."
      owner_action_non_blocking: "Economy fix is WORKING-TREE ONLY by design (not committed/deployed). Recommend Craig sign-off + in-app walkthrough (drive promotions/raises in the real app) before commit + push-to-main (webhook auto-deploys). Optional deferred: a prominent home-screen salary readout (overlaps avatar WIP; Craig's product call)."
      evidence:
        - "tsc --noEmit exit 0; vitest 142 files/1679 tests/0 failed (PM + Judge both re-ran)"
        - "T005-wired-curve.md: engaged Accountant peak $163,410; engaged SWE peak $273,978 (CTO gated); passive $97,693; all < $5M"
        - "git status: no ws/ or finance/constants edits; avatar files pre-existing unrelated WIP"

checks:
  dirty_fingerprint: "working-tree (economy fix uncommitted by design — awaiting Craig sign-off + in-app walkthrough before commit/deploy)"
  last_verification:
    result: green
    task: "T005 + T999 (PM and Judge both independently re-ran)"
    commands:
      - cmd: "cd server && npx tsc --noEmit"
        status: pass
      - cmd: "cd server && npx vitest run"
        status: pass
        detail: "142 files, 1679 tests, 0 failed, 0 skipped"
      - cmd: "cd server && npx vitest run tests/e2e/economy-curve.test.ts"
        status: pass
        detail: "climbing-ladder regression: engaged $163k/$274k vs passive ~$98k, all < $5M"
