diff --git a/.logs/goal-progress.md b/.logs/goal-progress.md
index 5dde084..83d804e 100644
--- a/.logs/goal-progress.md
+++ b/.logs/goal-progress.md
@@ -452,3 +452,111 @@ Checks to run for Milestone 1:
- `PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm typecheck`
- `PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm test`
- `PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm build`
+
+---
+
+# Sprint 4 Goal Progress
+
+Workspace: `/Users/tkevinbigham/MBD-main`
+Branch: `goal/sprint-4-front-office`
+Date: 2026-05-14
+
+## Pause Before Milestone 1 - Worker Shape Mismatch
+
+- Commit: none; stopped before implementation.
+- typecheck: not run; no milestone code was completed.
+- test: not run; no milestone code was completed.
+- Files touched: `STATUS.md`, `.logs/goal-progress.md`.
+- Scope decisions: Did not add worker methods, did not touch protected worker/sim/contracts/save files, and did not reinterpret contract-negotiation fields from trade package data.
+- Surprises: `getOpenNegotiations()` and `getNegotiation(id)` return `TradeNegotiationView` trade-package shapes (`offeringAssets`, `requestingAssets`, `counterOffer`, `phase`, `dialogue`, `expiresAtDay`), not contract salary asks/offered terms. Also, `getInteractivePressConference()` already has an `AppLayout` consumer through `PressConferenceModal`, so the audit's zero-consumer claim is stale.
+
+## Milestone 1 — Trade Negotiations Inbox scaffolding
+
+- Commit: c3eb524 `feat(trade-negotiations): add /trade-negotiations Inbox route`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`
+- Files touched: `apps/web/src/app/routes/index.tsx`, `apps/web/src/features/trade-negotiations/routes/TradeNegotiationsInboxPage.tsx`, `apps/web/src/features/trade-negotiations/routes/TradeNegotiationsInboxPage.test.tsx`
+- Scope decisions: Built the Inbox as a read-only route against `TradeNegotiationView`, sorting open negotiations first and then by earliest `expiresAtDay`. Error handling uses the existing `logger` plus `sonner` toast and an inline retry state.
+- Surprises: Focused red failed exactly because the page did not exist yet. Full test output includes existing Recharts/React act/service-worker/scouting warning noise and the new test's intentional mocked worker failure log.
+
+## Milestone 2 — Trade Negotiation detail
+
+- Commit: 3b1536d `feat(trade-negotiations): add /trade-negotiations/:id detail view`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/app/routes/index.tsx`, `apps/web/src/features/trade-negotiations/routes/TradeNegotiationDetailPage.tsx`, `apps/web/src/features/trade-negotiations/routes/TradeNegotiationDetailPage.test.tsx`
+- Scope decisions: Kept the detail surface read-only and used `getPlayer` only to resolve existing `TradeAsset` player IDs into profile links. The only action CTA deep-links to `/trade?negotiationId=...` for the existing builder to handle.
+- Surprises: `TradeCounterPackage` stores raw `TradeAsset[]`, not `TradeAssetView[]`, so the detail page formats draft picks and IFA pool space locally while resolving player names through the worker.
+
+## Milestone 3 — Sidebar Trade Negotiations entry
+
+- Commit: 2b99559 `feat(layout): add Trade Negotiations entry to Sidebar`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/app/layout/Sidebar.tsx`, `apps/web/src/app/layout/Sidebar.test.tsx`
+- Scope decisions: Placed Trade Negotiations directly after Trades and used the lucide `Handshake` icon, avoiding the News `Inbox` icon.
+- Surprises: None; the red check failed only on the missing sidebar label as intended.
+
+## Milestone 4 — Trade page cross-linking + deep-link from Inbox
+
+- Commit: 14043ed `feat(trade): cross-link player names + accept ?negotiationId deep link`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/features/trade/components/DeadlineDramaPanel.tsx`, `apps/web/src/features/trade/routes/TradePage.tsx`, `apps/web/src/features/trade/routes/TradePage.test.tsx`
+- Scope decisions: Linked structured player assets in Trade rows, offer cards, active negotiation packages, package summaries, multi-team summaries, and the deadline bidding-war target using the Roster `/players/:id` pattern. Added `/trade?negotiationId=...` loading through existing `getNegotiation` and `applyNegotiationToBuilder`, with toast + param clearing for stale or closed negotiations.
+- Surprises: Trade recap/ticker prose still contains narrative player names without a safe token map; no regex parsing was added.
+
+## Milestone 5 — Draft page cross-linking
+
+- Commit: 0ad438f `feat(draft): cross-link prospects to /players/:id`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/features/draft/routes/DraftPage.tsx`, `apps/web/src/features/draft/routes/DraftPage.test.tsx`
+- Scope decisions: Used `/players/:playerId?tab=development` for draft prospects, matching the existing Minors prospect precedent. Linked available prospects, selected prospect card, draft ticker, board cells, post-draft best picks, and the user's draft class without parsing prose summaries.
+- Surprises: Draft commentary and buzz entries have `playerId` fields but not display-safe player names in every entry, so the milestone kept links to places where a structured player ID and visible name were already paired.
+
+## Milestone 6 — Scouting page cross-linking
+
+- Commit: 0736d0c `feat(scouting): cross-link player names to /players/:id`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`; web suite `103 passed`, `639 passed`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/features/scouting/components/ScoutConflictsTab.tsx`, `apps/web/src/features/scouting/routes/ScoutingPage.tsx`, `apps/web/src/features/scouting/routes/ScoutingPage.test.tsx`
+- Scope decisions: Linked only structured scouting player/prospect identifiers: pro search results, generated pro reports, recent reports, selected IFA reports, IFA board prospects, and scout-conflict headlines. Kept action/status prose such as signing result messages as text because those strings are composed state, not structured link records.
+- Surprises: The route test mock was stale against older worker names, so the milestone updated it to the current `getScoutingStaff` / `getIFAPool` / `searchPlayers` surface before asserting the links.
+
+## Milestone 7 — Stats / leaderboards cross-linking
+
+- Commit: 5614f99 `feat(stats): cross-link leaderboard entries to /players/:id`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`; web suite `103 passed`, `639 passed`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/features/league/routes/LeadersPage.test.tsx`
+- Scope decisions: Production leaderboard rows already used `/players/:id` links, so this milestone added explicit regression coverage for both WAR and FIP leaderboard states instead of changing working route code.
+- Surprises: The only actual leaderboard surface lives under `apps/web/src/features/league/routes/LeadersPage.tsx`; `apps/web/src/features/stats/**` contains the stat encyclopedia and shared quality-scale test, not player leaderboard rows.
+
+## Milestone 8 — News page player-reference chips
+
+- Commit: c088244 `feat(news): cross-link player references in news items`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`; web suite `103 passed`, `640 passed`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/features/news/routes/NewsPage.tsx`, `apps/web/src/features/news/routes/NewsPage.test.tsx`
+- Scope decisions: Used the existing `relatedPlayerIds` field from `NewsItem` and resolved display labels through existing `worker.getPlayer`; unresolved or failed lookups fall back to the raw player ID. Moved related chips outside the clickable news-card button so player links are valid interactive elements.
+- Surprises: News already displayed related player IDs as plain text spans, so the milestone was a clean conversion to linked chips rather than a skip.
+
+## Milestone 9 — Trade Value on Player Profile
+
+- Commit: 0832a9b `feat(players): surface trade value on player profile`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`; web suite `103 passed`, `641 passed`
+- Files touched: `.logs/goal-progress.md`, `apps/web/src/features/players/routes/PlayerProfilePage.tsx`, `apps/web/src/features/players/routes/PlayerProfilePage.test.tsx`, `apps/web/src/shared/hooks/useWorker.ts`
+- Scope decisions: Rendered a compact sidebar card rather than adding another tab. The hook now forwards the existing sim-worker `getPlayerTradeValue` query; no new worker query/action was added.
+- Surprises: The sim worker already exposed `getPlayerTradeValue`, but `useWorker` did not forward it, so wiring the profile required a hook wrapper.
+
+## Milestone 10 — Browser smoke + screenshots + STATUS.md
+
+- Commit: a531964d04d9 `docs(sprint-4): browser smoke, STATUS report, and handoff`
+- typecheck: PASS — `Tasks: 9 successful, 9 total`
+- test: PASS — `Tasks: 8 successful, 8 total`; web suite `103 passed`, `641 passed`
+- build: PASS — `Tasks: 5 successful, 5 total`; PWA precache `122 entries (3286.88 KiB)`
+- Files touched: `.logs/goal-progress.md`, `STATUS.md`, `apps/web/docs/screenshots/sprint-4/*.png`
+- Scope decisions: Captured the full Milestone 10 screenshot set under `apps/web/docs/screenshots/sprint-4/` and rewrote STATUS with validation tails, bundle notes, route invariants, cross-link coverage, and rollback/next-goal notes. Left the pre-existing `.claude/launch.json` change untouched.
+- Surprises: The Codex in-app browser screenshot command timed out on `Page.captureScreenshot`, so the committed evidence was captured with a Playwright Chromium fallback against the same local dev server; no repo dependencies or manifests changed.
diff --git a/GOAL.md b/GOAL.md
index 07a2fbb..9ef4e3e 100644
--- a/GOAL.md
+++ b/GOAL.md
@@ -1,286 +1,433 @@
-# GOAL.md — Sprint 3.5: Hard-reload state survival
+# GOAL.md — Sprint 4 (REVISED): Trade Negotiations Inbox + Cross-Linking + Trade Value
> Single-mission contract for Codex (or any one-shot coding agent).
> Format: Goal Packet v2.0 — Kevin's one-shot ritual.
-> Built on top of Sprint 3 ([PR #76](https://github.com/KevinBigham/MBD/pull/76)). Will rebase onto `main` once Sprint 3 merges.
+> Branched from `main` at `93b3f5b` (post Sprint 3.5 [PR #77](https://github.com/KevinBigham/MBD/pull/77)).
+> **Revised after Codex's first-pass pause.** See "What Changed In This Revision" below.
-## Mission
+---
-When a user hard-reloads any in-game route (`/dashboard`, `/roster`, `/trade`, `/draft`, `/news`, etc.), they are currently redirected to the Save Hub because `apps/web/src/app/layout/AppLayout.tsx:446` checks `useGameStore().isInitialized` and `useGameStore` is a plain Zustand store with no persistence. The user's read state, unread count, and current view are preserved in IndexedDB but **invisible behind the redirect**.
+## What Changed In This Revision
-Fix it. Persist enough of `useGameStore` to `localStorage` (via Zustand `persist` middleware) so the active save id/slot survive a reload. On app boot, if the persisted save id resolves to a real IndexedDB record, auto-load it through the worker and call `initializeGame()` **before** `AppLayout`'s guard fires. While auto-loading, show a loading state. On error, clear the persisted state and fall through to Save Hub as today.
+The original Sprint 4 contract was wrong about two of the four "orphans":
-Stop only when every item in **Done When** is satisfied or a **Pause Condition** is hit.
+1. **`getOpenNegotiations()` returns `TradeNegotiationView[]`** (trade packages between teams), **not contract negotiations** (salary asks / years / agent terms). Codex correctly paused under Pause Condition 1.
+2. **`getInteractivePressConference()` is already consumed** by `apps/web/src/app/layout/AppLayout.tsx:167`, which feeds `PressConferenceModal`. It is not orphaned. The Press Conference milestone is dropped.
-## Background
+This revision:
-Sprint 3's STATUS.md identified this as pre-existing app-wide behavior surfaced during news-inbox testing:
+- Renames the orphan surface from "Negotiations Center" to **"Trade Negotiations Inbox"** and rewrites it against the actual `TradeNegotiationView` shape.
+- Drops the Press Conference milestone entirely.
+- Keeps Trade Value on Player Profile (genuinely orphaned).
+- Keeps the five cross-linking milestones (Trade / Draft / News / Scouting / Stats).
+- Tightens to **10 milestones** (was 12).
-> `AppLayout` returns `` whenever `useGameStore().isInitialized` is false after a browser reload. `useGameStore` is a plain Zustand store with no persistence middleware, so `isInitialized` resets to `false` on every hard reload. This redirect fires on **every** in-game route — `/dashboard`, `/roster`, `/trade`, `/news` — not just news.
+---
-Sprint 2's `BrowserRouter basename` fix solved URL parsing (`/MBD/news` now resolves to the `/news` route). Sprint 3.5 solves the next layer up: **state hydration**, so the resolved route actually renders.
+## Mission
-IndexedDB save state is already correct — the bug is purely in the boot sequence.
+Wire the genuinely orphaned worker surfaces into a coherent Trade & Player experience, and finish the player-profile cross-linking that Roster / Free Agency / Minors already have but Trade / Draft / News / Scouting / Stats lack.
-## Baseline
+The two orphaned worker surfaces:
-- This branch is built on top of Sprint 3 (`goal/sprint-3-news-inbox`, PR #76). When Sprint 3 merges to `main`, **rebase this branch onto the new main** before continuing: `git fetch origin && git rebase origin/main`.
-- Save schema: `CURRENT_GAME_SNAPSHOT_VERSION = 33`. Do not bump.
-- Test counts post-Sprint-3: web 99 files / 624 tests; sim-core 137/1610; contracts 1/20; UI 1/1.
+1. **`getOpenNegotiations()` → `TradeNegotiationView[]`** — list of the user's open trade negotiations. Currently the only consumer is `TradePage` for the **active** negotiation in the trade builder. There is no inbox view of all open negotiations.
+2. **`getNegotiation(negotiationId)` → `TradeNegotiationView | null`** — detail/lookup for a single trade negotiation.
+3. **`getPlayerTradeValue(playerId)` → `PlayerTradeValue | null`** — zero consumers anywhere.
-## Read first
+The five cross-linking gaps (Roster / Free Agency / Minors already pass, these still ship plain text):
-Inspect these before editing. Do not skip.
+4. **Trade page** — player names in trade-block lists are not clickable.
+5. **Draft page** — draft prospects are not clickable.
+6. **News page** — news items referencing players are not clickable (only if `NewsItem` has a machine-readable player ref).
+7. **Scouting page** — player names are not clickable.
+8. **Stats / leaderboards** — leaderboard entries are not clickable.
-**Repo orientation:**
-- `README.md`, `CHANGELOG.md`, `MASTER_CONTEXT.md`
-- `GOAL.md` (this file)
-- Sprint 3's `STATUS.md` — confirms the architectural diagnosis
+By the time you finish, every place a player is named in the app should link to `/players/:playerId`, the user should have a **Trade Negotiations Inbox** at `/trade-negotiations` with a detail view at `/trade-negotiations/:id`, and **Trade Value** should appear on the Player Profile.
-**Core state + boot path:**
-- `apps/web/src/shared/hooks/useGameStore.ts` — the Zustand store you'll add `persist` middleware to
-- `apps/web/src/app/App.tsx` — the BrowserRouter shell where boot logic lands (Sprint 2 added the `basename`; build on top of that)
-- `apps/web/src/app/layout/AppLayout.tsx` — confirms the `if (!isInitialized) return ` redirect at line 446 and the worker-readiness gate above it
-- `apps/web/src/app/routes/index.tsx` — confirms `/` is `SetupPage` and every in-game route is nested under `AppLayout`
-- `apps/web/src/shared/lib/saveSystem.ts` — `loadSaveSafely`, `inspectSaveById`, `listSaves`, the Dexie schema. This is the I/O layer you'll call during boot.
-- `apps/web/src/shared/hooks/useWorker.ts` — the worker bridge. You'll use `importSnapshot` (or whatever the canonical "load this save into the worker" method is — confirm from current code) to hydrate the worker before flipping `isInitialized`.
-- `apps/web/src/features/setup/routes/SetupPage.tsx` — the Save Hub path that today calls `loadGameById` → worker hydrate → `initializeGame`. Read it to understand the **exact** sequence the auto-resume must mimic.
+---
-**Save recovery (DO NOT break):**
-- `apps/web/src/features/save-recovery/SaveRecoveryProvider.tsx`
-- `apps/web/src/features/save-recovery/SaveLoadErrorBoundary.tsx`
-- `apps/web/src/features/save-recovery/SaveRecoveryDialog.tsx`
-- `apps/web/src/features/save-recovery/__tests__/` (if present)
+## Read-First (do this before writing anything)
-The recovery dialog already handles corrupt-save cases. Auto-resume must use the same path (`loadSaveSafely → SaveRecoveryProvider.showFailure on `{ ok: false }`) so corrupt saves don't crash the auto-load.
+1. `README.md`
+2. `CHANGELOG.md`
+3. `MASTER_CONTEXT.md` (architecture)
+4. The previous `STATUS.md` (Sprint 4 first-pass pause — useful context for what NOT to assume)
+5. This `GOAL.md`
+6. `apps/web/src/workers/sim.worker.queries.ts` lines 2596–2601 (`getNegotiation`, `getOpenNegotiations`).
+7. `apps/web/src/workers/sim.worker.trade.ts` lines 184–203 — the **`TradeNegotiationView` shape**. Read this carefully before scaffolding the inbox row UI. The fields you have to work with are: `id`, `teamId`, `teamName`, `teamAbbreviation`, `phase`, `roundsCompleted`, `expiresAtDay`, `dialogue`, `proposal`, `counterOffer`, `isComplete`, `canAccept`, `canCounter`, `canReject`. There are **no salary, years, or contract-terms fields** — this is a trade between teams.
+8. `apps/web/src/features/trade/routes/TradePage.tsx` lines 795–830 (state) and lines 1241+ (`applyNegotiationToBuilder`) — see how the trade builder consumes a single `TradeNegotiationView`. The Inbox should link **into** the trade builder for an active negotiation, not duplicate the builder.
+9. `apps/web/src/features/news/routes/NewsPage.tsx` — the **Sprint 3 pattern** for a worker-backed list route. Mimic this for `TradeNegotiationsInboxPage`.
+10. `apps/web/src/features/roster/routes/RosterPage.tsx` — search for `to={\`/players/${player.id}\`}`. This is the **canonical cross-link pattern**. Replicate it; do not invent a new one.
+11. `apps/web/src/features/players/routes/PlayerProfilePage.tsx` — 486 lines. Read it before adding the Trade Value panel. Trade Value should integrate, not displace existing tabs.
-**Tests to study:**
-- `apps/web/src/app/App.test.tsx`
-- `apps/web/src/app/layout/AppLayout.test.tsx`
-- `apps/web/src/features/setup/routes/SetupPage.test.tsx` — covers continue-existing-save; your auto-resume should produce the same end-state without a manual click
-- `apps/web/src/shared/lib/saveSystem.test.ts`
+---
-## Product contract
+## Allowed Write Scope
+
+**New files (expected):**
+
+- `apps/web/src/features/trade-negotiations/routes/TradeNegotiationsInboxPage.tsx`
+- `apps/web/src/features/trade-negotiations/routes/TradeNegotiationsInboxPage.test.tsx`
+- `apps/web/src/features/trade-negotiations/routes/TradeNegotiationDetailPage.tsx`
+- `apps/web/src/features/trade-negotiations/routes/TradeNegotiationDetailPage.test.tsx`
+- `apps/web/src/features/trade-negotiations/components/*.tsx` (subcomponents as needed — e.g. `TradeNegotiationRow.tsx`, `TradePackageSummary.tsx`)
+- `apps/web/src/features/players/components/TradeValuePanel.tsx` (or similar — fit into existing PlayerProfilePage)
+- `apps/web/docs/screenshots/sprint-4/*.png`
+
+**Updates (expected):**
+
+- `apps/web/src/app/routes/index.tsx` (add routes for `/trade-negotiations` and `/trade-negotiations/:negotiationId`)
+- `apps/web/src/app/layout/Sidebar.tsx` (add Trade Negotiations entry)
+- `apps/web/src/app/layout/Sidebar.test.tsx` (existing tests likely assert label list)
+- `apps/web/src/features/trade/routes/TradePage.tsx` (cross-link player names — and possibly accept a `?negotiationId=` query param to deep-link from the Inbox; read TradePage before deciding)
+- `apps/web/src/features/draft/**` (cross-link prospects)
+- `apps/web/src/features/news/routes/NewsPage.tsx` (cross-link players in news items, only if `NewsItem` has `playerId` or similar — see Milestone 8 details)
+- `apps/web/src/features/scouting/**` (cross-link players)
+- `apps/web/src/features/stats/**` (cross-link leaderboard entries)
+- `apps/web/src/features/players/routes/PlayerProfilePage.tsx` (only to integrate Trade Value — do not refactor existing tabs)
+- `STATUS.md`
+- `.logs/goal-progress.md`
+- `GOAL.md` (only to mark milestones complete in a working scratchpad — final commit should preserve the contract, not erase it)
-Build the smallest complete fix that:
+---
-1. **Persists**: `useGameStore` exposes `activeSaveId`, `activeSaveSlot`, `userTeamId`, `season`, `day`, `phase`, `teamName`, `gmName`, `difficulty` to `localStorage` via Zustand's `persist` middleware. **Do not persist** `isInitialized`, `isSimulating`, `playerCount`, `gamesPlayed`, or any function-typed fields — those derive from the worker after hydration.
-2. **Auto-resumes on boot**: When the React tree mounts, if `useGameStore.activeSaveId` is non-null AND a save with that id exists in IndexedDB, kick off an auto-load. The auto-load:
- - Calls `loadSaveSafely(activeSaveId)`.
- - On `{ ok: true }`, calls `worker.importSnapshot(save.snapshot)` (or the equivalent canonical method), then `useGameStore.initializeGame(...)` with the post-import worker state.
- - On `{ ok: false }`, hands off to `SaveRecoveryProvider.showFailure` (same path Save Hub uses).
- - On any worker/IndexedDB error, clears the persisted state and falls through to Save Hub. Toast the user briefly.
-3. **Loading state**: While the auto-load is in flight, show a route-level "Resuming…" skeleton (NOT the Save Hub). When done, the user lands on the route they hard-reloaded into. If they hard-reloaded `/news`, they end up on `/news`. If they hard-reloaded `/dashboard`, they end up on `/dashboard`.
-4. **First-time and post-clear behavior unchanged**: If `useGameStore.activeSaveId` is null (no prior save, or persisted state cleared), the app boots to Save Hub as today.
-5. **SaveHub still wins manual loads**: When the user navigates to `/` and clicks a save, the existing flow runs and updates the persisted store. No regression to the manual continue path.
+## Protected (DO NOT TOUCH)
+
+- `packages/sim-core/**` — sim logic stays untouched.
+- `packages/contracts/**` — no schema or version changes. Save schema stays at v33.
+- `packages/ui/**` — use existing primitives only. No new shared UI components.
+- `packages/design-tokens/**` — use existing tokens.
+- `apps/web/src/workers/sim.worker.queries.ts` — consume what exists. Do NOT add new worker methods.
+- `apps/web/src/workers/sim.worker.actions.ts` — same. Do NOT add new worker actions.
+- `apps/web/src/workers/sim.worker.trade.ts` — read-only.
+- `apps/web/src/shared/lib/saveSystem.ts` — Sprint 3.5 territory.
+- `apps/web/src/app/boot/AppBootGate.tsx` — Sprint 3.5 territory. Do not change boot order.
+- `apps/web/src/shared/hooks/useGameStore.ts` — Sprint 3.5 territory. Do not extend the persisted shell.
+- `apps/web/src/features/save-recovery/**` — use existing API only.
+- `apps/web/src/features/onboarding/**` — Sprint 2 territory.
+- `apps/web/src/features/press-room/**` — Press Conference is already wired in `AppLayout.tsx:167`. No Press Conference work in Sprint 4.
+- Any existing test that currently passes — do not modify to make new code work. If an existing test breaks, that is a regression — fix the new code, not the test.
-Prefer:
-- working over broad — get one hard-reload route (e.g. `/news`) surviving end-to-end before declaring victory across all routes;
-- composition over invention — reuse `loadSaveSafely`, `SaveRecoveryProvider`, existing worker methods;
-- no new dependencies;
-- the smallest diff that satisfies the contract.
+---
-## Allowed write scope
+## Non-Negotiables
-Write only inside:
-- `apps/web/src/shared/hooks/useGameStore.ts` — add `persist` middleware, declare the persisted slice, write the persistence config
-- `apps/web/src/app/App.tsx` — add the boot-time auto-resume hook + a route-level loading state. May extract a small `` wrapper component if the diff stays small
-- `apps/web/src/app/layout/AppLayout.tsx` — minimal change only if necessary (e.g. to render `` once `isInitialized` flips). If you can avoid touching this file, prefer that.
-- New files under `apps/web/src/app/boot/` (e.g. `useAutoResumeSave.ts`, `AppBootGate.tsx`) if needed
-- Test files matching the above paths
-- `.logs/goal-progress.md`
-- `STATUS.md` (rewrite for Sprint 3.5)
-- `GOAL.md` (this file — minor edits only)
-- `apps/web/docs/screenshots/sprint-3-5/` — browser smoke evidence
-
-## Protected scope
-
-Do not modify:
-- `packages/sim-core/**`
-- `packages/contracts/**`
-- `apps/web/src/workers/**` — worker methods stay as-is; you call them, you don't change them
-- `apps/web/src/features/save-recovery/**` — the recovery dialog already does the right thing; auto-resume must integrate with it, not modify it
-- `apps/web/src/features/setup/**` — Save Hub's manual continue path stays unchanged
-- `apps/web/src/features/news/**` — Sprint 3 just shipped; preserve it
-- `apps/web/src/features/onboarding/**` — Sprint 2 just shipped; preserve it
-- `apps/web/src/shared/lib/saveSystem.ts` — read-only; persistence helpers already exist
-- `apps/web/src/shared/lib/audio.ts`, `logger.ts`, `webVitals.ts`, etc.
-- `.github/**`, `package.json` (root), `turbo.json`, `pnpm-workspace.yaml`
-- `apps/web/src/build/bundleConfig.ts`, `apps/web/docs/BUDGETS.md`
-
-## Non-negotiables
-
-- **Schema v33.** No bump, no migration.
-- **Determinism.** No `Math.random()`. No new RNG paths.
-- **No new dependencies.** Zustand's `persist` middleware ships with the `zustand` package already in `apps/web/package.json` — use `import { persist, createJSONStorage } from 'zustand/middleware'`.
-- **No emoji.** lucide-react icons only. Bloomberg Terminal aesthetic.
-- **Do not delete or weaken tests** to make checks pass. New tests required for the auto-resume flow.
-- **Save Recovery must still trigger** on corrupt saves during auto-resume.
-- **Manual Save Hub flow must be unchanged.** Existing SetupPage.test.tsx assertions still pass.
-- **localStorage namespace**: use a stable key (e.g. `mbd:game-store@v1`). If you ever need to invalidate the persisted shape, bump the suffix — do **not** silently change it.
-- **Privacy**: persist only the minimal fields above. Do NOT persist the full snapshot, player data, or any large blob in localStorage — IndexedDB owns the heavy data.
-- **No commits on `main`.** Work only on `goal/sprint-3-5-hard-reload-survival`.
-- **No `git add -A`.**
-
-## Milestone loop
-
-For each milestone: inspect → state checkpoint → smallest change → smallest validation → fix → log to `.logs/goal-progress.md`.
-
-Suggested milestones:
-
-1. **Inventory.** Read every file in "Read first." Document the exact sequence Save Hub uses today to continue a save (which worker method hydrates the snapshot? what order does `initializeGame` get called in?). Map the auto-resume to the same sequence.
-2. **Persist `useGameStore`.** Add `persist` middleware with `createJSONStorage(() => localStorage)`. `partialize` to only the persisted slice listed above. Bump the version key if needed. Confirm `useGameStore` still works in tests with a localStorage mock.
-3. **Auto-resume hook.** New `useAutoResumeSave` (or in-file hook in `App.tsx`) that runs once at app mount: read `activeSaveId` from the store, if present call `loadSaveSafely`, then hydrate the worker, then call `initializeGame`. Handle the `SaveRecoveryProvider.showFailure` case. Handle generic errors by clearing `activeSaveId` from the store.
-4. **Boot loading state.** While the auto-resume is in flight, render a small route-level "Resuming…" skeleton. Do NOT show the Save Hub. Do NOT show the dashboard half-loaded. When done, the route renders normally — react-router preserves the URL across the hydration because we never navigated.
-5. **Tests.** Add:
- - `App.test.tsx` (or a new `useAutoResumeSave.test.ts`): with a persisted `activeSaveId` and a mock save in IndexedDB, the app hydrates and renders the in-game route, not Save Hub.
- - With a persisted `activeSaveId` but no IndexedDB record, the persisted state clears and the user lands on Save Hub.
- - With a corrupt save, the SaveRecoveryDialog appears.
- - Without a persisted `activeSaveId`, behavior is unchanged (Save Hub).
-6. **Verify gate.** `pnpm typecheck`, `pnpm test`, `pnpm build`.
-7. **Browser smoke.**
- - Start dev server.
- - Complete onboarding into Slot 1.
- - Land on dashboard.
- - Hard reload `/MBD/dashboard` → should land back on dashboard (not Save Hub).
- - Navigate to `/news`, hard reload `/MBD/news` → should land back on news.
- - Navigate to `/roster`, hard reload → roster.
- - Navigate to `/trade`, hard reload → trade.
- - Clear IndexedDB → hard reload `/MBD/dashboard` → should land on Save Hub (graceful recovery).
- - Capture screenshots for each scenario.
-8. **STATUS.md.** Rewrite for Sprint 3.5.
-
-Each `.logs/goal-progress.md` entry: timestamp, milestone, files changed, checks run, result, blocker or next step.
-
-## Validation loop
-
-Workspace root commands:
+1. **No new worker methods.** If you find yourself wanting one, STOP — that is Pause Condition 1.
+2. **No save schema changes.** Stays v33. No new fields, no version bump.
+3. **No `Math.random()` anywhere in new code.** The app is deterministic.
+4. **No new top-level packages or npm dependencies.**
+5. **All new test files use existing harness** — `@testing-library/react`, `vitest`, and the worker-mocking patterns from `NewsPage.test.tsx` and `RosterPage.test.tsx`.
+6. **Hard reload must continue to work** at every new route. Sprint 3.5 invariant.
+7. **Bundle budgets must not regress.** Check `apps/web/docs/BUDGETS.md` after the build.
+8. **Mobile 375×667** — every new route must render without horizontal overflow.
+9. **No `console.log`, `console.warn`, or `console.error` left in production code.** Use `logger` from `apps/web/src/shared/lib/logger.ts` if you need diagnostic output.
+10. **Cross-links must reuse the Roster pattern.** Same `` import, same hover/focus styling. Do not invent a "clickable name" component.
+11. **The Trade Negotiations Inbox is read-only in Sprint 4.** Action buttons (`canAccept`, `canCounter`, `canReject`) deep-link into the existing TradePage builder — they do NOT call worker actions directly from the Inbox. That keeps the Inbox surface a pure list/detail and respects "no new worker methods."
-```
+---
+
+## Milestone Loop
+
+Work milestones **in order**. After each milestone:
+
+1. Run `pnpm typecheck`.
+2. Run `pnpm test` (focused first if it's faster, then the full suite before commit).
+3. `git add` the milestone's files (specific files — no `git add -A`).
+4. Commit with the milestone's prescribed message.
+5. Append a milestone block to `.logs/goal-progress.md` (commit SHA, validation tail, scope decisions, surprises).
+6. Move to the next milestone.
+
+If a milestone's validation fails:
+
+- Fix forward. Do not commit a broken milestone.
+- If you cannot figure it out in three attempts, STOP and document under Pause Conditions.
+
+### Milestone 1 — Trade Negotiations Inbox scaffolding (`/trade-negotiations`)
+
+- Add a lazy import for `TradeNegotiationsInboxPage` in `apps/web/src/app/routes/index.tsx`.
+- Register `)} />` alongside the other top-level in-game routes.
+- Create `apps/web/src/features/trade-negotiations/routes/TradeNegotiationsInboxPage.tsx`:
+ - Calls `worker.getOpenNegotiations()` on mount.
+ - Renders the list. Each row shows: counterpart team (name + abbreviation, optionally team logo if a `TeamLogo` component exists), `phase` badge, `Round N` label from `roundsCompleted`, days-until-expiry derived from `expiresAtDay` minus current day (read current day from `useGameStore`), a one-line dialogue preview from the last `dialogue` entry if any, and a status indicator (`isComplete` → "Closed" badge; otherwise show which actions are available based on `canAccept`/`canCounter`/`canReject`).
+ - Row click navigates to `/trade-negotiations/:id`.
+ - Empty state: "No open trade negotiations" with a small "Visit the Trade Hub to start one" hint that links to `/trade`.
+ - Loading skeleton mimicking `NewsPage` skeleton style.
+ - Error path: toast + render an inline error card if the worker call throws.
+- Create `apps/web/src/features/trade-negotiations/routes/TradeNegotiationsInboxPage.test.tsx`:
+ - Happy path (mock worker, render rows).
+ - Empty state.
+ - Counterpart team name renders, phase badge renders, expires-in count renders.
+ - Row click navigates to `/trade-negotiations/:id` (use the same `MemoryRouter`/`useNavigate` mock pattern as `NewsPage.test.tsx`).
+- Sort order: open negotiations first (`isComplete === false`), then by smallest `expiresAtDay` first (most urgent on top). Closed last.
+- **DONE WHEN**: typecheck + test pass; `/MBD/trade-negotiations` (when the dev server is up) renders the list or the empty state.
+- **COMMIT**: `feat(trade-negotiations): add /trade-negotiations Inbox route`
+
+### Milestone 2 — Trade Negotiation detail (`/trade-negotiations/:negotiationId`)
+
+- Lazy import `TradeNegotiationDetailPage` in routes.
+- Register `)} />`.
+- Create `apps/web/src/features/trade-negotiations/routes/TradeNegotiationDetailPage.tsx`:
+ - Reads `negotiationId` from `useParams`.
+ - Calls `worker.getNegotiation(negotiationId)`.
+ - Renders the negotiation in detail:
+ - Header: counterpart team, phase, rounds completed, expires day.
+ - Two side-by-side panels: **Proposal** (the user's `proposal: TradeCounterPackage`) and **Counter-Offer** (the counterpart's `counterOffer`, or "Awaiting counter" if `null`). Render the `TradeCounterPackage` shape — read its type. If it includes player IDs, render the player names as `` to `/players/:playerId`.
+ - **Dialogue thread**: render `dialogue` entries chronologically as a chat-style log (sender name + message). Reuse existing chat/conversation styling if any exists in the codebase; otherwise compose with existing primitives.
+ - **Action area**: if `isComplete === false`, show a single CTA — "Open in Trade Builder" — that navigates to `/trade?negotiationId={id}` (deep link). Buttons for `canAccept`/`canCounter`/`canReject` are NOT wired in this sprint — they are visually disabled with a tooltip "Use the Trade Builder to act on this negotiation." If `isComplete === true`, show "This negotiation is closed."
+ - 404-ish state if `getNegotiation` returns `null`: "Trade negotiation not found" with a back link to `/trade-negotiations`.
+- Create `TradeNegotiationDetailPage.test.tsx`: happy path with proposal + counter-offer + dialogue, awaiting-counter path (`counterOffer === null`), not-found path.
+- **DONE WHEN**: typecheck + test pass; clicking a row in `/trade-negotiations` opens the detail page; bad ID renders not-found.
+- **COMMIT**: `feat(trade-negotiations): add /trade-negotiations/:id detail view`
+
+### Milestone 3 — Sidebar Trade Negotiations entry
+
+- Add a Trade Negotiations entry to `apps/web/src/app/layout/Sidebar.tsx`. Use a lucide icon — `Handshake`, `MessagesSquare`, or `Repeat2` are all reasonable. Pick one. (Note: Sprint 3 already used `Inbox` for News — pick something different.)
+- Place it adjacent to or under Trade. Your call.
+- Update `Sidebar.test.tsx` to include the new entry.
+- **DONE WHEN**: typecheck + test pass; sidebar shows Trade Negotiations and navigates to `/trade-negotiations`.
+- **COMMIT**: `feat(layout): add Trade Negotiations entry to Sidebar`
+
+### Milestone 4 — Trade page cross-linking + deep-link from Inbox
+
+- Find every place in `apps/web/src/features/trade/**` where a player name renders as plain text. Wrap each with `` (or equivalent). Match the styling pattern from `RosterPage.tsx:473`.
+- **Additionally**: support the `/trade?negotiationId={id}` deep link added in Milestone 2. On `TradePage` mount, read `searchParams.get('negotiationId')`, and if present, fetch that negotiation via `worker.getNegotiation(id)` and seed `activeNegotiation` / the trade builder via the existing `applyNegotiationToBuilder` flow. If the negotiation isn't found or has expired, show a toast and clear the param.
+- Update or add tests verifying the link target AND the deep-link behavior.
+- **DONE WHEN**: typecheck + test pass; clicking any player name in any Trade surface navigates to that player's profile; opening `/trade?negotiationId=` opens TradePage with that negotiation loaded.
+- **COMMIT**: `feat(trade): cross-link player names + accept ?negotiationId deep link`
+
+### Milestone 5 — Draft page cross-linking
+
+- Same `` pattern in `apps/web/src/features/draft/**` for draft prospects.
+- The `to={\`/players/${entry.playerId}?tab=development\`}` pattern from `apps/web/src/features/minors/components/ProspectBreakoutTracker.tsx:84` is a precedent for prospects specifically — consider whether `?tab=development` is the right default for the Big Board.
+- **DONE WHEN**: typecheck + test pass; draft Big Board entries are clickable.
+- **COMMIT**: `feat(draft): cross-link prospects to /players/:id`
+
+### Milestone 6 — Scouting page cross-linking
+
+- Same pattern in `apps/web/src/features/scouting/**`.
+- **DONE WHEN**: typecheck + test pass; scouting reports' player names are clickable.
+- **COMMIT**: `feat(scouting): cross-link player names to /players/:id`
+
+### Milestone 7 — Stats / leaderboards cross-linking
+
+- Same pattern in `apps/web/src/features/stats/**`.
+- **DONE WHEN**: typecheck + test pass; leaderboard entries are clickable.
+- **COMMIT**: `feat(stats): cross-link leaderboard entries to /players/:id`
+
+### Milestone 8 — News page player references (skip if NewsItem has no machine-readable player ref)
+
+- Inspect `NewsItem` from `@mbd/contracts`. Does it carry a machine-readable player reference field (`playerId`, `relatedPlayerIds`, `entities`, etc.)?
+- If YES: render those references as `` chips below the news body. Match existing news item styling.
+- If NO: SKIP this milestone. Do **not** introduce text-parsing or regex to extract player names. Document the skip in `.logs/goal-progress.md` and the final STATUS.md.
+- Update `NewsPage.test.tsx` if you wired links.
+- **DONE WHEN**: either links work, or skip is documented.
+- **COMMIT** (if shipped): `feat(news): cross-link player references in news items`
+
+### Milestone 9 — Trade Value on Player Profile
+
+- Wire `worker.getPlayerTradeValue(playerId)` into `PlayerProfilePage.tsx`.
+- Decide placement: a small panel/widget alongside the existing tabs, or a new tab. **Lean toward a small panel** rather than a new tab to keep tab count stable.
+- Render the `PlayerTradeValue` shape — read the type definition before designing.
+- Loading + null-handling.
+- Add a focused test in `PlayerProfilePage.test.tsx` for the trade-value render.
+- **DONE WHEN**: typecheck + test pass; the player profile shows Trade Value for any player.
+- **COMMIT**: `feat(players): surface trade value on player profile`
+
+### Milestone 10 — Browser smoke + screenshots + STATUS.md
+
+Run `pnpm --filter @mbd/web dev` and capture screenshots to `apps/web/docs/screenshots/sprint-4/`:
+
+1. `01-trade-negotiations-inbox.png` — `/MBD/trade-negotiations` with rows.
+2. `02-trade-negotiations-empty.png` — empty state.
+3. `03-trade-negotiation-detail.png` — `/MBD/trade-negotiations/` detail.
+4. `04-trade-negotiation-detail-awaiting-counter.png` — detail with `counterOffer === null`.
+5. `05-trade-deep-link-loaded.png` — `/MBD/trade?negotiationId=` showing the trade builder seeded with that negotiation.
+6. `06-trade-clickable-name.png` — hover/focus state on a clickable player name in Trade.
+7. `07-draft-clickable-prospect.png` — same in Draft.
+8. `08-scouting-clickable-name.png` — same in Scouting.
+9. `09-stats-clickable-leader.png` — same in Stats.
+10. `10-news-player-chip.png` — only if Milestone 8 shipped.
+11. `11-player-profile-trade-value.png` — Trade Value panel on profile.
+12. `12-trade-negotiations-mobile-375.png` — 375×667.
+13. `13-trade-negotiations-hard-reload.png` — Sprint 3.5 invariant check.
+
+Verify on each route:
+
+- Hard reload (Cmd+Shift+R) lands on the same route, not Save Hub. Sprint 3.5 invariant.
+- 375×667 viewport — no horizontal overflow.
+- No `console.error` in the browser console.
+
+Also run before final commit:
+
+```bash
PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm typecheck
PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm test
PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm build
-PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm --filter @mbd/web dev
```
-Targeted:
+Then rewrite `STATUS.md` at repo root with:
-```
-pnpm --filter @mbd/web test src/app/App.test.tsx
-pnpm --filter @mbd/web test src/app/layout/AppLayout.test.tsx
-pnpm --filter @mbd/web test src/features/setup/routes/SetupPage.test.tsx
-pnpm --filter @mbd/web test src/shared
+- What shipped (one-paragraph summary).
+- Files changed (`git diff --stat origin/main..HEAD`).
+- Validations run (typecheck + test + build — paste the final tail of each).
+- Browser evidence (screenshot list with one-line captions).
+- Bundle impact (chunk sizes; any movement against `BUDGETS.md`).
+- Worker methods newly consumed (`getOpenNegotiations`, `getNegotiation`, `getPlayerTradeValue`) — confirm zero new worker methods added.
+- Sprint 3.5 invariant confirmation.
+- Cross-linking coverage table (Roster ✅ / Free Agency ✅ / Minors ✅ / Trade ✅ / Draft ✅ / News ✅ or skipped / Scouting ✅ / Stats ✅).
+- Known limitations.
+- Risks.
+- Rollback notes (revert the merge commit; no schema bump).
+- Next `/goal` recommendation.
+
+- **DONE WHEN**: all required screenshots committed; STATUS.md and `.logs/goal-progress.md` complete.
+- **COMMIT**: `docs(sprint-4): browser smoke, STATUS report, and handoff`
+
+---
+
+## Validation Gates
+
+After **every** milestone:
+
+```bash
+PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm typecheck
+PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm test
```
-Browser flow (full smoke):
+Before the final Milestone 10 commit, also run `pnpm build`.
-1. `pnpm --filter @mbd/web dev`
-2. Open `http://localhost:5173/MBD/`
-3. Complete a new dynasty through onboarding → land on `/dashboard`
-4. Hard-reload `/MBD/dashboard` → expect dashboard renders, not Save Hub
-5. Navigate to `/news` → confirm the unread count and items match pre-reload state
-6. Hard-reload `/MBD/news` → expect news renders, not Save Hub
-7. Repeat for `/MBD/roster`, `/MBD/trade`, `/MBD/draft`
-8. In DevTools, clear IndexedDB for `mbd-saves` → hard-reload → expect graceful fallback to Save Hub
-9. In DevTools, set `localStorage` for `mbd:game-store@v1` to point at a non-existent save id → hard-reload → expect persisted state clears + Save Hub
-10. At 375×667 viewport, hard-reload `/MBD/dashboard` → no layout shift larger than a brief skeleton
+Browser smoke (Milestone 10 only): `PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm --filter @mbd/web dev` then drive through the screenshots list.
-## Evaluator-visible proof
+---
-Before declaring done, the transcript and `STATUS.md` must contain:
+## Pause Conditions
-- Exact commands run and pass/fail
-- Output summaries (test counts, build duration, bundle sizes — bundle should be unchanged or tiny)
-- Browser steps walked with screenshots under `apps/web/docs/screenshots/sprint-3-5/`
-- A `git diff --stat origin/main..HEAD` showing changes stayed inside allowed scope
-- localStorage snapshot before/after a reload (key + minimal value, no PII)
-- Confirmation that SaveHub manual continue still works (existing SetupPage.test.tsx assertions still pass)
+**STOP and surface to Kevin** (do not push, do not improvise) if any of these happen:
-## Autonomy rules
+1. A worker method you expected does not exist or returns an unexpected shape. Do not add new worker methods. Document the actual shape so the contract can be revised.
+2. The save schema would need to bump. Sprint 4 is **consumer-only**.
+3. A bundle budget in `apps/web/docs/BUDGETS.md` would be exceeded.
+4. Hard reload breaks at any new route. Sprint 3.5 invariant.
+5. An existing test in an unrelated area starts failing because of your changes. Investigate the coupling, don't paper over it.
+6. The `NewsItem` shape has no machine-readable player references AND you can't ship Milestone 8 cleanly. Skip; document; continue.
+7. You discover the audit was wrong about a "no consumers" claim (as happened with Press Conference last run). Document the surprise, skip the redundant work, continue.
+8. You cannot decide between two reasonable approaches and the Autonomy Rules below don't resolve it.
-When picking between:
-- (a) a `useAutoResumeSave` hook called inside `App.tsx`
-- (b) a wrapping `` component
+A pause is not a failure. A bad commit shipped through quietly is a failure. Codex's first-pass pause on this sprint was the right call — it caught a contract bug.
+
+---
-Pick whichever has the smaller diff in tests too. Both are valid.
+## Autonomy Rules
-When designing the "Resuming…" loading state, match the existing Suspense fallback (`LoadingFallback` in `apps/web/src/app/routes/index.tsx` — the MBD pulse + "Loading route..." pattern). Don't invent new spinners.
+Once the mission is clear, **do not ask for permission** on:
-When a persisted save id doesn't resolve in IndexedDB, **clear** `useGameStore` (call a new `clearActiveSave` action or `setActiveSave(null, null)`). Do NOT keep stale ids around.
+- Component composition (table vs. card list vs. row layout for the Inbox).
+- Lucide icon choice for Sidebar entry.
+- How to format the days-until-expiry chip (`expires in 3 days` vs. `3d`).
+- How to format dollar amounts in `TradeCounterPackage` (match what TradePage does).
+- Dialogue chat styling — invent something tasteful using existing tokens if no chat primitive exists.
+- Whether Trade Value on the Player Profile is a panel or a new tab. Lean panel.
+- Whether News player chips live above or below the body copy.
+- Sidebar grouping order.
+- Sort order tie-breakers within "open negotiations" (e.g. when two negotiations expire on the same day).
-When a worker error happens during hydration, route through `SaveRecoveryProvider.showFailure` with a synthetic `{ ok: false, reason: 'storage_failed', detail: ... }` so the user sees the same dialog they'd see from a manual load.
+Use judgment. Match the existing codebase's tone. Pause only on the Pause Conditions above.
-Log assumptions in `.logs/goal-progress.md` and continue.
+---
-## Pause conditions
+## Evaluator-Visible Proof
-Pause and write the blocker into `STATUS.md` only when:
+Maintain `.logs/goal-progress.md` continuously. After each milestone, append:
-- The auto-resume requires a sim-core change.
-- The auto-resume requires a worker-side method that doesn't exist.
-- A persistence-shape change requires bumping save schema (it shouldn't — localStorage is independent of `GameSnapshot`).
-- The same validation fails 3 times after serious repair attempts.
-- A protected file must be modified beyond `AppLayout.tsx`'s minimal touch (and even that touch should be avoided if possible).
-- Save Hub's manual continue path breaks and you can't restore parity in 2 fix attempts.
+```markdown
+## Milestone N —
-## Done when
+- Commit: ""
+- typecheck:
+- test:
+- Files touched:
+- Scope decisions:
+- Surprises:
+```
-All of the following are true:
+Treat `.logs/goal-progress.md` like a journal, not an afterthought. Kevin and future agents read it to understand what happened.
-- `useGameStore` persists to `localStorage` via Zustand `persist` with a versioned key.
-- On app boot, if a persisted `activeSaveId` resolves in IndexedDB, the app auto-loads it and renders the user's previous route — **NOT** Save Hub.
-- A "Resuming…" loading state shows while auto-load is in flight.
-- Corrupt saves trigger `SaveRecoveryDialog` (unchanged from today's manual-load behavior).
-- Missing saves clear the persisted state and fall through to Save Hub.
-- The Save Hub manual continue path is unchanged (existing tests pass).
-- Hard reload at `/MBD/dashboard`, `/MBD/news`, `/MBD/roster`, `/MBD/trade` all render the route, not Save Hub.
-- `pnpm typecheck` clean (all tasks).
-- `pnpm test` clean (no test deleted or weakened; new tests added for the auto-resume flow).
-- `pnpm build` clean (bundleBudget.test.ts passes; budget journal untouched).
-- Browser smoke walked end-to-end with screenshots under `apps/web/docs/screenshots/sprint-3-5/`.
-- `.logs/goal-progress.md` contains the milestone log.
-- `STATUS.md` exists with the final report.
-- Branch is on `goal/sprint-3-5-hard-reload-survival`.
+---
-## Final report
+## Done Criteria
+
+ALL of these must be true before Milestone 10's STATUS.md commit:
+
+1. ✅ `/trade-negotiations` renders open trade negotiations (or empty state).
+2. ✅ `/trade-negotiations/:id` renders detail or graceful not-found.
+3. ✅ Sidebar has a Trade Negotiations entry.
+4. ✅ Trade page player names link to `/players/:id`.
+5. ✅ TradePage accepts `?negotiationId=` and seeds the trade builder.
+6. ✅ Draft page prospects link to `/players/:id`.
+7. ✅ Scouting page player names link to `/players/:id`.
+8. ✅ Stats / leaderboards link to `/players/:id`.
+9. ✅ News page player references link to `/players/:id` OR Milestone 8 is documented as skipped with reason.
+10. ✅ Player Profile shows Trade Value.
+11. ✅ Each new route hard-reloads successfully (Sprint 3.5 invariant).
+12. ✅ Each new route renders cleanly at 375×667.
+13. ✅ All required screenshots committed under `apps/web/docs/screenshots/sprint-4/`.
+14. ✅ `pnpm typecheck` passes.
+15. ✅ `pnpm test` passes (with new tests added for Inbox, Detail, and Trade Value).
+16. ✅ `pnpm build` passes with no new budget violations.
+17. ✅ `STATUS.md` rewritten with the full report.
+18. ✅ `.logs/goal-progress.md` has 10 milestone blocks.
+19. ✅ Final commit pushed to `goal/sprint-4-front-office`.
+20. ✅ Zero new worker methods, zero schema changes, zero new dependencies.
-`STATUS.md` (rewrite from scratch) must include, in order:
+---
-1. **What shipped** — one paragraph summary.
-2. **Files changed** — `git diff --stat origin/main..HEAD` output.
-3. **Validations run** — exact commands and results.
-4. **Browser evidence** — screenshots under `apps/web/docs/screenshots/sprint-3-5/` with captions; localStorage key/value before & after; a list of routes you hard-reloaded successfully.
-5. **Save Recovery integration** — confirmation that corrupt saves still hit the dialog.
-6. **Known limitations** — anything out of scope you noticed.
-7. **Risks** — what could break in production and what to watch (e.g. quota, localStorage disabled in private mode).
-8. **Rollback notes** — revert the merge commit; localStorage entries will be ignored on the old code path.
-9. **Next /goal** — exact paste-ready prompt for **Sprint 4** (the audit's original next pick: wire orphaned player-profile + open-negotiations endpoints). Claude Code will draft that GOAL.md after Sprint 3.5 merges.
+## Final Report
-## Branch + commit hygiene
+When all Done Criteria are met:
-- Branch: `goal/sprint-3-5-hard-reload-survival` (built on top of `goal/sprint-3-news-inbox`). When Sprint 3 merges to `main`, rebase this branch.
-- Stage specific files; never `git add -A`.
-- Commit prefixes: `feat(app):` for the boot logic, `feat(store):` for `useGameStore` persistence, `test(app):` for tests, `docs(app):` for docs.
-- Co-author trailer on each commit:
+1. `git push origin goal/sprint-4-front-office`.
+2. Report back to Kevin (via the transcript / handoff doc you control) with:
+ - Final commit SHA.
+ - Branch name.
+ - Path to STATUS.md.
+ - Link to the draft PR (Claude Code will flip it to ready and merge once Kevin approves).
+ - The exact next `/goal` for the next sprint.
- ```
- Co-Authored-By: Codex GPT-5
- ```
+---
-- When done, push and open PR titled `Sprint 3.5 — Hard-reload state survival`. Body summarizes against this GOAL.md and links Sprint 3 PR #76 for lineage.
+## Bonus Round (only after all 20 Done Criteria are satisfied)
-## Out of scope (do not attempt this sprint)
+If Sprint 4 closes cleanly with daylight remaining, you may pick up **one** of the following clearly-scoped follow-ons. Do NOT start Bonus Round work until Milestone 10 is committed and pushed.
-- Granular player-profile endpoints (Sprint 4)
-- Open-negotiations resume pane (Sprint 4)
-- Press conference unification (Sprint 5)
-- Invariant runtime checks in DEV (Sprint 6)
-- Moving narrative generation off main thread (Sprint 6)
-- Team logo SVGs (Sprint 7)
-- Anything that touches `packages/sim-core/` or `packages/contracts/`
-- Service worker changes
-- Adding new dependencies
+### Bonus A — Active negotiation chip in TopBar
+
+If a user has open trade negotiations, show a small chip in `apps/web/src/app/layout/TopBar.tsx` (mirror Sprint 3's News chip pattern) that displays the count and links to `/trade-negotiations`.
+
+### Bonus B — Active negotiation panel on Player Profile
+
+Add a panel to `PlayerProfilePage` that calls `worker.getOpenNegotiations()` and surfaces any negotiation whose `proposal` or `counterOffer` includes this player. Links to `/trade-negotiations/:id`. Skip if the cross-reference is non-trivial to derive from the shape (read first, decide).
+
+### Bonus C — League activity on Inbox
+
+Below the user's open negotiations, show a small "League activity" summary count: "12 trade negotiations are happening league-wide" (if `getOpenNegotiations` returns league-wide, which you'll verify when you read it).
+
+**Bonus Round non-negotiables:**
+
+- Separate commits per task.
+- Same validation gates.
+- Update `.logs/goal-progress.md` with a "Bonus Round" section.
+- Append to `STATUS.md` under a "Bonus Round" heading.
+- If a Bonus Round task hits a Pause Condition, STOP and leave the bonus uncommitted — do not block Sprint 4's merge.
---
-*End of GOAL.md. Companion `/goal` slash command lives in Sprint 3.5's PR description and in Kevin's conversation with Claude Code.*
+## Operating Notes for Codex
+
+- This is Kevin's overnight sprint. He's asleep. He wants Codex working steady through the night. Pace yourself: 10 milestones over the night is roughly one milestone every ~50 minutes if you go steady. Quality > speed.
+- The previous run **paused correctly** under Pause Condition 1 because the original GOAL.md asked for fields that didn't exist on `TradeNegotiationView`. This revision fixes that. If you spot another contract bug like that, pause again — the precedent is established.
+- The `pnpm` binary lives at `/Users/tkevinbigham/.local/node-lts/bin/pnpm`. Every shell call needs `PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH` prepended.
+- The working tree is `/Users/tkevinbigham/MBD-main`. The branch is `goal/sprint-4-front-office`. The base is `main` at `93b3f5b`.
+- The draft PR ([#78](https://github.com/KevinBigham/MBD/pull/78)) already exists. Claude Code (reviewer) will flip it to ready and merge once Kevin approves in the morning.
+- If something feels off — a milestone is over-specified, an assumption is wrong, the worker shape doesn't match — STOP and surface it. The worst outcome is silent over-improvising. The second worst is asking permission for everything.
+
+Go.
diff --git a/STATUS.md b/STATUS.md
index 6e457b5..24c906d 100644
--- a/STATUS.md
+++ b/STATUS.md
@@ -1,165 +1,222 @@
-# STATUS - Sprint 3.5 Hard-Reload State Survival
+# STATUS - Sprint 4 Front Office Marathon
-Status: **COMPLETE** for the hard-reload state survival mission. The active save shell state now persists to localStorage, app boot auto-loads the persisted save through the existing safe save/worker path, and hard-reloaded in-game routes render their route instead of falling back to Save Hub.
+Status: **SHIPPED** on `goal/sprint-4-front-office`.
-## What shipped
+## What Shipped
-`useGameStore` now persists only the allowed shell fields under `mbd:game-store@v1`: active save id/slot, user team id, season, day, phase, team name, GM name, and difficulty. `AppBootGate` wraps the router, shows a `Resuming save...` route-level skeleton while loading, calls `loadSaveSafely(activeSaveId)`, imports the snapshot through `worker.importSnapshot`, then calls `initializeGame(...)` before `AppLayout` can redirect. Missing save ids clear the stale persisted active-save fields and fall through to Save Hub; corrupt saves route through the existing Save Recovery dialog.
+Sprint 4 wires the revised front-office scope end to end: a read-only Trade Negotiations Inbox at `/trade-negotiations`, a detail route at `/trade-negotiations/:negotiationId`, Sidebar navigation, Trade Builder deep-link loading from `?negotiationId=`, player-name cross-links across Trade/Draft/Scouting/Stats/News, and a Trade Value panel on Player Profile. The implementation stayed consumer-only: no save schema bump, no new worker query/action methods, no sim-core changes, and no new dependencies.
-## Files changed
+## Files Changed
-Current implementation diff is intentionally inside the Sprint 3.5 allowed scope plus proof artifacts:
+`git diff --stat origin/main..HEAD` before the Milestone 10 docs commit:
```text
-.logs/goal-progress.md
-STATUS.md
-apps/web/docs/screenshots/sprint-3-5/*.png
-apps/web/src/app/App.test.tsx
-apps/web/src/app/App.tsx
-apps/web/src/app/boot/AppBootGate.test.tsx
-apps/web/src/app/boot/AppBootGate.tsx
-apps/web/src/shared/hooks/useGameStore.test.ts
-apps/web/src/shared/hooks/useGameStore.ts
+ .logs/goal-progress.md | 89 ++++
+ GOAL.md | 589 +++++++++++++--------
+ STATUS.md | 178 +++----
+ apps/web/src/app/layout/Sidebar.test.tsx | 2 +
+ apps/web/src/app/layout/Sidebar.tsx | 2 +
+ apps/web/src/app/routes/index.tsx | 8 +
+ .../src/features/draft/routes/DraftPage.test.tsx | 4 +
+ apps/web/src/features/draft/routes/DraftPage.tsx | 41 +-
+ .../features/league/routes/LeadersPage.test.tsx | 4 +
+ .../web/src/features/news/routes/NewsPage.test.tsx | 23 +-
+ apps/web/src/features/news/routes/NewsPage.tsx | 89 +++-
+ .../players/routes/PlayerProfilePage.test.tsx | 40 +-
+ .../features/players/routes/PlayerProfilePage.tsx | 92 +++-
+ .../scouting/components/ScoutConflictsTab.tsx | 13 +-
+ .../features/scouting/routes/ScoutingPage.test.tsx | 228 +++++++-
+ .../src/features/scouting/routes/ScoutingPage.tsx | 51 +-
+ .../routes/TradeNegotiationDetailPage.test.tsx | 173 ++++++
+ .../routes/TradeNegotiationDetailPage.tsx | 376 +++++++++++++
+ .../routes/TradeNegotiationsInboxPage.test.tsx | 180 +++++++
+ .../routes/TradeNegotiationsInboxPage.tsx | 267 ++++++++++
+ .../trade/components/DeadlineDramaPanel.tsx | 8 +-
+ .../src/features/trade/routes/TradePage.test.tsx | 100 ++++
+ apps/web/src/features/trade/routes/TradePage.tsx | 168 +++++-
+ apps/web/src/shared/hooks/useWorker.ts | 6 +-
+ 24 files changed, 2299 insertions(+), 432 deletions(-)
```
-Pre-existing local dirt left untouched:
+Milestone 10 also adds `apps/web/docs/screenshots/sprint-4/*.png`, rewrites this `STATUS.md`, and commits the accumulated `.logs/goal-progress.md` journal entry from Milestone 9.
+
+Pre-existing local change left untouched:
```text
.claude/launch.json
-.claude/scheduled_tasks.lock
```
-`git diff --stat origin/main..HEAD`:
+## Validations Run
-```text
- .logs/goal-progress.md | 121 +++++++
- GOAL.md | 355 ++++++++++-----------
- STATUS.md | 180 ++++++-----
- .../sprint-3-5/01-dashboard-before-hard-reload.png | Bin 0 -> 156026 bytes
- .../sprint-3-5/02-dashboard-after-hard-reload.png | Bin 0 -> 139473 bytes
- .../sprint-3-5/03-news-before-hard-reload.png | Bin 0 -> 126471 bytes
- .../sprint-3-5/04-news-after-hard-reload.png | Bin 0 -> 103543 bytes
- .../sprint-3-5/05-roster-after-hard-reload.png | Bin 0 -> 135451 bytes
- .../sprint-3-5/06-trade-after-hard-reload.png | Bin 0 -> 150587 bytes
- .../sprint-3-5/07-draft-after-hard-reload.png | Bin 0 -> 93716 bytes
- .../sprint-3-5/08-save-hub-after-delete-slot.png | Bin 0 -> 135242 bytes
- .../09-missing-save-fallback-save-hub.png | Bin 0 -> 138231 bytes
- .../sprint-3-5/10-corrupt-save-recovery-dialog.png | Bin 0 -> 89768 bytes
- .../11-dashboard-mobile-375-after-hard-reload.png | Bin 0 -> 32785 bytes
- apps/web/src/app/App.test.tsx | 4 +
- apps/web/src/app/App.tsx | 33 +-
- apps/web/src/app/boot/AppBootGate.test.tsx | 283 ++++++++++++++++
- apps/web/src/app/boot/AppBootGate.tsx | 171 ++++++++++
- apps/web/src/shared/hooks/useGameStore.test.ts | 72 +++++
- apps/web/src/shared/hooks/useGameStore.ts | 118 ++++---
- 20 files changed, 1008 insertions(+), 329 deletions(-)
+```bash
+PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm typecheck
```
-## Validations run
+Final tail:
```text
-PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm --filter @mbd/web test src/shared/hooks/useGameStore.test.ts src/app/boot/AppBootGate.test.tsx
+Tasks: 9 successful, 9 total
+Cached: 8 cached, 9 total
+Time: 5.386s
```
-Red proof: FAIL as expected before implementation. The store persistence test saw `persisted.version` as `undefined`, and `AppBootGate.tsx` did not exist.
-
-```text
-PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm --filter @mbd/web test src/shared/hooks/useGameStore.test.ts src/app/boot/AppBootGate.test.tsx src/app/App.test.tsx
+```bash
+PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm test
```
-Latest focused result: PASS, 3 files / 8 tests.
+Final tail:
```text
-PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm typecheck
+@mbd/web:test: Test Files 103 passed (103)
+@mbd/web:test: Tests 641 passed (641)
+@mbd/web:test: Duration 79.87s
+
+Tasks: 8 successful, 8 total
+Cached: 7 cached, 8 total
+Time: 1m20.508s
```
-PASS. Turbo reported `Tasks: 9 successful, 9 total` in `5.418s`.
+Known test noise remained the existing Recharts zero-size warnings, React `act(...)` warnings, intentional mocked worker failure log in the Inbox error test, and the intentional service-worker registration failure test.
-```text
-PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm test
+```bash
+PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm build
```
-PASS. Turbo reported `Tasks: 8 successful, 8 total` in `1m23.712s`. Web passed 101 files / 629 tests, sim-core passed 137 files / 1610 tests, contracts passed 1 file / 20 tests, and UI passed 1 file / 1 test. Existing non-fatal console noise remained: Recharts zero-size warnings, React `act(...)` warnings, service worker failure-test log, and the existing ScoutingPage mock-function log.
+Final tail:
```text
-PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm build
-```
+@mbd/web:build: ✓ built in 3.23s
+@mbd/web:build: PWA v1.2.0
+@mbd/web:build: precache 122 entries (3286.88 KiB)
-PASS. Turbo reported `Tasks: 5 successful, 5 total` in `4.134s`; Vite built in `3.28s`; PWA precached 120 entries. Bundle budget stayed green. Main app chunk: `index-jENJPu8m.js` 205.74 KB raw / 58.38 KB gzip. Worker chunks remained `game-engine-core` 450.75 KB raw and `game-engine-story` 452.10 KB raw.
+Tasks: 5 successful, 5 total
+Cached: 4 cached, 5 total
+Time: 4.03s
+```
-## Browser evidence
+## Browser Evidence
Dev server:
-```text
+```bash
PATH=/Users/tkevinbigham/.local/node-lts/bin:$PATH pnpm --filter @mbd/web dev
```
-PASS at `http://localhost:5173/MBD/`.
+Captured screenshots:
+
+| File | Evidence |
+| --- | --- |
+| `01-trade-negotiations-inbox.png` | `/MBD/trade-negotiations` with open negotiation rows. |
+| `02-trade-negotiations-empty.png` | Empty Inbox state. |
+| `03-trade-negotiation-detail.png` | Detail route with proposal, counter-offer, and dialogue. |
+| `04-trade-negotiation-detail-awaiting-counter.png` | Detail route where `counterOffer === null`. |
+| `05-trade-deep-link-loaded.png` | `/MBD/trade?negotiationId=` with builder seeded from the negotiation. |
+| `06-trade-clickable-name.png` | Trade player name focused/hovered as a `/players/:id` link. |
+| `07-draft-clickable-prospect.png` | Draft prospect focused/hovered as a player-profile link. |
+| `08-scouting-clickable-name.png` | Scouting result/report player link. |
+| `09-stats-clickable-leader.png` | Leaderboard player link. |
+| `10-news-player-chip.png` | News related-player chip linking to `/players/:id`. |
+| `11-player-profile-trade-value.png` | Player Profile Trade Value panel. |
+| `12-trade-negotiations-mobile-375.png` | Inbox at 375x667 with no horizontal overflow. |
+| `13-trade-negotiations-hard-reload.png` | Inbox after hard reload, still on route. |
+
+Browser smoke checks:
+
+```text
+First pass: 01-06 captured; consoleErrors: []; pageErrors: [].
+Second pass: 07-13 captured; consoleErrors: []; pageErrors: [].
+Mobile no-overflow: /trade-negotiations true; /trade-negotiations/:id true.
+Hard reload: /trade-negotiations true; /trade-negotiations/:id true.
+```
-Screenshots under `apps/web/docs/screenshots/sprint-3-5/`:
+The Codex in-app browser screenshot command timed out on `Page.captureScreenshot`, so the committed screenshots were captured with a local Playwright Chromium fallback using the same running dev server. Playwright was installed into the user cache only; repo package manifests were not changed.
-- `01-dashboard-before-hard-reload.png` - dashboard loaded from Save Hub continue.
-- `02-dashboard-after-hard-reload.png` - `/MBD/dashboard` after hard reload, dashboard rendered.
-- `03-news-before-hard-reload.png` - News Inbox before hard reload.
-- `04-news-after-hard-reload.png` - `/MBD/news` after hard reload, News Inbox rendered.
-- `05-roster-after-hard-reload.png` - `/MBD/roster` after hard reload, Roster rendered.
-- `06-trade-after-hard-reload.png` - `/MBD/trade` after hard reload, Trade Center rendered.
-- `07-draft-after-hard-reload.png` - `/MBD/draft` after hard reload, Draft Room rendered.
-- `08-save-hub-after-delete-slot.png` - active Slot 1 deleted for missing-save fallback.
-- `09-missing-save-fallback-save-hub.png` - stale persisted id fell through to Save Hub.
-- `10-corrupt-save-recovery-dialog.png` - corrupt persisted save hit Save Recovery actions.
-- `11-dashboard-mobile-375-after-hard-reload.png` - 375x667 dashboard hard reload rendered dashboard with `scrollWidth=375`.
+## Cross-Linking Coverage
-Routes hard-reloaded successfully without Save Hub redirect:
+| Surface | Coverage |
+| --- | --- |
+| Roster | Existing baseline already links player names. |
+| Free Agency | Existing baseline already links player names. |
+| Minors | Existing baseline already links prospects to player profiles/development. |
+| Trade | Shipped links in trade assets, offer summaries, active negotiation packages, deadline panels, and deep-link builder flow. |
+| Draft | Shipped links for structured prospects and draft-board contexts. |
+| News | Shipped related-player chips from machine-readable `relatedPlayerIds`; no prose parsing. |
+| Scouting | Shipped links for structured pro/IFA reports, search results, recent reports, board entries, and conflict headlines. |
+| Stats | Existing production leaderboard links verified with regression coverage. |
+
+## Bundle Impact
+
+Build and bundle-budget test passed with no ceiling changes. New route chunks are lazy-loaded:
```text
-/MBD/dashboard
-/MBD/news
-/MBD/roster
-/MBD/trade
-/MBD/draft
+TradeNegotiationsInboxPage-D2RxZ2hf.js 7.27 kB | gzip 2.26 kB
+TradeNegotiationDetailPage-BeonUzVy.js 9.05 kB | gzip 2.73 kB
+TradePage-BlCLVm2_.js 68.41 kB | gzip 13.46 kB
+DraftPage-DjO87OaK.js 34.09 kB | gzip 7.07 kB
+ScoutingPage-CGK5jiXs.js 34.79 kB | gzip 6.98 kB
+LeadersPage-CT2DrRUP.js 7.84 kB | gzip 2.08 kB
+NewsPage-CFthlCeJ.js 10.62 kB | gzip 3.64 kB
+PlayerProfilePage-BACNN7vy.js 14.43 kB | gzip 4.57 kB
+index-B5O8n8bI.js 206.42 kB | gzip 58.54 kB
```
-localStorage before dashboard reload:
+`apps/web/src/build/bundleBudget.test.ts` passed as part of `pnpm test`. `apps/web/docs/BUDGETS.md` and `apps/web/src/build/bundleConfig.ts` were read; no budget lift was made.
-```json
-{"state":{"activeSaveId":"save-slot-1","activeSaveSlot":1,"userTeamId":"nym","season":1,"day":1,"phase":"preseason","teamName":"New York Tycoons","gmName":"Mobile Smoke","difficulty":"standard"},"version":1}
-```
+## Worker Method Confirmation
-localStorage after dashboard reload:
+Newly consumed existing methods:
-```json
-{"state":{"activeSaveId":"save-slot-1","activeSaveSlot":1,"userTeamId":"nym","season":1,"day":1,"phase":"preseason","teamName":"New York Tycoons","gmName":"Mobile Smoke","difficulty":"standard"},"version":1}
-```
+- `getOpenNegotiations()` in the Inbox route.
+- `getNegotiation(negotiationId)` in the detail route and Trade Builder deep-link load.
+- `getPlayerTradeValue(playerId)` on Player Profile through the existing worker API, newly forwarded by `useWorker`.
-Missing-save fallback cleared the stale active save id and rendered Save Hub. Corrupt-save fallback cleared the stale active save id and showed the existing Save Recovery action surface; post-recovery storage was:
+Confirmed:
-```json
-{"state":{"activeSaveId":null,"activeSaveSlot":null,"userTeamId":"nym","season":1,"day":1,"phase":"preseason","teamName":"New York Tycoons","gmName":"Smoke Tester","difficulty":"standard"},"version":1}
-```
+- Zero new worker query/action methods.
+- Zero edits to `apps/web/src/workers/sim.worker.queries.ts`.
+- Zero edits to `apps/web/src/workers/sim.worker.actions.ts`.
+- Zero edits to `apps/web/src/workers/sim.worker.trade.ts`.
+- Zero save schema changes and no `packages/contracts/**` edits.
+- No new dependencies.
+- No new `Math.random()` or production `console.*` calls in the Sprint 4 diff.
+
+## Sprint 3.5 Invariant
+
+The new routes hard-reload successfully:
+
+| Route | Result |
+| --- | --- |
+| `/MBD/trade-negotiations` | Reload lands on Inbox, not Save Hub. |
+| `/MBD/trade-negotiations/:id` | Reload lands on the detail route shell/graceful not-found for a non-persisted smoke negotiation, not Save Hub. |
+
+The new routes render at 375x667 without horizontal overflow:
-## Save Recovery integration
+| Route | Result |
+| --- | --- |
+| `/MBD/trade-negotiations` | `scrollWidth === clientWidth === 375`. |
+| `/MBD/trade-negotiations/:id` | `scrollWidth === clientWidth === 375`. |
-Auto-resume uses `loadSaveSafely(activeSaveId)`. Non-missing `{ ok: false }` results call `SaveRecoveryProvider.showFailure(...)` with a retry callback, matching the manual Save Hub load path. `AppBootGate.test.tsx` covers the corrupt-save branch, and browser evidence `10-corrupt-save-recovery-dialog.png` confirms the recovery action surface appears for a malformed persisted save.
+Protected Sprint 3.5 files were not changed: `saveSystem.ts`, `AppBootGate.tsx`, `useGameStore.ts`, and `features/save-recovery/**`.
-## Known limitations
+## Known Limitations
-- The in-app Browser read-only page scope did not expose `localStorage` or `indexedDB`, so exact storage snapshots and 375x667 viewport metrics were captured in a separate Playwright context against the same dev server.
-- Auto-resume intentionally does not persist or duplicate snapshots in localStorage. IndexedDB remains the source of truth for heavy save data.
-- If localStorage is disabled, the app behaves like the old flow and falls back to Save Hub on hard reload.
+- News links use structured `relatedPlayerIds` only; narrative player names without a machine-readable player reference remain plain text.
+- Trade narrative/ticker prose that lacks a structured player asset remains plain text; no regex/name parsing was introduced.
+- The smoke-created negotiation was in-memory for the live browser profile. Hard reload of that exact detail ID confirms route survival and graceful unavailable-state handling, while the live detail screenshot confirms the full detail UI before reload.
+- Stats leaderboard rows were already linked in production code; Sprint 4 added regression coverage rather than route-code churn.
## Risks
-- Browser storage edge cases remain the main risk: localStorage denied, IndexedDB blocked, quota pressure, or a stale persisted id after manual browser data cleanup. The implemented behavior clears stale active-save state and falls back to Save Hub.
-- Worker import failures now surface through Save Recovery as `storage_failed`; watch production telemetry/manual reports for any confusing copy if a future worker compatibility error is not truly storage-related.
+- The Inbox is intentionally read-only. Accept/counter/reject actions deep-link into the existing Trade Builder rather than calling worker actions from the Inbox.
+- Player Profile Trade Value depends on the existing worker method returning a value for the current player; the UI handles `null` by showing an unavailable state.
+- Browser screenshots used a copied local profile plus Playwright fallback because the in-app screenshot command timed out; this affects evidence collection only, not app code or dependencies.
-## Rollback notes
+## Rollback Notes
-Revert the Sprint 3.5 merge commit. No save schema bump, no migration, no worker/sim-core/contracts changes, no new dependencies. Existing `mbd:game-store@v1` localStorage entries become inert on the old code path; users can also remove that key manually if needed.
+Rollback is straightforward: revert the Sprint 4 feature/docs commits on `goal/sprint-4-front-office` or revert the eventual merge commit. No migration rollback is required because save schema remains v33, no contracts changed, and no new dependencies were added.
-## Next /goal
+## Exact Next /goal
```text
-/goal Implement Sprint 4: wire orphaned player-profile + open-negotiations endpoints. Read README.md, CHANGELOG.md, MASTER_CONTEXT.md, STATUS.md, GOAL.md, the player profile route/tests, trade negotiation route/tests, useWorker, and worker query/action surfaces first. Keep save schema v33 and do not touch sim-core/contracts unless GOAL.md explicitly allows it. Wire the existing worker endpoints into the player profile and open-negotiations UI so hard-reloaded long-running saves can inspect a player and resume active trade talks without dead controls. Add focused tests for player-profile endpoint wiring and open-negotiations resume behavior, then validate with pnpm typecheck + pnpm test + pnpm build and a dev-server browser smoke. Keep proof in .logs/goal-progress.md and finish with STATUS.md.
+/goal Review Sprint 4 PR #78 against GOAL.md and STATUS.md, inspect apps/web/docs/screenshots/sprint-4/, then have Claude Code flip the draft PR ready or request targeted fixes.
```
diff --git a/apps/web/docs/screenshots/sprint-4/01-trade-negotiations-inbox.png b/apps/web/docs/screenshots/sprint-4/01-trade-negotiations-inbox.png
new file mode 100644
index 0000000..b8f42ef
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/01-trade-negotiations-inbox.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/02-trade-negotiations-empty.png b/apps/web/docs/screenshots/sprint-4/02-trade-negotiations-empty.png
new file mode 100644
index 0000000..db945d2
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/02-trade-negotiations-empty.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/03-trade-negotiation-detail.png b/apps/web/docs/screenshots/sprint-4/03-trade-negotiation-detail.png
new file mode 100644
index 0000000..567c760
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/03-trade-negotiation-detail.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/04-trade-negotiation-detail-awaiting-counter.png b/apps/web/docs/screenshots/sprint-4/04-trade-negotiation-detail-awaiting-counter.png
new file mode 100644
index 0000000..064dcd9
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/04-trade-negotiation-detail-awaiting-counter.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/05-trade-deep-link-loaded.png b/apps/web/docs/screenshots/sprint-4/05-trade-deep-link-loaded.png
new file mode 100644
index 0000000..c9355a1
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/05-trade-deep-link-loaded.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/06-trade-clickable-name.png b/apps/web/docs/screenshots/sprint-4/06-trade-clickable-name.png
new file mode 100644
index 0000000..2468329
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/06-trade-clickable-name.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/07-draft-clickable-prospect.png b/apps/web/docs/screenshots/sprint-4/07-draft-clickable-prospect.png
new file mode 100644
index 0000000..1ad3807
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/07-draft-clickable-prospect.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/08-scouting-clickable-name.png b/apps/web/docs/screenshots/sprint-4/08-scouting-clickable-name.png
new file mode 100644
index 0000000..641c978
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/08-scouting-clickable-name.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/09-stats-clickable-leader.png b/apps/web/docs/screenshots/sprint-4/09-stats-clickable-leader.png
new file mode 100644
index 0000000..4683036
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/09-stats-clickable-leader.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/10-news-player-chip.png b/apps/web/docs/screenshots/sprint-4/10-news-player-chip.png
new file mode 100644
index 0000000..d58a5b0
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/10-news-player-chip.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/11-player-profile-trade-value.png b/apps/web/docs/screenshots/sprint-4/11-player-profile-trade-value.png
new file mode 100644
index 0000000..f79123e
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/11-player-profile-trade-value.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/12-trade-negotiations-mobile-375.png b/apps/web/docs/screenshots/sprint-4/12-trade-negotiations-mobile-375.png
new file mode 100644
index 0000000..e57db66
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/12-trade-negotiations-mobile-375.png differ
diff --git a/apps/web/docs/screenshots/sprint-4/13-trade-negotiations-hard-reload.png b/apps/web/docs/screenshots/sprint-4/13-trade-negotiations-hard-reload.png
new file mode 100644
index 0000000..54ef4fd
Binary files /dev/null and b/apps/web/docs/screenshots/sprint-4/13-trade-negotiations-hard-reload.png differ
diff --git a/apps/web/src/app/layout/Sidebar.test.tsx b/apps/web/src/app/layout/Sidebar.test.tsx
index a68e1be..71c26c0 100644
--- a/apps/web/src/app/layout/Sidebar.test.tsx
+++ b/apps/web/src/app/layout/Sidebar.test.tsx
@@ -94,6 +94,8 @@ describe('Sidebar', () => {
expect(container.textContent).toContain('Offseason');
expect(container.textContent).toContain('Compare');
expect(container.textContent).toContain('News');
+ expect(container.textContent).toContain('Trade Negotiations');
+ expect(container.querySelector('a[href="/trade-negotiations"]')?.textContent).toContain('Trade Negotiations');
expect(getDashboardSummary).toHaveBeenCalledTimes(1);
});
diff --git a/apps/web/src/app/layout/Sidebar.tsx b/apps/web/src/app/layout/Sidebar.tsx
index e5d572b..aa434f6 100644
--- a/apps/web/src/app/layout/Sidebar.tsx
+++ b/apps/web/src/app/layout/Sidebar.tsx
@@ -12,6 +12,7 @@ import {
Search,
FileText,
ArrowLeftRight,
+ Handshake,
HandCoins,
Flame,
MoreHorizontal,
@@ -56,6 +57,7 @@ const baseMainNavItems: NavItem[] = [
{ to: '/offseason', label: 'Offseason', icon: },
{ to: '/players/compare', label: 'Compare', icon: },
{ to: '/trade', label: 'Trades', icon: },
+ { to: '/trade-negotiations', label: 'Trade Negotiations', icon: },
{ to: '/league/standings', label: 'League', icon: },
{ to: '/stats', label: 'Stats', icon: },
{ to: '/records', label: 'Records', icon: },
diff --git a/apps/web/src/app/routes/index.tsx b/apps/web/src/app/routes/index.tsx
index f0ece81..b1493c2 100644
--- a/apps/web/src/app/routes/index.tsx
+++ b/apps/web/src/app/routes/index.tsx
@@ -37,6 +37,12 @@ const DraftPage = lazy(
const TradePage = lazy(
() => import('@/features/trade/routes/TradePage')
);
+const TradeNegotiationsInboxPage = lazy(
+ () => import('@/features/trade-negotiations/routes/TradeNegotiationsInboxPage')
+);
+const TradeNegotiationDetailPage = lazy(
+ () => import('@/features/trade-negotiations/routes/TradeNegotiationDetailPage')
+);
const StandingsPage = lazy(
() => import('@/features/league/routes/StandingsPage')
);
@@ -142,6 +148,8 @@ export function AppRoutes() {
)} />
)} />
)} />
+ )} />
+ )} />
)} />
)} />
diff --git a/apps/web/src/features/draft/routes/DraftPage.test.tsx b/apps/web/src/features/draft/routes/DraftPage.test.tsx
index 6fad107..4944e50 100644
--- a/apps/web/src/features/draft/routes/DraftPage.test.tsx
+++ b/apps/web/src/features/draft/routes/DraftPage.test.tsx
@@ -487,6 +487,10 @@ describe('DraftPage', () => {
expect(container.textContent).toContain('War Room');
expect(container.textContent).toContain('Buzz Tracker');
expect(container.textContent).toContain('Value Slide');
+ const availableProspectLink = container.querySelector('a[href="/players/prospect-1?tab=development"]');
+ const completedPickLink = container.querySelector('a[href="/players/bos-1?tab=development"]');
+ expect(availableProspectLink?.textContent ?? '').toContain('Eli Prospect');
+ expect(completedPickLink?.textContent ?? '').toContain('Marcus Early');
const watchButton = Array.from(container.querySelectorAll('button')).find(
(button) => button.textContent?.includes('Watch Draft'),
diff --git a/apps/web/src/features/draft/routes/DraftPage.tsx b/apps/web/src/features/draft/routes/DraftPage.tsx
index 2d29d94..1ee5cf2 100644
--- a/apps/web/src/features/draft/routes/DraftPage.tsx
+++ b/apps/web/src/features/draft/routes/DraftPage.tsx
@@ -8,6 +8,7 @@ import {
Users,
Zap,
} from 'lucide-react';
+import { Link } from 'react-router-dom';
import { useWorker } from '@/shared/hooks/useWorker';
import { useGameStore } from '@/shared/hooks/useGameStore';
import { getAudioEngine } from '@/shared/lib/audio';
@@ -56,6 +57,12 @@ function formatBonus(value: number | null | undefined): string {
return `$${(value ?? 0).toFixed(2)}M`;
}
+const DRAFT_PLAYER_LINK_CLASS = 'font-heading font-medium text-dynasty-text hover:text-accent-primary';
+
+function draftPlayerPath(playerId: string): string {
+ return `/players/${playerId}?tab=development`;
+}
+
function compensationContextLabel(
compensation: DraftRoomPick['compensation'] | DraftBoardCell['compensation'] | null | undefined,
): string | null {
@@ -207,7 +214,15 @@ function ProspectsPanel({