+ );
+}
+
+export function AssistantPanel({ tickerFeed = [] }: AssistantPanelProps) {
+ const location = useLocation();
+ const {
+ activeSaveId,
+ activeSaveSlot,
+ day,
+ phase,
+ season,
+ } = useGameStore();
+ const routeKey = resolveAssistantRouteKey(location.pathname);
+ const guidance = selectRouteGuidance(location.pathname);
+ const saveId = resolveSaveId(activeSaveSlot, activeSaveId);
+ const [state, setState] = useState(() => readAssistantState(saveId));
+ const [open, setOpen] = useState(false);
+ const [ratingsOpen, setRatingsOpen] = useState(false);
+ const [strategyOpen, setStrategyOpen] = useState(false);
+
+ useEffect(() => {
+ setState(readAssistantState(saveId));
+ setOpen(false);
+ setRatingsOpen(false);
+ setStrategyOpen(false);
+ }, [saveId]);
+
+ const dispatch = useCallback((event: AssistantStateEvent) => {
+ setState((current) => {
+ const next = reduceAssistantState(current, event);
+ writeAssistantState(saveId, next);
+ return next;
+ });
+ }, [saveId]);
+
+ const nextAction = useMemo(() => buildAssistantNextAction({
+ routeKey,
+ phase,
+ day,
+ season,
+ mode: state.mode,
+ }), [day, phase, routeKey, season, state.mode]);
+
+ const story = useMemo(() => buildStoryCallback({
+ phase,
+ day,
+ season,
+ routeKey,
+ seenStoryCallbacks: state.seenStoryCallbacks,
+ tickerFeed,
+ }), [day, phase, routeKey, season, state.seenStoryCallbacks, tickerFeed]);
+
+ const routeDismissed = state.dismissedRoutes[routeKey] === true;
+
+ const handleComplete = useCallback(() => {
+ dispatch({ type: 'completeRoute', routeKey });
+ if (story) {
+ dispatch({ type: 'markStorySeen', callbackId: story.id });
+ }
+ setOpen(false);
+ setRatingsOpen(false);
+ setStrategyOpen(false);
+ }, [dispatch, routeKey, story]);
+
+ const handleReplay = useCallback(() => {
+ dispatch({ type: 'replayRoute', routeKey });
+ setRatingsOpen(false);
+ setStrategyOpen(false);
+ setOpen(true);
+ }, [dispatch, routeKey]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onKey = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setOpen(false);
+ }
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [open]);
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/features/assistant/data/assistantGuidance.test.ts b/apps/web/src/features/assistant/data/assistantGuidance.test.ts
new file mode 100644
index 0000000..03308ef
--- /dev/null
+++ b/apps/web/src/features/assistant/data/assistantGuidance.test.ts
@@ -0,0 +1,102 @@
+import { describe, expect, it } from 'vitest';
+import {
+ ASSISTANT_GUIDANCE,
+ REQUIRED_ASSISTANT_ROUTE_KEYS,
+ buildAssistantNextAction,
+ buildStoryCallback,
+ resolveAssistantRouteKey,
+ selectRouteGuidance,
+} from './assistantGuidance';
+
+describe('assistantGuidance', () => {
+ it('normalizes aliases and dynamic routes to stable guidance keys', () => {
+ expect(resolveAssistantRouteKey('/')).toBe('setup');
+ expect(resolveAssistantRouteKey('/league/standings')).toBe('standings');
+ expect(resolveAssistantRouteKey('/standings')).toBe('standings');
+ expect(resolveAssistantRouteKey('/league/leaders')).toBe('leaders');
+ expect(resolveAssistantRouteKey('/players/abc-123')).toBe('player-profile');
+ expect(resolveAssistantRouteKey('/games/42')).toBe('box-score');
+ expect(resolveAssistantRouteKey('/unknown')).toBe('dashboard');
+ });
+
+ it('covers every required MBD route with route-aware guidance', () => {
+ for (const key of REQUIRED_ASSISTANT_ROUTE_KEYS) {
+ const guidance = ASSISTANT_GUIDANCE[key];
+ expect(guidance, `${key} guidance`).toBeTruthy();
+ expect(guidance.title.length, `${key} title`).toBeGreaterThan(0);
+ expect(guidance.pagePurpose.length, `${key} purpose`).toBeGreaterThan(0);
+ expect(guidance.suggestedAction.label.length, `${key} action`).toBeGreaterThan(0);
+ }
+ });
+
+ it('includes ratings focus on decision-critical player movement pages', () => {
+ for (const path of ['/roster', '/players', '/players/compare', '/players/example', '/scouting', '/draft', '/trade', '/free-agency', '/minors']) {
+ expect(selectRouteGuidance(path).ratingsFocus, `${path} ratingsFocus`).toMatch(/OVR|rating|ceiling|grade|confidence/i);
+ }
+ });
+
+ it('selects next best action from phase and route context', () => {
+ expect(buildAssistantNextAction({
+ routeKey: 'dashboard',
+ phase: 'offseason',
+ day: 1,
+ season: 2,
+ mode: 'newcomer',
+ })).toMatchObject({ route: '/offseason', label: 'Open offseason checklist' });
+
+ expect(buildAssistantNextAction({
+ routeKey: 'trade',
+ phase: 'regular',
+ day: 95,
+ season: 1,
+ mode: 'hardcore',
+ })).toMatchObject({ route: '/trade', label: 'Audit deadline value' });
+ });
+
+ it('creates deterministic story callbacks from phase and ticker context', () => {
+ expect(buildStoryCallback({
+ phase: 'playoffs',
+ day: 1,
+ season: 4,
+ routeKey: 'dashboard',
+ seenStoryCallbacks: {},
+ tickerFeed: [],
+ })).toMatchObject({
+ id: 'phase-playoffs-s4',
+ tone: 'excited',
+ });
+
+ expect(buildStoryCallback({
+ phase: 'regular',
+ day: 87,
+ season: 2,
+ routeKey: 'dashboard',
+ seenStoryCallbacks: {},
+ tickerFeed: [
+ {
+ id: 'ticker-trade-1',
+ category: 'trade',
+ text: 'League rivals are calling about late-inning relief.',
+ },
+ ],
+ })).toMatchObject({
+ id: 'ticker-trade-1',
+ tone: 'warning',
+ });
+
+ expect(buildStoryCallback({
+ phase: 'regular',
+ day: 87,
+ season: 2,
+ routeKey: 'dashboard',
+ seenStoryCallbacks: { 'ticker-trade-1': true },
+ tickerFeed: [
+ {
+ id: 'ticker-trade-1',
+ category: 'trade',
+ text: 'League rivals are calling about late-inning relief.',
+ },
+ ],
+ })).toBeNull();
+ });
+});
diff --git a/apps/web/src/features/assistant/data/assistantGuidance.ts b/apps/web/src/features/assistant/data/assistantGuidance.ts
new file mode 100644
index 0000000..35a4531
--- /dev/null
+++ b/apps/web/src/features/assistant/data/assistantGuidance.ts
@@ -0,0 +1,553 @@
+import type { AssistantMode } from '../lib/assistantState';
+
+export const REQUIRED_ASSISTANT_ROUTE_KEYS = [
+ 'setup',
+ 'onboarding',
+ 'dashboard',
+ 'roster',
+ 'minors',
+ 'players',
+ 'player-compare',
+ 'player-profile',
+ 'scouting',
+ 'staff',
+ 'draft',
+ 'trade',
+ 'standings',
+ 'leaders',
+ 'schedule',
+ 'box-score',
+ 'press-room',
+ 'playoffs',
+ 'free-agency',
+ 'offseason',
+ 'finance',
+ 'career',
+ 'history',
+ 'achievements',
+ 'rivalries',
+ 'front-office',
+ 'pulse',
+ 'scenarios',
+ 'stats',
+ 'records',
+ 'settings',
+] as const;
+
+export type AssistantRouteKey = typeof REQUIRED_ASSISTANT_ROUTE_KEYS[number];
+
+export interface AssistantAction {
+ label: string;
+ route: string;
+ reason: string;
+}
+
+export interface AssistantGuidance {
+ routeKey: AssistantRouteKey;
+ title: string;
+ pagePurpose: string;
+ whenToUse: string;
+ decision: string;
+ ratingsFocus: string;
+ suggestedAction: AssistantAction;
+ deeperStrategy: string;
+ mobileTip: string;
+}
+
+export interface AssistantNextActionContext {
+ routeKey: AssistantRouteKey;
+ phase: string;
+ day: number;
+ season: number;
+ mode: AssistantMode;
+}
+
+export interface AssistantTickerItem {
+ id: string;
+ category?: string;
+ text: string;
+}
+
+export interface AssistantStoryCallback {
+ id: string;
+ tone: 'neutral' | 'success' | 'warning' | 'excited';
+ title: string;
+ body: string;
+}
+
+export interface AssistantStoryContext {
+ phase: string;
+ day: number;
+ season: number;
+ routeKey: AssistantRouteKey;
+ seenStoryCallbacks: Record;
+ tickerFeed: readonly AssistantTickerItem[];
+}
+
+function action(label: string, route: string, reason: string): AssistantAction {
+ return { label, route, reason };
+}
+
+export const ASSISTANT_GUIDANCE: Record = {
+ setup: {
+ routeKey: 'setup',
+ title: 'Start the dynasty cleanly',
+ pagePurpose: 'The Save Hub is where you create, continue, branch, or delete local dynasties.',
+ whenToUse: 'Use it when starting a new club, continuing a long save, or branching a what-if timeline.',
+ decision: 'Choose Quick Start if you want the front office fast; choose Full Day One if you want more context and role-play.',
+ ratingsFocus: 'Team previews include player OVR so you can spot whether the club starts star-heavy, balanced, or rebuilding.',
+ suggestedAction: action('Start or continue a save', '/', 'A live save unlocks the Assistant and route-aware guidance.'),
+ deeperStrategy: 'For a first save, a balanced club is easier than a teardown. Hardcore players may prefer a weak roster with payroll room.',
+ mobileTip: 'Save actions stack vertically on mobile; keep the Assistant collapsed until the first dashboard.',
+ },
+ onboarding: {
+ routeKey: 'onboarding',
+ title: 'Get through Day One with intent',
+ pagePurpose: 'Onboarding introduces the owner, staff, roster, scouting, finances, and first front-office identity.',
+ whenToUse: 'Use this flow once per new dynasty, or replay guidance from Settings if the opening choices blur together.',
+ decision: 'Pick the style and early priorities that match the club you want to build.',
+ ratingsFocus: 'Roster and staff grades summarize current ability; treat them as a first diagnosis, not a final plan.',
+ suggestedAction: action('Finish Day One', '/onboarding', 'Completing onboarding hands you to the real dashboard with a prepared save.'),
+ deeperStrategy: 'Your choices should create a mental model: win now, develop, spend carefully, or aggressively reshape the club.',
+ mobileTip: 'Read one chapter at a time and use the primary CTA at the bottom of each panel.',
+ },
+ dashboard: {
+ routeKey: 'dashboard',
+ title: 'Know the next move before you sim',
+ pagePurpose: 'The dashboard is the command center for standings, roster health, farm, trades, finances, stories, and sim controls.',
+ whenToUse: 'Use it before every sim chunk to decide whether the club is ready for more days to pass.',
+ decision: 'Decide whether to sim, fix a roster issue, scout, check finances, review trades, or read the latest story.',
+ ratingsFocus: 'Dashboard cards do not show every OVR, but roster health, farm, and trade intel all point you toward rating checks.',
+ suggestedAction: action('Review one red or yellow card', '/dashboard', 'Handle urgent cards before simming a week or month.'),
+ deeperStrategy: 'A good rhythm is dashboard, one decision page, then sim. Avoid simming past deadlines without checking roster and budget.',
+ mobileTip: 'Use the Assistant route link instead of hunting through More when a card points to a deeper page.',
+ },
+ roster: {
+ routeKey: 'roster',
+ title: 'Turn ratings into a legal roster',
+ pagePurpose: 'Roster manages the active club, 40-man, lineup, depth chart, contracts, promotions, and compliance.',
+ whenToUse: 'Use it after injuries, trades, promotions, or any compliance alert.',
+ decision: 'Choose who plays, who sits, who moves levels, and who leaves the roster.',
+ ratingsFocus: 'OVR and grade show current talent; position, service time, options, stats, and role fit decide the actual move.',
+ suggestedAction: action('Check roster compliance', '/roster', 'A legal roster keeps simulation moving and prevents avoidable losses.'),
+ deeperStrategy: 'Do not chase OVR blindly. A 61 OVR shortstop can be more useful than a 64 OVR bat without a defensive home.',
+ mobileTip: 'Roster tables scroll horizontally; use Assistant explanations before making a dense mobile move.',
+ },
+ minors: {
+ routeKey: 'minors',
+ title: 'Develop the next core',
+ pagePurpose: 'Minors tracks affiliates, development, prospects, breakout signals, and promotion candidates.',
+ whenToUse: 'Use it monthly, after injuries, and before the deadline or offseason.',
+ decision: 'Decide whether a prospect should stay, move up, change role, or become trade capital.',
+ ratingsFocus: 'OVR is current readiness; ceiling, age, level, momentum, and scouting confidence tell the development story.',
+ suggestedAction: action('Review promotion candidates', '/minors', 'Ready prospects can patch the MLB roster or raise your long-term ceiling.'),
+ deeperStrategy: 'A promotion should solve a role and match the player timeline. Rushing a high-ceiling player can waste development value.',
+ mobileTip: 'Use one prospect card at a time; avoid comparing too many minor league rows on a small screen.',
+ },
+ players: {
+ routeKey: 'players',
+ title: 'Find the right player, not just the famous one',
+ pagePurpose: 'Players is the searchable league directory and quick scan for names, teams, positions, OVR, grades, and stats.',
+ whenToUse: 'Use it when chasing leaders, researching trade targets, or finding a player profile.',
+ decision: 'Decide who deserves a deeper profile or comparison.',
+ ratingsFocus: 'OVR and grade are the quick filter; recent stats and age decide whether to investigate more.',
+ suggestedAction: action('Search a need position', '/players', 'Start with role fit, then open profiles for ratings and story.'),
+ deeperStrategy: 'Hardcore scan: position, OVR, age, WAR, then profile tabs for durability, development, and contract context.',
+ mobileTip: 'Rows become cards on mobile; tap a player for the profile instead of over-reading the table.',
+ },
+ 'player-compare': {
+ routeKey: 'player-compare',
+ title: 'Choose between two paths',
+ pagePurpose: 'Compare puts two players side by side across ratings, attributes, production, and fit.',
+ whenToUse: 'Use it before trades, promotions, free-agent offers, or lineup decisions.',
+ decision: 'Decide which player fits the role, timeline, and risk profile.',
+ ratingsFocus: 'OVR is the headline; attribute spread, age, contract, and trend explain why two equal OVR players are not equal.',
+ suggestedAction: action('Compare two candidates', '/players/compare', 'A side-by-side view prevents name-value mistakes.'),
+ deeperStrategy: 'Use compare for replacement decisions: if the gain is marginal, keep the cheaper or more flexible player.',
+ mobileTip: 'Compare works best when you pick two players first, then read one section at a time.',
+ },
+ 'player-profile': {
+ routeKey: 'player-profile',
+ title: 'Read the full player file',
+ pagePurpose: 'Profiles explain a player through ratings, stats, scouting, development, personality, moments, and history.',
+ whenToUse: 'Use it before any meaningful player-specific decision.',
+ decision: 'Decide whether to play, promote, trade, extend, bench, or build around the player.',
+ ratingsFocus: 'Current OVR, grade bars, ceiling/floor, and tab-specific reports show talent, risk, and trajectory.',
+ suggestedAction: action('Check development and scouting tabs', '/players', 'Current OVR matters more when you understand where it is heading.'),
+ deeperStrategy: 'Profile reading order: header OVR, role/position, recent stats, development trend, contract, then story/personality.',
+ mobileTip: 'Use tabs as chapters; do not try to consume the full profile in one scroll.',
+ },
+ scouting: {
+ routeKey: 'scouting',
+ title: 'Reduce uncertainty before big bets',
+ pagePurpose: 'Scouting manages reports, international prospects, scout learning, and disagreements.',
+ whenToUse: 'Use it before draft picks, signings, trades, and international spending.',
+ decision: 'Decide who needs more looks and whose report you trust.',
+ ratingsFocus: 'Scouted OVR, ceiling, floor, confidence, and scout conflicts tell you how much to believe the number.',
+ suggestedAction: action('Scout one uncertain target', '/scouting', 'Better information prevents paying full price for guesswork.'),
+ deeperStrategy: 'Low-confidence high-upside reports are portfolio bets; high-confidence average reports are depth bets.',
+ mobileTip: 'Use the report panel for detail; tables are for narrowing the pool.',
+ },
+ staff: {
+ routeKey: 'staff',
+ title: 'Make development better on purpose',
+ pagePurpose: 'Staff controls coaches and development infrastructure.',
+ whenToUse: 'Use it during onboarding and offseason, or when development outcomes lag.',
+ decision: 'Decide which coaches improve your player pipeline and clubhouse fit.',
+ ratingsFocus: 'Teaching, impact, specialty, and fit are coach ratings; they change how player OVR evolves over time.',
+ suggestedAction: action('Review weakest staff role', '/staff', 'One poor coach can drag a whole development lane.'),
+ deeperStrategy: 'Match staff to club direction: rebuilding clubs need teaching; contenders need role fit and stability.',
+ mobileTip: 'Coach cards are easier than tables on mobile; compare one role at a time.',
+ },
+ draft: {
+ routeKey: 'draft',
+ title: 'Draft for value and timeline',
+ pagePurpose: 'Draft is where you scout, rank, pick, and sign amateur talent.',
+ whenToUse: 'Use it during offseason and pre-draft prep.',
+ decision: 'Decide between current ability, ceiling, signability, need, and risk.',
+ ratingsFocus: 'OVR is present value; ceiling is upside; confidence tells how much trust to place in the report.',
+ suggestedAction: action('Scout the top of your board', '/draft', 'Your best pick is usually the best blend of upside, confidence, and need.'),
+ deeperStrategy: 'A contender can draft upside; a thin system may need safer players who reach useful OVR sooner.',
+ mobileTip: 'Open details before picking; do not make draft decisions from one cramped row.',
+ },
+ trade: {
+ routeKey: 'trade',
+ title: 'Price the player and the situation',
+ pagePurpose: 'Trade lets you build packages, review offers, negotiate, and react to deadline pressure.',
+ whenToUse: 'Use it near the deadline, after injuries, during rebuilds, or when payroll needs a reset.',
+ decision: 'Decide who is expendable and what return matches your competitive window.',
+ ratingsFocus: 'OVR starts the value conversation; age, contract, control, role, scarcity, and GM personality finish it.',
+ suggestedAction: action('Audit deadline value', '/trade', 'Know what you can buy or sell before the market closes.'),
+ deeperStrategy: 'Never trade only from OVR. A cheaper 62 OVR player with control can be worth more than an expensive 68 rental.',
+ mobileTip: 'Use smaller packages on mobile and inspect selected-player summaries carefully.',
+ },
+ standings: {
+ routeKey: 'standings',
+ title: 'Know your window',
+ pagePurpose: 'Standings show division, playoff, streak, games-back, and run-differential context.',
+ whenToUse: 'Use it before deciding to buy, sell, hold, or push prospects.',
+ decision: 'Decide whether the season position justifies aggressive moves.',
+ ratingsFocus: 'Ratings are indirect here; standings tell whether your roster OVR is translating into wins.',
+ suggestedAction: action('Check run differential', '/league/standings', 'Run differential is often the truth behind the record.'),
+ deeperStrategy: 'A team with a bad record but strong differential may buy cautiously; a weak differential contender should avoid overpaying.',
+ mobileTip: 'Division cards stack cleanly; focus on your division first.',
+ },
+ leaders: {
+ routeKey: 'leaders',
+ title: 'See who is shaping the league',
+ pagePurpose: 'Leaders show top performers across hitting, pitching, and advanced stats.',
+ whenToUse: 'Use it for award races, scouting context, trade research, and story tracking.',
+ decision: 'Decide which production is real enough to investigate.',
+ ratingsFocus: 'OVR beside production helps separate true talent from streaks.',
+ suggestedAction: action('Open one leader profile', '/league/leaders', 'The profile explains whether the stat line is sustainable.'),
+ deeperStrategy: 'Production plus strong OVR is star signal; production without OVR is a sell-high or small-sample question.',
+ mobileTip: 'Use category switching sparingly on mobile to avoid losing context.',
+ },
+ schedule: {
+ routeKey: 'schedule',
+ title: 'Choose when to inspect the games',
+ pagePurpose: 'Schedule shows completed results, upcoming games, and links to box scores.',
+ whenToUse: 'Use it after sim chunks or before important series.',
+ decision: 'Decide whether a result deserves deeper review.',
+ ratingsFocus: 'Ratings are indirect; use box scores to see whether your high-OVR roster is performing.',
+ suggestedAction: action('Inspect a recent loss', '/schedule', 'Loss patterns can reveal lineup, rotation, or bullpen problems.'),
+ deeperStrategy: 'Hardcore players should audit repeated one-run losses, bullpen collapses, and weak starter turns.',
+ mobileTip: 'Tap one game at a time; the full schedule is dense.',
+ },
+ 'box-score': {
+ routeKey: 'box-score',
+ title: 'Learn from one game',
+ pagePurpose: 'Box Score explains the line score, player performance, and play-by-play.',
+ whenToUse: 'Use it after close games, playoff games, collapses, or milestone performances.',
+ decision: 'Decide whether a result points to a real roster issue or normal baseball noise.',
+ ratingsFocus: 'Ratings are context here; one bad game from a strong OVR player is not a crisis.',
+ suggestedAction: action('Read the decisive inning', '/schedule', 'The play-by-play often explains what the final score hides.'),
+ deeperStrategy: 'Look for repeatable problems: walks, poor defense, bullpen fatigue, or empty offense against a handedness split.',
+ mobileTip: 'Use play-by-play in short sections on mobile.',
+ },
+ 'press-room': {
+ routeKey: 'press-room',
+ title: 'Separate signal from story flavor',
+ pagePurpose: 'Press Room collects briefings, league news, rumors, press conferences, and transaction context.',
+ whenToUse: 'Use it after sim chunks and before big public-facing choices.',
+ decision: 'Decide which stories change your next move.',
+ ratingsFocus: 'Ratings appear indirectly when stories point to player performance, scouting, or market value.',
+ suggestedAction: action('Read unread briefings', '/press-room', 'Important stories can explain why a route needs attention.'),
+ deeperStrategy: 'Treat rumors as prompts to investigate, not as instructions to act.',
+ mobileTip: 'Filters help reduce the story feed on small screens.',
+ },
+ playoffs: {
+ routeKey: 'playoffs',
+ title: 'Manage the October moment',
+ pagePurpose: 'Playoffs shows brackets, series state, momentum, and postseason sim flow.',
+ whenToUse: 'Use it once the regular season ends or when watching league postseason outcomes.',
+ decision: 'Decide whether to inspect series state or advance.',
+ ratingsFocus: 'Ratings matter through matchup depth, rotation quality, bullpen leverage, and star reliability.',
+ suggestedAction: action('Review playoff matchup', '/playoffs', 'Know the opponent before advancing the series.'),
+ deeperStrategy: 'Short series magnify pitching depth and bullpen quality. Do not overreact to one game unless fatigue compounds.',
+ mobileTip: 'Bracket views are dense; use series panels first.',
+ },
+ 'free-agency': {
+ routeKey: 'free-agency',
+ title: 'Buy wins without buying regret',
+ pagePurpose: 'Free Agency is the market for signing external players and reading market intelligence.',
+ whenToUse: 'Use it during offseason or when planning future payroll.',
+ decision: 'Decide who is worth money, years, roster space, and opportunity cost.',
+ ratingsFocus: 'OVR gives current talent; age, trend, role, demand, and contract length decide whether the deal ages well.',
+ suggestedAction: action('Filter by roster need', '/free-agency', 'The best signing solves a specific weakness.'),
+ deeperStrategy: 'Hardcore rule: pay for prime years, avoid paying star money for decline years unless the title window is now.',
+ mobileTip: 'Select one target to read the detail panel before offering.',
+ },
+ offseason: {
+ routeKey: 'offseason',
+ title: 'Work the offseason in order',
+ pagePurpose: 'Offseason collects arbitration, extensions, free agency, draft prep, Rule 5, transactions, and next-season start.',
+ whenToUse: 'Use it after playoffs and before starting the next season.',
+ decision: 'Decide which required and optional tasks must happen before Opening Day.',
+ ratingsFocus: 'OVR appears in candidates and transaction targets, but budget, control, and roster limits decide sequencing.',
+ suggestedAction: action('Open offseason checklist', '/offseason', 'Completing the required tasks prevents accidental roster holes.'),
+ deeperStrategy: 'Start with obligations, then roster holes, then upside. The order prevents expensive moves from boxing you in.',
+ mobileTip: 'Treat each offseason panel as a step; avoid jumping between all tasks at once.',
+ },
+ finance: {
+ routeKey: 'finance',
+ title: 'Turn payroll into flexibility',
+ pagePurpose: 'Finance shows payroll, luxury tax, budget, commitments, and contract pressure.',
+ whenToUse: 'Use it before trades, free agency, extensions, and deadline buying.',
+ decision: 'Decide whether the team can afford the next move now and later.',
+ ratingsFocus: 'OVR is not enough; compare talent to salary, years, age, and roster window.',
+ suggestedAction: action('Check future commitments', '/finance', 'A move that fits this year can still damage the next three seasons.'),
+ deeperStrategy: 'Payroll flexibility is a strategic asset. Rebuilders should buy control; contenders can rent impact.',
+ mobileTip: 'Focus on one budget section at a time; finance tables are dense.',
+ },
+ career: {
+ routeKey: 'career',
+ title: 'Measure the GM legacy',
+ pagePurpose: 'GM Career tracks record, job history, legacy, and career-level outcomes.',
+ whenToUse: 'Use it between seasons or after major owner/front-office events.',
+ decision: 'Decide whether the current job path supports your long-term goals.',
+ ratingsFocus: 'Ratings are indirect; sustained player OVR and roster quality become wins, awards, and job security.',
+ suggestedAction: action('Review legacy score', '/career', 'Career context shows whether the dynasty is growing or stalling.'),
+ deeperStrategy: 'A strong GM career is not only titles: development, finances, and owner trust all compound.',
+ mobileTip: 'Career cards are scan-friendly; open details only when needed.',
+ },
+ history: {
+ routeKey: 'history',
+ title: 'Read the dynasty so far',
+ pagePurpose: 'History stores archives, timelines, season recaps, branches, and long-save memory.',
+ whenToUse: 'Use it after seasons, milestones, or when returning to an old save.',
+ decision: 'Decide what the current era means and what comes next.',
+ ratingsFocus: 'Historical OVR and peak OVR explain player eras, decline, breakouts, and record context.',
+ suggestedAction: action('Open latest season recap', '/history', 'The recap turns results into a story you can act on.'),
+ deeperStrategy: 'Use history to avoid repeating mistakes: failed windows, overpaid cores, and missed prospect waves.',
+ mobileTip: 'Use timeline chapters as anchors on small screens.',
+ },
+ achievements: {
+ routeKey: 'achievements',
+ title: 'Pick optional goals',
+ pagePurpose: 'Achievements give long-term challenges and progress markers.',
+ whenToUse: 'Use it when a save needs direction beyond the standings.',
+ decision: 'Decide which optional target would make the dynasty more fun.',
+ ratingsFocus: 'Ratings are indirect; many achievements reward building, development, records, and sustained talent.',
+ suggestedAction: action('Choose one active chase', '/achievements', 'A clear optional goal can make the next season more compelling.'),
+ deeperStrategy: 'Hardcore players can use achievements as constraints for self-imposed save identity.',
+ mobileTip: 'Scan progress bars, then open only the category that interests you.',
+ },
+ rivalries: {
+ routeKey: 'rivalries',
+ title: 'Account for enemies',
+ pagePurpose: 'Rivalries tracks intensity, head-to-head records, playoff meetings, trades, and narrative stakes.',
+ whenToUse: 'Use it before rivalry trades, playoff series, or division planning.',
+ decision: 'Decide whether rivalry context changes risk or value.',
+ ratingsFocus: 'Ratings are indirect; rivalry pressure changes how valuable wins and trades feel.',
+ suggestedAction: action('Check your hottest rivalry', '/rivalries', 'Rival context can explain owner, fan, and trade pressure.'),
+ deeperStrategy: 'Do not hand rivals the exact player archetype they need unless the return clearly wins your window.',
+ mobileTip: 'Read one rivalry card at a time.',
+ },
+ 'front-office': {
+ routeKey: 'front-office',
+ title: 'Keep ownership aligned',
+ pagePurpose: 'Owner Intel shows patience, expectations, chemistry, reputation, and front-office risk.',
+ whenToUse: 'Use it after losing streaks, budget stress, press events, and season checkpoints.',
+ decision: 'Decide how much risk ownership will tolerate.',
+ ratingsFocus: 'Ratings are indirect; roster OVR must become wins or credible development progress.',
+ suggestedAction: action('Check owner patience', '/front-office', 'Owner pressure changes how aggressive your next move should be.'),
+ deeperStrategy: 'A rebuild needs communication and visible prospect progress; a contender needs wins and payroll discipline.',
+ mobileTip: 'Owner cards are designed for scanning; focus on red/yellow signals first.',
+ },
+ pulse: {
+ routeKey: 'pulse',
+ title: 'Handle the monthly decision queue',
+ pagePurpose: 'Pulse summarizes monthly performance, urgent decisions, and scenario progress.',
+ whenToUse: 'Use it at month boundaries or after a pulse overlay.',
+ decision: 'Decide which monthly signal needs action before more simming.',
+ ratingsFocus: 'Ratings are indirect; Pulse points toward roster, farm, finance, or player pages when ratings matter.',
+ suggestedAction: action('Open highest-urgency item', '/pulse', 'Pulse is the save telling you what changed.'),
+ deeperStrategy: 'Monthly review prevents slow problems from becoming offseason surprises.',
+ mobileTip: 'Use urgency color first, then route links.',
+ },
+ scenarios: {
+ routeKey: 'scenarios',
+ title: 'Play with a constraint',
+ pagePurpose: 'Challenges offer scenario goals and alternate save identities.',
+ whenToUse: 'Use it when you want a focused objective instead of an open-ended dynasty.',
+ decision: 'Decide which constraint sounds fun and achievable.',
+ ratingsFocus: 'Ratings vary by scenario; OVR helps identify whether the challenge starts with talent, depth, or scarcity.',
+ suggestedAction: action('Read one scenario objective', '/scenarios', 'A clear constraint can make decisions sharper.'),
+ deeperStrategy: 'Pick scenarios that force uncomfortable tradeoffs, not just ones you already solve well.',
+ mobileTip: 'Scenario cards are easier to compare by objective, not flavor.',
+ },
+ stats: {
+ routeKey: 'stats',
+ title: 'Translate numbers into decisions',
+ pagePurpose: 'Stats Encyclopedia explains advanced metrics and baseball stat language.',
+ whenToUse: 'Use it when a stat in another page is unfamiliar.',
+ decision: 'Decide which stat answers your actual baseball question.',
+ ratingsFocus: 'Stats complement ratings: OVR estimates talent, stats show what happened, advanced stats help explain sustainability.',
+ suggestedAction: action('Look up one unclear stat', '/stats', 'Understanding the stat makes the next roster decision less guessy.'),
+ deeperStrategy: 'Use rate stats for skill, counting stats for volume, and advanced stats for context.',
+ mobileTip: 'Search or scan by category instead of reading the whole encyclopedia.',
+ },
+ records: {
+ routeKey: 'records',
+ title: 'Track legacy chases',
+ pagePurpose: 'Record Watch shows players chasing franchise and league records.',
+ whenToUse: 'Use it during strong seasons and long saves.',
+ decision: 'Decide whether a record chase should influence playing time or story focus.',
+ ratingsFocus: 'Ratings are historical context; great OVR plus production creates credible record chases.',
+ suggestedAction: action('Check active record chases', '/records', 'Records turn a good season into a save story.'),
+ deeperStrategy: 'A record chase is usually worth attention, but not if it damages playoff odds or development.',
+ mobileTip: 'Open active chases first; archives can wait.',
+ },
+ settings: {
+ routeKey: 'settings',
+ title: 'Tune the interface',
+ pagePurpose: 'Settings controls preferences, accessibility, diagnostics, save tools, and tutorial replay.',
+ whenToUse: 'Use it when the game feels too dense, too fast, too loud, or when you want help replayed.',
+ decision: 'Decide how the interface should support your play style.',
+ ratingsFocus: 'Ratings are not a Settings decision, but tutorial replay and density can make rating-heavy pages easier to read.',
+ suggestedAction: action('Review accessibility and tutorial controls', '/settings', 'A comfortable UI makes long saves easier to sustain.'),
+ deeperStrategy: 'Hardcore players may prefer denser tables; newcomers should keep more breathing room until routes feel familiar.',
+ mobileTip: 'Settings controls are mobile-safe; change one preference and test it before changing several.',
+ },
+};
+
+export function resolveAssistantRouteKey(pathname: string): AssistantRouteKey {
+ const normalized = pathname === '/' ? '/' : pathname.replace(/\/+$/, '');
+
+ if (normalized === '/') return 'setup';
+ if (normalized === '/onboarding') return 'onboarding';
+ if (normalized === '/dashboard') return 'dashboard';
+ if (normalized === '/roster') return 'roster';
+ if (normalized === '/minors') return 'minors';
+ if (normalized === '/players') return 'players';
+ if (normalized === '/players/compare') return 'player-compare';
+ if (normalized.startsWith('/players/')) return 'player-profile';
+ if (normalized === '/scouting') return 'scouting';
+ if (normalized === '/staff') return 'staff';
+ if (normalized === '/draft') return 'draft';
+ if (normalized === '/trade') return 'trade';
+ if (normalized === '/standings' || normalized === '/league/standings') return 'standings';
+ if (normalized === '/leaders' || normalized === '/league/leaders') return 'leaders';
+ if (normalized === '/schedule') return 'schedule';
+ if (normalized.startsWith('/games/')) return 'box-score';
+ if (normalized === '/press-room') return 'press-room';
+ if (normalized === '/playoffs') return 'playoffs';
+ if (normalized === '/free-agency') return 'free-agency';
+ if (normalized === '/offseason') return 'offseason';
+ if (normalized === '/finance') return 'finance';
+ if (normalized === '/career') return 'career';
+ if (normalized === '/history') return 'history';
+ if (normalized === '/achievements') return 'achievements';
+ if (normalized === '/rivalries') return 'rivalries';
+ if (normalized === '/front-office') return 'front-office';
+ if (normalized === '/pulse') return 'pulse';
+ if (normalized === '/scenarios') return 'scenarios';
+ if (normalized === '/stats') return 'stats';
+ if (normalized === '/records') return 'records';
+ if (normalized === '/settings') return 'settings';
+
+ return 'dashboard';
+}
+
+export function selectRouteGuidance(pathname: string): AssistantGuidance {
+ return ASSISTANT_GUIDANCE[resolveAssistantRouteKey(pathname)];
+}
+
+export function buildAssistantNextAction(context: AssistantNextActionContext): AssistantAction {
+ if (context.phase === 'offseason') {
+ return action('Open offseason checklist', '/offseason', 'Offseason tasks are easiest when handled in sequence.');
+ }
+
+ if (context.phase === 'playoffs') {
+ return action('Review playoff matchup', '/playoffs', 'Short series reward knowing the opponent before advancing.');
+ }
+
+ if (context.routeKey === 'trade') {
+ return context.mode === 'hardcore'
+ ? action('Audit deadline value', '/trade', 'Price talent against age, control, contract, and market pressure.')
+ : action('Review one trade target', '/trade', 'Start with one need instead of building a five-player package.');
+ }
+
+ if (context.routeKey === 'dashboard' && context.phase === 'regular' && context.day <= 30) {
+ return action('Scout early-season needs', '/scouting', 'Early reports give you time before the draft and deadline.');
+ }
+
+ if (context.routeKey === 'dashboard' && context.phase === 'regular' && context.day >= 60 && context.day <= 105) {
+ return action('Check trade and budget fit', '/trade', 'The deadline window rewards knowing needs before offers arrive.');
+ }
+
+ return ASSISTANT_GUIDANCE[context.routeKey].suggestedAction;
+}
+
+export function buildStoryCallback(context: AssistantStoryContext): AssistantStoryCallback | null {
+ const relevantTicker = context.tickerFeed.find((entry) => {
+ if (context.seenStoryCallbacks[entry.id]) return false;
+ return entry.category === 'trade' || entry.category === 'rumor' || entry.category === 'milestone' || entry.category === 'standings';
+ });
+
+ if (relevantTicker) {
+ const isTrade = relevantTicker.category === 'trade' || relevantTicker.category === 'rumor';
+ return {
+ id: relevantTicker.id,
+ tone: isTrade ? 'warning' : 'success',
+ title: isTrade ? 'Market signal' : 'Story signal',
+ body: relevantTicker.text,
+ };
+ }
+
+ if (context.phase === 'playoffs') {
+ const id = `phase-playoffs-s${context.season}`;
+ if (context.seenStoryCallbacks[id]) return null;
+ return {
+ id,
+ tone: 'excited',
+ title: 'October changes the job',
+ body: 'Before you advance, check matchups and pitching depth. One short series can rewrite the whole save.',
+ };
+ }
+
+ if (context.phase === 'offseason') {
+ const id = `phase-offseason-s${context.season}`;
+ if (context.seenStoryCallbacks[id]) return null;
+ return {
+ id,
+ tone: 'neutral',
+ title: 'The story turns into roster work',
+ body: 'Start with obligations, then fix holes. The best offseason is usually the one that keeps your next move flexible.',
+ };
+ }
+
+ if (context.phase === 'regular' && context.day >= 100) {
+ const id = `phase-stretch-s${context.season}`;
+ if (context.seenStoryCallbacks[id]) return null;
+ return {
+ id,
+ tone: 'warning',
+ title: 'Stretch-run checkpoint',
+ body: 'This is the time to decide whether the club is buying, selling, or protecting the next core.',
+ };
+ }
+
+ return null;
+}
diff --git a/apps/web/src/features/assistant/lib/assistantState.test.ts b/apps/web/src/features/assistant/lib/assistantState.test.ts
new file mode 100644
index 0000000..fbb62a6
--- /dev/null
+++ b/apps/web/src/features/assistant/lib/assistantState.test.ts
@@ -0,0 +1,55 @@
+import { describe, expect, it } from 'vitest';
+import {
+ ASSISTANT_STORAGE_VERSION,
+ assistantStorageKey,
+ createInitialAssistantState,
+ reduceAssistantState,
+ sanitizeAssistantState,
+} from './assistantState';
+
+describe('assistantState', () => {
+ it('creates save-scoped localStorage keys without touching GameSnapshot', () => {
+ expect(assistantStorageKey(null)).toBe('mbd:assistant:v1:global');
+ expect(assistantStorageKey('save-slot-2')).toBe('mbd:assistant:v1:save-slot-2');
+ expect(assistantStorageKey(3)).toBe('mbd:assistant:v1:save-slot-3');
+ });
+
+ it('sanitizes persisted state and defaults invalid mode to newcomer', () => {
+ expect(sanitizeAssistantState(null)).toEqual(createInitialAssistantState());
+ expect(sanitizeAssistantState({
+ version: ASSISTANT_STORAGE_VERSION,
+ mode: 'invalid',
+ dismissedRoutes: { dashboard: true, trade: false },
+ completedRoutes: { roster: true },
+ seenStoryCallbacks: { 'ticker-1': true, 'ticker-2': 'yes' },
+ })).toEqual({
+ version: ASSISTANT_STORAGE_VERSION,
+ mode: 'newcomer',
+ dismissedRoutes: { dashboard: true },
+ completedRoutes: { roster: true },
+ seenStoryCallbacks: { 'ticker-1': true },
+ });
+ });
+
+ it('tracks dismiss, complete, replay, story callback, and mode transitions', () => {
+ let state = createInitialAssistantState();
+
+ state = reduceAssistantState(state, { type: 'setMode', mode: 'hardcore' });
+ expect(state.mode).toBe('hardcore');
+
+ state = reduceAssistantState(state, { type: 'dismissRoute', routeKey: 'dashboard' });
+ expect(state.dismissedRoutes.dashboard).toBe(true);
+ expect(state.completedRoutes.dashboard).toBeUndefined();
+
+ state = reduceAssistantState(state, { type: 'completeRoute', routeKey: 'dashboard' });
+ expect(state.dismissedRoutes.dashboard).toBe(true);
+ expect(state.completedRoutes.dashboard).toBe(true);
+
+ state = reduceAssistantState(state, { type: 'replayRoute', routeKey: 'dashboard' });
+ expect(state.dismissedRoutes.dashboard).toBeUndefined();
+ expect(state.completedRoutes.dashboard).toBeUndefined();
+
+ state = reduceAssistantState(state, { type: 'markStorySeen', callbackId: 'trade-ticker' });
+ expect(state.seenStoryCallbacks['trade-ticker']).toBe(true);
+ });
+});
diff --git a/apps/web/src/features/assistant/lib/assistantState.ts b/apps/web/src/features/assistant/lib/assistantState.ts
new file mode 100644
index 0000000..478a99b
--- /dev/null
+++ b/apps/web/src/features/assistant/lib/assistantState.ts
@@ -0,0 +1,150 @@
+export const ASSISTANT_STORAGE_VERSION = 1;
+
+export type AssistantMode = 'newcomer' | 'hardcore';
+
+export interface AssistantState {
+ version: typeof ASSISTANT_STORAGE_VERSION;
+ mode: AssistantMode;
+ dismissedRoutes: Record;
+ completedRoutes: Record;
+ seenStoryCallbacks: Record;
+}
+
+export type AssistantStateEvent =
+ | { type: 'setMode'; mode: AssistantMode }
+ | { type: 'dismissRoute'; routeKey: string }
+ | { type: 'completeRoute'; routeKey: string }
+ | { type: 'replayRoute'; routeKey: string }
+ | { type: 'markStorySeen'; callbackId: string };
+
+export type AssistantSaveId = string | number | null;
+
+export function assistantStorageKey(saveId: AssistantSaveId): string {
+ if (saveId == null) {
+ return 'mbd:assistant:v1:global';
+ }
+
+ return `mbd:assistant:v1:${typeof saveId === 'number' ? `save-slot-${saveId}` : saveId}`;
+}
+
+export function createInitialAssistantState(): AssistantState {
+ return {
+ version: ASSISTANT_STORAGE_VERSION,
+ mode: 'newcomer',
+ dismissedRoutes: {},
+ completedRoutes: {},
+ seenStoryCallbacks: {},
+ };
+}
+
+function sanitizeTrueRecord(value: unknown): Record {
+ const output: Record = {};
+ if (!value || typeof value !== 'object') {
+ return output;
+ }
+
+ for (const [key, enabled] of Object.entries(value as Record)) {
+ if (typeof key === 'string' && key.length > 0 && enabled === true) {
+ output[key] = true;
+ }
+ }
+
+ return output;
+}
+
+export function sanitizeAssistantState(value: unknown): AssistantState {
+ if (!value || typeof value !== 'object') {
+ return createInitialAssistantState();
+ }
+
+ const record = value as Partial;
+ const mode: AssistantMode = record.mode === 'hardcore' ? 'hardcore' : 'newcomer';
+
+ return {
+ version: ASSISTANT_STORAGE_VERSION,
+ mode,
+ dismissedRoutes: sanitizeTrueRecord(record.dismissedRoutes),
+ completedRoutes: sanitizeTrueRecord(record.completedRoutes),
+ seenStoryCallbacks: sanitizeTrueRecord(record.seenStoryCallbacks),
+ };
+}
+
+export function reduceAssistantState(
+ state: AssistantState,
+ event: AssistantStateEvent,
+): AssistantState {
+ if (event.type === 'setMode') {
+ return {
+ ...state,
+ mode: event.mode,
+ };
+ }
+
+ if (event.type === 'dismissRoute') {
+ return {
+ ...state,
+ dismissedRoutes: {
+ ...state.dismissedRoutes,
+ [event.routeKey]: true,
+ },
+ };
+ }
+
+ if (event.type === 'completeRoute') {
+ return {
+ ...state,
+ dismissedRoutes: {
+ ...state.dismissedRoutes,
+ [event.routeKey]: true,
+ },
+ completedRoutes: {
+ ...state.completedRoutes,
+ [event.routeKey]: true,
+ },
+ };
+ }
+
+ if (event.type === 'replayRoute') {
+ const { [event.routeKey]: _dismissed, ...dismissedRoutes } = state.dismissedRoutes;
+ const { [event.routeKey]: _completed, ...completedRoutes } = state.completedRoutes;
+ return {
+ ...state,
+ dismissedRoutes,
+ completedRoutes,
+ };
+ }
+
+ return {
+ ...state,
+ seenStoryCallbacks: {
+ ...state.seenStoryCallbacks,
+ [event.callbackId]: true,
+ },
+ };
+}
+
+export function readAssistantState(saveId: AssistantSaveId): AssistantState {
+ if (typeof window === 'undefined') {
+ return createInitialAssistantState();
+ }
+
+ try {
+ return sanitizeAssistantState(window.localStorage.getItem(assistantStorageKey(saveId)) != null
+ ? JSON.parse(window.localStorage.getItem(assistantStorageKey(saveId)) ?? '')
+ : null);
+ } catch {
+ return createInitialAssistantState();
+ }
+}
+
+export function writeAssistantState(saveId: AssistantSaveId, state: AssistantState): void {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ try {
+ window.localStorage.setItem(assistantStorageKey(saveId), JSON.stringify(state));
+ } catch {
+ // localStorage is optional; the Assistant remains usable without persistence.
+ }
+}
diff --git a/docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md b/docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md
new file mode 100644
index 0000000..c7562db
--- /dev/null
+++ b/docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md
@@ -0,0 +1,487 @@
+# MBD Tutorial Assistant V1 Goal Contract
+
+## Repo Notes From Initial Inspection
+
+- Repository root: `/Users/tkevinbigham/Documents/GitHub/MBD`.
+- Working branch for this goal: `goal/tutorial-assistant-v1`.
+- Public game URL documented in current repo files: `https://kevinbigham.github.io/MBD/`.
+- Package manager and root gate: `pnpm@9.15.4`, with `pnpm run verify` expanding to Turbo typecheck, tests, and build.
+- Primary app stack: React 18, Vite 6, Tailwind CSS, Zustand, Dexie saves, Comlink worker, Zod contracts, Vitest.
+- Current app routes are defined in `apps/web/src/app/routes/index.tsx`.
+- Existing guidance surfaces include `TourProvider`, `tourDefinition`, `PageHelp`, `ContextualHelp`, `GameAdvisor`, and guided-start nudges.
+- Current save schema version is `CURRENT_GAME_SNAPSHOT_VERSION = 33` in `packages/contracts/src/schemas/save.ts`.
+- No filesystem `AGENTS.md` was present at the repo root during contract creation; follow Kevin's supplied AGENTS.md instructions for this sprint.
+- `MASTER_CONTEXT.md` exists but contains stale paths, schema version, and counts. Treat live repo inspection as authoritative.
+
+## Mission
+
+Drive Mr. Baseball Dynasty toward v1.0 public-release readiness by building a world-class in-game Assistant/Tutorial character that helps every player understand:
+
+- what to do next
+- why each screen matters
+- how each core system works
+- how to make better GM decisions
+- where player OVR/ratings matter
+- how their current save is turning into a story
+
+This sprint should maximize retention, review scores, enjoyability, playability, and addictiveness while preserving realistic baseball-sim depth.
+
+## Product Priorities
+
+1. Retention
+2. Review scores
+3. Enjoyability / playability / addictiveness
+4. Public-release confidence
+5. Mobile-first usability
+
+## Non-Negotiables
+
+- Build as much as possible autonomously.
+- Do not wait for user input unless truly blocked.
+- Make best-judgment product decisions when details are ambiguous.
+- Preserve deterministic simulation behavior.
+- Do not break save compatibility.
+- Do not remove existing features unless clearly dead, broken, or replaced.
+- Mobile web is first-class.
+- The Assistant must support both newcomers and hardcore sim players.
+- OVR/ratings must appear in more decision-critical surfaces.
+- Keep realism, but make the game easier to understand.
+- Prefer polished, shippable improvements over speculative half-built systems.
+- All new behavior should have tests where practical.
+- Run relevant typecheck/lint/test/build commands before declaring completion.
+- Do not mark the goal complete until the completion audit passes.
+
+## Operating Rules For This Goal
+
+Maintain these files throughout the sprint:
+
+1. `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md`
+ - live checklist
+ - current phase
+ - completed work
+ - known issues
+ - test results
+ - next action
+
+2. `docs/tutorial-assistant/coverage-matrix.md`
+ - every route/page
+ - whether it has Assistant guidance
+ - whether OVR/ratings are visible when relevant
+ - mobile status
+ - notes
+
+3. `docs/tutorial-assistant/release-gate.md`
+ - final acceptance checklist
+ - remaining blockers
+ - launch-readiness notes
+
+At the start of each new continuation:
+
+- read this goal contract
+- read the progress file
+- inspect current git status
+- identify the next highest-impact task
+- continue without asking the user unless blocked
+
+Before stopping:
+
+- update the progress file
+- document tests run
+- document changed files
+- document known issues
+- state whether the goal is complete, blocked, or still in progress
+- if still in progress and not blocked, continue rather than asking the user to type "continue"
+
+## Phase 0 - Preflight Repo Audit
+
+Deliver:
+
+- Confirm repo structure, framework, package manager, routes, test commands, build commands.
+- Locate existing onboarding/tutorial/help/assistant/narrative systems.
+- Locate route definitions and major game screens.
+- Locate player rating/OVR models and UI components.
+- Locate persistence/save/version code.
+- Locate mobile layout patterns.
+- Run the safest available checks first.
+
+Required outputs:
+
+- `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md`
+- `docs/tutorial-assistant/phase0-preflight.md`
+
+Acceptance criteria:
+
+- Current state is understood from actual repo, not stale memory.
+- Commands needed for validation are documented.
+- Any blockers are documented with specific evidence.
+
+## Phase 1 - FTUE + Decision-Point Audit
+
+Deliver:
+
+- Map current onboarding/tutorial flow.
+- Map the full core game loop.
+- Identify every "I don't know what to do next" moment.
+- Identify all pages where a player makes meaningful GM decisions.
+- Identify every place OVR/ratings should appear but currently do not.
+- Identify mobile pain points for tutorial/assistant UX.
+
+Core pages/routes include, but are not limited to:
+
+- Setup / Save Hub (`/`)
+- Onboarding (`/onboarding`)
+- Dashboard (`/dashboard`)
+- Roster (`/roster`)
+- Minors (`/minors`)
+- Players (`/players`)
+- Player Compare (`/players/compare`)
+- Player Profile (`/players/:playerId`)
+- Scouting (`/scouting`)
+- Staff (`/staff`)
+- Draft (`/draft`)
+- Trade (`/trade`)
+- Standings (`/standings`, `/league/standings`)
+- Leaders (`/leaders`, `/league/leaders`)
+- Schedule (`/schedule`)
+- Box Score (`/games/:gameIndex`)
+- Press Room (`/press-room`)
+- Playoffs (`/playoffs`)
+- Free Agency (`/free-agency`)
+- Offseason (`/offseason`)
+- Finance (`/finance`)
+- GM Career (`/career`)
+- History (`/history`)
+- Achievements (`/achievements`)
+- Rivalries (`/rivalries`)
+- Front Office / Owner Intel (`/front-office`)
+- Pulse (`/pulse`)
+- Scenarios / Challenges (`/scenarios`)
+- Stats Encyclopedia (`/stats`)
+- Record Watch (`/records`)
+- Settings (`/settings`)
+
+Required outputs:
+
+- `docs/tutorial-assistant/phase1-audit.md`
+- `docs/tutorial-assistant/ratings-visibility-audit.md`
+- updated `docs/tutorial-assistant/coverage-matrix.md`
+
+Acceptance criteria:
+
+- Every major route is accounted for.
+- Every high-friction decision moment has a recommended Assistant intervention.
+- Ratings/OVR visibility gaps are prioritized.
+
+## Phase 2 - Assistant Character + UX Spec
+
+Design the in-game Assistant as a coach/GM mentor, not a nagging pop-up.
+
+Deliver:
+
+- Assistant name, persona, visual direction, tone, and behavior rules.
+- Newcomer mode and hardcore mode.
+- Global "What should I do now?" action.
+- Route-aware help.
+- Contextual hints.
+- Skippable/replayable walkthroughs.
+- "Explain this rating" help.
+- "Why this matters" microcopy.
+- Non-intrusive mobile layout.
+- Assistant memory-lite concept for save-specific callbacks.
+
+Assistant personality requirements:
+
+- Baseball-smart
+- Clear
+- Encouraging
+- Practical
+- Not childish
+- Not annoying
+- Helpful for both casual players and deep sim players
+- Speaks like a trusted bench coach / assistant GM
+- Explains the next best action without taking away player agency
+
+Required outputs:
+
+- `docs/tutorial-assistant/character-spec.md`
+- `docs/tutorial-assistant/assistant-ux-spec.md`
+- `docs/tutorial-assistant/trigger-spec.md`
+- `docs/tutorial-assistant/asset-plan.md`
+
+Acceptance criteria:
+
+- The Assistant has a consistent voice.
+- UX rules avoid intrusive tutorial spam.
+- User can dismiss, replay, or ask for more help.
+- Spec is concrete enough to implement.
+
+## Phase 3 - Assistant Engine Foundation
+
+Implement a reusable Assistant/Tutorial framework.
+
+Required capabilities:
+
+- route-aware guidance
+- step/checkpoint engine
+- completion tracking
+- local/save-safe persistence for tutorial progress
+- skip/replay controls
+- "What should I do now?" global CTA
+- progressive hints
+- cooldowns to avoid repetition
+- mobile-friendly panel/drawer/modal behavior
+- accessibility-friendly controls and focus behavior
+- typed data model for guidance content
+
+Implementation guidance:
+
+- Prefer a small, robust framework over a sprawling one.
+- Keep guidance content data-driven where practical.
+- Reuse existing state/persistence patterns.
+- Avoid breaking deterministic simulation.
+- Avoid blocking gameplay unnecessarily.
+- Add tests for state transitions and completion tracking where practical.
+
+Acceptance criteria:
+
+- Assistant can appear globally.
+- Assistant can explain current page.
+- Assistant can guide at least the dashboard and one major decision page.
+- User can dismiss and replay help.
+- Tutorial progress persists safely.
+- Tests/checks pass.
+
+## Phase 4 - Core Page Walkthroughs
+
+Add useful Assistant guidance to the most important player flows.
+
+Priority order:
+
+1. Setup / Onboarding
+2. Dashboard
+3. Roster
+4. Player Profile
+5. Players / Compare
+6. Scouting
+7. Draft
+8. Trade
+9. Free Agency
+10. Finance
+11. Minors
+12. Schedule / Box Score
+13. Standings / Leaders
+14. Press Room / Pulse
+15. Offseason
+16. Playoffs
+17. Career / History / Achievements / Records
+18. Rivalries / Front Office / Scenarios
+19. Settings
+
+For each page, add:
+
+- what this page is for
+- when to use it
+- what decision the user can make
+- what stats/ratings matter
+- one suggested next action
+- optional deeper explanation
+
+Acceptance criteria:
+
+- A new player can understand the main game loop.
+- A hardcore player can skip basic help and still access deeper strategy.
+- Assistant copy is concise and useful.
+- Coverage matrix is updated.
+
+## Phase 5 - OVR/Ratings Visibility Expansion
+
+Goal:
+
+Make OVR and key ratings visible anywhere users make roster, player, scouting, draft, trade, free agency, lineup, minors, or comparison decisions.
+
+Deliver:
+
+- Add OVR/rating badges/cards/components where missing.
+- Explain ratings in plain language with tooltips or Assistant help.
+- Ensure mobile readability.
+- Ensure consistent visual grammar.
+- Avoid clutter: show the most relevant rating at the point of decision, with deeper detail one tap/click away.
+
+Likely surfaces:
+
+- roster lists
+- player cards
+- player profile header
+- comparison tables
+- trade candidate lists
+- draft boards
+- free agency lists
+- minors/development pages
+- scouting reports
+- lineup/decision views if present
+- transaction/offer screens if present
+
+Acceptance criteria:
+
+- Users do not have to hunt for OVR when making major decisions.
+- Ratings are legible on mobile.
+- Tooltips/help explain implications.
+- Tests/checks pass.
+
+## Phase 6 - Assistant Visual Assets
+
+Create polished Assistant visual assets if the environment supports image generation. If direct image generation is unavailable, create a complete asset spec and implement production-safe placeholders/hooks.
+
+Desired assets:
+
+- hero portrait
+- small avatar
+- neutral expression
+- excited expression
+- warning expression
+- success expression
+- transparent-background variants where possible
+- web-optimized formats
+- mobile-safe sizes
+
+Visual direction:
+
+- modern baseball front-office assistant / bench coach
+- friendly but sharp
+- polished sports-sim tone
+- not goofy
+- not copyrighted
+- not based on real people
+- should feel at home in MBD's UI
+
+Implementation:
+
+- Store assets in the appropriate public/static asset path.
+- Add fallback SVG or CSS avatar if generated assets are unavailable.
+- Add light pseudo-animation:
+ - subtle entrance
+ - expression change
+ - success pulse
+ - reduced-motion fallback
+- Respect prefers-reduced-motion.
+
+Acceptance criteria:
+
+- Assistant has a polished visual presence.
+- No broken image paths.
+- Reduced-motion users are respected.
+- Mobile layout remains clean.
+
+## Phase 7 - Narrative Memory Lite
+
+Goal:
+
+Make the Assistant react to the current save so MBD feels alive.
+
+Deliver:
+
+- Lightweight memory keys for tutorial/narrative callbacks.
+- Cooldowns to avoid repetitive lines.
+- Assistant callbacks for meaningful events:
+ - draft pick
+ - trade
+ - big signing
+ - losing streak
+ - winning streak
+ - playoff push
+ - budget/payroll issue
+ - prospect development
+ - star player performance
+- Season recap / "story so far" helper if feasible.
+- Keep deterministic simulation safe.
+
+Acceptance criteria:
+
+- Assistant can reference relevant save events.
+- Prompts do not repeat constantly.
+- Narrative adds clarity and emotional payoff without blocking gameplay.
+
+## Phase 8 - Mobile, Accessibility, Performance Hardening
+
+Deliver:
+
+- Test critical flows on small viewport.
+- Make Assistant usable by touch.
+- Ensure panels do not cover key decisions.
+- Ensure keyboard/focus behavior is sane.
+- Ensure contrast/readability.
+- Respect reduced motion.
+- Run performance/build checks where available.
+
+Acceptance criteria:
+
+- Mobile tutorial UX is first-class.
+- No obvious overlay traps.
+- No major layout regressions.
+- Checks pass.
+
+## Phase 9 - Closed Playtest Readiness
+
+Deliver:
+
+- Playtest checklist.
+- First-session test script.
+- Tutorial feedback capture plan.
+- Release gate.
+- Known issues list.
+- Suggested "share/social" feature if low-risk, such as a season recap/share card, dynasty card, or copyable summary.
+
+Required outputs:
+
+- `docs/tutorial-assistant/playtest-plan.md`
+- `docs/tutorial-assistant/release-gate.md`
+
+Acceptance criteria:
+
+- A closed tester can play the first session and always know what to do next.
+- Feedback can be collected in a simple way.
+- Remaining launch blockers are explicit.
+
+## Definition Of Done For The Overall Goal
+
+The goal is complete only when:
+
+1. Assistant exists in-game and actively guides users through the core loop.
+2. Users can always answer, "What should I do next?"
+3. The most important pages have contextual Assistant guidance.
+4. OVR/ratings are visible in major decision contexts.
+5. Mobile tutorial UX is first-class.
+6. Assistant guidance is dismissible, replayable, and not annoying.
+7. Newcomers and hardcore sim players are both supported.
+8. Save compatibility and deterministic sim behavior are preserved.
+9. Relevant tests/typechecks/lint/build checks pass, or any failures are clearly documented as pre-existing or blocked.
+10. Progress docs, coverage matrix, playtest plan, and release gate are updated.
+11. A completion audit against the actual current repo state passes.
+
+## Completion Audit
+
+Before marking this goal complete, perform this audit:
+
+- Read this goal contract again.
+- Read the progress file.
+- Inspect git diff.
+- Inspect coverage matrix.
+- Run relevant checks.
+- Confirm Assistant works on the highest-priority routes.
+- Confirm OVR/ratings improvements exist in decision-critical surfaces.
+- Confirm mobile behavior has been checked.
+- Confirm docs are updated.
+- List remaining known issues honestly.
+- Only then mark the goal complete.
+
+## Reporting Format
+
+At each meaningful stop, report:
+
+1. Current phase
+2. What was implemented
+3. Files changed
+4. Tests/checks run and results
+5. Known issues/risks
+6. Next highest-impact task
+7. Whether the goal is complete, blocked, or still in progress
diff --git a/docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md b/docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md
new file mode 100644
index 0000000..5660172
--- /dev/null
+++ b/docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md
@@ -0,0 +1,146 @@
+# MBD Tutorial Assistant V1 Progress
+
+Source contract: `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md`
+Last updated: 2026-05-05
+Branch: `goal/tutorial-assistant-v1` (work is uncommitted in working tree — branch is currently identical to `main`)
+Status: Implementation complete on disk; awaiting clean commit and closed playtest
+
+## Current Phase
+
+Implementation complete as a shippable V1 guidance layer. Independent Claude Code review confirmed the source/test files and docs match the goal contract. Two repo-level blockers must be cleared before merge:
+
+1. The local `.git/objects/pack/pack-0e3dc6...pack` is corrupt (`file` reports it as empty; `git verify-pack` reports `early EOF, pack is bad`). All in-pack history is unreadable — this is what caused the earlier `git status` / `git diff` hangs.
+2. Every Assistant change is currently untracked; `git rev-parse main HEAD` returns the same SHA. The work exists only in the working tree until the repo is repaired and a real commit lands.
+
+See `docs/tutorial-assistant/release-gate.md` for the recovery checklist before any merge or PR.
+
+## Completed Work
+
+- Created durable goal and progress docs.
+- Completed Phase 0 repo audit in `docs/tutorial-assistant/phase0-preflight.md`.
+- Completed Phase 1 FTUE, decision-point, route, and ratings audits.
+- Completed Phase 2 character, UX, trigger, and asset specs.
+- Implemented a reusable Assistant foundation:
+ - route-aware guidance for every current major route
+ - stable route normalization for aliases and dynamic routes
+ - localStorage state keyed by active save id/slot
+ - dismissed/completed route tracking
+ - replay controls
+ - Newcomer/Hardcore modes
+ - deterministic story callbacks from phase/ticker context with cooldowns
+- Implemented global Assistant UI:
+ - visible on Setup and Onboarding through route-level mount
+ - visible inside initialized app shell through `AppLayout`
+ - compact chip plus expanded panel
+ - "What now?" next action
+ - Explain ratings
+ - Deeper strategy
+ - Got it / Replay
+ - Escape-to-close behavior
+ - mobile-shaped fixed drawer/chip above bottom controls
+ - CSS/icon avatar placeholder with no external asset dependency
+- Updated route coverage matrix, playtest plan, release gate, and completion audit.
+
+## Live Checklist
+
+- [x] Create `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md`
+- [x] Create `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md`
+- [x] Phase 0 repo audit
+- [x] Create `docs/tutorial-assistant/phase0-preflight.md`
+- [x] Phase 1 FTUE + decision-point audit
+- [x] Create/update `docs/tutorial-assistant/coverage-matrix.md`
+- [x] Create `docs/tutorial-assistant/ratings-visibility-audit.md`
+- [x] Phase 2 Assistant character + UX spec
+- [x] Phase 3 Assistant engine foundation
+- [x] Phase 4 core page walkthroughs
+- [x] Phase 5 OVR/ratings visibility expansion
+- [x] Phase 6 Assistant visual assets placeholder/hooks
+- [x] Phase 7 narrative memory lite
+- [x] Phase 8 mobile, accessibility, performance hardening
+- [x] Phase 9 closed playtest readiness
+- [x] Create/update `docs/tutorial-assistant/release-gate.md`
+- [x] Completion audit
+
+## Test Results
+
+- `npx --yes pnpm@9.15.4 --version`: passed, `9.15.4`.
+- `npx --yes pnpm@9.15.4 install`: passed.
+- `npx --yes pnpm@9.15.4 run typecheck`: initially hung at `@mbd/web` under the default Node path before code changes; stopped and documented.
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/features/assistant/lib/assistantState.test.ts src/features/assistant/data/assistantGuidance.test.ts`: red first, then passed.
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/features/assistant/components/AssistantPanel.test.tsx`: red first, then passed.
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/app/layout/AppLayout.test.tsx -t "renders sim-to-playoffs"`: red first, then passed after stale unrelated Vitest processes were cleared.
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/app/routes/index.test.tsx`: red first, then passed.
+- Focused combined Assistant/AppRoutes/AppLayout test command: passed, 12 tests passed and 9 skipped by the `-t` filter.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 --filter @mbd/web typecheck`: passed.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 --filter @mbd/web build`: passed.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 --filter @mbd/contracts test`: passed, 18 tests.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 run verify:determinism`: passed, 3 tests.
+- Playwright mobile browser check at 390x844 on a static preview of `apps/web/dist`: passed. Verified Setup Assistant chip/panel, ratings explanation, quick-start save creation, Dashboard Assistant chip/panel after skipping the existing legacy tutorial modal, and route-aware Dashboard guidance.
+
+## Changed Files
+
+- `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md`
+- `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md`
+- `docs/tutorial-assistant/phase0-preflight.md`
+- `docs/tutorial-assistant/phase1-audit.md`
+- `docs/tutorial-assistant/ratings-visibility-audit.md`
+- `docs/tutorial-assistant/coverage-matrix.md`
+- `docs/tutorial-assistant/character-spec.md`
+- `docs/tutorial-assistant/assistant-ux-spec.md`
+- `docs/tutorial-assistant/trigger-spec.md`
+- `docs/tutorial-assistant/asset-plan.md`
+- `docs/tutorial-assistant/playtest-plan.md`
+- `docs/tutorial-assistant/release-gate.md`
+- `docs/tutorial-assistant/completion-audit.md`
+- `apps/web/src/features/assistant/lib/assistantState.ts`
+- `apps/web/src/features/assistant/lib/assistantState.test.ts`
+- `apps/web/src/features/assistant/data/assistantGuidance.ts`
+- `apps/web/src/features/assistant/data/assistantGuidance.test.ts`
+- `apps/web/src/features/assistant/components/AssistantPanel.tsx`
+- `apps/web/src/features/assistant/components/AssistantPanel.test.tsx`
+- `apps/web/src/app/layout/AppLayout.tsx`
+- `apps/web/src/app/layout/AppLayout.test.tsx`
+- `apps/web/src/app/routes/index.tsx`
+- `apps/web/src/app/routes/index.test.tsx`
+
+## Known Issues / Risks
+
+- Manual mobile browser verification completed on Setup and Dashboard at 390x844; closed tester playtest is still recommended before public launch handoff.
+- Generated Assistant portraits are deferred; V1 uses production-safe CSS/icon avatar hooks.
+- The Assistant explains OVR/ratings through route guidance rather than adding new columns to already-dense tables.
+- Default Node path hung on web typecheck/build; bundled Codex Node completed typecheck and build.
+- Stale unrelated Vitest/tsc processes were found and cleared during verification.
+- Final `git status`/`git diff` attempts hung in this desktop session after branch verification. Changed files were reviewed directly from the filesystem, and no save schema, sim engine, RNG, or contracts code was touched.
+
+## Next Action
+
+Claude Code review should focus on:
+
+1. Whether the Assistant copy is too verbose on mobile.
+2. Whether any route-specific next action should be made more dynamic through worker data.
+3. Whether generated portrait assets should be added in a follow-up art slice.
+4. Whether playtest feedback shows any remaining OVR/rating visibility gaps requiring visible badges in specific tables.
+
+## Goal Status
+
+Implementation complete for Tutorial Assistant V1. Blocked on repo health (corrupt packfile and uncommitted work) before merge or PR.
+
+## Independent Review (2026-05-05, Claude Code)
+
+Verified directly against the working tree:
+
+- Code structure: 6 source files (`apps/web/src/features/assistant/{lib,data,components}/...` + tests) plus mounts in `apps/web/src/app/layout/AppLayout.tsx:498` and `apps/web/src/app/routes/index.tsx:126-133`.
+- Route coverage: 31 keys in `REQUIRED_ASSISTANT_ROUTE_KEYS`, every key has a guidance entry; aliases (`/league/standings`, `/league/leaders`) and dynamic routes (`/players/:id`, `/games/:id`) are normalized in `resolveAssistantRouteKey`.
+- TypeScript: `tsc --noEmit` over the web app exits 0 with no diagnostics.
+- Module logic: direct Node smoke test confirms reducer, sanitizer, storage key shape, route resolution, all 31 guidance entries, phase-aware next-action override, and ticker/phase story-callback cooldown behavior.
+- Build artifacts: `apps/web/dist/assets/index-*.js` already contains `"Mack Mercer"`, `"Got it"`, `"Hardcore mode"`, `"Replay"` from the prior Codex build.
+- Save safety: `CURRENT_GAME_SNAPSHOT_VERSION` is still `33`, no `Math.random` calls in the Assistant code paths, persistence is `localStorage` keyed by save id/slot via `assistantStorageKey`.
+- Accessibility: `role="dialog"`, `aria-label`, `aria-live="polite"`, `motion-safe:` animation on the avatar, `min-h-11` touch targets, Escape-to-close handler.
+- Tailwind tokens: every `dynasty-*` and `accent-*` class used by the panel resolves to entries in `packages/design-tokens/src/colors.ts`.
+- Existing systems: `TourProvider`, `PageHelp`, `ContextualHelp`, `GameAdvisor`, and onboarding flows are untouched. Mobile bottom nav at `Sidebar.tsx:201` and the Assistant chip both sit at `z-40`; the chip clears the nav by 32 px (`bottom-24` chip vs ~64 px nav height).
+
+Could not be re-verified in this review session:
+
+- Vitest hangs on this machine for both the Codex bundled Node 24.14.0 and Homebrew Node 25.x. Test code itself was read and is consistent with the smoke results above. The `pnpm test` results captured during the Codex sprint should be re-run on a clean shell once the repo is repaired.
+- Web `pnpm build` hangs the same way on this machine; the prior Codex build artifact in `dist/` confirms it has succeeded at least once on this branch.
+- Mobile browser QA at 390×844 was not re-run — Codex's Playwright static-preview check stands as the latest evidence and matches the chip placement / overlay analysis above.
diff --git a/docs/tutorial-assistant/asset-plan.md b/docs/tutorial-assistant/asset-plan.md
new file mode 100644
index 0000000..37d0cab
--- /dev/null
+++ b/docs/tutorial-assistant/asset-plan.md
@@ -0,0 +1,30 @@
+# Tutorial Assistant V1 Asset Plan
+
+## V1 Asset Approach
+
+Ship with a production-safe CSS/SVG-style avatar first, then replace with generated raster assets when available.
+
+## Placeholder Requirements
+
+- Small avatar for compact chip.
+- Larger avatar in expanded panel.
+- Neutral, warning, success, and excited states represented through border/accent/icon state.
+- No broken image paths.
+- No dependency on external asset hosting.
+- Reduced-motion users see no pulse/entrance animation.
+
+## Future Generated Assets
+
+Desired files under the appropriate `apps/web/public/` assistant asset folder:
+
+- `mack-mercer-hero.webp`
+- `mack-mercer-avatar.webp`
+- `mack-mercer-neutral.webp`
+- `mack-mercer-excited.webp`
+- `mack-mercer-warning.webp`
+- `mack-mercer-success.webp`
+- transparent PNG fallbacks if useful
+
+Prompt direction:
+
+Modern fictional baseball front-office assistant / bench coach, polished sports management sim tone, friendly but sharp, dark UI compatible, not goofy, no real person likeness, no team logos, no copyrighted marks.
diff --git a/docs/tutorial-assistant/assistant-ux-spec.md b/docs/tutorial-assistant/assistant-ux-spec.md
new file mode 100644
index 0000000..78f877e
--- /dev/null
+++ b/docs/tutorial-assistant/assistant-ux-spec.md
@@ -0,0 +1,50 @@
+# Tutorial Assistant V1 UX Spec
+
+## Primary Surface
+
+The Assistant is a global fixed control mounted in `AppLayout`.
+
+- Desktop: compact bottom-right panel above sim controls.
+- Mobile: bottom drawer/chip above the mobile nav and sim controls.
+- Default state: compact chip labeled "Assistant" / "What now?"
+- Expanded state: route-aware guidance with one next action, why the page matters, ratings focus, and deeper strategy.
+
+## Core Actions
+
+- What should I do now?
+- Explain this page.
+- Explain ratings.
+- Show deeper strategy.
+- Got it / dismiss.
+- Replay current page guidance.
+- Switch newcomer/hardcore mode.
+
+## Persistence
+
+- Store tutorial progress, dismissed route guidance, mode, and story callback cooldowns in localStorage.
+- Key by active save id/slot when available.
+- Use a global fallback key before a save is active.
+- Do not change `GameSnapshot` for V1 unless the save schema is intentionally bumped.
+
+## Mobile Rules
+
+- Expanded panel maxes out below full-screen height and scrolls internally.
+- Never cover bottom nav/sim controls without the user opening the drawer.
+- All controls meet 44px touch target.
+- No horizontal scrolling.
+- Text is short and wraps.
+
+## Accessibility Rules
+
+- Expanded panel uses `role="dialog"` or a clearly labeled complementary region.
+- Buttons have explicit labels.
+- Escape closes expanded panel.
+- Focus remains sane; no forced focus trap for the compact chip.
+- Respect reduced motion.
+- Story updates use polite aria-live, not assertive interruptions.
+
+## Integration Rules
+
+- Keep existing `TourProvider`, `PageHelp`, `ContextualHelp`, `GameAdvisor`, and guided-start nudges working.
+- Assistant V1 becomes the unifying route-aware surface, not a destructive replacement.
+- Reuse route definitions and localStorage patterns where possible.
diff --git a/docs/tutorial-assistant/character-spec.md b/docs/tutorial-assistant/character-spec.md
new file mode 100644
index 0000000..46c2700
--- /dev/null
+++ b/docs/tutorial-assistant/character-spec.md
@@ -0,0 +1,43 @@
+# Tutorial Assistant V1 Character Spec
+
+## Name
+
+Mack Mercer
+
+## Role
+
+Mack is the permanent in-game Assistant: part bench coach, part assistant GM, part patient front-office operator. He is not the same thing as the generated Day One AGM candidates; he is the product-level guide who helps users understand the app and their save.
+
+## Voice
+
+- Baseball-smart and practical.
+- Clear before colorful.
+- Encouraging without cheerleading.
+- Comfortable with both scouting language and plain English.
+- Speaks like a trusted coach in the room: "Start here", "This matters because", "The move depends on your window."
+- Never childish, sarcastic, or spammy.
+
+## Behavior Rules
+
+- Always preserve player agency.
+- Prefer one suggested next action over a long checklist.
+- Explain why a page matters before explaining all controls.
+- Use ratings as decision context, not as an absolute truth.
+- In newcomer mode, define acronyms and reduce jargon.
+- In hardcore mode, use denser strategy language and fewer basics.
+- Do not block gameplay unless the user explicitly opens walkthrough mode.
+- Dismissed guidance stays dismissed until replayed.
+- Story callbacks should feel save-aware, not random.
+
+## Visual Direction
+
+- Modern baseball front-office assistant / bench coach.
+- Friendly but sharp, polished sports-sim tone.
+- Works in MBD's dark Bloomberg-style interface.
+- Avoid real people, copyrighted marks, mascots, goofy costumes, or cartoon exaggeration.
+
+## First Implementation Asset Strategy
+
+- Ship production-safe CSS/SVG avatar hooks first.
+- Add generated bitmap portraits later if available.
+- Use neutral/success/warning/excited expression states through CSS classes and accessible labels.
diff --git a/docs/tutorial-assistant/claude-code-audit-2026-05-05.md b/docs/tutorial-assistant/claude-code-audit-2026-05-05.md
new file mode 100644
index 0000000..4b215ca
--- /dev/null
+++ b/docs/tutorial-assistant/claude-code-audit-2026-05-05.md
@@ -0,0 +1,133 @@
+# Tutorial Assistant V1 — Claude Code Independent Review
+
+Date: 2026-05-05
+Reviewer: Claude Code (independent senior reviewer / QA / release-hardening pass)
+Branch reviewed: `goal/tutorial-assistant-v1`
+Scope: audit Codex's Tutorial Assistant V1 against the goal contract, the actual repo state, the actual diff, and the actual app behavior.
+
+## 1. Audit Verdict
+
+**Needs fixes — blocked by repo state, not by the Assistant implementation.**
+
+The Assistant V1 implementation itself is shippable as a guidance-layer V1: code is well-structured, types pass, route coverage is complete, save safety is preserved, accessibility basics are in place, and the prior Codex build is already in `dist/`. But the local clone is in a state where the work cannot be safely committed, reviewed via PR, or merged.
+
+Two repo-level P0 blockers must clear before this branch can become a PR:
+
+1. The local pack file is corrupt (full diagnosis below). All `git status`/`git diff` hangs Codex saw trace to this — it is not just a slow command.
+2. Every Codex change is untracked. The branch SHA equals `main`, so `git diff main..HEAD` is empty even though ~12 docs and 6 source files were authored.
+
+Once those clear, Tutorial Assistant V1 can ship behind one closed-tester playtest pass.
+
+## 2. What Was Verified Manually
+
+### Code structure (read end-to-end)
+
+- `apps/web/src/features/assistant/lib/assistantState.ts` — typed state, save-scoped storage key, sanitizer that defaults invalid modes to `newcomer`, pure reducer for dismiss / complete / replay / story / mode events.
+- `apps/web/src/features/assistant/data/assistantGuidance.ts` — `REQUIRED_ASSISTANT_ROUTE_KEYS` (31 keys), `ASSISTANT_GUIDANCE` map, `resolveAssistantRouteKey` covering aliases (`/league/standings`, `/league/leaders`) and dynamic routes (`/players/:id`, `/games/:id`), `buildAssistantNextAction` with phase/route overrides, `buildStoryCallback` with cooldowns from `seenStoryCallbacks` and ticker filtering by category.
+- `apps/web/src/features/assistant/components/AssistantPanel.tsx` — chip + expanded panel, role="dialog", Escape-to-close, ratings/strategy expanders, mode toggle, save-scoped state via `useGameStore` (`activeSaveId`, `activeSaveSlot`, `phase`, `day`, `season`).
+- `apps/web/src/app/layout/AppLayout.tsx:498` — global mount inside the initialized shell.
+- `apps/web/src/app/routes/index.tsx:126-133` — `PreGameAssistantMount` puts the panel on `/` and `/onboarding`, which sit outside `AppLayout`.
+
+### Route coverage
+
+- All 31 `REQUIRED_ASSISTANT_ROUTE_KEYS` map 1:1 to entries in `ASSISTANT_GUIDANCE` (smoke-tested directly via Node).
+- Every route in `apps/web/src/app/routes/index.tsx` either matches a key by path or is normalized to one (e.g. `/players/:id` → `player-profile`, `/games/:id` → `box-score`, unknown → `dashboard`).
+- `selectRouteGuidance(path)` returns valid guidance for `/`, `/onboarding`, `/dashboard`, `/roster`, `/minors`, `/players`, `/players/compare`, `/players/abc-123`, `/scouting`, `/staff`, `/draft`, `/trade`, `/standings`, `/league/standings`, `/leaders`, `/league/leaders`, `/schedule`, `/games/42`, `/press-room`, `/playoffs`, `/free-agency`, `/offseason`, `/finance`, `/career`, `/history`, `/achievements`, `/rivalries`, `/front-office`, `/pulse`, `/scenarios`, `/stats`, `/records`, `/settings`.
+
+### OVR / ratings visibility
+
+- Existing surfaces already expose OVR/grade columns or panels: roster (`LineupBuilder`, `DepthChartDnD`, `RosterPage`), trade (`TradePage`), free agency (`FreeAgencyPage`), draft (`DraftPage`), scouting (`ScoutingPage`), players (`ProfileHeader`, `DevelopmentTab`, `ScoutingTab`, `StatsTab`, `HistoryTab`, `PlayersPage`, `PlayerComparisonPage`), minors (`PipelineView`, `MinorsPage`).
+- Codex's strategy of explaining ratings via Assistant `ratingsFocus` rather than adding new columns is correct given existing density. Coverage matrix and ratings-visibility audit accurately reflect the surfaces.
+- Test `assistantGuidance.test.ts` already enforces `ratingsFocus` mentions OVR/rating/ceiling/grade/confidence on `/roster`, `/players`, `/players/compare`, `/players/example`, `/scouting`, `/draft`, `/trade`, `/free-agency`, `/minors`.
+
+### Save safety / determinism
+
+- `CURRENT_GAME_SNAPSHOT_VERSION` is still `33` in `packages/contracts/src/schemas/save.ts:513`. No save migrations introduced.
+- No `Math.random` / RNG calls anywhere in `apps/web/src/features/assistant/`, `apps/web/src/app/layout/AppLayout.tsx`, or `apps/web/src/app/routes/index.tsx`.
+- Persistence is `localStorage`-only via `assistantStorageKey(saveId)` → keys like `mbd:assistant:v1:save-slot-2` or `mbd:assistant:v1:global`. The `GameSnapshot` schema is not touched.
+- Story callbacks are deterministic: they read phase/day/season/route + ticker feed + previously-seen callback ids, no randomness.
+
+### Accessibility
+
+- `role="dialog"`, `aria-label="Mack Mercer Assistant"` on expanded panel; `aria-label="Open Assistant"` / `"Close Assistant"` on toggle/close; `aria-live="polite"` on story callouts.
+- Escape closes the expanded panel via a window keydown listener cleaned up on close.
+- All interactive controls are `min-h-11` (44 px / iOS minimum).
+- Avatar uses `motion-safe:animate-[fadeIn_160ms_ease-out]` so reduced-motion users get a static avatar.
+- Initial focus on open is not yet placed on the close button — minor a11y improvement, not a blocker.
+
+### Visual / mobile stacking
+
+- Tailwind classes used by the panel (`dynasty-base|surface|elevated|border|muted|text|textBright`, `accent-primary|info|warning|success`) all resolve in `packages/design-tokens/src/colors.ts`.
+- AssistantPanel is `fixed inset-x-3 bottom-24 z-40 sm:left-auto sm:right-4 sm:w-[25rem]`. Mobile bottom nav (`Sidebar.tsx:201`) is `fixed inset-x-0 bottom-0 z-40 ... md:hidden`. With ~64 px nav height and `bottom-24` (96 px) chip placement, the chip clears the nav by ~32 px and shares z-index without overlap.
+- `MonthlyPulseOverlay` (z-40), `MomentCardOverlay` (z-50), `CommandPalette` (z-50), `KeyboardShortcutsPanel` (z-50), `TourStep` (z-[60]) all render after the AssistantPanel in the AppLayout JSX, so when those open they correctly cover the chip.
+
+### Existing systems untouched
+
+- `TourProvider`, `PageHelp`, `ContextualHelp`, `GameAdvisor`, onboarding flows: not edited. Verified by file scan; only AppLayout adds the Assistant mount and new imports.
+
+## 3. What Was Fixed In This Review
+
+- `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md` — corrected the branch claim (work is uncommitted, branch == main), promoted Codex's "git status hung" note to the actual diagnosis (corrupt packfile), added an "Independent Review" section enumerating verified vs unverifiable items, and changed the goal status to "Implementation complete on disk; awaiting clean commit and closed playtest".
+- `docs/tutorial-assistant/release-gate.md` — added two P0 blockers (repo health and uncommitted work) ahead of the existing follow-ups, with the exact recovery sequence and an explicit "do not `git gc`/`git repack`/wipe `.git/objects/pack/` without first preserving the working tree" warning.
+
+No source code edits were applied. Codex's V1 implementation does not have any P0/P1 issues that justify changing code while the repo itself is unhealthy. P3 polish items are listed below for the next sprint.
+
+## 4. Files Changed (this review)
+
+- `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md`
+- `docs/tutorial-assistant/release-gate.md`
+- `docs/tutorial-assistant/claude-code-audit-2026-05-05.md` (new)
+
+## 5. Tests / Checks Run And Results
+
+- `git rev-parse main HEAD` → `1d45741... 1d45741...` (branch is identical to `main`).
+- `git ls-files --error-unmatch docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md` → `did not match any file(s) known to git` (proving Codex's docs are untracked).
+- `git count-objects -v` → `count: 5, in-pack: 5901, packs: 1, size-pack: 3721`.
+- `file .git/objects/pack/pack-0e3dc6a6...pack` → `empty` (header bytes are zeroed despite 3.6 MB on disk).
+- `git verify-pack -v` on the pack idx → `fatal: early EOF` / `pack is bad`.
+- `git fsck --no-progress` → hung; killed.
+- `tsc --noEmit -p apps/web` (via `apps/web/node_modules/typescript/bin/tsc`) → exit 0, no diagnostics.
+- Direct Node smoke test (Homebrew node 25.8.2 + `--experimental-strip-types`) on `assistantState.ts` and `assistantGuidance.ts` → `STATE OK` and `GUIDANCE OK` covering reducer transitions, sanitizer, storage key shape, route resolution incl. dynamic/alias paths, all 31 guidance entries, hardcore-trade override, offseason override, ticker story callback, and seen-cooldown.
+- `apps/web/dist/assets/index-*.js` already contains `"Mack Mercer"`, `"Got it"`, `"Hardcore mode"`, `"Replay"` from the prior Codex build — proving `vite build` previously succeeded with the Assistant code.
+- `pnpm --filter @mbd/web test`, `pnpm --filter @mbd/web build`, `pnpm --filter @mbd/contracts test`, `pnpm run verify:determinism` could not be re-run cleanly: vitest and `vite build` hung from this shell on both the bundled Codex Node 24.14.0 and Homebrew Node 25.8.2. The Codex sprint's recorded results in the release gate stand; rerun once the repo is healthy.
+
+## 6. Remaining Known Issues
+
+- **P0** Local pack file is corrupt; full git history is unreadable from this clone.
+- **P0** All Codex work is untracked relative to `main`.
+- **P2** Closed playtest has not been performed. The first-session script in `playtest-plan.md` is the right pass.
+- **P2** Generated Assistant portrait art is deferred. The CSS/icon avatar is fine for V1 but the visual ceiling is low.
+- **P2** Some of the longer guidance bodies (`pagePurpose`, `deeperStrategy`) may feel verbose on a 390 px viewport. Worth measuring during playtest before any rewrite.
+- **P3** `readAssistantState` calls `localStorage.getItem(...)` twice on the same key — once to null-check and once to parse. Cosmetic; works correctly. Suggested rewrite:
+
+ ```ts
+ const raw = window.localStorage.getItem(assistantStorageKey(saveId));
+ return sanitizeAssistantState(raw != null ? JSON.parse(raw) : null);
+ ```
+- **P3** Initial focus on panel open is not placed; users currently rely on Tab to reach the close button. A `useRef` + `focus()` on the close button when `open` flips to `true` would tighten keyboard a11y.
+- **P3** When `MonthlyPulseOverlay` is open it is `z-40` (same as the chip). DOM order makes it render above the chip, which is the correct outcome, but the implicit ordering is fragile — consider raising the overlay to `z-[45]` or lowering the chip when an overlay is active.
+
+## 7. Is The Branch Safe To Commit / PR?
+
+**Not yet.** The branch as currently reachable by git contains zero of Codex's work. Committing into this clone risks writing new objects whose deltas may reference the corrupt pack. The right path is:
+
+1. `tar czf ~/mbd-assistant-v1-backup-$(date +%Y%m%d).tar.gz -C /Users/tkevinbigham/Documents/GitHub/MBD docs/goals/MBD_TUTORIAL_ASSISTANT_V1_GOAL.md docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md docs/tutorial-assistant apps/web/src/features/assistant apps/web/src/app/layout/AppLayout.tsx apps/web/src/app/layout/AppLayout.test.tsx apps/web/src/app/routes/index.tsx apps/web/src/app/routes/index.test.tsx`
+2. Re-clone: `git clone git@github.com:KevinBigham/MBD.git ~/Documents/GitHub/MBD-fresh && cd ~/Documents/GitHub/MBD-fresh && git switch -c goal/tutorial-assistant-v1`
+3. Restore: `tar xzf ~/mbd-assistant-v1-backup-*.tar.gz -C ~/Documents/GitHub/MBD-fresh`
+4. Re-run sanity in the fresh clone: `pnpm install`, `pnpm --filter @mbd/web typecheck`, `pnpm --filter @mbd/web test -- src/features/assistant src/app/routes/index.test.tsx src/app/layout/AppLayout.test.tsx`, `pnpm --filter @mbd/web build`, `pnpm --filter @mbd/contracts test`, `pnpm run verify:determinism`.
+5. `git add` only the explicit Assistant paths, commit (no `git add -A`), push, open the PR.
+
+After step 5 the branch is review-safe. The PR description suggested by ChatGPT in the kickoff message is fine — Codex's V1 matches it.
+
+## 8. Recommended Next Sprint
+
+**MBD Assistant Polish + Closed Playtest Sprint**, prioritized in this order:
+
+1. **Closed playtest** with the first-session script in `playtest-plan.md`, on both desktop and a real ~390×844 device (not just resized desktop). Capture which lines feel verbose, where mobile users tap-then-back, and whether OVR meaning landed.
+2. **Mobile copy density.** Tighten `pagePurpose` + `deeperStrategy` to one short sentence each on mobile; keep the longer copy for desktop. Likely lands as a `mobileCopy` field on guidance entries plus a `useMatchMedia('(max-width: 640px)')` selector in the panel.
+3. **Portrait art slice.** Replace the CSS/icon avatar with a real Mack Mercer portrait set (neutral, encouraging, warning, success, excited). Wire the existing `tone` switch into the asset.
+4. **Selective rating badges.** If playtest reveals confusion in trade or draft tables specifically, add small OVR/grade badges at point-of-decision. Avoid blanket badge spam.
+5. **Focus management** (P3 above) — first-focus-on-open and an overlay-aware z-index for the chip.
+6. **Optional share/social slice** described in `playtest-plan.md` — a copyable Dynasty Status card sourced from history + Assistant story line. Local-only and text-first.
+
+Codex did the build sprint cleanly. The remaining V1 risk is environmental, not engineering.
diff --git a/docs/tutorial-assistant/completion-audit.md b/docs/tutorial-assistant/completion-audit.md
new file mode 100644
index 0000000..20736a6
--- /dev/null
+++ b/docs/tutorial-assistant/completion-audit.md
@@ -0,0 +1,46 @@
+# Tutorial Assistant V1 Completion Audit
+
+Date: 2026-05-05
+
+## Contract Re-Read
+
+- Goal contract reviewed at sprint start.
+- Progress file reviewed at sprint start.
+- Current git status inspected at sprint start.
+
+## Diff Review
+
+Final `git status`/`git diff` attempts hung in this desktop session. The changed file set was reviewed directly from the filesystem, and targeted source scans found no `Math.random()`, save schema, sim engine, RNG, or contracts changes in the Assistant implementation surface.
+
+Expected change areas:
+
+- goal/progress docs
+- tutorial-assistant docs
+- Assistant route guidance/data/state/component tests
+- Assistant route guidance/data/state/component implementation
+- App route and app shell mounting tests
+- App route and app shell mounting implementation
+
+No save schema, migration, sim engine, RNG, or contracts code was changed.
+
+## Requirement Check
+
+1. Assistant exists in-game and actively guides users through the core loop: pass.
+2. Users can always answer "What should I do next?": pass through global next-action CTA.
+3. Important pages have contextual Assistant guidance: pass through full route guidance table.
+4. OVR/ratings are visible in major decision contexts: pass for existing surfaces plus Assistant ratings explanations.
+5. Mobile tutorial UX is first-class: pass by implementation design and Playwright 390x844 Setup/Dashboard verification.
+6. Assistant guidance is dismissible, replayable, and not annoying: pass.
+7. Newcomers and hardcore sim players are both supported: pass.
+8. Save compatibility and deterministic sim behavior are preserved: pass, no schema/sim/RNG changes.
+9. Relevant checks pass or failures documented: pass, see release gate.
+10. Progress docs, coverage matrix, playtest plan, and release gate updated: pass.
+11. Completion audit against actual repo state passes: pass with known closed-tester playtest follow-up.
+
+## Known Issues
+
+- Manual closed playtest has not been performed in this run.
+- Browser verification used a static preview of the built app because `vite preview` hung in this desktop session; the built app itself had already passed `@mbd/web build`.
+- Final git status/diff review could not complete because read-only git commands hung; this was documented rather than worked around with destructive git operations.
+- Generated Assistant art is deferred behind the asset plan.
+- The default Node runtime on this machine hung on web typecheck/build; bundled Codex Node completed both.
diff --git a/docs/tutorial-assistant/coverage-matrix.md b/docs/tutorial-assistant/coverage-matrix.md
new file mode 100644
index 0000000..b5b1d2d
--- /dev/null
+++ b/docs/tutorial-assistant/coverage-matrix.md
@@ -0,0 +1,39 @@
+# Tutorial Assistant V1 Coverage Matrix
+
+Date: 2026-05-05
+
+| Route | Page | Assistant Guidance | OVR/Ratings Visible When Relevant | Mobile Status | Notes |
+| --- | --- | --- | --- | --- | --- |
+| `/` | Setup / Save Hub | Implemented | Partial | Existing LC-3 pass | Assistant covers first-save guidance |
+| `/onboarding` | Onboarding | Implemented | Partial | Existing LC-3 pass | Existing AGM and guided-start nudge systems |
+| `/dashboard` | Dashboard | Implemented | Indirect | Existing LC-3 pass | Existing GameAdvisor, global Assistant mounted |
+| `/roster` | Roster | Implemented | Yes | Existing LC-3 pass | PageHelp exists; OVR and grades in tables |
+| `/minors` | Minors | Implemented | Yes | Existing LC-3 pass | Assistant explains promotion and development |
+| `/players` | Players | Implemented | Yes | Existing LC-3 pass | OVR and grade in directory |
+| `/players/compare` | Player Compare | Implemented | Yes | Existing LC-3 pass | Assistant explains compare decisions |
+| `/players/:playerId` | Player Profile | Implemented | Yes | Existing LC-3 pass | Header and tabs expose ratings |
+| `/scouting` | Scouting | Implemented | Yes | Existing LC-3 pass | Assistant explains confidence and bias |
+| `/staff` | Staff | Implemented | Yes | Existing LC-3 pass | Coach grades are rating-like |
+| `/draft` | Draft | Implemented | Yes | Existing LC-3 pass | PageHelp and guided-start nudge exist |
+| `/trade` | Trade | Implemented | Yes | Existing LC-3 pass | PageHelp exists; needs value model help |
+| `/standings` | Standings alias | Implemented | N/A | Existing LC-3 pass | Alias route |
+| `/league/standings` | Standings | Implemented | N/A | Existing LC-3 pass | PageHelp exists |
+| `/leaders` | Leaders alias | Implemented | Yes | Existing LC-3 pass | Alias route |
+| `/league/leaders` | Leaders | Implemented | Yes | Existing LC-3 pass | OVR visible in leaders table |
+| `/schedule` | Schedule | Implemented | N/A | Existing LC-3 pass | Assistant explains sim and inspect rhythm |
+| `/games/:gameIndex` | Box Score | Implemented | N/A | Existing LC-3 pass | Assistant explains result reading |
+| `/press-room` | Press Room | Implemented | N/A | Existing LC-3 pass | Existing press narratives |
+| `/playoffs` | Playoffs | Implemented | N/A | Existing LC-3 pass | Assistant explains postseason flow |
+| `/free-agency` | Free Agency | Implemented | Yes | Existing LC-3 pass | OVR visible in market table |
+| `/offseason` | Offseason | Implemented | Yes | Existing LC-3 pass | OVR visible in candidates/Rule 5 surfaces |
+| `/finance` | Finance | Implemented | Indirect | Existing LC-3 pass | Assistant explains talent-to-payroll decisions |
+| `/career` | GM Career | Implemented | N/A | Existing LC-3 pass | Assistant explains career stakes |
+| `/history` | History | Implemented | Historical | Existing LC-3 pass | Peak/current OVR in some history surfaces |
+| `/achievements` | Achievements | Implemented | N/A | Existing LC-3 pass | Optional goal guidance |
+| `/rivalries` | Rivalries | Implemented | N/A | Existing LC-3 pass | Assistant explains consequences |
+| `/front-office` | Owner Intel | Implemented | Indirect | Existing LC-3 pass | Assistant explains patience and reputation |
+| `/pulse` | Pulse | Implemented | Indirect | Existing LC-3 pass | Monthly decision spotlight exists |
+| `/scenarios` | Challenges | Implemented | Varies | Existing LC-3 pass | Scenario-specific goals |
+| `/stats` | Stats Encyclopedia | Implemented | Explanatory | Existing LC-3 pass | Best home for deeper rating/stat definitions |
+| `/records` | Record Watch | Implemented | Historical | Existing LC-3 pass | Legacy guidance |
+| `/settings` | Settings | Implemented | N/A | Existing LC-3 pass | Assistant explains replay and accessibility controls |
diff --git a/docs/tutorial-assistant/phase0-preflight.md b/docs/tutorial-assistant/phase0-preflight.md
new file mode 100644
index 0000000..cbde007
--- /dev/null
+++ b/docs/tutorial-assistant/phase0-preflight.md
@@ -0,0 +1,117 @@
+# Tutorial Assistant V1 Phase 0 Preflight
+
+Date: 2026-05-05
+Branch: `goal/tutorial-assistant-v1`
+Repo: `/Users/tkevinbigham/Documents/GitHub/MBD`
+
+## Repo Structure
+
+- Root monorepo: pnpm workspaces + Turbo.
+- Web app: `apps/web` with React 18, Vite 6, Tailwind, Zustand, Dexie, Comlink, Vitest.
+- Deterministic sim engine: `packages/sim-core`.
+- Shared save/contracts: `packages/contracts`.
+- Shared UI/tokens: `packages/ui`, `packages/design-tokens`.
+- Current public URL in repo: `https://kevinbigham.github.io/MBD/`.
+
+## Package Manager And Commands
+
+- Root `package.json` declares `pnpm@9.15.4`.
+- Global `pnpm` is not installed in this environment.
+- Use `npx --yes pnpm@9.15.4 ...` for all pnpm commands.
+- `node_modules` was missing at sprint start; `npx --yes pnpm@9.15.4 install` completed.
+
+Primary commands:
+
+- Root typecheck: `npx --yes pnpm@9.15.4 run typecheck`
+- Root test: `npx --yes pnpm@9.15.4 run test`
+- Root build: `npx --yes pnpm@9.15.4 run build`
+- Root full gate: `npx --yes pnpm@9.15.4 run verify`
+- Web focused gate: `npx --yes pnpm@9.15.4 --filter @mbd/web typecheck && npx --yes pnpm@9.15.4 --filter @mbd/web test && npx --yes pnpm@9.15.4 --filter @mbd/web build`
+- Contracts save gate: `npx --yes pnpm@9.15.4 --filter @mbd/contracts test`
+- Determinism gate: `npx --yes pnpm@9.15.4 run verify:determinism`
+
+## Baseline Check Results
+
+- `npx --yes pnpm@9.15.4 --version`: passed, `9.15.4`.
+- `npx --yes pnpm@9.15.4 install`: passed; installed workspace dependencies from the existing lockfile.
+- `npx --yes pnpm@9.15.4 run typecheck`: started and completed package typechecks through shared packages, then hung at `@mbd/web` with no output for several minutes. The command was stopped and recorded as an environment/runtime issue for the root gate. Use focused package gates during implementation and retry the root gate before completion.
+
+## Route Inventory
+
+Routes are defined in `apps/web/src/app/routes/index.tsx`.
+
+| Route | Label | Component |
+| --- | --- | --- |
+| `/` | Save Hub | `SetupPage` |
+| `/onboarding` | Onboarding | `RevisedOnboardingPage` |
+| `/dashboard` | Dashboard | `DashboardPage` |
+| `/roster` | Roster | `RosterPage` |
+| `/minors` | Minors | `MinorsPage` |
+| `/players` | Players | `PlayersPage` |
+| `/players/compare` | Player Comparison | `PlayerComparisonPage` |
+| `/players/:playerId` | Player Profile | `PlayerProfilePage` |
+| `/scouting` | Scouting | `ScoutingPage` |
+| `/staff` | Staff | `StaffPage` |
+| `/draft` | Draft | `DraftPage` |
+| `/trade` | Trade | `TradePage` |
+| `/standings`, `/league/standings` | Standings | `StandingsPage` |
+| `/leaders`, `/league/leaders` | Leaders | `LeadersPage` |
+| `/schedule` | Schedule | `SchedulePage` |
+| `/games/:gameIndex` | Box Score | `BoxScorePage` |
+| `/press-room` | Press Room | `PressRoomPage` |
+| `/playoffs` | Playoffs | `PlayoffsPage` |
+| `/free-agency` | Free Agency | `FreeAgencyPage` |
+| `/offseason` | Offseason | `OffseasonPage` |
+| `/finance` | Finance | `FinancePage` |
+| `/career` | GM Career | `GMCareerPage` |
+| `/history` | History | `HistoryPage` |
+| `/achievements` | Achievements | `AchievementsPage` |
+| `/rivalries` | Rivalries | `RivalriesPage` |
+| `/front-office` | Owner Intel | `FrontOfficePage` |
+| `/pulse` | Pulse | `PulsePage` |
+| `/scenarios` | Challenges | `ScenarioCatalogPage` |
+| `/stats` | Stats Encyclopedia | `StatsEncyclopediaPage` |
+| `/records` | Record Watch | `RecordWatchPage` |
+| `/settings` | Settings | `SettingsPage` |
+
+## Existing Tutorial / Help / Assistant Systems
+
+- `apps/web/src/shared/components/TourProvider.tsx`: localStorage-backed guided tour for new games, mounted in `AppLayout`.
+- `apps/web/src/shared/lib/tourDefinition.ts`: static tour steps for dashboard, sim controls, sidebar, roster, draft/trade, press room, and help system.
+- `apps/web/src/shared/components/PageHelp.tsx` + `apps/web/src/shared/lib/pageHelpDefinitions.ts`: slide-in page help. Currently used directly on Roster, Draft, Trade, and Standings.
+- `apps/web/src/shared/components/ContextualHelp.tsx`: top-bar contextual help keyed by pathname.
+- `apps/web/src/features/dashboard/components/GameAdvisor.tsx`: dashboard-only "What should I do?" recommendation card.
+- `apps/web/src/features/onboarding/nudges/*`: save-slot keyed localStorage guided-start nudges.
+- `packages/sim-core/src/onboarding/assistantGM.ts`: deterministic onboarding assistant GM profile generation.
+- `apps/web/src/workers/sim.worker.monthlyPulse.ts` and related files: monthly reports and decision spotlights that already carry some guidance.
+
+## Ratings / OVR Systems
+
+- Internal ratings are 0-550 in sim/contract data.
+- Display ratings use the 20-80 style scale through helpers such as `toDisplayRating` and DTO fields named `displayRating`.
+- Current save player shape stores `overallRating`.
+- OVR is visible in many high-value surfaces: roster tables, players directory, player profile, free agency, trade tables, draft/scouting reports, minors pipeline, leaders, and setup preview.
+- Gaps are mostly consistency/explanation gaps: users can see OVR but do not always know what it means, when to prioritize it, or why a lower OVR player may still be the right decision.
+
+## Persistence / Save Safety
+
+- Current save schema version: `CURRENT_GAME_SNAPSHOT_VERSION = 33` in `packages/contracts/src/schemas/save.ts`.
+- Migration tests live in `packages/contracts/tests/save.migration.test.ts`.
+- Existing guided-start nudge persistence uses localStorage keys under `mbd:nudges:` and explicitly avoids changing `GameSnapshot`.
+- Tutorial Assistant V1 should prefer localStorage keyed by active save id/slot for tutorial progress and cooldowns. Any future `GameSnapshot` persistence requires a version bump, migration, and sample fixture update.
+
+## Mobile Layout Patterns
+
+- `apps/web/docs/lc3-mobile-audit.md` is the current detailed mobile audit.
+- `AppLayout` uses desktop sidebar and mobile bottom nav / more drawer.
+- Mobile controls target a 44px floor after LC-3.
+- Assistant surfaces must avoid covering the fixed bottom sim controls and mobile tab bar.
+- Use fixed bottom drawer/chip behavior on mobile, not large centered popups.
+- Respect existing `useReducedMotion` and `prefers-reduced-motion` patterns.
+
+## Blockers / Risks
+
+- No repo-root `AGENTS.md` exists; Kevin's supplied AGENTS.md text is the active project-specific instruction source.
+- `MASTER_CONTEXT.md` contains stale paths and counts; do not use it as authoritative.
+- Root typecheck currently hangs in this desktop environment at web typecheck. Retry before completion; use focused gates to keep implementation moving.
+- Existing guidance systems are fragmented. The Assistant should unify behavior around a single global entry point while reusing existing data and not deleting working features.
diff --git a/docs/tutorial-assistant/phase1-audit.md b/docs/tutorial-assistant/phase1-audit.md
new file mode 100644
index 0000000..dd8770d
--- /dev/null
+++ b/docs/tutorial-assistant/phase1-audit.md
@@ -0,0 +1,91 @@
+# Tutorial Assistant V1 Phase 1 Audit
+
+Date: 2026-05-05
+
+## Current FTUE
+
+- Save Hub (`/`) creates or resumes dynasties.
+- New saves route to `/onboarding`.
+- Onboarding uses `RevisedOnboardingPage`, deterministic assistant GM / Day One systems, and guided-start nudges.
+- Quick Start saves are registered for localStorage nudges.
+- The global tour can auto-start on `/dashboard` once the welcome briefing is dismissed.
+- Dashboard has `GameAdvisor`, but it is dashboard-only and disappears when no recommendation is generated.
+
+## Core Game Loop
+
+1. Create or resume a save.
+2. Complete Quick Start or Full Day One.
+3. Review the dashboard and immediate advisor/season-flow cards.
+4. Make roster, lineup, scouting, finance, trade, and development decisions.
+5. Sim days/weeks/months through the season.
+6. Respond to monthly pulse, press conferences, roster compliance, injuries, trades, and prospect movement.
+7. Enter playoffs or offseason.
+8. Run arbitration/extensions, free agency, draft, Rule 5, staff/scouting, and season prep.
+9. Start the next season and let records, rivalries, achievements, story arcs, and career history accumulate.
+
+## Highest-Friction "What Now?" Moments
+
+| Moment | Why It Hurts | Assistant Intervention |
+| --- | --- | --- |
+| First dashboard after onboarding | Too many cards and sim controls compete for attention | Global "What should I do now?" opens with one next action, why it matters, and a route link |
+| Roster compliance | Blocking issue, but baseball roster rules are dense | Explain the violation, which OVR/position/service-time columns matter, and what action to try first |
+| Early scouting/draft prep | Users may ignore scouting until draft day | Route-aware hint: scout before you draft; OVR is current value, ceiling is upside, confidence is report quality |
+| Trade center | Users may not know how to value packages | Explain OVR, age, contract, control, position scarcity, and GM personality |
+| Free agency/offseason | Many tasks unlock at once | Checklist-style guidance: arbitration/extensions, FA, draft prep, roster holes, budget |
+| Minors/development | Promotion timing is opaque | Explain OVR vs age/level/readiness and why rushing prospects can hurt |
+| Finance | Payroll and tax pressure are not inherently fun | Explain how payroll affects owner patience, deadline flexibility, and offseason plans |
+| Player profile | Deep tabs can overwhelm users | Explain which tabs answer current ability, future, stats, scouting, story, and history |
+| Mobile navigation | Many routes are hidden under More | Assistant should link directly to the suggested route and avoid requiring nav discovery |
+
+## Page-Level Guidance Plan
+
+| Page | Decision Moment | Assistant Intervention |
+| --- | --- | --- |
+| Setup / Save Hub | Which mode/save/team to start | Explain Quick Start vs Full Day One and what makes a good first dynasty |
+| Onboarding | Day One choices and AGM | Explain each chapter's stakes and reassure users that choices shape style, not hidden failure |
+| Dashboard | Next best action | Summarize season state, point to one route, and explain what to check before simming |
+| Roster | Promote/demote/lineup/depth | Explain OVR, grade, position, service, options, and compliance |
+| Minors | Promotion/development | Explain readiness, ceiling/floor, age/level fit, and prospect story |
+| Players | Search/comparison | Explain OVR vs stats vs fit and where to go for deeper profile |
+| Player Compare | Choose between players | Explain side-by-side attributes, OVR, age, contract, and role fit |
+| Player Profile | Understand one player | Explain header ratings and relevant tabs |
+| Scouting | Assign attention | Explain report confidence, scout disagreement, and current vs potential value |
+| Staff | Coach hires | Explain teaching, impact, fit, and development consequences |
+| Draft | Pick prospects | Explain current OVR, ceiling, signability, confidence, and team need |
+| Trade | Build packages | Explain value ingredients and why AI GMs respond differently |
+| Standings | Playoff context | Explain GB, run differential, streaks, and when to buy/sell |
+| Leaders | Star context | Explain leaderboards as scouting and awards context |
+| Schedule | Advance/inspect games | Explain when to sim and when to inspect box scores |
+| Box Score | Learn from result | Explain key lines and play-by-play lessons |
+| Press Room | Read story/state | Explain what news changes decisions versus flavor |
+| Playoffs | Postseason choices | Explain momentum, series state, and sim flow |
+| Free Agency | Sign players | Explain OVR, age, asking price, market pressure, and payroll |
+| Offseason | Sequence tasks | Explain required ordering and blocking tasks |
+| Finance | Budget/payroll | Explain payroll room, tax, commitments, and owner pressure |
+| GM Career | Long-term career | Explain score, jobs, and owner outcomes |
+| History | Dynasty memory | Explain season archives and story so far |
+| Achievements | Optional goals | Explain progress and why it can guide a save |
+| Rivalries | Relationship context | Explain trade/playoff intensity and consequences |
+| Front Office | Owner intel | Explain patience, chemistry, reputation, and risk |
+| Pulse | Monthly decisions | Explain decision queue and urgency |
+| Scenarios | Challenge mode | Explain constraints and first steps |
+| Stats | Stat reference | Explain how to use stat definitions during decisions |
+| Records | Legacy chase | Explain active record chases and story stakes |
+| Settings | Controls/accessibility | Explain tour replay, density, audio, saves, diagnostics |
+
+## Mobile Pain Points To Guard
+
+- Assistant must not cover bottom sim controls or the mobile nav.
+- Assistant expanded view should be a bottom drawer with bounded height.
+- Controls need 44px touch targets.
+- Text should remain scannable at 360px width.
+- Dismiss/replay controls must be reachable without horizontal scroll.
+- Reduced motion must turn animation into opacity/position-free transitions.
+
+## Recommended Phase 3 Scope
+
+- Add a global Assistant entry point in `AppLayout`.
+- Data-drive guidance for every route in one typed table.
+- Persist progress/cooldowns in localStorage keyed by active save id/slot.
+- Preserve the existing tour, page help, guided-start nudges, and GameAdvisor in this slice; integrate rather than remove.
+- Use the Assistant as the new unifying surface for "what should I do now?", route help, ratings explanation, replay, and story callbacks.
diff --git a/docs/tutorial-assistant/playtest-plan.md b/docs/tutorial-assistant/playtest-plan.md
new file mode 100644
index 0000000..84fcd1c
--- /dev/null
+++ b/docs/tutorial-assistant/playtest-plan.md
@@ -0,0 +1,56 @@
+# Tutorial Assistant V1 Playtest Plan
+
+Date: 2026-05-05
+
+## First-Session Script
+
+1. Open the public/local app at the Save Hub.
+2. Start a new dynasty with Quick Start.
+3. Confirm the Assistant chip is visible on Setup and Onboarding.
+4. Open the Assistant on Setup and read the first-save guidance.
+5. Complete onboarding and land on Dashboard.
+6. Open "What now?" and follow its suggested route.
+7. Visit Roster, Players, Trade, Draft, Free Agency, Finance, Minors, and Player Profile.
+8. On each page, confirm the Assistant explains:
+ - what the page is for
+ - what decision the user can make
+ - what OVR/ratings/stat context matters
+ - one next action
+9. Toggle Explain ratings and Deeper strategy.
+10. Toggle Hardcore mode, then return to Newcomer mode.
+11. Dismiss route help with Got it, navigate away/back, and use Replay.
+12. Sim enough to see ticker/story context and confirm the Assistant can show/dismiss a story callback.
+
+## Mobile Script
+
+Viewport target: 360x640 or 375x667.
+
+1. Repeat Setup, Onboarding, Dashboard, Roster, Trade, and Draft checks.
+2. Confirm Assistant compact chip does not hide the bottom nav or sim controls.
+3. Open the Assistant and confirm the panel scrolls internally.
+4. Confirm all Assistant buttons are touch-sized and text wraps.
+5. Confirm Escape closes the expanded panel on desktop/tablet.
+6. Confirm reduced-motion users are not dependent on animation.
+
+## Feedback Capture
+
+Ask testers to answer:
+
+- Did you always know the next thing to try?
+- Which page was still confusing?
+- Did OVR/ratings make more sense after using Explain ratings?
+- Did the Assistant feel helpful or intrusive?
+- Did Hardcore mode remove enough basics?
+- Did anything cover controls on mobile?
+- Which Assistant line felt too long, too vague, or too repetitive?
+
+## Low-Risk Share/Social Suggestion
+
+If a follow-up slice is available, add a copyable "Dynasty status card" from History or Dashboard:
+
+- team, season, record, phase
+- best player / top prospect
+- current goal
+- one story line from the Assistant
+
+Keep it local-only and text-based first.
diff --git a/docs/tutorial-assistant/ratings-visibility-audit.md b/docs/tutorial-assistant/ratings-visibility-audit.md
new file mode 100644
index 0000000..162db30
--- /dev/null
+++ b/docs/tutorial-assistant/ratings-visibility-audit.md
@@ -0,0 +1,40 @@
+# Tutorial Assistant V1 Ratings Visibility Audit
+
+Date: 2026-05-05
+
+## Rating Model
+
+- Engine values are internal 0-550 ratings.
+- UI values generally expose display ratings around the familiar baseball 20-80 scale through `displayRating`.
+- Contracts still store `overallRating`; UI DTOs frequently provide both `overallRating` and `displayRating`.
+
+## Current Strengths
+
+- Roster position-player and pitcher tables show OVR and letter grade.
+- Players directory shows OVR and grade.
+- Player profile header shows current OVR, grade bars, and deeper tabs.
+- Free agency table and selected-player panel show OVR.
+- Trade tables and selected-player rows show OVR.
+- Draft and scouting surfaces show current/potential report values.
+- Minors pipeline shows OVR for prospects.
+- Setup preview shows selected club player OVR.
+
+## Priority Gaps
+
+| Priority | Surface | Gap | Recommendation |
+| --- | --- | --- | --- |
+| P1 | Global understanding | OVR appears often, but there is no consistent explanation of what it means | Add Assistant "Explain ratings" panel with OVR, grade, ceiling, floor, confidence, and when stats/fit beat OVR |
+| P1 | Dashboard | Users make sim/strategy decisions without seeing rating context | Assistant should explain which roster/health/trade/farm cards imply OVR checks |
+| P1 | Draft | Prospect current value vs ceiling/signability needs explicit framing | Assistant route guidance should explain OVR/current ability, ceiling/upside, confidence/risk |
+| P1 | Trade | Users need value model, not just OVR column | Assistant should explain OVR + age + contract + control + position scarcity |
+| P2 | Minors | Promotion decisions need level/age context | Assistant should explain readiness and why raw OVR is not the only promotion signal |
+| P2 | Finance | Contract decisions need player-value tie-in | Assistant should explain that OVR helps estimate talent, but payroll efficiency and window matter |
+| P2 | Staff/scouting | Staff grades and scout confidence are rating-like but not always connected to player outcomes | Assistant should explain teaching, impact, fit, confidence, and bias |
+
+## Implementation Direction
+
+- Do not add clutter to every table just to satisfy visibility; many OVR columns already exist.
+- Add a reusable Assistant ratings explainer first.
+- Add route-specific `ratingsFocus` copy to guidance data for decision pages.
+- Add visible OVR/rating badges only where an audited decision surface truly lacks them.
+- Keep mobile density tight: show one primary rating at decision point and deeper detail in Assistant.
diff --git a/docs/tutorial-assistant/release-gate.md b/docs/tutorial-assistant/release-gate.md
new file mode 100644
index 0000000..4f8832d
--- /dev/null
+++ b/docs/tutorial-assistant/release-gate.md
@@ -0,0 +1,56 @@
+# Tutorial Assistant V1 Release Gate
+
+Date: 2026-05-05
+
+## Acceptance Checklist
+
+- [x] Goal contract exists and was re-read during implementation.
+- [x] Progress file exists and is updated.
+- [x] Phase 0 preflight exists.
+- [x] Phase 1 audit exists.
+- [x] Ratings visibility audit exists.
+- [x] Coverage matrix accounts for all current routes.
+- [x] Character, UX, trigger, and asset specs exist.
+- [x] Global Assistant appears on Setup and Onboarding.
+- [x] Global Assistant appears inside the initialized app shell.
+- [x] Assistant is route-aware across every current major route.
+- [x] Assistant offers "What now?" next action guidance.
+- [x] Assistant has Newcomer and Hardcore modes.
+- [x] Assistant can explain ratings/OVR for decision-critical pages.
+- [x] Assistant can show deeper strategy.
+- [x] Assistant guidance is dismissible and replayable.
+- [x] Assistant progress persists locally by save id/slot without changing `GameSnapshot`.
+- [x] Assistant story callbacks use deterministic current-state/ticker context and cooldown keys.
+- [x] Assistant ships with production-safe CSS/avatar placeholder, no external image paths.
+- [x] Mobile layout uses fixed chip/drawer behavior above bottom controls.
+- [x] Mobile browser check verified Setup and Dashboard Assistant behavior at 390x844.
+- [x] Focus/Escape behavior exists for the expanded panel.
+- [x] Save schema remains v33.
+
+## Verification
+
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/features/assistant/lib/assistantState.test.ts src/features/assistant/data/assistantGuidance.test.ts src/features/assistant/components/AssistantPanel.test.tsx`: passed.
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/app/routes/index.test.tsx`: passed.
+- `npx --yes pnpm@9.15.4 --filter @mbd/web test -- src/app/layout/AppLayout.test.tsx -t "renders sim-to-playoffs"`: passed after stale Vitest processes were cleared.
+- Focused combined Assistant/AppRoutes/AppLayout test command: passed, 12 tests passed and 9 skipped by the `-t` filter.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 --filter @mbd/web typecheck`: passed.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 --filter @mbd/web build`: passed.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 --filter @mbd/contracts test`: passed, 18 tests.
+- `PATH=/Users/tkevinbigham/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH npx --yes pnpm@9.15.4 run verify:determinism`: passed, 3 tests.
+- Playwright mobile browser check at 390x844 on a static preview of `apps/web/dist`: passed. Verified Setup Assistant chip/panel, ratings explanation, quick-start save creation, Dashboard Assistant chip/panel after skipping the existing legacy tutorial modal, and route-aware Dashboard guidance.
+
+## Remaining Blockers
+
+- **P0 — Repo health.** `.git/objects/pack/pack-0e3dc6...pack` is corrupt: `file` reports it as empty and `git verify-pack` reports `early EOF, pack is bad`. All in-pack history (~5,901 objects) is unreadable from this clone. Symptoms: `git status`, `git diff`, `git fsck` hang; `git rev-parse main HEAD` returns the same SHA so the branch is currently identical to `main`. **Fix before any commit / merge / PR**: re-clone fresh from `git@github.com:KevinBigham/MBD.git`, then copy the working-tree files for the Assistant feature and docs over the fresh checkout. Do NOT run destructive git commands (`git gc`, `git repack`, `rm -rf .git/objects/pack/`) without first preserving the working tree.
+- **P0 — Work is uncommitted.** Every file listed under "Changed Files" in `docs/goals/MBD_TUTORIAL_ASSISTANT_V1_PROGRESS.md` is untracked relative to `main`. Once the repo is healthy, stage with explicit paths (`git add apps/web/src/features/assistant docs/goals/MBD_TUTORIAL_ASSISTANT_V1_*.md docs/tutorial-assistant apps/web/src/app/layout/AppLayout.tsx apps/web/src/app/layout/AppLayout.test.tsx apps/web/src/app/routes/index.tsx apps/web/src/app/routes/index.test.tsx`) — never `git add -A`.
+- Manual closed-tester playtest is still recommended before public launch handoff.
+- The Assistant uses a CSS/icon avatar placeholder; generated bitmap portraits are deferred.
+- The Assistant explains OVR/ratings through guidance instead of adding new rating columns to already-dense tables. Future slices can add visual rating badges where playtest shows hunting/confusion.
+- Root typecheck/build under the default Node path hung before using the bundled Codex Node runtime. Use the bundled Node command form above on this machine.
+- During independent review on 2026-05-05, vitest and `vite build` hung when invoked from this shell with both bundled and Homebrew Node, while `tsc --noEmit` exited cleanly. The previously generated `apps/web/dist/` already contains the Assistant bundle, so the build is known to pass — but a clean re-run on a healthy repo is part of the merge gate.
+
+## Launch-Readiness Notes
+
+- V1 is shippable as a guidance layer: it is global, route-aware, persistent, dismissible, replayable, mobile-shaped, and save-safe.
+- It deliberately keeps existing TourProvider, PageHelp, ContextualHelp, GameAdvisor, and guided-start nudges intact.
+- No sim logic, save schema, RNG, or migration code was changed.
diff --git a/docs/tutorial-assistant/trigger-spec.md b/docs/tutorial-assistant/trigger-spec.md
new file mode 100644
index 0000000..23adf89
--- /dev/null
+++ b/docs/tutorial-assistant/trigger-spec.md
@@ -0,0 +1,41 @@
+# Tutorial Assistant V1 Trigger Spec
+
+## Trigger Types
+
+| Trigger | Behavior |
+| --- | --- |
+| Manual open | Always available from the global Assistant chip |
+| Route change | Update current page guidance silently |
+| First route visit | Show compact "new guidance available" state, not a blocking popup |
+| Dismiss | Mark current route guidance seen for this save |
+| Replay | Clear current route dismissal and open guidance |
+| Mode switch | Persist newcomer/hardcore mode |
+| Story callback | Show one save-aware line when relevant and not on cooldown |
+
+## Initial Story Callback Sources
+
+- Current phase and day from `useGameStore`.
+- Active save id/slot from `useGameStore`.
+- Ticker feed from `AppLayout`.
+- Route context from `useLocation`.
+
+## Cooldown Rules
+
+- Do not repeat a story callback once dismissed for the same save.
+- Prefer exact ticker ids when available.
+- Fall back to route/phase callback ids.
+- Do not generate random callback lines.
+
+## Priority Order
+
+1. Blocking/urgent next action from phase/route context.
+2. Route-specific page guidance.
+3. Ratings explanation when the page is decision-heavy.
+4. Save story callback.
+5. Deeper strategy.
+
+## Non-Triggers
+
+- Do not auto-open over Monthly Pulse, Moment Card, press conference, command palette, or save recovery dialogs.
+- Do not auto-open repeatedly on every route change.
+- Do not interrupt simulation.