# GoalBuddy v2 state.yaml
# Board truth lives here. goal.md is the editable charter; notes/PROGRESS.md holds the living release-readiness checklist.

version: 2

goal:
  title: "BaoLife — Ship It: Fun, Playable, Complete, Releasable"
  slug: "baolife-release-ready"
  kind: open_ended
  tranche: "Apex campaign: continuously assess the four dimensions (fun/playable/complete/releasable) across all domains, pick the highest-leverage gap, execute a focused tranche (spawn sub-goals/waves as needed), verify + ship, until a release-readiness audit + Craig's launch sign-off."
  status: active
  oracle:
    signal: "Four dimensions proven by in-app walkthroughs + green builds + a maintained release-readiness checklist: FUN (engaging balanced loop), PLAYABLE (every flow works end-to-end iOS+Android, no broken/dead features, no crashes), COMPLETE (avatar shipped, core systems wired+surfaced, Android parity), RELEASABLE (store compliance, working monetization+IAP receipt validation, stability, store assets)."
    cadence: "assess periodically; verify after each tranche ships; final audit + Craig launch sign-off"
    final_proof: "All four dimensions met with evidence + checklist release-blockers cleared + Craig signs off to launch => full_outcome_complete: true."
  intake:
    original_request: "Set a higher-level goal to work autonomously across domains toward a fun, playable, complete, releasable game."
    interpreted_outcome: "Drive BaoLife to launch-ready across every domain, autonomously, until Craig signs off."
    input_shape: open_ended
    audience: "BaoLife players + Craig (shippable store product)"
    authority: approved
    proof_type: demo
    completion_proof: "Four-dimension oracle met + checklist blockers cleared + Craig launch sign-off; final audit full_outcome_complete: true."
    likely_misfire: "Endless polish/churn instead of clearing release-blockers; shipping a regression; silently making content/policy/launch/data decisions; gold-plating one domain while another is broken; scope-creeping net-new features beyond completing the intended game."
    blind_spots_considered:
      - "Android has had little hands-on validation — likely the weakest pillar."
      - "Avatar feature is half-built + uncommitted — blocks 'Complete'."
      - "IAP server-side receipt validation is a stub."
      - "Release needs store assets/metadata/privacy/stability/age-rating, not just code."
      - "'Fun' is subjective — needs real play, not just green tests."
    existing_plan_facts:
      - "Pillars shipped: balance/survival (feel-pacing/fun-balance/polish-plus), economy db3dd13b, systems-integrity 322b9c33/72a4f0a8, in-app validation+content-safety (98bf9d1a/27a44532/c03d4e71/4d1af840)."
      - "Open pillars: avatar feature (uncommitted WIP), Android parity, IAP receipt validation, release-ops, deferred passive quests."
      - "Authorized to ship VERIFIED work across domains; PAUSE for content-policy/production-data/launch/net-new-feature-design."
  current_baseline: "HEAD b33bb862; tsc clean; vitest 151 files / 1718 tests / 0 failed."

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-release-ready"

active_task: T014

tasks:
  - id: T001
    type: scout
    assignee: Scout
    status: done
    reasoning_hint: high
    objective: "RELEASE-READINESS ASSESSMENT. Score BaoLife across the four dimensions (Fun / Playable / Complete / Releasable) and enumerate the gaps per domain (server TS, iOS, Android, content/balance, monetization/IAP, avatar feature, retention, UX/onboarding, release-ops/store/compliance/stability). Use: the prior goal boards + memory (what's shipped/validated), code inspection for wiring/completeness/dead-code/parity, and note where a real in-app (iOS) or Android build+drive is needed to judge. Produce a prioritized RELEASE-READINESS CHECKLIST: each item tagged release-blocker | should-fix | nice-to-have, with the domain + evidence + rough size. Identify the SINGLE highest-leverage next tranche (likely: finish the avatar feature [Complete], or an Android build+parity pass [Playable], or IAP receipt validation [Releasable]) + why."
    inputs:
      - "docs/goals/{baolife-economy,baolife-mechanics,baolife-release-hardening,baolife-app-validation}/ + memory project_* notes"
      - "Working tree: uncommitted avatar WIP (avatar.ts, avatarLibrary.*, Player.ts hunk, ios MainCharacterView/QuickStatsCard)"
      - "android/ (parity state), server/src/monetization (IAP), iOS app + Android build"
    constraints:
      - "Read-only. Concrete evidence per checklist item. Tag release-blocker vs should-fix vs nice-to-have honestly (avoid gold-plating)."
      - "Cover BREADTH across all four dimensions + all domains before going deep. Flag where hands-on (iOS/Android drive) is required to judge."
      - "Do not start implementation. Flag content/policy/launch/net-new-feature-design items for Craig."
    expected_output:
      - "Four-dimension scorecard + per-domain gaps"
      - "Prioritized release-readiness checklist (blocker/should-fix/nice-to-have)"
      - "Recommended single highest-leverage next tranche + rationale"
      - "Owner/launch decisions to surface"
    receipt:
      result: done
      summary: "4-dim scorecard: FUN/PLAYABLE mostly-there (need real drive); COMPLETE/RELEASABLE weak. TOP BLOCKER (corrected from intake): IAP grants diamonds with NO receipt validation — validateIAPReceipt() at monetization/validation.ts:265 is FULLY IMPLEMENTED+tested but has ZERO callers; handler purchases.ts:129 is a comment then purchaseInAppItem (shop_manager.ts:625) unconditionally grants. Forged productId = free diamonds. 2nd blocker: no iOS PrivacyInfo.xcprivacy. Avatar library functional but uncommitted + skewed assets (32 eastasian/4 black/0 white/latino/southasian). Android (200 kt) unvalidated since 1a51fbea. tsc clean incl avatar WIP."
      recommended_tranche: "Wire IAP receipt validation (validateIAPReceipt into handleInAppPurchase before grant; add env shared-secret config + Google Play verify path) + co-ship iOS PrivacyInfo.xcprivacy. Both RELEASABLE blockers, cheap, exploitable."
      owner_decisions:
        - "Avatar: finish+commit curated-library migration now, or ship DiceBear + defer? + approve expanding asset coverage to all ethnicities (assets already generated in POC)."
        - "IAP secrets: App Store Connect shared secret + Google Play service-account creds (production credential — Craig only)."
        - "Android: in-scope for launch (parity) or iOS-first? Determines if Android build+drive tranche needed."
        - "Offline event-catalog gap: accept as known edge case (current deferred stance) or fix."
        - "Store submission: age-rating answers, store copy/screenshots (Craig authors/approves)."
      needs_real_drive:
        - "FUN cadence/reward-feel + crash-free stability — in-app iOS drive."
        - "Android parity — build + emulator/device drive to size."
      evidence:
        - "server/src/handlers/purchases.ts:129"
        - "server/src/services/shop/shop_manager.ts:625-636"
        - "server/src/monetization/validation.ts:265 (validateIAPReceipt, no callers)"
        - "ios/ has no PrivacyInfo.xcprivacy"
        - "server/src/services/character/avatarLibrary.ts (uncommitted)"
        - "android/ANDROID_PARITY_TASKS.md, CODE_REVIEW_ISSUES.md"
      contradictions_corrected:
        - "IAP validation is UNWIRED, not a stub — validator exists at validation.ts:265 (intake said purchases.ts stub)."
        - "iOS WIP diff (MainCharacterView/QuickStatsCard) is a health-% display fix, NOT avatar UI."

  - id: T002
    type: judge
    assignee: Judge
    status: done
    reasoning_hint: high
    objective: "From the assessment, pick the highest-leverage next tranche (prefer release-blockers; balance breadth over depth) and define how to execute it — a Worker contract for a contained slice, OR spawn/continue a focused domain sub-goal/wave for multi-step work. Confirm intent + scope; flag owner/launch decisions."
    inputs:
      - "T001 assessment + checklist"
    constraints:
      - "Do not implement. Ground the pick in the checklist + the four-dimension oracle. Reject churn/gold-plating. Require green-before-ship + in-app proof where visual + online/offline mirror where loop-related."
    expected_output:
      - "Chosen tranche + rationale (which dimension it advances)"
      - "Worker contract OR sub-goal/wave plan; owner decisions to surface"
    receipt:
      result: done
      decision: "APPROVE Scout tranche with tightened design: wire env-gated IAP receipt validation into handlePurchaseInAppItem (fail-closed when enabled, DEFAULT DISABLED = current behavior preserved). Co-ship privacy manifest only if grounded in a real data-collection audit (PM scoped this — no fabricated declarations)."
      full_outcome_complete: false
      rationale: "Advances RELEASABLE (top blocker). GROUND-TRUTH CORRECTION: iOS sends only {productId}, NO receiptData/transactionId (Constants.swift:216); validateIAPReceipt requires all three. So a fully-enforced path is BLOCKED on an iOS client change + Craig's APPLE_SHARED_SECRET. Largest SAFE server-only slice = env-gated wiring: flag OFF (default) preserves today's behavior so 1714 tests + dev + prod stay green and Apple is never hit; flag ON requires receipt+transactionId, calls validateIAPReceipt (which does idempotency + awardDiamonds itself — do NOT also call the unguarded purchaseInAppItem), fails closed on missing/invalid."
      owner_decisions:
        - "APPLE_SHARED_SECRET + Google Play creds (Craig-only) block ONLY the prod-enable step, not this slice."
        - "Flipping IAP_VALIDATION_ENABLED=true in prod is a launch decision Craig owns; Worker must NOT enable it."
      deferred:
        - "iOS client change to actually send receiptData+transactionId (required before validation can be enabled)."
        - "Android transactionId/purchaseToken -> validation mapping."
        - "Avatar library WIP (owner decision); Android device-build parity; store-ops assets."
      evidence:
        - "ios/.../Constants.swift:216 — IAP payload is {productId} ONLY"
        - "server/src/monetization/validation.ts:265 — validateIAPReceipt(playerId, receiptData, transactionId, productId), calls Apple network"
        - "server/tests/monetization/monetization.test.ts:253 — 'Receipt Validation' tests are local/fake, won't break"
        - "server/src/config.ts — no IAP_VALIDATION flag exists yet"

  - id: T003
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "Wire env-gated IAP receipt validation into the in-app purchase handler. Add config flag IAP_VALIDATION_ENABLED (boolean, default false). Flag OFF = current behavior byte-for-byte unchanged. Flag ON = require receiptData+transactionId in payload, call existing validateIAPReceipt (it awards diamonds + idempotency itself — do NOT also call purchaseInAppItem), fail CLOSED (grant nothing) on missing receipt/transactionId or validation failure; on success savePlayer + send player object + inAppPurchaseComplete. Add tests (mock the Apple fetch — NO real network) covering: flag-OFF preserves grant; flag-ON missing-receipt rejects with no grant; flag-ON dispatches to validateIAPReceipt. SEPARATELY: audit the iOS app's ACTUAL data collection + required-reason API usage (grep for analytics/crash SDKs, UserDefaults, file timestamps, device IDs) and author ios/lichunWebsocket/PrivacyInfo.xcprivacy declaring ONLY what the app truly does — NO fabricated SDKs. Check whether the Xcode project uses file-system-synchronized groups (auto-include) or needs explicit target membership; place the file correctly or flag the membership step in the receipt."
    allowed_files:
      - "server/src/handlers/purchases.ts"
      - "server/src/config.ts"
      - "server/tests/handlers/purchases.iap-validation.test.ts"
      - "server/.env.example"
      - "ios/lichunWebsocket/PrivacyInfo.xcprivacy"
    verify:
      - "cd server && npx tsc --noEmit"
      - "cd server && npx vitest run"
      - "cd server && git diff --stat"
    stop_if:
      - "Any test in the ~1714 baseline regresses, or tsc --noEmit is not clean."
      - "The change requires editing Player.ts, avatar.ts, avatarLibrary.ts, avatarLibrary.manifest.json, MainCharacterView.swift, or QuickStatsCard.swift (uncommitted avatar WIP)."
      - "The change requires editing ws/ (legacy) or modifying the iOS .xcodeproj/existing Swift files (manifest must be standalone new file; if target membership needs a pbxproj edit, STOP and flag it instead)."
      - "A test on the enabled path would make a real network call to Apple/Google instead of a mock."
      - "The flag defaults to enabled, or the disabled-path behavior changes in any way."
      - "You'd need to set/hardcode a real APPLE_SHARED_SECRET or Google credential, OR you cannot ground a privacy-manifest declaration in actual code (then author only the grounded subset + flag the rest)."
    receipt:
      result: done
      commit: b33bb862
      changed_files:
        - server/src/handlers/purchases.ts
        - server/src/config.ts
        - server/.env.example
        - server/tests/handlers/purchases.iap-validation.test.ts
        - ios/lichunWebsocket/PrivacyInfo.xcprivacy
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "151 files / 1718 tests / 0 failed (+4 new, no regressions)"
        - cmd: "git push origin main"
          status: pass
          detail: "b984bde1..b33bb862 — webhook deploy (behavior-neutral, flag default OFF)"
      summary: "Wired env-gated IAP receipt validation (config.IAP_VALIDATION_ENABLED default OFF; flag ON requires receipt+transactionId, calls validateIAPReceipt, fails closed, no double-grant). Authored grounded iOS PrivacyInfo.xcprivacy (UserDefaults CA92.1, tracking false, zero collected-data types). PM cleanup: removed pre-existing dead PurchaseItemPayload interface, merged duplicate import, added sex:'Female' to test fixture."
      flags:
        - "PRIVACY TARGET MEMBERSHIP: pbxproj has no PBXFileSystemSynchronizedRootGroup, so PrivacyInfo.xcprivacy will NOT auto-join the build target. Craig must add it to the lichunWebsocket target's membership in Xcode before submission."
        - "DEFERRED (owner-gated): prod-enable needs (1) iOS build sending receiptData+transactionId, (2) APPLE_SHARED_SECRET, (3) IAP_VALIDATION_ENABLED=true. Worker must not enable."
        - "PRIVACY FIREBASE: when Craig adds Firebase SPM dep, manifest MUST declare its collected data types + re-review NSPrivacyTracking."

  - id: T004
    type: scout
    assignee: Scout
    status: done
    reasoning_hint: medium
    objective: "IN-APP iOS VALIDATION DRIVE (FUN + PLAYABLE). Build the iOS app, boot the sim, and drive the surfaces NOT yet validated in the prior loop: Store item purchases, in-app Dating/Social actions (Date/Gift/Chat), daily rewards + quests UI, the 'Do an Activity' stat loop, Messaging/Chat, Profile sheet deep-dive, Settings (data export / account deletion UI). Find UX/display/correctness/crash bugs tests miss; root-cause each with file:line. Slow/stop game time first (event cadence outruns navigation). Tag each finding blocker|should-fix|nice-to-have. Read-only — produce findings, not fixes."
    inputs:
      - "memory project_inapp_validation_loop.md (setup: xcodebuild scheme BaoLife, mobile-mcp device=UUID, app hits PRODUCTION)"
      - "docs/goals/baolife-app-validation/notes/PROGRESS.md ('Not yet driven in-app' list)"
    constraints:
      - "Read-only. App hits production backend — do NOT mutate prod data destructively (no real account deletion execution; inspect the UI/flow only)."
      - "Flag content/policy/launch/production-DATA items for Craig. Do not start fixes (Judge picks the fix tranche)."
    expected_output:
      - "Per-surface validated-clean vs bug list with file:line evidence + severity"
      - "Recommended next fix tranche if blockers found"
    receipt:
      result: done
      summary: "iOS drive pass (real prod backend, char 'Mia TesterV'). Confirmed 2 surfaces CLEAN + found 1 behavioral gap (owner-design call). Drove: dilemma event, Shop. Remaining un-driven (next pass): Activities/Do-an-Activity, Social/Dating, daily quests/rewards UI, Messages, Profile/Settings."
      findings:
        - "[should-fix / OWNER DESIGN CALL] Event/dilemma money-cost choices are NOT affordability-gated -> arbitrary NEGATIVE money. Drove it: char had money 0, chose 'Hire an expensive lawyer' (5,000 money) -> balance went to -5,000 (UI shows red -5,000); +10 happiness still applied. Root cause effects.ts:50-52 — money delta applied with no floor/gate, UNLIKE diamonds in the SAME function (effects.ts:53-68, unaffordable spend no-ops) and UNLIKE shop_manager.ts:509 (money<price -> 'Insufficient funds'). DESIGN Q for Craig: gate (no-op like diamonds — but reintroduces dead choices for low-money players), floor at 0, or ALLOW DEBT (likely intent — why these are money not diamonds; realistic life-sim debt) + signal it in UI? Downstream impact of negative money UNVERIFIED."
        - "[CLEAN] Dilemma choice costs are money/energy, NOT diamonds (Hire 5000money / Settle 2000money / Represent 50energy / Public-defender free) — confirms 58d0d3c4 deployed correctly."
        - "[CLEAN] Shop affordability gating works: shop_manager.ts:509 rejects money<price with 'Insufficient funds'; handler purchases.ts:104 sends {type:error}; iOS WebSocketService.swift:438 toasts it. (No silent failure.)"
      evidence:
        - "server/src/events/v2/engine/effects.ts:50-52 (money, ungated) vs :53-68 (diamonds, gated)"
        - "server/src/services/shop/shop_manager.ts:509 (shop money gate)"
        - "server/src/handlers/purchases.ts:104-108 (error surfaced)"
        - "ios WebSocketService.swift:438-440 (error -> toast)"
      owner_decisions:
        - "Negative-money on event choices: gate / floor-at-0 / allow-debt(+UI signal)? (game-design call)"
      recommended_next: "Surface the negative-money design call to Craig with the broader owner decisions; meanwhile a focused fix tranche can floor/gate once design is chosen. Continue the iOS drive on the remaining un-driven surfaces in a follow-up pass."

  - id: T005
    type: pm
    assignee: PM
    status: done
    reasoning_hint: medium
    objective: "Checkpoint: surface accumulated owner/design decisions to Craig (avatar scope, Android scope, IAP secret/prod-enable, store submission, negative-money design call) and take his steer on the next tranche."
    constraints:
      - "Do not make content/policy/launch/production-DATA/net-new-design decisions for Craig — present options + recommendation."
      - "Keep doing safe local work if Craig is unavailable; do not set goal.status blocked for missing owner input."
    expected_output:
      - "Owner decisions surfaced + Craig's steer recorded"
      - "Next active tranche selected"
    receipt:
      result: done
      summary: "Surfaced owner decisions + got Craig's steer via AskUserQuestion."
      craig_steer:
        next_tranche: "Finish iOS drive + fix bugs (continue the in-app validation; fix what surfaces, incl. negative-money)."
        negative_money: "GATE it to match the shop (start simple). Craig noted a proper debt-handler may come later if other systems need negative balances, but start with shop-style gating now."
      still_pending_owner_decisions:
        - "Avatar scope (finish curated library now vs ship DiceBear + defer) + asset coverage expansion — NOT yet answered."
        - "Android launch scope (parity vs iOS-first) — NOT yet answered."
        - "IAP APPLE_SHARED_SECRET + store submission assets/age-rating — Craig provides when ready (does not block current work)."

  - id: T006
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "Fix the negative-money bug on event/dilemma choices by GATING affordability to match the shop (Craig's call). TWO parts of one bug: (A) SERVER authoritative gate in events/v2/engine/respond.ts: in EventResponder.respond, after resolving the chosen `choice` and BEFORE answering/applying, compute the effective money delta the SAME way applyChoiceEffects does (baseEffects.resources.money if defined, else choice.moneyCost ? -choice.moneyCost : 0). If that delta < 0 AND player money + delta < 0, return buildEventError({ code: 'INSUFFICIENT_FUNDS', message, eventId, instanceId }) and apply NOTHING (do not call answerEventInstance/applyChoiceEffects/resolveEventInstance) — mirrors shop_manager.ts:509 reject-whole semantics. Read player money from player.c.money via the asNumber-style coercion already used in effects.ts. Money ONLY (energy already floors at 0; diamonds already no-op when unaffordable — do NOT change those). (B) CLIENT UX gate in ios EventModalView.swift: a money choice the player cannot afford (answer.moneyCost > webSocketService.person.money) must be DISABLED (greyed/dimmed, non-tappable) so the optimistic deduct at lines 478-479 never drives the client negative; and handleAnswerSelection must early-guard unaffordable money choices (show an error/toast, do not send/deduct). Match the documented canAfford pattern (energy uses person.calcEnergy >= cost). Money only."
    allowed_files:
      - "server/src/events/v2/engine/respond.ts"
      - "server/src/events/v2/engine/EventEngine.ts"
      - "server/tests/events-v2/respond.insufficient-funds.test.ts"
      - "server/tests/e2e/lifetime-simulator.ts"
      - "ios/lichunWebsocket/Features/Events/Views/EventModalView.swift"
    verify:
      - "cd server && npx tsc --noEmit"
      - "cd server && npx vitest run"
      - "cd server && git diff --stat"
    stop_if:
      - "Any test in the ~1718 baseline regresses, or tsc --noEmit is not clean."
      - "The change requires editing effects.ts, EventEngine.ts (passive path — forced events should NOT be affordability-gated), Player.ts, or the avatar WIP files."
      - "The change requires editing ws/ (legacy)."
      - "You'd need to change diamond or energy gating behavior (out of scope — money only)."
      - "The server gate would also block net-positive or affordable money choices (test it both ways), or block non-money choices."
    receipt:
      result: done
      commit: 6eb29958
      changed_files:
        - server/src/events/v2/engine/respond.ts
        - server/src/events/v2/engine/EventEngine.ts
        - server/tests/events-v2/respond.insufficient-funds.test.ts
        - server/tests/e2e/lifetime-simulator.ts
        - ios/lichunWebsocket/Features/Events/Views/EventModalView.swift
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "152 files / 1722 tests / 0 failed"
        - cmd: "xcodebuild ... BaoLife build"
          status: pass
          detail: "BUILD SUCCEEDED"
        - cmd: "git push origin main + webhook deploy"
          status: pass
          detail: "prod HEAD 6eb29958, service restarted 17:00:51 UTC"
      summary: "SHIPPED + deployed + IN-APP VERIFIED. Server INSUFFICIENT_FUNDS gate (respond.ts), envelope enrichment (EventEngine.ts surfaces effects-based money into per-choice moneyCost — also fixes the 'cost shows 0' display bug), iOS disabled-choice gate (EventModalView.swift), e2e affordability-aware pickChoice. 152 files/1722 tests/0 failed. In-app (char at -4,925): free/energy/money-reward choices ENABLED + work (money +100 applied); unaffordable money choices DISABLED (taps no-op). Gate confirmed working end-to-end."
      in_app_finding: "SOFT-LOCK discovered: the 'family reunion' dilemma has ONLY money options ($200/$50 via effects.resources.money). At -4,925 BOTH are disabled by the client gate → player stuck on a paused question with no affordable choice + no dismiss. My own change turned a go-into-debt dilemma into a dead-end. Only affects the NEW (unshipped) iOS client; current prod (old client) avoids the hard lock (optimistic question removal) but may refire the question. Spawned T007 to fix."

  - id: T007
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "Fix the soft-lock the money gate introduced: when EVERY choice in an event is an unaffordable money spend, the player gets stuck (all choices disabled, game paused on the question). Refine the gate so a money choice is blocked ONLY IF at least one OTHER choice is affordable; if ALL choices are unaffordable, leave them enabled so the player can always proceed (into debt — Craig sanctioned 'sometimes allow negative money'). Apply in BOTH: (A) SERVER respond.ts — before returning INSUFFICIENT_FUNDS, check definition.choices: compute each choice's net money delta (effects.resources.money else -moneyCost else 0); a choice is affordable if NOT (delta<0 && currentMoney+delta<0). Only reject the chosen choice if some OTHER choice is affordable; if none affordable, allow it (apply normally, money may go negative). (B) iOS EventModalView.swift — compute anyAffordable = answers.contains(canAffordMoney); disable/guard an unaffordable money choice ONLY when anyAffordable is true. Update tests: the existing 'unaffordable rejects' unit test must include an affordable alternative choice; ADD a test 'all choices unaffordable -> chosen choice is ALLOWED (money goes negative, not rejected)'. Keep diamond/energy untouched."
    allowed_files:
      - "server/src/events/v2/engine/respond.ts"
      - "server/tests/events-v2/respond.insufficient-funds.test.ts"
      - "ios/lichunWebsocket/Features/Events/Views/EventModalView.swift"
    verify:
      - "cd server && npx tsc --noEmit"
      - "cd server && npx vitest run"
      - "cd server && git diff --stat"
    stop_if:
      - "Any test regresses, or tsc not clean."
      - "Requires editing effects.ts/EventEngine.ts/Player.ts/avatar WIP/ws."
      - "You'd change diamond/energy gating (money only)."
      - "The 'all unaffordable -> allowed' path would also let an AFFORDABLE-alternative case slip through (must still reject when an affordable alternative exists)."
    receipt:
      result: done
      commit: 8c759cef
      changed_files:
        - server/src/events/v2/engine/respond.ts
        - server/tests/events-v2/respond.insufficient-funds.test.ts
        - ios/lichunWebsocket/Features/Events/Views/EventModalView.swift
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "152 files / 1723 tests / 0 failed (+1 new all-unaffordable test)"
        - cmd: "rm -rf /tmp/baolife-dd && xcodebuild ... clean build"
          status: pass
          detail: "BUILD SUCCEEDED (clean build required — incremental build had silently skipped EventModalView)"
        - cmd: "git push origin main + webhook deploy"
          status: pass
          detail: "prod HEAD 8c759cef"
      summary: "Refined the gate to only block a money choice when an affordable alternative exists (server: definition.choices.some(isAffordable); iOS: anyChoiceAffordable()). Genuine all-unaffordable case allows the spend (debt fallback) — unit-tested."
      in_app_verification: "VERIFIED on the 'familyArc.familyReunion' dilemma (char money -4,925). It has 4 choices: organize($200/⚡15), attend($50/⚡5), 'Show up briefly'(free), 'Skip it this year'(free). In-app: the two MONEY options render DIMMED + DISABLED (taps no-op); the two FREE options render vibrant + ENABLED; tapping 'Show up briefly' resolved (+3 happiness/+3 social). Gate works exactly as designed."
      misdiagnosis_corrected: "My earlier 'SOFT-LOCK' finding (T006 receipt) was WRONG: the family-reunion dilemma DOES have free options (brief/skip) — they were below the fold and I hadn't scrolled. The gate correctly disables only the money options because affordable alternatives exist. NO soft-lock. T006+T007 code is correct as shipped; T007 is still valuable for a genuine all-money dilemma (covered by the new unit test)."
      residual_ux_finding: "REAL minor UX (should-fix, not blocker): a 4-choice dilemma does not fit on screen — the 2 free/affordable options were below the fold while the 2 visible options were disabled, so a player could think they're stuck before scrolling. Consider: order affordable/enabled choices first, add a scroll affordance, or fit the layout. Logged for a later UX tranche."
      build_gotcha: "Xcode incremental build (-derivedDataPath build) SILENTLY skipped recompiling the edited EventModalView.swift — BUILD SUCCEEDED but the binary ran OLD code, making my first in-app check test stale behavior. Had to rm -rf derivedData + clean build. (strings|grep for a private Swift symbol is NOT a reliable staleness check — symbols are mangled/stripped.) See memory project_inapp_validation_loop.md."

  - id: T008
    type: pm
    assignee: PM
    status: done
    reasoning_hint: medium
    objective: "Continue the in-app iOS validation drive (Craig's steer: finish drive + fix bugs) on the remaining un-driven surfaces: Activities/'Do an Activity' stat loop, Social/Dating actions (Date/Gift/Chat), daily rewards + quests UI, Messaging/Chat, Profile sheet, Settings (data export / account-deletion UI). Slow/stop time to beat event cadence. Find UX/display/correctness/crash bugs; root-cause + fix safe ones (commit/deploy server, rebuild iOS w/ CLEAN build + verify in-app). Also log the dilemma off-screen-choices UX residual for a later polish tranche. Surface owner decisions; don't block on them."
    constraints:
      - "Green-before-ship; CLEAN iOS builds before in-app verification (incremental builds silently go stale). App hits PRODUCTION — deploy server changes before in-app verify."
      - "Don't accidentally tap 'Start a New Life' (it wipes the test char). Verify layout coords before tapping near it."
      - "Pause for content/policy/production-DATA/launch/net-new-design decisions."
    expected_output:
      - "Per-surface clean/bug findings + any shipped fixes"
      - "Updated checklist + owner decisions"
    receipt:
      result: done
      craig_steer: "Deletion cancel = BOTH (auto-cancel on login + explicit button + scheduled banner). Daily quests = WIRE the dead hooks (carefully — double-count hazard). Next pillar = keep polishing iOS/server (defer avatar/Android)."
      summary: "Switched from high-friction live driving to a code-inspection Scout for the remaining surfaces. Scout found 2 concrete bugs; 4 surfaces wired-clean. Got Craig's steer; spawned T009 (deletion cancel) + T010 (wire quest hooks) + T011 (dilemma off-screen UX, queued)."
      scout_findings:
        - "[should-fix, Releasable/trust] Account-deletion CANCEL is dead end-to-end: handleCancelAccountDeletion (data.ts:145) NOT registered in COMMAND_REGISTRY (handlers/index.ts); iOS never sends a cancel; nothing clears deletionScheduledAt on reconnect. AccountDeletionView.swift:90 promises 'cancel by logging back in' — FALSE. Fix direction is an owner call (auto-cancel-on-login vs explicit button vs fix copy)."
        - "[should-fix, Fun/Playable] Daily quests can be unwinnable: generateDailyQuests (dailyQuests.ts:870) randomly picks from pools incl. socialize/attend_class/work_hours/earn_money which have NO live updateQuestProgress caller (the on* hooks in integration.ts are orphaned). Players regularly get a quest with an unobtainable reward. Fix: prune dead types from pools (simple/safe) vs wire the hooks (double-count hazard, deferred prior). Prior board (app-validation T006) already DEFERRED wiring work/earn/class — so pruning is the aligned interim fix."
        - "[CLEAN] Activities perform loop (skill-scaled deltas applied/clamped/persisted), Dating (all UI cmds -> handlers, 18+ gate intact romance.ts:101), Messaging (conversation->handleConversation, energy-gated), data export."
      owner_decisions_surfaced:
        - "Deletion-cancel fix direction (sensitive — semantics)."
        - "Daily-quest fix approach (recommend: prune dead types)."
        - "Plus still-pending: avatar scope, Android scope."

  - id: T009
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "Make account-deletion CANCEL real (Craig: BOTH auto-cancel-on-login + explicit button + scheduled banner). Coherent design: (1) SERVER — register 'cancelAccountDeletion' -> handleCancelAccountDeletion in COMMAND_REGISTRY (handlers/index.ts; import from data.js — handler already exists at data.ts:145). (2) SERVER auto-cancel-on-login (safety net, honors existing UI copy): in initializeSession (WebSocketServer.ts, the init path) after the player loads, if player.deletionScheduledAt is set, clear it (=null) + savePlayer + send {type:'accountDeletionCancelled', reason:'login'}. (3) iOS — parse deletionScheduledAt from the playerObject (add to the Player model if missing); show a persistent BANNER 'Account scheduled for deletion on <date> — Cancel' whenever it's set; wire an explicit Cancel button (banner + the Settings AccountDeletionView) to SEND {type:'cancelAccountDeletion'}; handle the incoming 'accountDeletionCancelled' (WebSocketService.swift:428 already listens) to clear local state + toast; update AccountDeletionView copy so the 'cancel by logging back in' promise is now actually true. Keep the existing 30-day purge (offline+past-due) untouched."
    allowed_files:
      - "server/src/handlers/index.ts"
      - "server/src/server/WebSocketServer.ts"
      - "server/src/contracts/websocket-commands.ts"
      - "server/tests/contracts/fixtures/commands.json"
      - "ios/lichunWebsocket/Features/Settings/Views/AccountDeletionView.swift"
      - "ios/lichunWebsocket/WebSocketService.swift"
      - "ios/lichunWebsocket/Shared/Utilities/Constants.swift"
      - "ios/lichunWebsocket/Core/Models/Player.swift"
    verify:
      - "cd server && npx tsc --noEmit"
      - "cd server && npx vitest run"
      - "rm -rf /tmp/baolife-dd && CLEAN xcodebuild (PM will run + verify in-app)"
    stop_if:
      - "Any server test regresses or tsc not clean."
      - "Auto-cancel-on-login would require touching the purge job or data.ts deletion-schedule logic beyond clearing the marker (out of scope) — or you'd need to change WHO gets purged."
      - "Need files outside allowed_files (esp. a shared home/banner view not listed — if a banner needs a new view file or ContentView edit, STOP and report the exact file so PM can expand scope)."
      - "Touching avatar WIP / ws / Player.ts avatar hunk (Player.swift is iOS, fine; do NOT touch server Player.ts)."
    receipt:
      result: done
      commit: 78a0793e
      changed_files:
        - server/src/handlers/index.ts
        - server/src/server/WebSocketServer.ts
        - server/src/contracts/websocket-commands.ts
        - server/tests/contracts/fixtures/commands.json
        - ios/lichunWebsocket/Core/Models/Player.swift
        - ios/lichunWebsocket/WebSocketService.swift
        - ios/lichunWebsocket/Shared/Utilities/Constants.swift
        - ios/lichunWebsocket/Features/Settings/Views/AccountDeletionView.swift
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "152 files / 1724 tests / 0 failed (+1 contract fixture)"
        - cmd: "rm -rf /tmp/baolife-dd && xcodebuild ... clean build"
          status: pass
          detail: "BUILD SUCCEEDED (clean)"
        - cmd: "git push origin main + webhook"
          status: pass
          detail: "prod HEAD 78a0793e (verified deployed)"
      summary: "SHIPPED + deployed. Cancel command registered + contract-declared; auto-cancel-on-login in initializeSession (clears deletionScheduledAt before initial sync, sends accountDeletionCancelled{reason:login}, purge untouched); iOS parses deletionScheduledAt + ScheduledDeletionBanner + explicit Cancel button + response handling + honest copy. Server changes reviewed (correct). 1724 tests green, clean iOS build SUCCEEDED."
      verification_status: "VERIFIED by: tests (1724/0) + clean iOS build + thorough diff review of server auto-cancel + iOS banner/button/response. IN-APP UI round-trip (schedule->banner->cancel) NOT completed this session — blocked by event-cadence + a modal-tap automation artifact (taps not registering on a scrolled 4-choice dilemma; same surface as the T011 overflow UX). Recommend confirming the cancel UI in a future drive when the app isn't behind a stuck dilemma."

  - id: T010
    type: judge
    assignee: Judge
    status: done
    reasoning_hint: high
    objective: "Design the SAFE firing points to wire the 4 dead daily-quest types (work_hours, earn_money, attend_class, socialize) so they actually complete — WITHOUT the known online/offline double-count hazard. The on* hooks (integration.ts: onWorkHours/onMoneyEarned/onClassAttended/onActivityCompleted/onDateNight) are orphaned. Determine: for each quest type, the single correct call site that fires ONCE per real occurrence and is mirrored/consistent across the online (PlayerSession) and offline (LoopManager/GameEngine) loops without double-counting (prior board deferred work/earn/class specifically for gross-vs-net + prorated-offline double-count). Produce a Worker contract with exact files + the firing logic + how to avoid double-count + tests. If a type can't be made safe cheaply, recommend pruning just that one from the pool instead."
    inputs:
      - "T008 scout findings; services/retention/integration.ts + dailyQuests.ts; PlayerSession + engine/LoopManager weekly/work ticks; prior app-validation T006 receipt (double-count hazard)"
    constraints:
      - "Read-only. Mirror online+offline. No double-count. Green-before-ship. Ground every firing site in real code."
    expected_output:
      - "Per-quest-type firing design + Worker contract (allowed_files/verify/stop_if), or prune recommendation"
    receipt:
      result: done
      decision: "WIRE socialize + earn_money; PRUNE work_hours + attend_class. (Grounded in both loops.)"
      design:
        - "socialize: WIRE — fire updateQuestProgress('socialize',1)+sendQuestProgress in performActivity.ts fireActivityQuestProgress when activity.id==='socialize' (sibling to existing increase_affinity). Single player-action site; offline loop never runs the command handler → no double-count."
        - "earn_money: WIRE against GROSS (computeWeeklyFinances(person).grossIncome, NOT net person.money). Judge recommended mirroring all 3 finance sites; PM OVERRIDE → ONLINE-ONLY (PlayerSession.ts:520 applyWeeklyFinances for the player char) to avoid any offline-diamond-award surface + keep the diff small. Online-only is appropriate daily-engagement semantics. Player char + earning occupation + gross>0 only."
        - "work_hours: PRUNE — passive dailyPlan location, minute-by-minute in both loops, no per-shift event, only clean number is gross-dollars not hours. Remove from QUEST_TEMPLATES + QUEST_CHAINS + WEEKLY_CHALLENGE_TEMPLATES(weekly_work)."
        - "attend_class: PRUNE — passive school location, education processed per-person weekly in both loops (no playerId), double-count-prone. study quest already covers the education loop. Remove from QUEST_TEMPLATES."
      pm_decision: "earn_money ONLINE-ONLY (override Judge's mirror-both rec) — avoids offline finance-loop edits + offline quest-diamond awards; lower risk; better daily-quest semantics. Drops GameEngine/LoopManager from the Worker's allowed_files."
      net_effect: "All difficulty pools winnable: easy {talk_to_characters,buy_item,socialize}, medium {go_on_date,complete_activities,study}, hard {spend_energy,earn_money,increase_affinity}."

  - id: T012
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "Wire 2 dead daily-quest types + prune 2 (per T010 Judge, with PM override = earn_money ONLINE-ONLY). (A1) socialize: in server/src/handlers/performActivity.ts fireActivityQuestProgress, when the performed activity is the 'socialize' activity, also fire updateQuestProgress(player.userId,'socialize',1,player) + sendQuestProgress (mirror the existing increase_affinity fire there). (A2) earn_money ONLINE-ONLY: at server/src/game/PlayerSession.ts ~line 520 where applyWeeklyFinances runs for the player on the weekly tick, fire updateQuestProgress(player.userId,'earn_money', computeWeeklyFinances(player.c).grossIncome, player.c) — GROSS income, NOT net player.c.money — guarded to: player character, earning occupation, grossIncome>0. Do NOT add the offline GameEngine/LoopManager calls (online-only by PM decision). Do NOT use the onMoneyEarned/onWorkHours wrappers. (B) PRUNE: remove work_hours + attend_class from QUEST_TEMPLATES, work_hours from QUEST_CHAINS, and the weekly_work entry from WEEKLY_CHALLENGE_TEMPLATES in server/src/services/retention/dailyQuests.ts. No diamond/reward/finance-economy changes."
    allowed_files:
      - "server/src/handlers/performActivity.ts"
      - "server/src/game/PlayerSession.ts"
      - "server/src/services/retention/dailyQuests.ts"
      - "server/tests/services/retention/dailyQuests.test.ts"
      - "server/tests/services/retention/questWiring.test.ts"
      - "server/tests/services/retention/questDepth.test.ts"
    verify:
      - "cd server && npx tsc --noEmit"
      - "cd server && npx vitest run (baseline 152 files / 1724 tests / 0 failed must stay green)"
      - "New tests: (1) socialize activity advances socialize quest by exactly 1; (2) earn_money advances by computeWeeklyFinances(player.c).grossIncome (gross not net) on one online weekly application; (3) QUEST_TEMPLATES lacks work_hours+attend_class, every difficulty bucket non-empty, every remaining template type has a live fire path."
    stop_if:
      - "earn_money would use net/floored player.c.money instead of computeWeeklyFinances().grossIncome — STOP."
      - "You'd fire socialize or earn_money from more than one path for the same occurrence (double-count) — STOP."
      - "You'd add offline (GameEngine/LoopManager) earn_money firing (PM chose online-only) — STOP."
      - "Any change to diamond rewards, reward amounts, or rent/savings/finance economy math — STOP."
      - "You'd fabricate an hours/per-class proxy to revive work_hours/attend_class instead of pruning — STOP."
      - "Touch avatar WIP / ws / scripts/t016g-* — STOP. Full vitest not green — STOP."
    receipt:
      result: done
      commit: e65c8835
      changed_files:
        - server/src/handlers/performActivity.ts
        - server/src/game/PlayerSession.ts
        - server/src/services/retention/dailyQuests.ts
        - server/tests/services/retention/dailyQuests.test.ts
        - server/tests/services/retention/questWiring.test.ts
        - server/tests/services/retention/questDepth.test.ts
      commands:
        - cmd: "cd server && npx tsc --noEmit"
          status: pass
        - cmd: "cd server && npx vitest run"
          status: pass
          detail: "153 files / 1728 tests / 0 failed (+4 questWiring tests)"
        - cmd: "git push origin main + webhook"
          status: pass
          detail: "prod HEAD e65c8835 (verified deployed)"
      summary: "SHIPPED + deployed. WIRED socialize (performActivity single-fire) + earn_money (PlayerSession weekly tick, GROSS via computeWeeklyFinances().grossIncome, online-only). PRUNED work_hours + attend_class from QUEST_TEMPLATES/QUEST_CHAINS/weekly challenge. Every difficulty bucket non-empty; every issuable quest type now has a live fire path. NOTE: completed by PM directly after the worker hit a transient server rate-limit twice (left only partial imports); PM finished the wire + prune + tests. Fixed 2 collateral questDepth chain tests (used pruned work_hours example -> earn_money)."
      verification: "New tests prove socialize +1 per activity, earn_money advances by GROSS (500, not net), and templates lack work_hours/attend_class with all buckets winnable. tsc clean, full suite green. (iOS not involved — server-only.)"

  - id: T013
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: medium
    objective: "RELEASE-BLOCKER found during deletion-cancel in-app verification: AccountDeletionView + DataExportView had ZERO call sites — never presented anywhere, so the App Store-required in-app account-deletion path (Guideline 5.1.1(v)) + GDPR data export were UNREACHABLE (and my T009 cancel UI was orphaned with them). FIX: add a 'Settings & Privacy' card to ProfileMenuView (Profile menu) that presents a new SettingsView hub (defined inside ProfileMenuView.swift to avoid a pbxproj edit — project has no synchronized groups, files are explicitly referenced) with Export My Data (-> DataExportView), Delete Account (-> AccountDeletionView), a scheduled-deletion notice, and app version. Card badges when deletionScheduledAt is set."
    allowed_files:
      - "ios/lichunWebsocket/Features/Home/Views/ProfileMenuView.swift"
    verify:
      - "rm -rf /tmp/baolife-dd && CLEAN xcodebuild BUILD SUCCEEDED + in-app: Profile -> Settings & Privacy -> Delete Account reachable; schedule -> banner+Cancel; reconnect -> auto-cancel"
    stop_if:
      - "Would require a pbxproj edit (keep SettingsView inside an already-referenced file)."
    receipt:
      result: done
      commit: 6bf7860b
      changed_files:
        - ios/lichunWebsocket/Features/Home/Views/ProfileMenuView.swift
      commands:
        - cmd: "rm -rf /tmp/baolife-dd && xcodebuild ... clean build"
          status: pass
          detail: "BUILD SUCCEEDED"
        - cmd: "git push origin main"
          status: pass
          detail: "6bf7860b (iOS-only; reaches users next app build)"
      summary: "RELEASE-BLOCKER fixed: AccountDeletionView + DataExportView were never presented (zero call sites) -> App Store-required in-app account deletion + GDPR export were UNREACHABLE. Added a 'Settings & Privacy' card to ProfileMenuView opening a new SettingsView hub (defined inside ProfileMenuView.swift -> no pbxproj edit; project has no synchronized groups) with Export My Data, Delete Account, scheduled-deletion notice, app version. Card badges when deletionScheduledAt is set."
      verification: "VERIFIED in-app end-to-end: Profile -> Settings & Privacy -> Delete Account now reachable; typed DELETE + toggle + Delete My Account -> Final Confirmation -> Yes,Delete -> 'Deletion Scheduled' + 'Cancel Deletion' button (T009) shown; tapped Cancel Deletion -> reverted to idle (scheduled state cleared, field reset, Delete button disabled). Clean iOS build SUCCEEDED. This also completes the T009 in-app UI round-trip that was previously unverified."

  - id: T014
    type: pm
    assignee: PM
    status: active
    reasoning_hint: medium
    objective: "Checkpoint after clearing the open items (T011 dilemma-overflow + T009/T013 account-deletion reachability+cancel, both verified in-app). Surface the updated release-readiness state + the remaining OWNER-GATED pillars (avatar scope, Android launch scope, IAP APPLE_SHARED_SECRET + prod-enable, store-ops/submission assets) and take Craig's next steer. Until steered, the next autonomous-safe work is more iOS-drive validation of still-un-driven surfaces (Messaging deep-dive, Dating actions, daily rewards/quests UI, Do-an-Activity) + any content-balance items logged (e.g. medical-emergency affordability)."
    constraints:
      - "Don't make avatar/Android/launch/store/content-policy decisions for Craig — present options + recommendation."
      - "Keep doing safe local work if unsteered; don't block the goal on owner input."
    expected_output:
      - "Updated checklist + Craig's next steer"
    receipt: null

  - id: T011
    type: worker
    assignee: Worker
    status: done
    reasoning_hint: low
    objective: "UX: dilemma choices overflow the screen so disabled money options can hide the affordable/free options below the fold (player thinks they're stuck). Fix minimally: order choices so enabled/affordable ones are reachable (e.g. sort affordable-first, or add a clear scroll affordance, or ensure the choice list scrolls with an indicator). iOS-only (EventModalView.swift). Verify with a clean iOS build + in-app on a 4-choice money dilemma."
    allowed_files:
      - "ios/lichunWebsocket/Features/Events/Views/EventModalView.swift"
    verify:
      - "rm -rf /tmp/baolife-dd && CLEAN xcodebuild BUILD SUCCEEDED + in-app check (PM)"
    stop_if:
      - "Reordering choices would change which choice maps to which choiceId/effect (must preserve choiceId->action mapping; only reorder DISPLAY)."
      - "Need files outside EventModalView.swift."
    receipt:
      result: done
      changed_files:
        - ios/lichunWebsocket/Features/Events/Views/EventModalView.swift
      commands:
        - cmd: "rm -rf /tmp/baolife-dd && xcodebuild ... clean build"
          status: pass
          detail: "BUILD SUCCEEDED"
        - cmd: "git push origin main"
          status: pass
          detail: "6bf7860b (iOS-only)"
      summary: "Added `orderedAnswers` (stable affordable-first sort) used by the choice grid, so enabled/affordable choices render before dimmed unaffordable ones — the player always sees a pickable option without scrolling past disabled ones. Display-only reorder (each AnswerOption keeps its choiceId, so choice->action mapping is preserved). No-op when all/none affordable."
      verification: "VERIFIED in-app on the chest-pains medical dilemma (char money -4,925): the free 'Wait and see' option moved from position 4 (below the fold) to position 1 (top-left, vibrant); the $5,000 'Call 911' is dimmed/disabled. Tapped 'Wait and see' -> resolved (Health -20 etc.). Clean iOS build SUCCEEDED."
      content_note: "Observed (content-balance, deferred not bug): a broke player's only AFFORDABLE option in the chest-pains emergency is 'Wait and see' (Drive/Call-911/Call-family all carry money costs they can't afford) -> harsh medical-event outcome. Flag for a future content-balance pass; not a code bug (gate working as designed)."

  - id: T999
    type: judge
    assignee: Judge
    status: queued
    reasoning_hint: xhigh
    objective: "Release-readiness FINAL audit: are all four dimensions (fun/playable/complete/releasable) met with evidence (in-app walkthroughs + green builds + checklist blockers cleared)? Map every shipped tranche to a verified outcome. full_outcome_complete: true ONLY when the checklist's release-blockers are cleared AND Craig signs off to launch."
    inputs:
      - "All tranche receipts + ship verifications + the release-readiness checklist + Craig's launch decision"
    constraints:
      - "Do not implement. Do NOT mark complete on a green suite alone — require in-app evidence + Craig launch sign-off. Reject completion if any release-blocker is open."
    expected_output:
      - "complete | not_complete"
      - "full_outcome_complete: true | false (gated on Craig launch sign-off)"
      - "four-dimension evidence map + any remaining blockers"
    receipt: null

checks:
  dirty_fingerprint: unknown
  last_verification:
    result: green
    task: "T003 IAP validation + privacy manifest (b33bb862)"
    commands:
      - cmd: "cd server && npx tsc --noEmit"
        status: pass
      - cmd: "cd server && npx vitest run"
        status: pass
        detail: "151 files, 1718 tests, 0 failed (+4 new)"
      - cmd: "git push origin main"
        status: pass
        detail: "b984bde1..b33bb862 webhook deploy"
