> For the complete documentation index, see [llms.txt](https://faction-os.gitbook.io/faction-os-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://faction-os.gitbook.io/faction-os-docs/docs/progress.md).

# FactionOS — Build Progress

> **Continuation doc.** The AI agent building this app gets interrupted often. Read this first on every restart and keep it updated.

***

## What FactionOS is

FactionOS is a gamified observability layer for AI coding CLIs (Claude Code, Cursor, OpenCode, OpenClaw): hook events from those CLIs are POSTed to a local Express + WebSocket server, broadcast to a Vite + React + Three.js frontend that renders each agent as a fantasy hero (orc / human / elf / undead / lobster) doing missions on a 3D battlefield.

## Commits on `main`

| SHA        | Scope                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `_p86_`    | docs: backfill Phase 84 SHA placeholder (`_p85_` → `d98dcc8b`). Keeps PROGRESS.md in a clean resumable state so the next session reads the Commits-on-main table with every row resolved. Closes the Phase 81 → 82 → 83 → 84 Mission Complexity feature train docs hygiene. No code or test changes — pure docs sync. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `d98dcc8b` | Phase 84 — Mission Complexity command palette + keyboard shortcut. Closes the Phase 81 → 82 → 83 feature train: adds the `open:complexity` palette catalog entry under Overlays AND a `g x` keyboard sequence so power users can reach the Phase 83 overlay without leaving the keyboard. Mirrors the Phase 80 deep-link discipline. `apps/web/src/lib/commandPalette.ts` widens `CommandHandlers` to carry `setComplexityOverlayOpen: (open: boolean) => void` and registers the `open:complexity` entry (label "Open fleet mission complexity", group "Overlays", kind "action") with a keyword set spanning both the tier vocabulary (`quick`, `routine`, `heavy`, `epic`, `weight class`, `tier`, `tiers`, `intensity`, `difficulty`, `hard`, `easy`, `mountain`, `load`, `size`, `scale`) AND the user-facing question (`complexity`, `weight`, `how hard`, `browse`) so the ranker surfaces the entry under either intent; `run()` calls `handlers.setComplexityOverlayOpen(true)`. `apps/web/src/components/CommandPalette.tsx` reads `setComplexityOverlayOpen` from `useSettingsStore` and threads it through both the `buildCommands` handlers object and the `useMemo` deps array. `apps/web/src/lib/useKeyboardShortcuts.ts` extends the routing rules: `SHORTCUTS_HELP` gains a new `{ combo: "g x", description: "Open the fleet mission complexity overlay.", category: "Overlays" }` entry placed directly after `g t` so the sequence family (`g r` / `g l` / `g t` / `g x`) clusters in the help overlay; `ShortcutAction` union widens to include `"complexity_overlay_open"`; `routeKey` handles the `g`-prefix → `x` resolution (returns `{action: "complexity_overlay_open", nextPrefix: null, preventDefault: true}`, mirroring the `g r` / `g l` / `g t` rules); the hook reaches into the store for `setComplexityOverlayOpen` and the dispatch switch gains a `case "complexity_overlay_open"` that calls `setComplexityOverlayOpen(true)` (open-only, not toggle — same rule as `g r` / `g l` / `g t` since `g <letter>` is a navigation family). `apps/web/tests/commandPaletteRanking.test.ts`'s `noopHandlers` factory extended with `setComplexityOverlayOpen: vi.fn()` so the existing 200+ catalog test cases keep type-checking against the augmented `CommandHandlers` interface. Tests at `apps/web/tests/complexityOverlayDeepLink.test.tsx` (\~30 cases across 4 describe blocks): catalog entry (id / label / group / kind shape, keywords span both vocab axes — tier nouns AND question vocabulary, `run()` invokes `setComplexityOverlayOpen(true)`, doesn't shadow Phase 60 / 70 / 80 paths, Phase 80 `open:tool-usage` still routes through `setToolUsageOpen`); ranker surfacing (`"complexity"` / `"tier"` / `"epic"` / `"how hard"` / `"weight"` queries all surface the entry in the top-5); `g x` routing (resolves to `"complexity_overlay_open"`, shift-X does NOT resolve, lone `x` doesn't arm a prefix, regression guards on `g r` → `trophy_open` + `g l` → `leaderboard_open` + `g t` → `tool_usage_open`, unknown follow-up clears prefix, typing-target gate, every modifier key blocks the chord, `ShortcutAction` compile-time membership); SHORTCUTS\_HELP membership (`g x` entry present with right description + Overlays category, clustered directly after `g t`, no lone `x` binding, `g r` / `g l` / `g t` family still present). Total test surface: \~1540 → \~1570. Backfills the Phase 83 `_p84_` placeholder to `f0d6eddd` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 46 / 60 / 70 / 76 / 80 command-palette + shortcut patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `f0d6eddd` | Phase 83 — Fleet Mission Complexity overlay modal. Wires the Phase 81 `aggregateMissionComplexity` rollup into a standalone center-screen modal at `apps/web/src/components/MissionComplexityOverlay.tsx` (\~370 LOC), filling the gap between Phase 82's HeroDetailDrawer ComplexitySection (single-hero drill-down) and the future Phase 84 command palette deep-link (no UI of its own): this is the dedicated browse-the-tiers surface where the user picks an axis (count / share / tokens / duration / tools / averageScore / failed) and inspects every tier's footprint across the fleet at once. Store extension: `apps/web/src/store/useSettingsStore.ts` gains `complexityOverlayOpen: boolean` (transient, not persisted) + `setComplexityOverlayOpen(open: boolean)` setter following the Phase 48 / 52 / 60 / 79 pattern. Trigger button: `TitleBar.tsx` gains a ⛰ Complexity button after the Tools button (aria-label "Open fleet mission complexity", data-testid `complexity-overlay-trigger`). Mount point: `App.tsx` imports `MissionComplexityOverlay` and mounts it after `<ToolUsageOverlay />`. Modal layout: (1) header with `complexity-overlay-title` heading + summary line ("Fleet skews heavy — 8 heavy, 5 routine, 3 quick, 1 epic · 17 missions") + close button; (2) 7-pill metric row (`role="group"`, aria-pressed, `title="Higher is better"` / `"Lower is better"` per pill via the Phase 81 `complexityMetricLowerIsBetter`); (3) main `<table>` with semantic `<thead>` (Tier / metric label / Share / Factions columns) + `<tbody>` rows showing tier name + accent dot + tier label coloured in the accent + Dominant badge if applicable + sub-line "N missions · M tools · K tokens · score X" + headline value cell + share bar (inverts to min/value on lower-is-better metrics so the leanest tier shows the longest bar, mirrors Phase 74's `factionEfficiencyShare` and Phase 79's `toolUsageShare`) + 5-segment faction-share strip (one segment per `FACTION_IDS` slot with the faction's glyph + per-faction proportional bar); (4) empty-state sentinel ("No missions yet") when `totalMissions === 0`; (5) footer with Σ missions / Σ tokens / avg score / failed (count + percent) + direction note matching the active metric. Modal contract mirrors every other overlay: `role="dialog"` + `aria-modal="true"` + `aria-labelledby="complexity-overlay-title"` + backdrop closes + Escape closes + inner-card stopPropagation + close button. Lifecycle: opens via `setComplexityOverlayOpen(true)`, resets metric to "count" on each open (mirrors Phase 60 / 68 / 79 lifecycle so deep-link transitions land on a known default). `data-metric` + `data-direction` + `data-dominant-tier` attributes on the modal root surface the active state for tests + assistive tech. Bar share inversion lives in the JSX via `lowerIsBetter ? complexityShare(value, metricMin, true) : complexityShare(value, metricMax, false)` — single dispatch point that can never drift from the helper's direction flag. Tests at `apps/web/tests/MissionComplexityOverlay.test.tsx` (\~33 cases across 8 describe blocks): lifecycle (`complexityOverlayOpen` toggle, close button, Escape, backdrop, inner card no-close, metric reset on each open via close → re-open dance); accessibility + header (role + aria-modal + aria-labelledby + title id matching, summary line surfaces tier counts and totals); metric pills (7 pills present, `count` default, title direction per pill, click flips aria-pressed, `data-metric` mirrors active, `data-direction` flips on `failed`); rows (all four tiers always rendered even when zero, `data-tier` + `data-count` + `data-dominant` per row, Dominant badge only on dominant row, all five faction segments per row, `data-count` on faction segment matches rollup, share bar `data-share` ∈ (0, 1], leader row reads 1.000 on count); ranking (count DESC default puts heaviest-count tier first, failed pill flips to ASC so least-failed sorts first); empty fleet (empty state sentinel + "No missions yet" summary + `data-dominant-tier="none"`); footer (Σ missions / Σ tokens / avg score / failed counts, direction note swaps with metric, failed share percent when missions exist); modal data-dominant-tier reflects helper's dominantTier across mixed fleets (`epic` wins on a quick + epic fleet). Total test surface: \~1507 → \~1540. Backfills the Phase 82 `_p83_` placeholder to `7932e5ef` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 52 / 60 / 68 / 75 / 79 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `7932e5ef` | Phase 82 — HeroDetailDrawer ComplexitySection. Wires the Phase 81 `aggregateMissionComplexityForHero` helper into `apps/web/src/components/HeroDetailDrawer.tsx` as a new sub-section mounted between the Phase 78 Tools section and the Phase 37 MissionTimeline. Reads the hero's single-hero complexity report through a `useMemo` keyed on `[hero, heroMissionsAll]` so a fleet-wide event tick doesn't re-rollup; conditionally rendered when `report.totalMissions > 0` (matches the data-honesty rule Tools / Specialization / Efficiency use: heroes with no mission history get nothing rather than a misleading "zero missions, quick tier" readout). New `<ComplexitySection />` component (\~115 LOC) renders: heading ("Complexity"); a dominant-tier chip (testid `hero-complexity-tier`) with the tier accent (emerald for quick, sky for routine, amber for heavy, rose for epic), the tier label, and an `× N` counter showing the count of missions reaching that tier; a one-line caption (testid `hero-complexity-caption`) listing tier counts DESC joined with midline separators ("3 quick · 1 heavy"), collapsing to a single entry ("2 quick") when only one tier has missions; a 4-cell stats grid (testid `hero-complexity-stats`) showing total missions, the 2-decimal average score, the heaviest tier label (em-dash when no missions), and the failed mission count. Hero-scoped — sibling heroes' missions never leak in because `aggregateMissionComplexityForHero` is single-hero by construction. Dismissed heroes opened in the drawer still get the section (matches the existing Tools / Specialization rule — the drawer is showing that hero on purpose). Section semantics: `aria-labelledby="hero-complexity-heading"` paired with the heading id; `data-dominant-tier` + `data-total-missions` attributes on the section root surface the count for tests + assistive tech. Drawer imports widened to pull `aggregateMissionComplexityForHero` + `complexityTierColor` + `complexityTierLabel` + `formatComplexityValue` + the `ComplexityTier` / `MissionComplexityReport` types from `apps/web/src/lib/missionComplexity`. Tests at `apps/web/tests/HeroDetailDrawerComplexity.test.tsx` (\~28 cases across 7 describe blocks): conditional render (no-mission hero gets nothing, at-least-one-mission renders, in-flight-only renders too, dismissed hero in drawer mode still renders); dominant-tier chip (quick / routine / heavy / epic dispatch via worked-test-case missions, tier accent matches `COMPLEXITY_TIER_COLOR`, aria-label format with plural/singular handling, × N counter inside the chip); caption (tier counts DESC with midline join, single-tier collapse to bare entry, every non-zero tier included); stats grid (4 cells with right labels, missions count reflects totalMissions, heaviest label surfaces dominantTier label, failed reflects failed+cancelled state count, avg score formatted to 2 decimals); cross-hero isolation (other heroes' missions don't leak in via data-total-missions assertion, zero-failed reported as 0); DOM ordering (Complexity sits AFTER Tools in document order via `compareDocumentPosition`, heading 'Complexity' rendered as `<h3>`, aria-labelledby + heading id paired); report state surfacing (data-dominant-tier reflects helper's dominantTier, data-total-missions reflects helper's count, in-flight missions contribute to total). Total test surface: \~1479 → \~1507. Backfills the Phase 81 `_p82_` placeholder to `cd5fa4dc` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 65 / 66 / 72 / 78 component patterns and the Phase 81 helper's classification math.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `cd5fa4dc` | Phase 81 — Mission Complexity pure helper. Opens a new feature train alongside Phases 71-76 (efficiency) and 77-80 (tool usage): the first surface that exposes the *per-mission weight class* axis (quick / routine / heavy / epic) rather than the existing volume / discipline / specialization / tool-usage axes. `apps/web/src/lib/missionComplexity.ts` (\~870 LOC) ships `classifyMissionComplexity(mission, heroes, opts?)` for the single-mission entry plus an `aggregateMissionComplexity({heroes, missions, options?})` fleet rollup, `aggregateMissionComplexityForHero({hero, missions, options?})` for the hero-scoped variant, `complexityByFaction({heroes, missions, options?})` always-five-factions per-faction grid (matches Phase 52 / 74 / 77 convention), `complexityByHero({heroes, missions, options?})` sorted DESC by `totalMissions` with name ASC tie-break, and `dominantTierForHero(heroId, missions, opts?)` single-chip convenience. Companion exports: `rankComplexity(rows, metric)` with the seven `COMPLEXITY_METRICS` (`count` / `share` / `tokens` / `duration` / `tools` / `averageScore` / `failed`), `complexityCaption(report, limit?)` for the "8 heavy · 14 routine · 3 epic" summary line, `formatComplexityValue(value, metric)` + `formatComplexityDuration(durationMs)` + `formatIntegerCount(value)` formatter ladder, `complexityShare(value, ref, lowerIsBetter?)` bar-share math with Phase 74-style inversion for lower-is-better axes, plus accent helpers `complexityTierColor(tier)` + `complexityTierLabel(tier)` + `complexityMetricLabel(metric)` + `complexityMetricLowerIsBetter(metric)`. Tier dispatch on a composite `score ∈ [0, 1]`: `score < 0.20` → quick, `< 0.45` → routine, `< 0.75` → heavy, otherwise epic. Score is a weighted sum of three sub-axes (0.4 tool / 0.4 token / 0.2 duration, every axis normalised through a Hill saturating curve `(value / ceiling) / (1 + value / ceiling)` so 50%-of-ceiling hits 0.5, 3× ceiling hits 0.75, asymptotic to 1). Default ceilings calibrated against the mock-event seed: 8 tools / 50K tokens / 10 minutes — picked so a 4-tool 13K-token 3-min mission lands squarely in routine (score ≈ 0.25), a 12-tool 90K-token 12-min mission lands in heavy (score ≈ 0.61), and a 30-tool 400K-token 60-min mission lands in epic (score ≈ 0.84). Defensive rules mirror Phase 71 / 74 / 77: dismissed heroes drop by default (override via `{includeDismissed: true}`), ghost missions still score but contribute `null` faction, in-flight missions get `durationAxis = 0` so they're never libeled by a partial measurement, negative tokens or non-finite values clamp to zero, malformed `toolUses` arrays drop silently, negative clock-skew durations clamp to zero (preserves the "suspect call" signal without disqualifying the mission), and the `resolveThresholds` helper sorts out-of-order tier boundaries so a malformed override can never mis-classify the whole fleet as quick. Custom `weights` / `thresholds` / `ceilings` overrides flow through `resolveWeights` / `resolveThresholds` / `resolveCeilings` which sanitise NaN / negative / zero-sum inputs back to defaults. Pure helper only — UI integration to HeroDetailDrawer deferred to Phase 82 under the established Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 / 74 → 75 / 77 → 78 split discipline. Tests at `apps/web/tests/missionComplexity.test.ts` (\~88 cases across 14 describe blocks): constants + metadata (four tiers in lightest → heaviest order, seven metrics in canonical UI order, every tier + metric has a non-empty label + fallthrough branch for bad inputs, only `failed` is lower-is-better, every tier maps to a 6-hex colour, default weights sum to 1, default thresholds monotonic, default ceilings positive); internal helpers via `__testing` (blankTierRow / makeByTier shape, resolveWeights normalises a custom split + falls back on zero-sum + drops NaN/negative, resolveThresholds sorts out-of-order, resolveCeilings replaces non-positive with defaults, clamp01 enforces bound, nonNegativeInt floors, parseTime ISO + falsy, computeDuration clamps clock skew, saturatingNormalize hits 0.5 at ceiling + 0.75 at 3× + 0.9 at 9× + asymptotic to 1 for huge inputs + tolerant of NaN/zero/negative, pickDominantTier returns heaviest non-empty tier or null); classifyMissionComplexity happy path (1-tool 800-token snack → quick, 4-tool 15K-token routine, 12-tool 90K-token heavy, 30-tool 400K-token epic, every axis ∈ \[0,1], faction resolution from heroes lookup, ghost mission → null faction); defensive guards (null mission, missing toolUses, whitespace tool names dropped, failed + pending tool counters, NaN/negative token clamp, in-flight durationAxis = 0, missionFailed flag for failed + cancelled states, custom thresholds override flows through); aggregateMissionComplexity (empty inputs → all four tiers present, non-array tolerance, full rollup of counts/tokens/durations/tools per tier, dismissed default + override, ghost missions classify but credit no faction, share sums to 1, share = 0 on empty, totalFailed + per-tier failed sum); aggregateMissionComplexityForHero (null hero, scope to hero's missions only, dismissed kept in single-hero mode, non-array tolerance); complexityByFaction (always-five-factions, scope per-faction, ghosts drop from every faction, includeDismissed flows through); complexityByHero (per-live-hero rows sorted DESC by missions with alpha tie-break, dismissed dropped by default, zero-mission heroes still listed); dominantTierForHero (unknown hero null, empty input null, heaviest tier reached); metric helpers + rankComplexity (every metric pulls the right field with default-branch fallthrough, DESC for count + ASC for failed, tie-break on count DESC + canonical tier ASC, non-array → empty); complexityShare (value/ref higher-is-better, inverts to ref/value for lower-is-better, \[0,1] clamp, non-positive reference → 0, NaN guards, zero-value lower-is-better → 1); formatters (formatComplexityDuration ms/s/m/h ladder + em-dash for null/NaN/negative, formatIntegerCount K/M shortener + em-dash for bad inputs, formatComplexityValue dispatches per metric); complexityCaption (no-missions-yet sentinel, tiers by count DESC, respects limit); realistic 4-faction scenario (orcs heavy + quick, humans routine, elves epic, undead Arthas dismissed + dropped, fleet.dominantTier = epic, rankComplexity sorted, custom weights skew the result with token-only landing the same inputs in quick instead of routine/heavy/epic). Total test surface: \~1391 → \~1479. Backfills the previous docs-hygiene placeholder `_p81_` to `a77c2e1a` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 71 / 74 / 77 helper shape and tier-dispatch math traced against worked test cases. |
| `a77c2e1a` | docs: backfill Phase 80 SHA placeholder (`_p80_` → `34b1822b`). Keeps PROGRESS.md in a clean resumable state so the next session reads the Commits-on-main table with every row resolved. Closes the Phase 77 → 78 → 79 → 80 tool-usage feature train docs hygiene. No code or test changes — pure docs sync. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `34b1822b` | Phase 80 — Tool Usage command palette + keyboard shortcut. Closes the Phase 77 → 78 → 79 feature train: adds the `open:tool-usage` palette catalog entry under Overlays AND a `g t` keyboard sequence so power users can reach the Phase 79 overlay without leaving the keyboard. Mirrors the Phase 70 / 76 deep-link discipline. `apps/web/src/lib/commandPalette.ts` widens `CommandHandlers` to carry `setToolUsageOpen: (open: boolean) => void` and registers the `open:tool-usage` entry (label "Open fleet tool usage", group "Overlays", kind "action") with a keyword set spanning both the tool noun (`bash`, `read`, `write`, `edit`, `grep`, `glob`, `websearch`, `webfetch`, `shell`, `filesystem`) AND the user-facing question (`tools`, `tool`, `usage`, `tooling`, `which`, `audit`, `browse`) so the ranker surfaces the entry under either intent; `run()` calls `handlers.setToolUsageOpen(true)`. `apps/web/src/components/CommandPalette.tsx` reads `setToolUsageOpen` from `useSettingsStore` and threads it through both the `buildCommands` handlers object and the `useMemo` deps array. `apps/web/src/lib/useKeyboardShortcuts.ts` extends the routing rules: `SHORTCUTS_HELP` gains a new `{ combo: "g t", description: "Open the fleet tool usage overlay.", category: "Overlays" }` entry placed directly after `g l` so the sequence family (`g r` / `g l` / `g t`) clusters in the help overlay; `ShortcutAction` union widens to include `"tool_usage_open"`; `routeKey` handles the `g`-prefix → `t` resolution (returns `{action: "tool_usage_open", nextPrefix: null, preventDefault: true}`, mirroring the `g r` / `g l` rules); the hook reaches into the store for `setToolUsageOpen` and the dispatch switch gains a `case "tool_usage_open"` that calls `setToolUsageOpen(true)` (open-only, not toggle — same rule as `g l` / `g r` since `g <letter>` is a navigation family). `apps/web/tests/commandPaletteRanking.test.ts`'s `noopHandlers` factory extended with `setToolUsageOpen: vi.fn()` so the existing 200+ catalog test cases keep type-checking against the augmented `CommandHandlers` interface. Tests at `apps/web/tests/toolUsageDeepLink.test.tsx` (\~21 cases across 3 describe blocks): catalog entry (id / label / group / kind shape, keywords span both vocab axes, `run()` invokes `setToolUsageOpen(true)`, doesn't shadow Phase 60 / 70 / 76 paths, regression guards on `open:leaderboard` + `open:faction-radar` + `open:faction-efficiency` ); ranker surfacing (`"tool usage"` / `"bash"` / `"which tools"` queries all surface the entry in the top-5); `g t` routing (resolves to `"tool_usage_open"`, shift-T does NOT resolve, lone `t` doesn't arm a prefix, regression guards on `g r` → `trophy_open` + `g l` → `leaderboard_open`, unknown follow-up clears prefix, typing-target gate, every modifier key blocks the chord); SHORTCUTS\_HELP membership (`g t` entry present with right description + Overlays category, clustered next to `g r` / `g l`, no lone `t` binding). Total test surface: \~1370 → \~1391. Backfills the Phase 79 `_p79_` placeholder to `81cc87f9` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 46 / 60 / 70 / 76 command-palette + shortcut patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `81cc87f9` | Phase 79 — Fleet Tool Usage overlay modal. Wires the Phase 77 `aggregateToolUsage` rollup into a standalone center-screen modal at `apps/web/src/components/ToolUsageOverlay.tsx` (\~420 LOC), filling the gap between Phase 78's HeroDetailDrawer Tools section (single-hero drill-down) and the future Phase 80 command palette deep-link (no UI of its own): this is the dedicated browse-the-tools surface where the user picks an axis (calls / successRate / avg dur / total dur / missions / heroes / failures) and inspects every tool's footprint at once. Store extension: `apps/web/src/store/useSettingsStore.ts` gains `toolUsageOpen: boolean` (transient, not persisted) + `setToolUsageOpen(open: boolean)` setter following the Phase 48 / 52 / 60 pattern. Trigger button: `TitleBar.tsx` gains a 🛠 Tools button between Leaderboard and AchievementsGallery (aria-label "Open fleet tool usage", data-testid `tool-usage-trigger`). Mount point: `App.tsx` imports `ToolUsageOverlay` and mounts it after `<Leaderboard />`. Modal layout: (1) header with `tool-usage-title` heading + summary line ("3 Bash · 2 Read · 25 calls across 3 missions") + close button; (2) 7-pill metric row (`role="group"`, aria-pressed, `title="Higher is better"` / `"Lower is better"` per pill via the Phase 77 `toolUsageMetricLowerIsBetter`); (3) "Show unused" toggle (default off — hide zero-call rows; on — surface the 14 KNOWN\_TOOL\_NAMES seeds for discoverability); (4) main `<table>` with semantic `<thead>` (Tool / metric label / Share / Factions columns) + `<tbody>` rows showing tool name + category accent dot + category badge + in-flight `× N live` pill (when `pendingCalls > 0`) + sub-line "N missions · M heroes · D avg · X% ok" + headline value cell + share bar (inverts to min/value on lower-is-better metrics so the leanest tool shows the longest bar, mirrors Phase 74's `factionEfficiencyShare`) + 5-segment faction-share strip (one segment per `FACTION_IDS` slot with the faction's glyph + per-faction proportional bar); (5) empty-state sentinel ("No tool calls yet") when no rows pass the filter; (6) footer with Σ calls / Σ tools / Σ heroes / overall success percentage + direction note matching the active metric. Modal contract mirrors every other overlay: `role="dialog"` + `aria-modal="true"` + `aria-labelledby="tool-usage-title"` + backdrop closes + Escape closes + inner-card stopPropagation + close button. Lifecycle: opens via `setToolUsageOpen(true)`, resets metric to "calls" + showZero to false on each open (mirrors Phase 60 / 68 lifecycle so deep-link transitions land on a known default). `data-metric` and `data-direction` attributes on the modal root surface the active state for tests + assistive tech. Bar share inversion lives in the JSX via `lowerIsBetter ? toolUsageShare(value, metricMin, true) : toolUsageShare(value, metricMax, false)` — single dispatch point that can never drift from the helper's direction flag. Tests at `apps/web/tests/ToolUsageOverlay.test.tsx` (\~35 cases across 9 describe blocks): lifecycle (`toolUsageOpen` toggle, close button, Escape, backdrop, inner card no-close, metric + show-zero reset on each open); accessibility + header (role + aria-modal + aria-labelledby + title id matching, summary line); metric pills (7 pills present, `calls` default, title direction per pill, click flips aria-pressed, `data-metric` mirrors active, `data-direction` flips on `failedCalls`); rows (Bash sorts first at 5 calls, category exposed for accent dispatch, value cell formatted, share bar share = 1.0 for leader + < 1 for non-leader, in-flight pending badge present when pendingCalls > 0, all five faction segments per row, data-count on faction segment); show-zero toggle (default hides zero rows, click surfaces them, aria-pressed flips); ranking by different metrics (clicking successRate puts 100% row first, clicking failedCalls handles all-zero case without crash); custom MCP tool (category=other, slate accent); footer (Σ counts present, overall success percentage, direction note swaps with metric); empty fleet (empty state sentinel + "No tool calls yet" summary). Total test surface: \~1335 → \~1370. Backfills the Phase 78 `_p78_` placeholder to `8c8ab29e` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 52 / 60 / 68 / 75 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `8c8ab29e` | Phase 78 — HeroDetailDrawer ToolUsageSection. Wires the Phase 77 `aggregateToolUsageForHero` helper into `apps/web/src/components/HeroDetailDrawer.tsx` as a new sub-section mounted between the Phase 65 Specialization section and the Phase 37 MissionTimeline. Reads the hero's single-hero tool report through a `useMemo` keyed on `[hero, heroMissionsAll]` so a fleet-wide event tick doesn't re-rollup; conditionally rendered when `report.totalCalls > 0` (matches the data-honesty rule Specialization and Efficiency use: heroes with no tool history get nothing rather than a misleading zero readout). New `<ToolUsageSection />` component (\~110 LOC) renders: heading ("Tools"); a top-tool chip (testid `hero-tool-top`) with the category-derived accent (cyan for `Read`, amber for `Edit`, rose for `Bash`, lime for `Grep`, violet for `WebSearch`/`WebFetch`, slate for planning + other), the tool name, and an `× N` counter showing the call count; a one-line caption (testid `hero-tools-caption`) listing the top 3 tools by call count joined with midline separators (`"12 Bash · 8 Read · 5 Grep"`); a 4-cell stats grid (testid `hero-tools-stats`) showing total calls, overall success rate (em-dash when nothing resolved), unique tool count, and the top tool's average duration via `formatDurationMs` (em-dash when no call has `endedAt`). Hero-scoped — sibling heroes' tool uses never leak in because `aggregateToolUsageForHero` is single-hero by construction. Custom MCP tools paint with the slate "other" accent so they're visually distinct from first-party tools. Section semantics: `aria-labelledby="hero-tools-heading"` paired with the heading id, `data-total-calls` attribute on the section root surfaces the count for tests + assistive tech. Drawer imports widened to pull `aggregateToolUsageForHero` + `formatDurationMs` (aliased as `formatToolDuration`) + `formatIntegerCount` (aliased as `formatToolCount`) + `toolColorFor` + `ToolUsageReport` + `ToolUsageRow` types from `apps/web/src/lib/toolUsage`. Tests at `apps/web/tests/HeroDetailDrawerToolUsage.test.tsx` (\~27 cases across 7 describe blocks): conditional render (no-tool-calls hero gets nothing, no-mission hero gets nothing, at-least-one-tool-call renders the section, in-flight missions with tool calls also render because `state` doesn't gate); top-tool chip (data-tool surfaces most-called tool, data-category dispatches accent, accent colour matches `TOOL_CATEGORY_COLOR[category]`, aria-label matches `Top tool <name>: <N> call(s)`, × counter inside the chip); caption (top-3 join with midline, capped at 3 entries even when more tools exist); stats grid (4 cells with dt/dd pairs, totalCalls in cell 1, success percentage in cell 2 when resolved + em-dash when not, unique-tool count in cell 3, top dur in cell 4 with em-dash + duration ladder); cross-hero isolation (Aragorn's drawer never shows Bilbo's tools even when Bilbo is louder); DOM ordering (Tools section sits AFTER Specialization + Efficiency in document order, aria-labelledby + section heading paired, data-total-calls reflects the report); custom MCP tools (category = "other", slate accent applied); edge accent colours (Read → filesystem-read cyan, Edit → filesystem-write amber, WebSearch → web violet). Total test surface: \~1308 → \~1335. Backfills the Phase 77 `_p77_` placeholder to `efde50f2` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 65 / 66 / 72 / 73 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `efde50f2` | Phase 77 — Tool Usage pure helper. Opens a new feature train alongside Phases 71-76's faction-efficiency train: the first surface that exposes the *tool* axis (which tools the heroes actually lean on) rather than the volume / token / discipline axes the existing helpers cover. `apps/web/src/lib/toolUsage.ts` (\~870 LOC) ships an `aggregateToolUsage({heroes, missions, includeDismissed?})` rollup that walks every `mission.toolUses` entry and produces per-tool rows with `calls` / `successCalls` / `failedCalls` / `pendingCalls` / `successRate` (null when nothing resolved) / `totalDurationMs` / `avgDurationMs` (null when no `endedAt` present) / `missionsUsing` (distinct mission ids) / `heroesUsing` (distinct hero ids) / `factionShare` (per-faction call count). Companion exports: `extractToolUses(missions, heroes, {includeDismissed?})` flattens with faction-attribution; `rankTools(rows, metric)` for the seven `TOOL_USAGE_METRICS` (`calls` / `successRate` / `avgDurationMs` / `totalDurationMs` / `missionsUsing` / `heroesUsing` / `failedCalls`); `toolUsageByFaction({heroes, missions})` returns one `FactionToolUsage` per `FACTION_IDS` slot (all five always present, matches Phase 52 / 74 convention); `toolUsageByHero({heroes, missions})` returns one `HeroToolUsage` per live hero sorted by `totalCalls` DESC then name ASC; `topToolForHero(heroId, missions)` is the single-hero convenience; `aggregateToolUsageForHero({hero, missions})` is the hero-scoped rollup (keeps dismissed heroes in single-hero mode); `summarizeToolUsageReport(report, limit?)` formats a `"12 Bash · 8 Read · 5 Grep"` summary line. Formatter ladder: `formatToolUsageValue(value, metric)` collapses null to em-dash, percents to `%`, durations through the ms/s/m/h ladder, counts through the K/M shortener; `formatDurationMs` / `formatIntegerCount` are exported for downstream UI reuse. Category accent system: `toolCategoryFor(tool)` maps every `KNOWN_TOOL_NAMES` entry to a seven-bucket category (filesystem-read / filesystem-write / shell / search / web / planning / other) and custom MCP tools fall through to `other`; `TOOL_CATEGORY_COLOR` provides the hex accent palette (cyan for read, amber for write, rose for shell, lime for search, violet for web, slate for planning + other) so Phase 78 / 79 UI surfaces share a single colour vocabulary. Defensive rules mirror Phase 71 / 74: dismissed heroes drop by default (override via `includeDismissed`), ghost missions (hero unresolvable) still count in `calls` but credit no faction, malformed `toolUses` arrays skip silently without crashing, negative durations from clock skew clamp to zero (preserves the suspect-call signal), tool names trim whitespace and drop empty strings, custom MCP tool names dedupe case-sensitively (so `mcp__github__SearchRepos` and `mcp__github__searchrepos` are distinct rows — user-controlled axis). `ordered` always seeds the 14 first-party `KNOWN_TOOL_NAMES` in canonical "read → write → run" flow order so a quiet session still surfaces the discoverable surface, then appends custom MCP tools by calls DESC + alpha tie-break. `rankTools` tie-break ladder: chosen metric (DESC; null sinks last regardless of axis direction) → `calls` (DESC) → KNOWN\_TOOL\_NAMES index ASC → name ASC. `toolUsageMetricLowerIsBetter(metric)` flags only `failedCalls` as lower-is-better in the current set; durations stay "higher = more work done" because the user opens the table to spot the dominant tools, not the snappy ones. `toolUsageShare(value, max, lowerIsBetter?)` mirrors Phase 74's bar-share math — inverts to `min / value` for lower-is-better axes so the leanest tool still shows the longest bar. Pure helper only — UI integration to HeroDetailDrawer deferred to Phase 78 under the established Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 / 74 → 75 split discipline. Tests at `apps/web/tests/toolUsage.test.ts` (\~81 cases across 17 describe blocks): constants + metadata (seven metrics in order, all labels non-empty, only `failedCalls` lower-is-better, every known tool maps to non-other category, custom tools bucket as other, every category has a colour, `toolColorFor` delegation); internal helpers via `__testing` (blankRow shape, parseTime ISO + falsy, computeDuration clamps negatives); extractToolUses (empty / malformed inputs, multi-mission flatten with faction attribution, whitespace trim, ghost-mission null faction, dismissed exclusion + `includeDismissed` override, startedAt/endedAt/duration triangulation); empty + edge aggregateToolUsage (seeded blank rows, null-input tolerance, distinct vs total counts, dismissed default + override, pending separation, null successRate when nothing resolved); duration accounting (avg averages only end-resolved calls, clock-skew clamp, null avg when no resolved); faction share (per-call credit by hero faction, ghost calls drop from factionShare but count in calls); custom MCP tools (case-sensitive dedupe); ordering (KNOWN first + customs by calls DESC); rank ladder (calls DESC, successRate ranks 100% above partial, null sinks last regardless of axis, tie-break respects KNOWN order); metricValueForRow (every metric pulls the correct field, unknown metric returns null); formatters (em-dash for null, percent rounding, duration ladder ms/s/m/h, integer K/M shortening); toolUsageShare (higher-is-better ratio, lower-is-better inversion `min/value`, degenerate inputs guard, `[0,1]` clamp); aggregateToolUsageForHero (scope-to-hero, null-hero tolerance, dismissed hero kept in single-hero mode); topToolForHero (most-called wins, unknown hero null, tie-break favours KNOWN order); toolUsageByFaction (all five always present, scope per faction, top-tool selection, zero-row filtering, includeDismissed override); toolUsageByHero (sort DESC by calls, alpha tie-break on name, dismissed default + override); summarizeToolUsageReport (lazy "no tool calls" empty marker, top-N midline-formatted, limit respected); realistic 4-faction scenario (orcs lean on Bash, humans on Read+Edit with one pending Bash, elves heavy on Grep, undead Arthas dismissed + dropped, ghost WebSearch credits no faction). Total test surface: \~1227 → \~1308. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 71 / 74 helper shape.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `1c393588` | Phase 76 — Faction efficiency command palette + deep-link. Mirrors the Phase 70 radar deep-link pattern for Phase 75's new Efficiency view. `Cmd+K` → "efficiency" / "lean" / "tier" / "score" / "discipline" / "per-mission" / "tokens" / "throughput" now opens the FactionStandings modal AND deep-links directly into the new Phase 75 Efficiency view — closes the discoverability gap where palette users had to first open the modal then click the Efficiency button. Mechanics: `apps/web/src/store/useSettingsStore.ts` widens `StandingsView` from `"table" \| "radar"` to `"table" \| "radar" \| "efficiency"` so the `standingsInitialView` hint can now accept the new value; `apps/web/src/lib/commandPalette.ts` adds the new `open:faction-efficiency` entry under Overlays (label "Open faction efficiency", group "Overlays", keywords cover the efficiency vocabulary AND the user-facing question — "lean", "tier", "discipline", "per-mission"); the entry's `run()` calls `setStandingsInitialView("efficiency")` THEN `setStandingsOpen(true)` so the FactionStandings open-transition effect picks up the hint on its first re-render. `apps/web/src/components/FactionStandings.tsx` simplifies — the Phase 75 local type widening (\`StandingsView                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `71276c1c` | docs: backfill Phase 75 SHA placeholder (`_p75_` → `2b464466`). Keeps PROGRESS.md in a clean resumable state so the next session reads the Commits-on-main table with every row resolved. No code or test changes. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `2b464466` | Phase 75 — FactionStandings Efficiency view. Wires the Phase 74 `buildFactionEfficiency` helper into `apps/web/src/components/FactionStandings.tsx` as a new third view alongside the Phase 52 Table and Phase 68 Radar. Adds a third "Efficiency" button to the existing view toggle so the user can flip between "who's done the most volume" (Table), "who's the most balanced" (Radar), and "which house is the most disciplined per mission" (Efficiency) without leaving the modal. When Efficiency view is active, the metric pill row swaps from the 6-pill Phase 52 family (`STANDINGS_METRICS`) to the 7-pill Phase 74 family (`FACTION_EFFICIENCY_METRICS`: avgScore / successRate / tokensPerMission / costPerMission / throughput / completedMissions / scoredHeroes); pill `title` attributes call out "Higher is better" or "Lower is better" so the user understands what each axis rewards. New `EfficiencyView` function (\~180 LOC) renders one row per faction with: rank pill (1-5), faction glyph + house name + `heroCount live · scoredHeroCount scored · completedMissions closed` summary line, headline metric value formatted via `formatFactionEfficiencyValue` + metric label, share bar (inverted for lower-is-better via `factionEfficiencyShare` so the LEANEST faction always shows the longest bar — preserves the "bigger = better" reading convention regardless of axis direction), tier-distribution chips (S/A/B/C/D with `TIER_COLOR` accent + count badge, one chip per non-zero bucket, with "No scored heroes yet" placeholder when the faction has no closed missions), top-profile chip (best-scoring live hero from the faction with their score, or "absent" / "no signal" for empty vs in-flight-only factions), and a footer summarising fleet avg score + scored / total hero count + total closed missions + a one-line direction reminder. Pill family swap is governed by `view === "efficiency"`, so the Phase 52 metric state and the new Phase 75 `efficiencyMetric` state don't collide. Both reset on modal re-open (Phase 52 → "missions", Phase 75 → "avgScore"). The view itself resets to "table" on re-open, matching the Phase 68 default (the Phase 70 deep-link hint is still consumed first, so palette deep-links continue to land on Radar; widening the hint to accept "efficiency" is deferred to a future phase). All five factions always appear (Phase 52 "empty rows make a point" convention). Empty-faction rows render with collapsed bar opacity + em-dash value + the "No scored heroes yet" tier placeholder + the "absent" empty marker. In-flight-only factions (undead with active missions but zero closed) show the bar at 0%, no tier chips, and the "no signal" marker instead of "absent" — the two readouts are honestly distinct. Lower-is-better metric rows decorate the container with `data-direction="lower-better"` so tests can assert direction without re-importing the helper. Tests at `apps/web/tests/FactionStandingsEfficiency.test.tsx` (\~37 cases) cover the new view toggle (3 pills now: Table / Radar / Efficiency, click flips, Efficiency hides Table list + Radar SVG, view button label, aria-pressed state, Radar↔Efficiency swap), metric pill family swap (Efficiency view replaces standings pills with efficiency pills + vice versa, one pill per `FACTION_EFFICIENCY_METRICS` entry, default = avgScore, click flips aria-pressed, lower-is-better title attributes, re-open resets to avgScore), efficiency rows (5 rows always present + data-faction + data-rank + 1..5 ranking, lean human ranks above bulk orc on avgScore by default, in-flight undead + empty lobster sink to last two slots, value cell shows 2-decimal score for scored factions and em-dash for empty, ranking re-runs on metric change, data-direction reflects metric semantics), share bar direction (leanest faction shows longest bar on tokensPerMission, top faction shows longest bar on completedMissions, share = 1.0 for the winning row), tier chips (scored factions render ≥1 chip, aria-label matches `"<TIER> Tier: <N> hero(es)"` pattern, empty factions show 'No scored heroes yet' placeholder, every chip has a valid data-tier), top-profile chip (scored factions show it, data-hero-id resolves to the expected hero, empty factions show "absent" marker, in-flight-only factions show "no signal" marker, chip surfaces 2-decimal score), efficiency footer (renders "Fleet avg" / "Σ scored" / "Σ closed", direction note swaps with metric), dismissal (Escape / Close / Backdrop all still work from Efficiency view), and lifecycle reset (re-opening returns to Table view default). Total test surface: \~1174 → \~1211. Pure additive — no helper or test edits to the Phase 74 surface. Backfills the Phase 74 `_p74_` placeholder to `b8be0fdb` in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 52 / 68 / 70 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `b8be0fdb` | Phase 74 — Faction Efficiency pure helper. Aggregates the Phase 71 per-hero `HeroEfficiencyProfile` into a per-faction view so the future Phase 75 `<FactionStandings />` integration can rank houses by per-mission discipline — the question Phase 52's volume-based standings can't answer. Where Phase 60's Leaderboard ranks individuals by volume and Phase 71's `buildEfficiencyTable` ranks individuals by per-mission discipline, this pulls back to the FACTION axis so the user can spot "the elves carry twice the per-mission token efficiency of the orcs even though the orcs have closed more total work" at a glance. New `apps/web/src/lib/factionEfficiency.ts` (\~600 LOC) exports `buildFactionEfficiency({heroes, missions, nowMs?})` (returns `{rows: Record<FactionId, FactionEfficiencyRow>, total: FactionEfficiencyTotals, ordered: FactionEfficiencyRow[]}`), `rankFactionsByEfficiency(rows, metric)`, `metricValueForFaction(row, metric)`, `formatFactionEfficiencyValue(value, metric)`, `factionEfficiencyMetricLabel(metric)`, `factionEfficiencyMetricLowerIsBetter(metric)`, `factionEfficiencyShare(row, metric, rows)`, `computeFactionEfficiencyFromHero(hero, missions, nowMs?)` (convenience re-export over Phase 71's `computeHeroEfficiency` for drawer mocks), plus the `FactionEfficiencyMetric` / `FactionEfficiencyRow` / `FactionEfficiencyTotals` / `FactionEfficiencyResult` types and the `FACTION_EFFICIENCY_METRICS` array. Per-row fields: `heroCount` (live only, dismissed dropped), `scoredHeroCount` (subset with non-null efficiency score), `totalMissions` / `completedMissions` / `failedMissions` (failed + cancelled) / `inFlightMissions`, `totalTokens` (across every mission regardless of state), `totalCost` (live-fleet sum), `avgTokensPerMission` / `avgCostPerMission` (null when no missions), `successRate` (null when no closed missions), `throughputPerHour` (sum of per-hero throughput within the faction), `avgScore` (mean of non-null per-hero scores, null when no scored hero), `tierCounts` (S/A/B/C/D distribution, sourced from the fleet-wide `buildEfficiencyTable` ranking — NOT recomputed inside the faction since per-faction percentile ranks would meaninglessly mark a lone S-tier hero as both 100th and 0th percentile), `topProfile` (best-scoring live hero from the faction). Tier source-of-truth: fleet-wide ranking — directly comparable across factions. Defensive rules mirror Phase 52 / 57: dismissed heroes drop everywhere; ghost missions (no resolvable hero) drop from per-faction rows but still count in `total` so the fleet-wide footer stays honest; all five factions always appear in the output (Phase 52 "empty rows make a point" convention); NaN / negative token values guarded. Seven metrics defined: `avgScore` (headline, higher = better), `successRate`, `tokensPerMission` (lower = better), `costPerMission` (lower = better), `throughput`, `completedMissions`, `scoredHeroes`. `rankFactionsByEfficiency` honours per-metric direction (asc for lower-is-better), with stable tie-breaks on `avgScore` DESC → `heroCount` DESC → canonical FACTION\_IDS order; null metric values always sort to the bottom. `factionEfficiencyShare` returns `value / max` on higher-is-better axes and `min / value` on lower-is-better axes so the leanest faction shows the longest bar. Pure helper only — UI integration to FactionStandings as a new "Efficiency" metric pill deferred to Phase 75 under the established Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 split discipline; locks the aggregation math in by tests before any React mounts it. Tests at `apps/web/tests/factionEfficiency.test.ts` (\~70 cases): empty-input invariants (5 rows, zero counts, null aggregates, ghost missions count in total only, non-array inputs defensively handled), dismissed-hero exclusion (heroCount + cost + missions), mission-state breakdown (failed + cancelled equivalence, in-flight separation, 100% + 0% successRate), token + cost aggregation (in-flight tokens count, avgTokensPerMission math + null when no missions, NaN/negative guards, totalCost live-fleet only), score + tier aggregation (scoredHeroCount excludes in-flight-only, avgScore is mean of scored hero scores, tierCounts sum to scoredHeroCount, topProfile picks highest score not highest volume, throughput sums within faction), fleet totals (ghost missions counted, dismissed costs excluded), `rankFactionsByEfficiency` (DESC by avgScore / successRate / completedMissions, ASC by tokensPerMission / costPerMission, null values sink, tie-break ladder, no mutation, canonical FACTION\_IDS as final tie-break), `metricValueForFaction` (full matrix across 7 metrics including null fallback for zero throughput), `formatFactionEfficiencyValue` (avgScore 2-decimal, successRate percent clamped to \[0,1], tokensPerMission K/M ladder, costPerMission $ + `<$0.01`, throughput `/h` suffix, integer rendering for completed + scored, em dash for null + non-finite), `factionEfficiencyMetricLabel` (every metric has a label), `factionEfficiencyMetricLowerIsBetter` (tokensPerMission + costPerMission true, others false), `factionEfficiencyShare` (1.0 for extreme, ratio for non-extreme, inversion for lower-is-better, 0 for null, 0 when all null, \[0,1] clamp), `computeFactionEfficiencyFromHero` (returns Phase 71 profile shape directly), realistic 5-faction scenario (humans top by avgScore at 100% success + low tokens; orcs lead completedMissions but trail in score; undead with poor success rate sit below scored peers; lobster with only in-flight missions has null avgScore and sinks; totals add up correctly), `FACTION_EFFICIENCY_METRICS` export contract (canonical display order + label coverage). Total test surface \~1104 → \~1174. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 52 / 57 / 71 helper patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `2a86fcd5` | Phase 73 — HeroCard efficiency chip. Mirrors the Phase 66 specialization-chip pattern: surfaces the Phase 71 `computeHeroEfficiency` profile directly on each HeroRoster card so the user can scan-the-fleet at-a-glance to spot the efficient heroes (S-tier) and the wasteful ones (D-tier) without opening every HeroDetailDrawer. New `<EfficiencyChip tier={...} score={...} />` (\~55 LOC at the bottom of `HeroCard.tsx`): tier letter (single character) + 2-decimal score, bordered + tinted with the matching `TIER_COLOR` hex (falls back to `TIER_COLOR.B` cyan when the single-hero path returns `tier: null` — same fallback the Phase 72 drawer chip uses). Total chip footprint under \~50px so it fits alongside the Phase 66 specialization chip in the same flex row without pushing the metrics row off-screen. `apps/web/src/components/HeroCard.tsx`: extracts the previously-inline hero-scoped mission filter into a shared `heroMissions` memo (so both `specialization` and `efficiency` memos share one O(n) pass over `s.missions`); adds an `efficiency = useMemo(() => computeHeroEfficiency({hero, missions: heroMissions}), [hero, heroMissions])`; the previous `<div className="mt-1">…SpecializationChip…</div>` block is now a single flex container `<div className="mt-1 flex flex-wrap items-center gap-1" data-testid="hero-card-chips">` holding both chips conditionally — spec on `totalMissions > 0`, efficiency on `efficiencyScore !== null`. Container renders when either condition is true so a hero with only in-flight missions (spec chip true, efficiency chip false) still gets the spec chip in its own row. Tests at `apps/web/tests/HeroCardEfficiency.test.tsx` (\~13 cases): conditional render (no chip on zero-mission or in-flight-only heroes; chip renders on single completed; failed-only also renders), data attributes (`data-tier="null"`, `data-score` matches `0.XX`, aria-label includes "efficiency"), score text in the chip body, fallback border style references `TIER_COLOR.B`, `tabular-nums` class for stable score width across cards, chips-row layout (both spec + eff chips are direct children of the chips row container; row mounts even when only spec chip qualifies for render), and hero scoping (sibling-hero missions don't leak into the score). Total test surface \~1091 → \~1104. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 66 / 72 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `3bb40f0c` | Phase 72 — HeroDetailDrawer Efficiency section. Wires the Phase 71 `computeHeroEfficiency` helper into the right-edge hero drawer as a new `<EfficiencySection profile={...} />` block mounted between Metrics (raw counts) and Specialization (qualitative role). Sits in the natural "quantitative bridge" position: Metrics shows what was spent, Efficiency shows what was produced per unit spend, Specialization shows what role those numbers add up to. Section renders a tier chip (`S`/`A`/`B`/`C`/`D` with `TIER_COLOR` accent + matching tint background, like the Phase 65 specialization chip) next to the 2-decimal composite `efficiencyScore`, a one-line caption (`3/4 completed · 1.2K/mission · 90s avg · $0.12`), and a 4-cell stats grid (Success · Tokens/mission · Throughput · Avg dur). Tier defaults to `B` colour fallback when `profile.tier === null` (the single-hero `computeHeroEfficiency` path returns `tier: null` because fleet ranking only happens inside `buildEfficiencyTable`); the score still renders correctly, so the chip reads as "the score is real, the fleet ranking isn't ready yet" without any awkward dead state. Section is conditionally rendered when `profile.efficiencyScore !== null` — heroes with only in-flight missions get nothing (same data-honesty rule as the Phase 65 Specialization section, which omits when `totalMissions === 0`). Stats grid uses the existing `<Stat />` component (`label`/`value` shape) so the visual rhythm matches the Metrics grid above it pixel-for-pixel. Em dash fallbacks for every nullable sub-metric (throughput when span = 0, avg duration when no completed mission has timing). Profile derived via `useMemo` over the same `heroMissionsAll` slice MissionTimeline + Specialization already consume so the section adds zero extra mission-iteration cost. `apps/web/src/components/HeroDetailDrawer.tsx`: imports `computeHeroEfficiency` + `TIER_COLOR` + `formatTier` + `formatTokenCount` + `formatDuration` + `formatCost as formatEfficiencyCost` + the `HeroEfficiencyProfile` type from the Phase 71 helper; adds the `efficiency` memo right next to the existing `specialization` memo; mounts the new section above the Specialization section in JSX flow; defines `EfficiencySection` at the bottom of the file alongside `SpecializationSection` (\~120 LOC). Tests at `apps/web/tests/HeroDetailDrawerEfficiency.test.tsx` (\~19 cases): conditional render (no section on zero-mission or in-flight-only heroes; section renders on single-completed; failed-only also renders), tier chip data attributes (`data-tier="null"` from the single-hero path, `data-score` matches `0.XX`, aria-label includes "efficiency score"), score sibling renders `0.XX`, caption surfaces completed/closed ratio (`2/2 completed`, `1/2 completed`) plus duration text, stats grid has exactly 4 children, success cell renders `100%` / `50%` according to the success rate, em-dash on missing timing, hero scoping (other heroes' missions don't leak in), semantic anchors (`aria-labelledby` points at the section heading, heading text is `"Efficiency"`, tier border style references `TIER_COLOR.B`), and DOM ordering (Efficiency precedes Specialization via `compareDocumentPosition` flag `4`). Total test surface \~1072 → \~1091. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 65 / 66 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `066a3ca4` | Phase 71 — Hero Efficiency pure helper. New `apps/web/src/lib/heroEfficiency.ts` (\~480 LOC) ships per-hero efficiency profiles that complement the Phase 60 Leaderboard's volume-based ranking — where the Leaderboard answers "who's done the most", the efficiency profile answers "who's most disciplined per mission". A Haiku-class hero closing 8 missions at 500 tokens apiece with 100% success is empirically MORE valuable than an Opus-class hero burning 15K tokens per mission with a 40% success rate; the volume leaderboard ranks them backwards on the question that actually matters ("who do I want to delegate the next ticket to"). Profile fields: `totalMissions` / `completedMissions` / `failedMissions` (failed + cancelled treated identically) / `inFlightMissions` (active + pending + awaiting\_approval — neither success nor failure yet, so they're excluded from the rate denominator) / `totalTokens` (summed across ALL missions regardless of state — the hero spent the tokens regardless of outcome) / `totalCost` (pulled off `hero.metrics.estimatedCostUsd` since that's the canonical billing-blessed number) / `avgTokensPerMission` / `avgCostPerMission` / `avgMissionDurationMs` (mean of `endedAt - startedAt` across completed missions only; `null` when no completed mission has a parseable duration) / `successRate` (`completed / (completed + failed)`, `null` when in-flight only — a hero who hasn't finished anything yet shouldn't appear as 0% successful) / `throughputPerHour` (`completed / hoursSinceSpawn`, span uses `spawnedAt → max(lastActive, now)`) / `efficiencyScore` (composite in \[0, 1], `null` when no closed missions) / `tier` (S / A / B / C / D / null). Composite score: `successRate × (1 / (1 + avgTokensPerMission / 5000))` — soft curve where 5K tokens/mission sits at 0.5 (the "average coding agent" target on Sonnet), wildly efficient heroes pull toward 1.0, wildly expensive heroes bottom out near 0.1 without going negative. Tier assignment via percentile rank across the fleet's scored heroes (live only — dismissed heroes drop out, same rule as Phase 52 / 60): S = top 20%, A = next 20%, B = next 20%, C = next 20%, D = bottom 20%. Heroes with no closed mission get `tier: null` and sort to the bottom rather than getting libelled with a D Tier for being brand new. Exported surface: `computeHeroEfficiency({hero, missions, nowMs?})`, `buildEfficiencyTable({heroes, missions, nowMs?})`, `compareEfficiencyProfile`, `computeEfficiencyScore`, `summarizeEfficiencyTable`, `percentileRank`, `tierForPercentile`, `missionDurationMs`, `formatTier` / `formatTierLong` / `formatTokenCount` / `formatCost` / `formatDuration` / `formatEfficiencyValue` / `efficiencyMetricLabel`, plus the `EfficiencyTier` / `HeroEfficiencyProfile` / `EfficiencyMetric` / `EfficiencyTableSummary` types and the `TIER_RANK` / `TIER_PERCENTILE_GATES` / `TIER_COLOR` / `EFFICIENCY_METRICS` / `TOKEN_EFFICIENCY_PIVOT` constants. Pure helper only — UI integration to Leaderboard (as a new metric option) deferred to Phase 72 under the established Phase 57 → 58 / 61 → 62 / 64 → 65 / 67 → 68 split discipline; locks the math in by tests before any React mounts it. Tests at `apps/web/tests/heroEfficiency.test.ts` (\~80 cases): exported-surface invariants (metrics order, tier rank monotonicity, percentile-gate monotonicity, hex-string tier colours), `formatTier` / `formatTierLong` happy paths + null fallback, `missionDurationMs` positive duration / missing timestamps / unparseable / clock-skew / instantaneous, `formatTokenCount` / `formatCost` / `formatDuration` K-M-h ladders + NaN guards, `percentileRank` distribution + edge cases (1-entry, 0-entry, overflow), `tierForPercentile` gate matrix + clamping, `computeEfficiencyScore` null when no closed missions / pivot-point math / 0% success / NaN defensiveness / negative-token guard / output clamp, `computeHeroEfficiency` mission counting / failed-cancelled equivalence / token summing across ALL states / cost off hero.metrics / completed-only duration / in-flight-only null successRate / spawnedAt → now throughput / zero-completed throughput / zero-span throughput / mission filtering / empty-input / malformed-hero throw / identity preservation / NaN token defensiveness, `buildEfficiencyTable` S→D ranking / dismissed exclusion / null-tier placement / single-hero S / empty fleet / nullish-hero handling / tie-break ladder / two-hero S+D split, `compareEfficiencyProfile` tier > score > completed > name ladder, `summarizeEfficiencyTable` tier counts + mean score + top profile, `formatEfficiencyValue` matrix across all 7 metrics, and a realistic 3-hero scenario (haiku 8/0 → sonnet 5/0 → opus 2/3 lands S → B → D with score 0.9 → 0.625 → 0.1). Total test surface \~992 → \~1072. Also backfills the Phase 70 SHA placeholder (`_p70_` → `e86daf3a`) was already complete in the prior docs-hygiene commit (`0f0edf23`); this row uses `_p71_` as the still-being-assigned placeholder, to be backfilled in the next session's first hygiene commit. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 52 / 60 / 64 / 67 helper patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `e86daf3a` | Phase 70 — Faction radar command palette + deep-link. Adds a new `open:faction-radar` entry to the Phase 46 command palette (`Cmd+K` → "radar" / "spider" / "shape") that opens the FactionStandings modal AND deep-links straight into the Phase 68 Radar view without an extra click. Mechanics: new \`useSettingsStore.standingsInitialView: "table"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `b56764c2` | Phase 69 — README sync to Phases 60-68. Top-level `README.md` rebased on the current feature surface. Test count bumped `~718` → `~978`. Phase 52 standings section rewritten to describe the new Phase 68 Table / Radar view toggle (including the 360×340 SVG, the legend with `data-empty` markers, the radar caption, and the per-metric leader strip), with `factionRadar.ts` added to the "pure helpers" inventory. Four new dedicated sections after the Phase 56 demo block: Phase 60 hero leaderboard, Phases 61/62/63 mission tag inference + filters, Phases 64/65/66 hero specialization (helper + drawer section + card chip), Phases 67/68 faction radar (helper + UI integration). "What's intentionally not built" closing paragraph updated to reference Phases 30-68 with the new shipped milestones called out (hero leaderboard, mission tag inference, hero specialization, faction radar) — the full list is still just the two original entries (licensed binary assets, PWA push backend). No code changes — pure docs sync. Also backfills the Phase 68 SHA placeholder (`_p68_` → `005e3e23`) in this Commits-on-`main` table. Same shape as Phases 33 / 42 / 51 / 59. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `005e3e23` | Phase 68 — Faction Radar UI integration. Wires the Phase 67 `buildRadarLayout` helper into `apps/web/src/components/FactionStandings.tsx` as a new Table / Radar view toggle. Header gains a `role="group"` toggle between the Phase 52 row table and a 360×340 SVG radar / spider chart that plots all five factions across every `STANDINGS_METRICS` axis simultaneously. The radar SVG ships: concentric grid rings (faint stroke, level=1 ring slightly heavier to anchor the eye), spoke lines from center to each axis tip, axis labels rendered as `<text>` elements with `data-active` mirroring the current metric selection (click-to-select to flip the metric without leaving the radar), per-faction `<path>` polygons (fill = `style.hex` at 18% opacity, stroke = `style.hex` at 90% opacity, painted DESC by `areaShare` so larger polygons stay underneath smaller ones), vertex `<circle>` markers at non-zero axes only (so empty axes don't stack dots at the center), and `<title>` per polygon for hover tooltips of the form `"The Crimson Horde: 78% of chart"`. Below the SVG: a legend row with one chip per FACTION\_IDS (5 chips, including empty factions marked `data-empty="true"` with a desaturated dot so the user knows they exist but aren't competing), a one-line `radarCaption` (`Faction radar — no activity yet` or `Orc covers ~78% of the chart`), and a per-metric leader strip (one row per axis showing the faction with the highest value on THAT metric — "Tokens · elf · 10.0K" — so the orcs-lead-missions-but-humans-lead-tools insight reads explicitly even when the polygon shapes alone don't make it obvious). View choice is component-local (resets on close), matches the metric pill / view pill aria-pressed contract everywhere else. Empty-fleet edge case mounts the SVG with zero polygons, the no-activity caption, and every leader row reading `no signal`. Tests at `apps/web/tests/FactionStandingsRadar.test.tsx` (\~28 cases) cover view-toggle lifecycle (role=group, default=table, click flips, re-open resets), radar SVG structure (role=img + aria-label = caption, grid-ring count matches `RADAR_DEFAULT_GRID_LEVELS`, spoke count matches `STANDINGS_METRICS.length`, axis-label per metric, active-axis highlighting, click axis re-selects metric), polygon rendering (one per non-empty faction, valid areaShare 0..1, title tooltip with house name + percent, vertex markers at non-zero levels, no polygons on empty fleet, DESC paint order by areaShare), legend (5 chips with data-empty marker, percentages match helper math), caption + per-metric leaders (orcs lead missions, humans lead tools, elves lead tokens, no signal when nobody has any value), coexistence with table view (close button + toggle in header, metric pills still re-order table, metric pills still update radar active axis). Total test surface \~950 → \~978. Also backfills the Phase 67 SHA placeholder (`_p67_` → `861ce449`) in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 52 / 60 / 65 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `861ce449` | Phase 67 — Faction Radar pure helper. New `apps/web/src/lib/factionRadar.ts` (\~340 LOC) computes the SVG geometry for a 6-axis radar / spider chart that compares all five factions across the Phase 52 `STANDINGS_METRICS` (missions / tools / tokens / cost / achievements / heroes) in a single visual. Where the Phase 52 table answers "which faction wins THIS metric?", the radar answers "which faction has the most balanced footprint?" — orcs might top missions while elves quietly own tools + tokens, and the radar makes the asymmetry immediately legible. Exports: `buildRadarLayout(rows, config)` returns a `RadarChartLayout` with `cx` / `cy` / `radius` / effective `metrics` / `axes` (one per metric: tip + label coordinates) / `gridRings` (concentric polygons at the configured fractions, default `[0.25, 0.5, 0.75, 1.0]`) / `factionPolygons` (one per `FACTION_IDS` slot — empty rows collapse to a degenerate polygon at the center) / `maxValues` per metric / `leader` convenience pick. Supporting helpers: `axisCoordinates(cx, cy, r, angle)`, `normalizeMetric(value, max)` (clamped to \[0,1], defensive against NaN / Infinity / max≤0), `radarMaxValues(rows, metrics?)`, `polygonArea(pts)` (shoelace, sign-invariant), `maxPolygonArea(n, r)` (regular `n`-gon inscribed in circle of radius `r`), `polygonAreaShare(pts, n, r)` (clamped to \[0,1]), `polygonPath(pts)` (SVG path with M+L+Z + 3-decimal precision + near-zero collapse to `"0"` to kill `-0` / `1e-16` artifacts), `formatCoord`, `radarCaption(layout)` (one-line summary with leader name + area share percent). Constants: `RADAR_DEFAULT_GRID_LEVELS`, `RADAR_DEFAULT_START_ANGLE = -π/2` (first axis points up), `RADAR_DEFAULT_LABEL_OFFSET = 1.18` (labels sit outside tips). Per-metric normalisation against the fleet-wide max for that metric — vs. a global denominator that would crush every non-token axis to a needle. All five factions ALWAYS appear in the output (matching Phase 52's "empty rows make a point" rule) — an empty faction renders as a degenerate polygon collapsed at center; caller can skip drawing zero-area polygons if it gets noisy. Pure helper only — UI integration deferred to Phase 68 under the Phase 57 → 58 / 64 → 65 split discipline. Tests at `apps/web/tests/factionRadar.test.ts` (\~67 cases): `normalizeMetric` clamping + NaN/Infinity/negative guards, `axisCoordinates` at every quarter-turn + radius 0 edge case, `polygonArea` for empty / 1-pt / 2-pt / unit square / sign-invariance, `maxPolygonArea` degenerate inputs + unit hexagon = 3√3/2, `polygonAreaShare` regular hexagon at full radius = 1, radius 0 = 0, n<3 = 0, clamp on enormous inputs, `polygonPath` empty / 1-pt / 2-pt (no Z) / 3+pt (closed Z) / precision rounding / -0 collapse / NaN guard, `radarMaxValues` empty / picks-per-metric / NaN guard / custom subset, `buildRadarLayout` 6-axis canonical order / first-axis-up / even spacing / English labels / label offset 1.18 / custom startAngle / custom subset / custom labelOffset, grid rings default count + ASC sort + clamp + closed `Z` paths, polygons FACTION\_IDS-ordered + empty fleet collapse + tied-at-max hits level 1 / hexagon areaShare 1 / half-max plots at 0.5 + areaShare 0.25 / raw + normalised value capture / L-count = N-1 / no input mutation / canonical order on reversed rows, leader null on empty / largest areaShare wins / degenerate single-axis = no leader, defensive guards (negative radius / NaN cx / empty metrics fallback / empty gridLevels fallback), `radarCaption` empty fallback + leader caption + name capitalisation, realistic 3-faction comparison (orc leads missions, elf leads tokens, human leads tools — empty undead + lobster still appear with areaShare 0), type-safety (metrics array matches axes + polygon-point order). Test surface \~883 → \~950. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phases 52 / 60 / 64 helper patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `ad8d8a1`  | docs (README + RECON + SPEC + PROGRESS) + LICENSE + `@factionos/protocol` (169-event WS + REST types)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `a739de8`  | `@factionos/server` (Express + ws + mock generator + roster/missions/notices/scrolls/achievements managers)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `b4aa613`  | `@factionos/web` (Vite + React + Three.js client) + `public-demo/index.html` + GitHub Pages workflow + `BUILD_LINKS.md`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `00cc4b1`  | `@factionos/hooks` (9 Claude Code hook scripts + ws-listener), `@factionos/cli` (init/status/doctor/uninstall), `@factionos/warroom` (Cloudflare Worker + Durable Object)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `f5e155b`  | 5 LLM engines (planDecomposer, idleSuggestionEngine, transcriptMiner, onDemandAnalysisEngine, autoImprover) with maintained prompts + canned per-engine fallbacks; achievement engine with real triggers; session archive + memory store; upgraded Three.js battlefield (composed silhouettes, faction keeps, real lighting); voice barks wired to hero state transitions                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `342cab7`  | `assets/showcase/heroes-banner.jpg` — generated painted illustration of all 5 factions                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `c34c8ad`  | Upgraded demo (splash screen + improved battlefield) + comprehensive README                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `e15bfb9`  | 5 painted faction bust portraits wired into FactionPicker + demo HeroCards + README gallery                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `131bcd7`  | `POST /llm/scan-codebase` route + `codebaseWalker.ts` (sync tree walker, mtime DESC, strict ignore list); server README rewrite with the LLM endpoint table                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `9a26fdf`  | `@factionos/adapters` workspace — Discord webhook + Telegram bot bridges; shared bridge/formatter/throttle modules, 9 event-type formatter                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `046649f`  | Public demo PWA — service worker (versioned cache, SWR for CDN deps), web manifest, 3 icon sizes rendered from the favicon SVG, registration in `index.html`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `a1081f6`  | Trophy room — AchievementsGallery modal, TitleBar trigger, locked silhouettes (titles redacted with mid-dots), tier-tinted medallions, esc/click-outside dismiss                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `9b9077e`  | Vitest scaffolding + 36 substantive unit tests across protocol, server, adapters (factions, achievements, codebaseWalker, formatter, throttle)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `3867340`  | Phase 20 — CI workflow + \~47 new tests across HTTP routes, adapter senders, adapter config, and protocol events                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `3296db4`  | Phases 21–24 — SettingsDrawer localStorage persistence + battlefield character refinements + LLM endpoint test coverage + README polish                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `9df0212`  | Phase 25 — Battlefield click-to-select-hero (raycast onto root group), camera tween (cubic ease-out, \~700ms), 3rd-person orbit follow, floating hero-name label that fades after 2.5s                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `4d73e54`  | Phase 26 — autoImprover golden tests (\~30 cases): gating, output shaping, safeParse fallbacks, 24h cool-down semantics + cool-down only consumed on successful fire                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `_p27_`    | Phase 27 — apps/web Vitest suite (happy-dom): MissionLog + ToastTray + AchievementsGallery component tests (\~36 cases); root config gains per-glob env selection so server/adapter tests stay on node                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `42aac88`  | Phases 28 + 29 — War Room E2E tests (`apps/warroom/tests/warroom.test.ts` + `worker.test.ts`, \~32 cases against a thin DurableObjectState + WebSocketPair fake) + animated battlefield strip (`assets/showcase/factionos-strip.webp`, 4-frame painted cycle — dusk muster → mid combat → victory chorus → night watch — wired into README via `<picture>` with a JPEG fallback).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `96dcde3`  | Phase 30 — `HeroDetailDrawer.tsx` flyout wired to Phase 25's `selectedHeroId`. Right-edge 460-px drawer surfaces identity (cwd / tty / spawned / last-active) + 6-cell metrics grid + active mission preview + 5-most-recent hero-scoped missions + achievement contributions chip row + a "Speak" button that fires a one-off Web Audio bark in the hero's faction voice. Mounted as a sibling to `<PlanWorkpad />` in `App.tsx`. Happy-dom test (`apps/web/tests/HeroDetailDrawer.test.tsx`, \~20 cases) covers null-when-no-selection, all aria contracts (`role="dialog"`, `aria-modal`, `aria-labelledby`), identity + metrics + active-mission + history rendering, hero-scoped filtering (no other-hero leakage), empty-state copy, all 5 dismissal paths (Escape / backdrop / header close / footer close / re-open), Speak button audio-on + audio-off gating, and achievement-contribution faction+timestamp filtering.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `76fd8a4`  | Phase 31 — codebase scan UI surfaced in `SettingsDrawer.tsx`. New "Codebase scan" section: persistent root-path input (`useSettingsStore.scanRoot` + `setScanRoot`, round-trips through localStorage), a Scan button that POSTs to `http://localhost:<serverPort>/llm/scan-codebase` with `{root}`, plus three status pills — "walking…" while in-flight, "✓ N files · M issues" on success, "✗ failed" with the error title attribute on failure. Toast lifecycle: an info "Scan started" toast on dispatch, a `kind: success/warn` "Scan complete" toast on success (warn when issues>0), and a `kind: error` "Scan failed" toast on HTTP error / network throw. Required new `pushToast` selector on `useGameStore` for UI-originated toasts (vs. the existing server-sourced `applyEvent("server_toast")` path). Tests under `apps/web/tests/SettingsScan.test.tsx` (\~9 cases): empty-path disables button, typing persists into the store, POST hits the right URL with the right body shape, success pill + summary render, success toast pushed, 4xx error pill + error toast pushed, network-throw error pill + error toast with the thrown message, missing `issues` array counts as 0 issues.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `541d632`  | Phase 32 — replay scrubber + WS event ring buffer. `useGameStore` now captures every `applyEvent` dispatch into an `eventLog: ReplayEntry[]` ring buffer (cap 500 = `REPLAY_BUFFER_CAP`), skips capture while a `replayingSinceMs` flag is set (no feedback loops), and exposes `replayLast(windowMs, speed)` + `clearEventLog()`. New `apps/web/src/components/ReplayPanel.tsx` (\~180 LOC) — bottom-left flyout with window selector (30 s / 2 m / 5 m), speed selector (1× / 2× / 4×), "▶ Replay last X" button, "Clear buffer" button, an event timeline (last 50 entries with relative timestamps + event type + one-line summary), and the same Escape / backdrop / aria contract as `HeroDetailDrawer`. Mounted in `App.tsx`, opened via a new "⏯ Replay scrubber" button in the SettingsDrawer "Dev / data" section. `useSettingsStore` gains a transient `replayPanelOpen` boolean (not persisted) + setter. Tests at `apps/web/tests/replayBuffer.test.ts` (\~7 cases): capture appends with wall-clock timestamp, FIFO eviction at 500, no capture while replaying, `clearEventLog` is local to the log, window filter picks only in-range events, returns immediately with no events, sets/clears the `replayingSinceMs` flag around the await, and replayed events don't re-enter the buffer. Total test surface: \~210 → \~217.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `80e5836`  | Phase 33 — README sync. README's stack section bumped to \~217 tests; added a dedicated HeroDetailDrawer section, a Codebase scan UI section, and a Replay scrubber section; Settings table extended with the new `scanRoot` key; "What's intentionally not built" list refreshed with the two new follow-ups (localStorage replay persistence + server-side hero attribution) so the gap inventory stays honest. PROGRESS.md backlog re-ordered to reflect what's still open.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `862af34`  | Phase 34 — localStorage-persisted replay buffer. `useGameStore` gains `REPLAY_PERSIST_CAP=100` + `REPLAY_PERSIST_KEY="factionos-replay-v1"` + `loadPersistedReplayBuffer()` + `persistReplayBuffer()` + `__flushReplayPersist()` test helper. `applyEvent` debounces a write (250 ms) to localStorage after every capture, gated on `!replayingSinceMs` so replays don't pollute storage. Store init hydrates from the snapshot. `clearEventLog` wipes the persisted snapshot too. `ReplayPanel` header now advertises "cap 500 (last 100 survive reload)". New `apps/web/tests/replayPersist.test.ts` (\~12 cases): persist/load round-trip, cap-on-write + cap-on-read, empty-array clears the key, JSON.stringify errors swallowed (BigInt seed), malformed entries skipped on read, applyEvent triggers a debounced write that `__flushReplayPersist` drains synchronously, no persist during replay, clearEventLog clears the snapshot, ring-tail slicing preserved across restarts. Phase 32 suite tweaked to clear localStorage in beforeEach + drain in afterEach so persistence doesn't leak between cases. Total test surface: \~217 → \~229.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `08f53af`  | Phase 35 — Server-side hero attribution on achievements. Protocol's `Achievement` interface grows three optional fields: `heroId?`, `unlockedByHeroName?`, `unlockedByFaction?` plus a new `AchievementAttribution` payload type. `AchievementManager.unlock()` accepts an `attribution?` argument and snapshots it onto the persisted achievement. `AchievementEngine` adopts per-achievement attribution heuristics: first-hero → oldest spawned hero, first-mission → hero whose completed mission has the earliest `endedAt`, hundred-missions → hero who finished the 100th completion, plan-completed → ctx.lastPlanHeroId fallback to most-recently-active, five-tomes → ctx.lastScrollHeroId, alliance-joined + auto-improver remain authorless (user / server engine). `server.ts` populates `lastPlanHeroId` from plan\_completed/plan\_fully\_complete events and `lastScrollHeroId` from scroll\_collected events. `HeroDetailDrawer` flips its contribution heuristic to PRIMARY (`a.heroId === hero.id`) with the legacy faction-mention path retained as a fallback for pre-Phase-35 unlocks; each chip carries \`data-attributed="true                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `1d2c04b`  | Phases 36 + 37 — battlefield achievement celebration + 24h mission timeline. **Phase 36** introduces `apps/web/src/components/battlefield/achievementCelebration.ts` — a manager that spawns gold particle bursts (60 dodecahedron particles per burst, 3 s lifetime, gravity-pulled fountain trajectory) when an `achievement_unlocked` event lands. Tier-derived colour (gold for bronze/silver, silver-white for gold/platinum), capped at `CELEBRATION_MAX_CONCURRENT = 6` bursts with FIFO eviction. **Phase 37** introduces `apps/web/src/components/MissionTimeline.tsx` — a pure-SVG 24-hour sparkline (24 hourly buckets, mission-count bars + tool-use polyline overlay, faction-coloured fills).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `c43111b`  | Phase 38 — HeroRoster activity sort. Header gains a `State` / `Active` segmented toggle (`role="group"`, `aria-pressed` on each segment, `aria-label="Sort roster"`). State mode keeps the lifecycle band order but breaks within-band ties by `metrics.lastActiveAt DESC` (was insertion order before). Activity mode ignores the state band entirely and sorts every live hero by `metrics.lastActiveAt DESC`, with `spawnedAt DESC` as the fallback for heroes that never acted. Sort mode persists to `useSettingsStore.rosterSort` (round-trips through `factionos-settings-v1`). Exported pure `sortRoster(heroes, mode)` helper so the ordering rules get focused unit-test coverage without rendering the roster. Tests at `apps/web/tests/HeroRosterSort.test.tsx` (\~10 cases): state-band ordering, within-band tie-break, activity-mode cross-state ranking, never-acted heroes at bottom, dismissed heroes dropped, input-array not mutated, empty-state copy, count-pill matches live size, aria-pressed mirrors current mode, click flips mode + persists, ARIA group label.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `3972a24`  | Phase 39 — mission-detail flyout. New `MissionDetailDrawer.tsx` is a center-screen modal that surfaces: prompt (full, not clamped), faction badge + hero name + state pill, 8-cell stats grid (started / ended / duration / tools / tokens in / tokens out / tokens total / plan?), tool-use timeline (vertical list with status pill ok/error/in-flight, per-call duration, optional path + inputPreview), full streamed `assistantText` in a scrollable code block, and an optional `summary` section when the mission has completed. Backed by a new `selectedMissionId: string \| null` field + `setSelectedMissionId` setter on `useGameStore`. `MissionLog` rows are now `<button>` elements (Tab/Enter accessible) that call `setSelectedMissionId(mission.id)` on activation — replaces the prior `<li><div>` rows. `HeroDetailDrawer` mission history rows are also clickable buttons, and the active-mission preview gains an explicit "Open mission →" button. Drawer mounted in `App.tsx` as a sibling of `HeroDetailDrawer`. Pure helpers exported for unit testing: `missionDurationMs(m)` (handles in-flight + unparseable + negative), `toolUseDurationMs(tu)` (null for in-flight), `formatDuration(ms)` (ms / s / m / h ladders with sub-unit precision). Tests at `apps/web/tests/MissionDetailDrawer.test.tsx` (\~17 cases) cover null-when-no-selection, null-when-mission-missing, prompt + hero + faction + state render, tools empty-state, three tool statuses, durations only on completed tools, assistantText populated vs empty, summary conditional render, escape close, backdrop close, card-click no-close, header X close, ARIA contract (`role="dialog"` + `aria-modal="true"` + `aria-labelledby="mission-detail-title"`), token totals in stats grid, hero-id fallback when hero missing. Plus `apps/web/tests/MissionLogClickable.test.tsx` (\~5 cases): rows are native buttons with `aria-label`, click opens, Enter opens, multiple rows open the right one, hero-filtered roster still wires correctly. Total test surface: \~285 → \~307.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `c963a5c`  | Phase 40 — MissionLog filter row. Header gains a 4-chip state filter (`All` / `Active` / `Done` / `Failed`) + free-text search input that fuzzy-matches against prompt + summary + assistantText (case-insensitive substring). State filter buckets: `active` includes `awaiting_approval` + `pending`; `failed` includes `cancelled`. Filters compose with the existing hero-scope filter (`selectedHeroId`) so opening a hero on the battlefield narrows the mission log first, then state + search refine further. Empty-result state explains it's the filter, not a missing roster ("No missions match the current filter"). Pure `filterMissions({...})` helper exported for unit tests. Filter state is component-local (`useState`), not persisted — same pattern as the search input in any RC of similar shape. Tests at `apps/web/tests/MissionLogFilter.test.tsx` (\~16 cases): pure helper for all four state buckets + search casing + hero scope composition + whitespace trim; component for chip aria-pressed, click narrows the list, search filter, clear-search button, filter-empty state copy, component-local reset across remount, ARIA group label + search input label. Total test surface: \~307 → \~323.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `958bc56`  | Phase 41 — AchievementsGallery tier grouping, progress ring, recently-unlocked. The trophy room was previously a single flat sorted list; Phase 41 redesigns it as four collapsible tier sections (Platinum / Gold / Silver / Bronze, with each section showing "X of Y unlocked"), a pinned "Recently unlocked" section at the top (up to 3 most-recent entries across all tiers, sorted by `unlockedAt DESC` with tier-weight tie-breaker), and a circular SVG progress ring in the modal header showing the unlocked-percentage rounded to whole digits. Collapse state is per-section, component-local — closing the modal resets every section to expanded so the user always lands in the same state. Each section header is a real `<button>` with `aria-expanded` and `aria-label="Toggle <tier> section"`. Pure helpers exported: `groupByTier(achievements)` (returns `Record<Tier, Achievement[]>` with unlocked-first / alpha sort inside each bucket) and `recentUnlocks(achievements, limit)` (DESC by timestamp, tier-weight tie-break, locked entries dropped). Tests at `apps/web/tests/AchievementsGalleryGrouping.test.tsx` (\~12 cases): both pure helpers across all sort + tie-break paths; component opens via trigger, progress ring percent, tier-header counts, header click collapses + expands, recently-unlocked top section with 3-entry cap + correct order, no recent section when nothing unlocked, re-opening resets collapse to expanded, empty-state copy when 0 achievements registered. Total test surface: \~323 → \~335.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `e8d20c7`  | Phase 42 — README sync. Top-level `README.md` rewritten to reflect Phases 34-41: stack section bumped to \~335 tests, new dedicated sections for the HeroRoster sort toggle (Phase 38), MissionDetailDrawer (Phase 39), MissionLog filter + search (Phase 40), AchievementsGallery tier sections + progress ring (Phase 41), battlefield achievement celebration (Phase 36), and the localStorage-persisted replay buffer (Phase 34). Settings table gains the `rosterSort` row. "What's intentionally not built" pruned: localStorage replay persistence, server-side hero attribution, and HeroDetail flyout were marked "not built" before — all three have shipped, so only three items remain (licensed binary assets, PWA push notifications, optional wrangler dev integration test). No code changes — pure docs sync.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `64e2a7b1` | Phase 66 — HeroCard specialization chip. Surfaces the Phase 64 role inference directly in the HeroRoster (left-sidebar) cards so the user can scan-the-fleet at-a-glance to see who specializes in what without opening HeroDetailDrawer. `apps/web/src/components/HeroCard.tsx` imports `summarizeSpecialization` + adds a hero-scoped mission filter via a new `useMemo` over `s.missions` (Zustand-subscribed once per card; memoized on `[hero, missions]` so sibling-card re-renders don't pay the inference cost). New `<SpecializationChip />` helper component at the bottom of the file (\~50 LOC): compact variant of the Phase 65 drawer chip with smaller paddings, 9px font, 1px confidence dot — fits inside the existing card's vertical rhythm without pushing the metrics row off-screen. Same accent-on-tinted-background scheme as Phase 65 (`borderColor` + `color` + `background: ${accent}1a`), same confidence-dot semantics (`high` = solid, `medium` = hollow border, `low` = omitted), same `data-role` / `data-confidence` / `data-generalist` test surface. Conditionally rendered (only when `totalMissions > 0`) so heroes with zero missions don't show "Generalist" / "no data" noise. Mounted between the archetype/cli/model line and the active-mission preview so the role reads as "this hero, this role" without competing with the inline mission text. Tests: new `apps/web/tests/HeroCard.test.tsx` (\~7 cases) covers no-mission no-chip, specialized hero (20 bug missions → Bug Hunter high confidence), generalist label on flat distribution, low-confidence dot-omitted, hero-scoping (sibling missions don't leak), aria-label includes role, and renders alongside existing card content without breaking name/badge/metrics. Also backfills the Phase 65 SHA placeholder (`_p65_` → `e04acafd`) in this Commits-on-`main` table. Test surface \~876 → \~883. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 65 chip semantics.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `e04acafd` | Phase 65 — HeroDetailDrawer Specialization section. Wires the Phase 64 `summarizeSpecialization` helper into the right-edge hero drawer (Phase 30 + 35). `apps/web/src/components/HeroDetailDrawer.tsx` gains a `<SpecializationSection profile={...} />` block mounted between the Metrics grid and the Phase 37 MissionTimeline sparkline. Component derives the profile via `useMemo` over the full hero-scoped mission set (`heroMissionsAll`, the same superset MissionTimeline uses). Rendered surface: (1) a role chip with faction-tag accent border + tinted background (`{accent}1a` wash) showing the role name (`Bug Hunter` / `Feature Architect` / `Generalist` / etc.) plus a confidence dot — solid for `high`, hollow border for `medium`, omitted for `low`; (2) an optional `also <secondary>` text chip that appears only when the hero is non-generalist AND has a distinct secondary tag; (3) a one-line caption (the helper's `specializationCaption` minus the leading `<heroName> —` prefix since the drawer's header already names the hero); (4) a horizontal distribution bar split into one segment per non-zero tag, each segment using `missionTagAccentHex` for its background and carrying both a `<span class="sr-only">` label and an `aria-label` of the form `Bug 78% (22)` for screen-reader parity. Section uses `role="group"` + `aria-label` on the bar container and `aria-labelledby` on the section heading. Tooltip on the section element (`<title>` attribute via React's `title` prop) repeats the full caption. The whole block is conditionally rendered — heroes with zero missions get nothing at all so the drawer doesn't show "Specialization · no data" noise. Tests at `apps/web/tests/HeroDetailDrawer.test.tsx` extended with a new `Phase 65` describe block (\~10 cases): zero-mission no-render, single-mission render, role-label match on specialized hero, generalist label on flat distribution, confidence-dot solid for high, confidence-dot absent for low, distribution-bar segment count matches non-zero tags, segment aria-label format, caption format sans hero-name prefix, secondary-tag chip presence with distinct primary/secondary, hero-scoping (other heroes' missions don't bleed). Also backfills the Phase 64 SHA placeholder (`_p64_` → `f2dd4316`) in this Commits-on-`main` table. Test surface \~866 → \~876. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 60 / 62 / 63 component patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `f2dd4316` | Phase 64 — Hero Specialization helper. New `apps/web/src/lib/heroSpecialization.ts` (\~290 LOC) classifies a hero by their dominant mission tag(s) and translates that into a narrative role label. Exports: `summarizeSpecialization({hero, missions})` returns a `SpecializationProfile` with `{ heroId, heroName, totalMissions, tagCounts, primaryTag, secondaryTag, distinctTags, primaryShare, role, accentHex, confidence, generalist }`; `summarizeFleetSpecialization(heroes, missions)` returns one profile per hero in canonical order; `groupHeroesBySpecialization(heroes, missions)` partitions a roster into 9 buckets (8 `MISSION_TAGS` + `"generalist"`); `specializationCaption(profile)` returns a one-line caption like `"Ada — Bug Hunter (78% Bug, 22 missions, high confidence)"`; `specializationRoleForTag(tag)` exposes the tag→role mapping. Plus constants: `SPECIALIZATION_THRESHOLDS = { LOW_SAMPLE_MAX: 5, HIGH_SAMPLE_MIN: 15, DOMINANCE_RATIO: 0.6 }`, `GENERALIST_ROLE = "Generalist"`, `GENERALIST_ACCENT = "#9aa4ad"`. Role table is single-tag-per-role (Feature Architect / Bug Hunter / Refactor Specialist / Test Engineer / Scribe / Operator / Quartermaster / Wandering Generalist) — no combinatorial hybrid labels. Generalist gate fires when no tag clears the dominance ratio so a 4-bug+3-feature+3-test mix doesn't lie as "Bug Hunter". Three-tier confidence (`low` / `medium` / `high`) gated on sample size + dominance. Canonical-order tie-break for primary/secondary picks so two equally-weighted tags resolve deterministically. Re-uses Phase 61's `inferMissionTags` directly — zero protocol surface change, zero new wire format, zero adapter changes. Pure helper only — UI integration to `HeroDetailDrawer` deferred to Phase 65 under the Phase 57 → Phase 58 / Phase 61 → Phase 62 split discipline. Tests at `apps/web/tests/heroSpecialization.test.ts` (\~36 cases) cover the empty/zero-mission profile, multi-hero scoping, shape-complete `tagCounts`, single-tag dominance for every keyword class, secondary-tag distinct-from-primary, canonical-order tie-break, multi-tag mission contribution to multiple buckets, prompt-less `unknown` bucket, primary-share computation across multi-tagged fleets (stays in \[0,1]), generalist gating both ways, three confidence tiers, role label table, fleet survey, group buckets (including dedicated generalist bucket + skip-zero-mission heroes + shape-complete output), and caption phrasing for each profile shape. Also backfills the Phase 63 SHA placeholder (`_p63_` → `c95b8078`) in this Commits-on-`main` table. Test surface \~830 → \~866. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 60 / 61 helper patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `c95b8078` | Phase 63 — Mission Heatmap tag-pill row. Extends the Phase 48 helper (`apps/web/src/lib/missionHeatmap.ts`) with an optional `tagFilter: Set<MissionTag> \| null` parameter — when non-empty, only missions whose inferred tags intersect the set contribute to the 7×24 calendar. Composition: `heroId` (Phase 25) ∩ `factionId` (Phase 54) ∩ `tagFilter` (Phase 63); the hero filter still wins over faction when both are set; tag filter applies on top of whichever survives. Empty / null tag set is treated as a no-op so callers never have to nullify it. Mission-tag inference re-uses Phase 61's `inferMissionTags(mission)` — zero protocol-surface changes, zero adapter changes, zero hook-script changes. Component (`apps/web/src/components/MissionHeatmap.tsx`) gains a `TagPillRow` mounted between the header and the SVG grid, mirroring Phase 62's MissionLog row in shape, accessibility, and accent palette: 8 pills (`Feature` / `Bug` / `Refactor` / `Test` / `Docs` / `Config` / `Chore` / `Untagged`) with faction-tinted accent dot + count badge, `aria-pressed` + `aria-label`, `role="group" + aria-label` on the container, dimmed empty pills, and a `× Clear tags` affordance that only appears when ≥1 pill is active. Counts are computed against the POST-SCOPE / PRE-TAG slice (via a new `scopedMissions` `useMemo` + `tagCountSummary`) so each label shows what would actually surface if that pill were clicked given the current scope — important when the user has narrowed to a faction and the fleet-wide counts would be misleading. Both the scope `<select>` and the tag set reset to their unfiltered state every time the modal re-opens (matches the Phase 54 reset). \~12 new helper-test cases in `apps/web/tests/missionHeatmap.test.ts` (undefined / null / empty Set no-op semantics, single-tag narrow, multi-tag union, multi-tag mission via any-of, `unknown` bucket surface, zero-match returns zero peak, hero ∩ tag composition, faction ∩ tag composition, rollups respect the filter, input-Set immutability) plus \~10 new component cases in `apps/web/tests/MissionHeatmap.test.tsx` (pill count = 8, default aria-pressed=false, clear affordance lifecycle, single-click narrows, double-click toggles off, multi-select union, count-badge values, scope-narrowed counts shrink, modal-re-open reset, untagged-pill surface). Test surface \~808 → \~830. Also backfills the Phase 62 SHA placeholder (`_p62_` → `90c52268`) in this Commits-on-`main` table. Validation gap: sandbox cannot run `npm install` / `tsc` / `vitest` / `vite build`; code hand-reviewed against Phase 62 / 54 / 48 patterns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `90c52268` | Phase 62 — MissionLog tag-pill row. Wires the Phase 61 `missionTags` helpers into `apps/web/src/components/MissionLog.tsx` as a second filter row below the Phase 40 state/search row. Eight pills (one per `MISSION_TAGS` entry including `Untagged`) render with a count badge showing how many missions in the **post-state/search/hero-scope** slice would surface if the pill were active — so the chip labels reflect "would I see anything if I clicked this?" not a stale fleet-wide count. Clicking a pill toggles its membership in a component-local `Set<MissionTag>` filter; clicking a second pill ANDs the union (multi-select). Composition order: hero-scope → state filter → search → tag filter. When at least one pill is active, a `× Clear tags` affordance appears next to the row so the user can reset without un-clicking each pill. Pills with `count === 0` are dimmed (lower opacity on the dot, muted text + count chip) so the user can see WHICH tags are present in the current slice without being tricked into clicking a no-op. Active pills get a faction-themed coloured border + tinted background (`{accent}1a` for a subtle wash) and matching tinted dot for the visual identity match Phase 60 / 52 / 48 chose for their own metric/scope chips. EmptyState now honors `tagFilter.size > 0` so the "No missions match the current filter" copy fires when the tag filter is what narrowed everything away. Component test at `apps/web/tests/MissionLogTags.test.tsx` (\~11 cases): pill render count (8), default aria-pressed false, count-badge values match the inferred tag math, click narrows to that tag (1 row for `bug`), double-click toggles off (4 rows again), multi-select union (`bug` + `feature` → 2 rows), Untagged surfaces only unmatched missions (`m4`), Clear-tags button visibility + reset behavior, state + tag composition (state `completed` ∩ tag `bug` on an active-bug mission → empty), EmptyState copy uses "filter" wording when tags are responsible, role="group" + aria-label sanity. No new helper logic — this phase is the pure UI wire-up. Total test surface: \~797 → \~808.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `2b4b8361` | Phase 61 — Mission Tag Inference helper. New `apps/web/src/lib/missionTags.ts` (\~210 LOC) classifies missions into a fixed set of intent-tags (`feature` / `bug` / `refactor` / `test` / `docs` / `config` / `chore` / `unknown`) using keyword regex matching against the concatenated `prompt + summary + assistantText`. Exports the pure helpers `inferTagsFromText(text)`, `inferMissionTags(mission)`, `tagCountSummary(missions)`, `filterMissionsByTags(missions, allowed)`, `groupMissionsByTag(missions)`, plus the `MISSION_TAGS` canonical-order array, the `TAG_PATTERNS` regex map, and `missionTagLabel` + `missionTagAccentHex` for chip rendering. Patterns use `\b` word boundaries to avoid substring false positives (e.g. `bug` doesn't match `debugger`, `test` doesn't match `testimony`). Inflections are honored (`fix` / `fixed` / `fixing` / `fixes`). Multi-tagging is the norm — "refactor the failing tests" tags as both `refactor` AND `test`. When no pattern matches but text exists the mission tags as `unknown` so the future filter UI can show an "Untagged" pill instead of silently hiding rows. Faction-colored accents (`#3aa56a` elf green for feature, `#d34a32` orc crimson for bug, `#3a87e8` alliance blue for refactor, `#9b4cc2` undead purple for test, `#fb6324` lobster orange for docs, steel for config, parchment for chore) so the chip row reads as native to the rest of the app. Pure helper only — UI integration deferred to a follow-up phase under the established Phase 57 (helper) → Phase 58 (UI) discipline. Tests at `apps/web/tests/missionTags.test.ts` (\~31 cases) cover the canonical-order array, label + accent surfaces for every tag, the regex word-boundary guarantee, every keyword-class with positive matches + inflections + case-insensitivity + multi-tag composition, the mission-level fallback to `unknown` when no field matched (vs. `[]` from the text-level helper), `tagCountSummary` across empty / single-tag / multi-tag / untagged inputs, `filterMissionsByTags` across empty-allowed (no-op) / single-tag / multi-tag (any-match) / `unknown` (untagged-only) / non-mutation, `groupMissionsByTag` canonical-order + multi-bucket-membership + untagged-bucket population. Total test surface: \~766 → \~797.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `886f0cae` | Phase 60 — Hero Leaderboard. New `apps/web/src/lib/heroLeaderboard.ts` (\~250 LOC) exports the pure helpers `buildLeaderboard({heroes, achievements, metric, limit?})`, `rankHeroes(heroes, metric, achievements?)`, `metricValueFor(hero, metric)`, `formatLeaderboardValue(value, metric)`, `leaderboardMetricLabel(metric)`, `countAchievementsPerHero(heroes, achievements)`, plus the `LEADERBOARD_METRICS` array and `DEFAULT_LEADERBOARD_LIMIT = 10` constant. Live heroes only (dismissed dropped); achievement attribution honors Phase 35 `heroId` snapshot with a `unlockedByHeroName` fallback for pre-Phase-35 entries; tie-break ladder is `value DESC → lastActiveAt DESC → name ASC` so the "currently carrying" hero wins ties over the "historically tied" one. Share bars scale to the post-limit table max (not fleet-wide) so the podium reads cleanly on a fresh session. New `apps/web/src/components/Leaderboard.tsx` (\~270 LOC) is a center-screen modal with header summary ("top N of M live · ranked by X"), 5-pill metric selector (missions / tools / tokens / cost / achievements) with `role="group"` + `aria-pressed`, top-10 rows each a real `<button>` (Tab/Enter accessible) that closes the modal and opens HeroDetailDrawer with the selected hero pre-seeded. `useSettingsStore` gains a transient `leaderboardOpen` flag + `setLeaderboardOpen` setter (not persisted — matches `commandPaletteOpen` / `heatmapOpen` / `standingsOpen` pattern). TitleBar gets a new 🏅 Leaderboard button alongside Heatmap + Standings. Command palette gains an `open:leaderboard` entry under Overlays with keywords `["leaderboard", "ranking", "heroes", "top", "podium", "carry", "champions", "mvp"]`. Keyboard shortcut `g l` opens the leaderboard (same prefix family as `g r` for trophy room — both are "go to a ranking surface"); `SHORTCUTS_HELP` registers it; `routeKey` handles it; new `leaderboard_open` action wired into the hook. Tests: `apps/web/tests/heroLeaderboard.test.ts` (\~29 cases) covers `metricValueFor` defensive guards, the K/M/$ formatter ladder, every metric label, achievement counting (heroId path / name fallback / authorless-skip / unlocked-only / unknown-hero-skip), `buildLeaderboard` matrix (dismissed dropped, DESC ranking, lastActiveAt tie-break, alphabetic last-resort, default + custom limit, share scaling, empty-state share=0 no-NaN, total aggregation, achievements metric, entry shape, no-mutation, no-throw across metrics), and `rankHeroes` parity. `apps/web/tests/Leaderboard.test.tsx` (\~16 cases) covers lifecycle (null-when-closed, mounted-when-open, aria-modal contract), 4 dismissal paths (Escape / backdrop / inner-card-no-close / close button), empty-state copy, row rendering + ranking with the default metric, metric pill click flips ranking, aria-pressed state, value-cell formatting, deferred-via-setTimeout hero detail open, metric reset on re-open, and the achievements-metric path. `apps/web/tests/keyboardShortcuts.test.ts` extended with 2 new cases for `g l` (open + shift-rejected) and the `SHORTCUTS_HELP` surface bumped to include `"g l"`. `apps/web/tests/commandPaletteRanking.test.ts` noopHandlers factory gains `setLeaderboardOpen: vi.fn()` so the CommandHandlers interface change stays type-safe. Total test surface: \~718 → \~766.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `c8b090e`  | Phase 59 — README sync to Phases 52-58. Top-level `README.md` had drifted: it claimed `~600` tests when the actual count is `~718`, and its newest deep-dive section was Phase 49's CSV/JSON export — nothing on Phase 52 (faction standings), Phase 53 (webhook adapter), Phase 54 (per-faction heatmap filter), Phases 55 + 57 + 58 (cost projection), or Phase 56 (demo v2). Test count bumped to `~718`. Six new sections added between the CSV/JSON export and Theming sections: **Faction standings (Phase 52)** with the metric matrix + top-performer chip explanation, **Generic webhook adapter (Phase 53)** with the full + compact payload shapes + auth-header + redact-URL behaviour, **Per-faction mission heatmap filter (Phase 54)** with the optgroup encoding + hero-wins-when-both-set rule + drop-unresolvable-hero rule, **Cost projection (Phases 55 + 57 + 58)** consolidating the trailing-window math, the per-faction partitioner, and the FactionStandings chip integration into one section, and **Public demo refresh (Phase 56)** with the full v2 surface inventory. "What's intentionally not built" closing sentence updated to credit phases 30-58 (was 30-50) and to enumerate the recently shipped surfaces. Pure docs sync — no code or test changes, test surface unchanged at \~718.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `8d171ff`  | Phase 58 — FactionStandings UI integrates the Phase 57 projection. `apps/web/src/components/FactionStandings.tsx` now subscribes to `useGameStore.missions`, runs `extrapolateCostByFaction({heroes, missions})` via `useMemo`, and renders a per-row `proj · 1h` chip (gated on `factionProjection.hasSignal`) next to the existing per-row value cell, plus a fleet-wide `proj · 1h` chip in the footer (gated on `projection.total.hasSignal`). Chips are faction-tinted (`style.hex`) on the per-row variant and carry a `title=` tooltip explaining the trailing-window source data + daily extrapolation. Defensive guard: `Object.values(missionsById ?? {})` handles test mocks that don't seed the missions field. No new tests — the math is covered by Phase 57's `factionCostProjection.test.ts`, and the chip is purely additive UI (existing FactionStandings.test.tsx assertions still pass). Component JSDoc updated to note the Phase 58 augmentation.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `28fe8dd`  | Phase 57 — Per-faction cost projection helper. New `apps/web/src/lib/factionCostProjection.ts` exports `extrapolateCostByFaction({heroes, missions, nowMs?, windowMs?, costPerMtokIn?, costPerMtokOut?})` + `rankFactionsByProjection(result)`. Partitions the incoming missions by their owning hero's faction (lookup via `factionByHero` Map) and runs Phase 55's `extrapolateCost` per bucket, so the standings table can answer "which faction is burning the budget?" with the same defensive guards as the fleet-wide projection. Missions whose `heroId` doesn't resolve to a known hero are dropped from the per-faction rows but still counted in the `total` row (same data-honesty rule as Phase 52's standings + Phase 54's faction heatmap filter). Returns `{ rows: Record<FactionId, CostProjection>, total: CostProjection }` so the caller gets per-row chips and the footer total in one pass. `rankFactionsByProjection` orders factions DESC by `projectedHourlyUsd`, sinking no-signal factions to the bottom while preserving canonical `FACTION_IDS` order among them. Tests at `apps/web/tests/factionCostProjection.test.ts` (\~14 cases): empty input returns all-five-zero rows, partitioning routes each mission to the right faction, aggregation sums multiple missions per faction, unresolvable heroIds drop from per-faction rows but stay in `total`, windowing respects the trailing window per bucket, custom `windowMs` threads correctly, rate overrides thread to every bucket, `hasSignal` per-row honesty, ranker DESC ordering, ranker no-signal sink behavior, ranker deterministic empty-input fallback. Total test surface: \~704 → \~718.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `178b8be`  | Phase 56 — Public demo refresh (v2). `public-demo/index.html` rewritten to surface the modern UI vocabulary from Phases 30-55 without touching `apps/web`. Header tools row gains five focused buttons: 🏆 `unlocked/total` trophy room, 🗓 Heatmap, ⚖ Standings, ⌘K command palette, and ? shortcuts. New `SessionRollup` strip mirrors the Phase 43 fleet rollup (Σ missions / tools / tokens / cost). Five new overlay modals — `AchievementsGallery` (recently-unlocked + tier sections + circular progress ring), `MissionHeatmap` (24×7 calendar w/ fleet / faction / hero scope dropdown — same `buildHeatmapBuckets` math as `apps/web/src/lib/missionHeatmap.ts`), `FactionStandings` (6-metric switcher, horizontal share bars, top performer per faction — mirrors `apps/web/src/lib/factionStandings.ts`), `CommandPalette` (Cmd/Ctrl+K, ranker matches the Phase 46 `rankCommands` shape: prefix-100 / word-60 / substring-30, ↑↓ nav, ⏎ run, Esc dismiss, 11 stock commands), `KeyboardShortcutsModal` (full bindings cheatsheet), `ReplayPanel` (compact share-link copier). Global key handler implements the Phase 45 shortcut grammar: `?` → cheatsheet, `Esc` → dismiss overlay, `/` → focus mission search, `r` → replay, `g r/h/s` → trophy/heatmap/standings (900ms two-step), Cmd/Ctrl+K → palette, all input-aware so typing in search/inputs doesn't trigger. `MissionLog` upgraded to the Phase 40 surface: 4-chip state filter (All/Active/Done/Failed) + search box wired to `useState` + filter-empty copy, hero-scope still composes. Splash adds a `demo v2 · Phases 30-55` badge and a "press ? for shortcuts" hint. `SEED_ACHIEVEMENTS` seeds 7 baseline trophies with 2 unlocked + tier ordering (bronze→legendary). Synthetic mission generator now backfills 7 days × 24 hours of weighted random missions on first render so the heatmap has shape immediately (weekday business-hours bias + weekend dampener + per-faction sampling). All modals share a single `Modal` shell with Esc handler + click-outside dismiss + `role="dialog"` + `aria-modal`. No `apps/web` changes — pure docs/demo refresh, test surface unchanged at \~704.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `d2131c4`  | Phase 55 — Cost projection helper. New `apps/web/src/lib/costProjection.ts` exports `extrapolateCost({missions, nowMs?, windowMs?, costPerMtokIn?, costPerMtokOut?})` + `formatProjectedCost(usd)` + `projectionLabel(horizon)` + the constants `DEFAULT_PROJECTION_WINDOW_MS = 15 min`, `DEFAULT_COST_PER_MTOK_IN = 3`, `DEFAULT_COST_PER_MTOK_OUT = 15`, `HOUR_MS`, `DAY_MS`. Algorithm: window mission `startedAt` to the trailing 15 minutes (configurable), sum `tokensIn` + `tokensOut`, multiply by the per-Mtok rate (defaults to Sonnet 4.5's published $3 in / $15 out), then scale the window cost up by `HOUR_MS / windowMs` for the hourly projection and `DAY_MS / windowMs` for the daily projection. Defensive guards: unparseable timestamps dropped, future-dated missions (clock skew) rejected, custom window of `<= 0` falls back to the default, non-finite cost rates fall back, negative cost rates fall back (zero is treated as a legitimate "free model" override), `hasSignal` reports `false` when the window has no missions with measurable cost so the UI can hide the chip. `formatProjectedCost(usd)` mirrors the Phase 43 SessionRollup formatter (`$0` / `<$0.01` / `$X.XX` / `$XXX` / `$X.Xk` ladder). `apps/web/src/components/SessionRollup.tsx` consumes the helper via `useMemo(() => extrapolateCost({ missions: Object.values(missions) }), [missions])` and renders a new `proj · next 1h` pill in the rollup strip — gated on `projection.hasSignal` so the strip stays clean when nothing is firing. Tests at `apps/web/tests/costProjection.test.ts` (\~21 cases) cover empty input, window filtering (inside / before / future / unparseable / custom window / zero-window fallback), projection math (hourly + daily formulas, custom rate override, NaN/negative/zero rate handling), hasSignal lifecycle, exported constants, formatProjectedCost across all five tiers ($0 / <$0.01 / $X.XX / $XXX / $X.Xk), and projectionLabel humanisation. Total test surface: \~683 → \~704.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `22c2fd3`  | Phase 54 — Per-faction filter for the mission heatmap. Extends `buildHeatmapBuckets({…, factionId?, heroes?})` so the helper can scope by faction membership in addition to the Phase 48 per-hero filter. When `factionId` is set the resolver looks each mission's `heroId` up in `heroes` and drops it unless the matched hero's `faction === factionId`; missions with an unresolvable hero are dropped while the faction filter is active so a ghost agent can't sneak into the wrong scope. When `heroId` AND `factionId` are both set, the hero filter wins (narrower scope). When `factionId` is set but `heroes` is omitted, every mission is dropped (defensive — no lookup possible). `MissionHeatmap.tsx` swaps its hero-only `<select>` for a unified scope dropdown with optgroups: a single "All heroes (fleet)" entry, a "By faction" optgroup with all five faction shortcuts (`faction:orc` / `faction:human` / `faction:elf` / `faction:undead` / `faction:lobster`), and a "By hero" optgroup with each live hero (`hero:<id>`). The encoded `value` round-trips through one `<select>` so the modal doesn't need parallel state. Accent colour picks up the faction's hex when in faction-scope (matching the per-hero behaviour from Phase 48). Scope resets to "all" every modal re-open (same UX as Phase 48 + Phase 52). Tests at `apps/web/tests/missionHeatmap.test.ts` extended with \~7 new cases under "buildHeatmapBuckets · faction scope": filters by faction, no-op when null/undefined, hero filter wins when both set, drops unresolvable hero ids while faction-filter is active, zero-result for an empty faction, zero-result when `heroes` lookup is missing, rollup composition stays correct. `apps/web/tests/MissionHeatmap.test.tsx` updates: option count was 4 (1 all + 3 heroes) → now 9 (1 all + 5 faction + 3 heroes) with faction:orc / faction:lobster / hero:h1 specifically asserted; hero-pick test updated to use `hero:h2` encoding; new "narrows the cells when a faction is picked" case; reset-on-reopen test updated to use `hero:h1` encoding. Total test surface: \~674 → \~683.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `8597348`  | Phase 53 — Generic webhook adapter. apps/adapters grows a third bridge alongside the existing Discord + Telegram senders. `apps/adapters/src/shared/config.ts` gains `WebhookConfig` + `readWebhookConfig()` — reads `FACTIONOS_WEBHOOK_URL` (required, must be `http://` or `https://`), optional `FACTIONOS_WEBHOOK_AUTH_HEADER_NAME` + `FACTIONOS_WEBHOOK_AUTH_HEADER_VALUE` (must be set together or not at all), `FACTIONOS_WEBHOOK_PAYLOAD_SHAPE` (`full` default / `compact`), `FACTIONOS_WEBHOOK_USER_AGENT` (default `factionos-adapter/0.1`), and `FACTIONOS_WEBHOOK_TIMEOUT_MS` (default 5000, non-finite / non-positive values fall back to default). `apps/adapters/src/webhook/sender.ts` exports `buildWebhookPayload(msg, shape, rawEvent, nowIso?)` + `buildWebhookHeaders(cfg)` + `sendToWebhook(cfg, msg, opts)`. Two payload shapes: **`full`** ships `{source, version: 1, severity, title, text, emittedAt, event}` with the raw FactionOS event as a passthrough so downstream consumers can re-derive any field the formatter dropped; **`compact`** ships `{severity, title, text, emittedAt}` — the shape ntfy / Pushover / generic chat hooks consume. `sendToWebhook` honors a per-request `AbortController` timeout (`fetchImpl` + `controllerFactory` test seams keep the unit tests off the wire), translates `AbortError` into a "Webhook timeout after Nms" error, propagates non-2xx responses with the HTTP status code in the message, and cleans up the abort timer on both success and failure paths. `apps/adapters/src/webhook/run.ts` is the entry point — same lifecycle shape as `discord/run.ts` / `telegram/run.ts` (startBridge → formatEvent → ChannelThrottle → sendToWebhook), plus a `redactUrl` helper that masks the token portion of the URL in the boot log so a webhook secret doesn't leak into journalctl. Dispatcher `apps/adapters/src/index.ts` picks up the new `webhook` case + falls back to it in `pickDefault` when `FACTIONOS_WEBHOOK_URL` is set and no other adapter env is. package.json gains an `npm run webhook` script. `.env.example` documents every new env var with ntfy / Pushover examples. README rewritten with a dedicated "Generic webhook (Phase 53)" section that pictures both payload shapes, lists common auth patterns (Authorization Bearer, X-Api-Key, ntfy basic), and explains when to pick the webhook bridge vs. the Discord / Telegram surface. Tests at `apps/adapters/tests/webhookSender.test.ts` (\~13 cases) exercise full + compact payload shapes, default title fallback, raw-event-null passthrough, runtime emittedAt stamping, header construction (default UA, content-type, auth header set together vs. missing one half, custom UA), fetch lifecycle (POSTs to the right URL with the right method + headers + body, propagates non-2xx with status in error message, propagates network throws, translates AbortError into a timeout error), compact-shape body comparison, abort timer scheduling/clearing on success, and no-timer-when-timeoutMs-is-0. `apps/adapters/tests/webhookConfig.test.ts` (\~16 cases) cover the env-loader matrix: missing URL throws, whitespace URL throws, missing-protocol URL throws, https + http accepted, default payload shape / UA / timeout, payload-shape case-insensitive + unknown-value fallback, UA whitespace handling, timeout parse (positive int, fractional floor, zero/negative/non-finite fallback), auth header lifecycle (registered when both set, throws when only one set), URL leading/trailing-whitespace trim. Total test surface: \~645 → \~674.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `8840bc9`  | Phase 52 — Faction Standings modal. New `apps/web/src/lib/factionStandings.ts` (\~280 LOC) exports six pure helpers: `groupByFaction({heroes, achievements})` partitions a live roster into one row per FACTION\_ID (canonical order, even empty factions) and rolls up missions / tools / tokens / cost / achievement-credit (via Phase 35 `unlockedByFaction`); `rankFactions(rows, metric)` sorts DESC by the chosen metric with `heroCount` + canonical-FACTION\_IDS-order tie-breaks; `metricValue` + `metricShare` + `formatStandingsValue` + `metricLabel` + `computeStandings` ship the table-rendering math. Dismissed heroes are excluded from every metric (same rule as the Phase 43 `sessionStats` helper). Top performer per faction is the live hero with the highest `missionsCompleted`, with `toolCallsTotal` as the tie-breaker and alphabetic name as the last-resort stable sort. New `apps/web/src/components/FactionStandings.tsx` (\~265 LOC) is a center-screen modal: header with title + summary + close, 6-pill metric selector (`role="group"`, `aria-pressed` per pill), one row per faction with rank pill, faction glyph, hero/active/medal counts, share bar (width tracks share of table max), a top-performer chip that closes the modal and selects the hero, and a footer with Σ tokens / Σ cost / Σ tool-calls + an attribution disclaimer. Opens from a new "⚔ Standings" button in TitleBar + a new "Open faction standings" command in the palette. `useSettingsStore` gains a transient `standingsOpen: boolean` flag + `setStandingsOpen` setter (matches the existing `commandPaletteOpen` / `heatmapOpen` / `replayPanelOpen` / `shortcutsHelpOpen` pattern). `CommandHandlers` interface picks up `setStandingsOpen`; the existing `commandPaletteRanking.test.ts` `noopHandlers()` factory updated to include it so the test suite stays type-safe. Tests at `apps/web/tests/factionStandings.test.ts` (\~28 cases) cover the pure helper matrix (canonical-order layout, empty-roster rows, missions/tools/tokens/cost aggregation, dismissed-hero drop, activeCount distinct from heroCount, achievement attribution gating on both unlockedAt + unlockedByFaction, top-performer tie-breakers, defensive missing-metrics tolerance, input non-mutation, unknown-faction skip, rankFactions sort + tie-breaks across every metric, metricShare clamping + zero-denominator safety, formatStandingsValue K/M ladder + cost floor + non-currency floor, computeStandings totals + leader resolution). `apps/web/tests/FactionStandings.test.tsx` (\~17 cases) cover lifecycle (null-when-closed, mounted-when-open, aria contract), all 4 dismissal paths (Escape, backdrop, inner-card-no-close, close button), keydown listener cleanup, row rendering (all 5 factions visible, rank order DESC, empty-state copy when no heroes, top-performer chip data when populated, achievement count credit), metric pills (default + aria-pressed, click flips sort + re-ranks, re-open resets to missions), share bars (100% for solo leader, 0 for empty table), top-performer chip click closes modal + selects hero, summary line populated with totals. Total test surface: \~600 → \~645.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `782d90b`  | Phase 51 — README sync to Phases 43-50. Top-level `README.md` previously stopped at Phase 41 — this commit rewrites it to reflect the full surface that's actually shipped. Test count bump (\~335 → \~600). Settings table gains the `notificationsEnabled` row + a new "transient flags" sub-table for `replayPanelOpen` / `shortcutsHelpOpen` / `commandPaletteOpen` / `heatmapOpen` (Phases 32 / 45 / 46 / 48). Seven new dedicated sections: SessionRollup strip (Phase 43), Browser notifications (Phase 44), Keyboard shortcuts overlay + binding table (Phase 45), Command palette Cmd+K (Phase 46), Share-replay link (Phase 47), Mission heatmap 24×7 calendar (Phase 48), CSV/JSON session export endpoint (Phase 49). "What's intentionally not built" pruned from 3 items to 2 — wrangler-dev integration test has shipped (Phase 50, opt-in via `FACTIONOS_RUN_WRANGLER_DEV=1`), so the only honest gaps are the licensed binaries (FBX/TGA/MP3) and a push backend (the SW + Notification plumbing landed in Phase 44, only a server is missing). Footer line bumped from "shipped across phases 30–41" to "30–50" + a pointer to the four sub-surfaces in PROGRESS.md (Commits table, build-phases checklist, Decisions log, How-to-resume). Pure docs sync — no code changes. Same shape as Phase 33 (README sync to \~217 tests) and Phase 42 (README sync to \~335 tests).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `5a35ba7`  | Phase 50 — Wrangler-dev integration test (skip-aware). New `apps/warroom/tests/wranglerDev.test.ts` is the OPT-IN companion to Phase 28's in-process WarRoom suite — runs the **real** `wrangler dev` runtime via `child_process.spawn`, waits for the "Ready on <http://127.0.0.1>:" log line, then hits `POST /<code>/init` against the real worker. Activates only when both (a) `wrangler` resolves on PATH (probed via a 4-second `--version` spawn) AND (b) the developer opts in via `FACTIONOS_RUN_WRANGLER_DEV=1` — both guards are defensive: a sandbox / CI environment without the Cloudflare CLI skips silently, and an opt-out keeps `npm test` from booting a 30-second workerd runtime by default. Uses `it.runIf` so the skipped path is invisible rather than printed as a noisy "skipped" line. 45-second spawn timeout caps any hung start-up. Process lifecycle is explicit: `beforeAll` resolves the handle (or `null`), `afterAll` kills the process unconditionally. The in-process fake suite (\~32 cases) continues to cover the property graph (broadcast-excluding-sender, ring buffer cap, role gating, persistence, CORS, reconnect catch-up, etc.) deterministically; this test is the drift-detector for when the fake and the real Cloudflare runtime diverge. Total test surface: \~598 → \~600 (only 2 wired here, but the gate matters more than the count).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `94e3aef`  | Phase 49 — CSV/JSON export endpoint. New `apps/server/src/lib/exportSession.ts` (\~150 LOC) exports `buildSessionExport(snapshot, format, nowIso?)` (returns `{format, body, contentType, filename}` with a timestamped filename + correct MIME type), `sessionToCsv(snapshot, nowIso?)` (single-document, two-labeled-section CSV: heroes block + missions block, separated by a blank line + `# Section: <name>` comment marker; RFC-4180-compliant quoting via `escapeCsvField` for embedded commas / quotes / newlines), `sessionToJson(snapshot, nowIso?)` (pretty-printed `{exportedAt, schemaVersion:1, counts, heroes, missions}` payload; the `schemaVersion` lets a future v2 add fields without breaking pinned consumers), `escapeCsvField(value)` + `heroesToCsv(heroes)` + `missionsToCsv(missions)` + `isExportFormat(s)` guard. New `apps/server/src/routes/export.ts` registers \`POST /export/session?format=csv                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `3fe7155`  | Phase 48 — Mission heatmap (24×7 calendar). New `apps/web/src/lib/missionHeatmap.ts` (\~210 LOC) exports `buildHeatmapBuckets({missions, nowMs?, heroId?})` (pure: lays out a 7-day × 24-hour grid for the last week, tallies missions per cell with optional hero-id scoping, returns `{cells, rows, hours, stats}` including `peakCell` + `hottestDay` + `hottestHour` rollups; tolerant of unparseable timestamps; drops missions outside the window; honors DST by deriving day-of-month from each mission's local Date accessors rather than raw subtraction) + `tileOpacity(count, peak)` (maps a cell count to an SVG-fill opacity in \[0.06, 1.0], clamped + finite for the `peak === 0` defensive case). New `apps/web/src/components/MissionHeatmap.tsx` (\~230 LOC) is a center-screen modal mounted in `App.tsx`: SVG grid with sparse hour labels (every 3 hours), day + date labels per row, faction-coloured tiles (accent follows the optional hero filter, neutral steel for fleet-wide), `<title>` tooltips per cell, footer with hottest-day + hottest-hour rollups + a 5-step intensity legend, and an in-modal `<select>` hero filter that resets to "all" every time the modal re-opens. Opens from a new "🗓 Heatmap" button in `TitleBar.tsx` (`data-heatmap-trigger`). `useSettingsStore` gains a transient `heatmapOpen: boolean` + `setHeatmapOpen` setter (not persisted — same pattern as `commandPaletteOpen`, `shortcutsHelpOpen`, `replayPanelOpen`). Command palette gains an "Open mission heatmap" command (`open:heatmap`). Tests at `apps/web/tests/missionHeatmap.test.ts` (\~22 cases) cover the pure helper (grid shape, hour label padding, day-label set, today→bottom-row placement, hero-id filter, all-heroes mode, tool-use sum within a cell, mission-array non-mutation, peakCell detection, hottest day/hour rollup, null rollups when nothing fired, dayIndex monotonicity, ordering across the window) + the `tileOpacity` math (idle baseline, minimum visible, peak clamp, peak=0 defensive case, exceeds-peak clamp). `apps/web/tests/MissionHeatmap.test.tsx` (\~17 cases) covers lifecycle (null-when-closed, mounted-when-open, aria-modal contract), dismissal (Escape / backdrop / inner-card-no-close / close button), grid render (168 cells, 7 day labels, 8 sparse hour labels, data-count on a hot cell), summary surface (empty-state copy, populated text with peak callout, hottest-day footer, no-activity footer fallback), and hero filter (initial value, option count, narrowing the count on change, reset-on-reopen). Total test surface: \~519 → \~558.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `d61ff46`  | Phase 47 — Share-replay link. New `apps/web/src/lib/replayLink.ts` (\~290 LOC) exports six pure helpers: `encodeReplayLink(entries)` (versioned base64url JSON, returns `null` on empty), `decodeReplayToken(token)` (strict-defensive, returns `null` on every error path), `readReplayHash(rawHash)` (extracts + validates), `buildShareUrl(base, token)` (strips existing hash, appends), `dispatchReplayEntries(entries, sink, opts)` (cadence-preserving async dispatcher with speed scaling + per-gap maxGap clamp + replayingFlag callback), and `bootShareReplay(opts)` (orchestrates the boot path: read → strip → info-toast → dispatch → success-toast). Wire format is `#replay=v1.<base64url>` over JSON `{v:1, t0, e:[[offsetMs, event]]}` — offsets keep the payload compact, plain JSON keeps it auditable, base64url keeps the URL safe across copy/paste in chat clients that mangle high-bit chars. Hard cap of 200 entries (`REPLAY_LINK_MAX_ENTRIES`) — typical 5-minute scrub fits comfortably; \~17 KB URL stays well under the 32 KB Edge / 8 KB modest-proxy budget. `apps/web/src/components/ReplayPanel.tsx` gains a "Share link" button between Replay and Clear, sharing the same \`inFlight                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `b0b32dd`  | Phase 46 — Command palette (Cmd+K / Ctrl+K). New `apps/web/src/lib/commandPalette.ts` (\~280 LOC) exports `buildCommands(inputs, handlers)` to materialise the runtime command catalog from a live game-store + settings-store snapshot, and a pure `rankCommands(query, commands, limit)` ranker. Ranker is multi-token substring (case-insensitive, every token must match somewhere in `label + group + keywords + detail + id`), with a scoring matrix that boosts label-prefix matches (100 pts) over label-interior (60 pts) over group / keywords / detail / id; empty query returns the catalog in insertion order so the most-useful surfaces (jump-to-hero, jump-to-mission) sit at the top by default. Catalog includes: jump-to-hero for every live (non-dismissed) hero, jump-to-mission for the 25 most-recent missions (recent-first), overlay openers (trophy room / replay scrubber / settings drawer / shortcuts help), live-state-aware toggle commands (audio / motion / mock / notifications / roster sort / theme / 5 severity filters), faction switcher for all 5 factions, plus reset-to-seed and reset-settings. New `apps/web/src/components/CommandPalette.tsx` (\~250 LOC) renders a center-screen modal triggered by Cmd+K with: search input that auto-focuses on open (via `setTimeout(0)` so it doesn't fight any drawer the previous action opened), ranked result list with `aria-selected` per row, keyboard nav (ArrowUp / ArrowDown wrap, Enter activates the focused row, Escape closes without firing), mouse hover that updates the active index (so click + keyboard share the same focused-row state), click-to-activate, footer counter showing visible / total commands. Activation closes the palette first and then defers the side effect by one tick so any opening drawer doesn't fight the palette input. `useKeyboardShortcuts` extended with a `palette_toggle` action that fires on `Cmd+K` / `Ctrl+K` (uppercase `K` accepted for caps-lock). The Cmd+K binding is the ONE permitted modifier chord because it's the universal palette binding (VS Code / Linear / GitHub / Notion / ChatGPT); the routing check happens BEFORE the general modifier guard so Cmd+R / Ctrl+F etc. continue to escape the shortcut surface unchanged. `Cmd+Shift+K` and `Cmd+Alt+K` are explicitly rejected to avoid colliding with Firefox devtools + browser Bookmarks chords. `useSettingsStore` gains a transient `commandPaletteOpen: boolean` + setter, alongside `shortcutsHelpOpen` and `replayPanelOpen`. `SHORTCUTS_HELP` gains a `Cmd K` row. Mounted in `App.tsx` as a sibling to `<KeyboardShortcutsModal />`. Tests at `apps/web/tests/commandPaletteRanking.test.ts` (\~22 cases) exercise the ranker (empty-query insertion order, label-prefix beats label-interior, label-interior beats group/keywords, case-insensitivity, multi-token AND semantics, multi-token order-insensitivity, no-match returns empty), the `buildCommands` catalog (live heroes only — dismissed heroes excluded; missions in recent-first order; faction switcher per faction; toggle labels reflect current state; theme command flags current theme; hero/run() side effect), and the `trimLabel` helper. `apps/web/tests/CommandPalette.test.tsx` (\~16 cases) covers null-when-closed, the aria-modal contract, input auto-focus, three close paths (Escape / backdrop / inner-card stop-propagation), search-typing filters the list, empty-state row when no matches, seed hero appears as a jump-to row, footer count, ArrowDown / ArrowUp navigation with wrap, Enter activates the focused row and closes the palette, Enter is a no-op on empty list, click-to-activate, mouseEnter updates active index, `Open settings drawer` action fires its handler. `apps/web/tests/keyboardShortcuts.test.ts` extended with 7 new cases for Cmd+K / Ctrl+K (open, uppercase K, typing-target allowed, Shift / Alt rejected, bare `k` is not a binding). Total test surface: \~428 → \~473.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `418f8e55` | Phase 45 — Keyboard shortcuts overlay. New `apps/web/src/lib/useKeyboardShortcuts.ts` (\~270 LOC) registers a single document-level `keydown` listener that drives every global binding: `?` toggles the help overlay (Shift+/), `Escape` closes any overlay, `/` focuses the mission-log search input, `r` toggles the replay scrubber, `s` opens the SettingsDrawer (via the existing `[data-settings-trigger]` button), and `g` then `r` opens the trophy room (the AchievementsGallery dialog via `aria-label="Open trophy room"`). Sequence prefix (`g`) lives in a `useRef` with a 1.2 s timeout so an abandoned combo clears itself. All single-key bindings (and prefix arming) silently no-op when the active element is an `<input>` / `<textarea>` / `<select>` / contenteditable region, except `?` and `Escape` which are always honored. Ctrl / Meta / Alt always escape the shortcut surface so `Cmd+R`, `Ctrl+S`, etc. stay native. Pure `routeKey({...})` helper exported with no DOM dependencies so the entire routing matrix is unit-tested without a `window`. New `apps/web/src/components/KeyboardShortcutsModal.tsx` (\~140 LOC) renders a center-screen overlay with the registered shortcut inventory grouped by category (General / Navigation / Overlays / Dev), each combo rendered as `<kbd>` chips (so `g r` becomes two adjacent chips). Closes via Escape / backdrop click / explicit close button / re-pressing `?`. `useSettingsStore` gains a transient `shortcutsHelpOpen: boolean` (not persisted) + setter for the modal lifecycle. Mounted in `App.tsx` as a sibling to `<ReplayPanel />` and the hook called once at the top of the component. Tests at `apps/web/tests/keyboardShortcuts.test.ts` (\~31 cases) exercise every binding via the pure `routeKey` helper (single keys, sequence combos, modifier guards, typing-target guard, prefix preservation in typing surface), the `isTypingTarget` heuristic across native inputs + contenteditable, the three default DOM helpers (focusMissionSearch / openSettings / openTrophyRoom — including the no-op when the trigger is already expanded), and the `SHORTCUTS_HELP` surface contract. `apps/web/tests/KeyboardShortcutsModal.test.tsx` (\~14 cases) covers null-when-closed, the aria-modal contract, four close paths (Escape / backdrop / inner-card stop-propagation / explicit button), listener cleanup after close, every help row surfacing, multi-key combo `<kbd>` rendering, category grouping, registered-count footer, and the `groupShortcuts` helper. Total test surface: \~383 → \~428.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `083e9139` | Phase 44 — Browser notifications for achievement unlocks. New `apps/web/src/lib/notifications.ts` (\~210 LOC) implements an opt-in notification surface with capability detection (`isNotificationsSupported`), permission state (`currentPermission`), permission request (`requestPermission`), throttled fire (`notify` with `NOTIFY_MIN_INTERVAL_MS = 1500` per category), achievement convenience (`notifyAchievement(achievement)` formats title + body with Phase 35 hero attribution + tags by achievement id), and test fire (`notifyTest`). Dual delivery path: active-tab uses the `Notification` constructor directly, background-tab routes through `ServiceWorkerRegistration.showNotification` when the document is hidden — same call site for both. New `apps/web/src/lib/useNotifications.ts` hook (\~70 LOC) subscribes to `useGameStore.achievements`, diffs new unlocks against a baseline snapshot (same pattern as Phase 36's `diffNewUnlocks`), and fires `notifyAchievement` for transition edges only — no re-fire on re-broadcast. Hook also kicks off `registerServiceWorker()` at mount (idempotent, no-op on non-secure origins). New `apps/web/public/sw.js` (\~95 LOC) handles `push` events (parses JSON payload, falls back to defaults on malformed data) and `notificationclick` (focuses an existing client or opens a new window, navigates to `data.url` when supported). `useSettingsStore` gains `notificationsEnabled: boolean` (persisted, default `false` — strict opt-in). `SettingsDrawer` gains a new "Notifications" section with a toggle that runs the permission flow on first enable, a live permission badge (granted / denied / awaiting / unsupported), and a "Send test" button that surfaces the throttle/permission state in a toast when delivery is suppressed. Tests at `apps/web/tests/notifications.test.ts` (\~24 cases), `apps/web/tests/settingsNotifications.test.ts` (\~7 cases), and `apps/web/tests/useNotifications.test.tsx` (\~7 cases) cover capability detection, permission state, request flow, throttle window, background SW path, click target, achievement formatting + attribution, SW registration guard, store persistence + hydration + reset, and hook diff semantics (no fire on seed, fire on transition edge, no re-fire on re-broadcast, no fire when disabled, no fire without permission, no replay on re-enable). Total test surface: \~345 → \~383.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `8415a1c`  | Phase 43 — Session rollup strip in TitleBar. New `apps/web/src/components/SessionRollup.tsx` (\~80 LOC) renders four aggregate metric pills in the existing TitleBar: `Σ missions` / `Σ tools` / `Σ tokens` (in + out, K/M-formatted) / `Σ cost` (USD, 2-decimal precision, `<$0.01` floor). Dismissed heroes excluded from every aggregate so a reaped agent doesn't inflate totals after they leave the roster. Strip is `hidden lg:flex` so the existing TitleBar layout stays uncrowded on narrow viewports. Pure `sessionStats(heroes)` helper exported with the same pattern as Phase 38/40/41 — drops dismissed heroes, tolerates missing `metrics` keys defensively, returns `{ missions, tools, tokensIn, tokensOut, cost, heroes, active }` so future strips can reuse it without re-walking the roster. Tests at `apps/web/tests/SessionRollup.test.tsx` (\~10 cases): pure helper sums across multiple heroes, drops dismissed, tracks active distinctly from live, tolerates missing metrics, empty-roster zeros; component renders all four pills with the right values + K/M ladder + sub-cent cost floor + accessible aria-label + zeros when empty. Total test surface: \~335 → \~345.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

## Build phases & status

* [x] **Phase 0 — Recon ingest.** Read historical intake now kept under `EXAMPLES/`, draft SPEC, draft this PROGRESS.
* [x] **Phase 1 — Skeleton commit.** README, RECON, SPEC, PROGRESS, LICENSE, packages/protocol.
* [x] **Phase 2 — Server.** /event, /health, /heroes, /missions, /notices, /scrolls, /achievements, /permission-response, /plan/approve, ws broadcaster, mock event generator.
* [x] **Phase 3 — Web shell.** Layout, TitleBar, HeroRoster, HeroCard, MissionLog, NoticeBoard, ScrollShelf, QuestBoard, FactionPicker, SettingsDrawer, ToastTray, Zustand store, ws client.
* [x] **Phase 4 — Battlefield.** Three.js scene with composed faction silhouettes (cylinder body + sphere head + box shoulders + plane cape), faction-specific keeps at the four compass points, real lighting (directional sun w/ shadows + blue rim + hemisphere fill), ACES tone mapping, state-tinted ground rings, orbiting particles for active heroes, rune circle for selected hero, edge fog.
* [x] **Phase 5 — Notice board + quests + achievements + plan workpad + permission modal.**
* [x] **Phase 6 — Static demo.** `public-demo/index.html` (zero-install showcase, single HTML, splash screen using `assets/showcase/heroes-banner.jpg`, mirrors the full battlefield).
* [x] **Phase 7 — War Room federation.** Cloudflare Worker + Durable Object with 6-letter room codes, leader/member/observer/pending roles, max 16 participants, ring buffer of last 100 events for catch-up, WebSocket fanout that excludes the sender.
* [x] **Phase 8 — CLI + hooks.** `factionos init/status/doctor/uninstall` + the 9 Claude Code hook handler scripts + `ws-listener.js` per-terminal bridge + automatic backup/restore of `~/.claude/settings.json`.
* [x] **Phase 9 — LLM engines.** All 5 maintained system prompts inlined as auditable `.md` files. Engines: planDecomposer (Sonnet, schema-strict), idleSuggestionEngine (Haiku, drops forbidden generic suggestions), transcriptMiner (content-hash dedupe), onDemandAnalysisEngine (5-issue cap, false-positive guards), autoImprover (off by default, 24h cool-down, 5-file budget, protocol freeze). Every engine has a canned fallback so the server runs offline.
* [x] **Phase 10 — Persistence.** Session archive (`~/.factionos/sessions/<date>.jsonl`, daily rotation) + memory store (`memory.jsonl` with content-hash dedup).
* [x] **Phase 11 — Achievement engine.** Real unlock triggers for all 7 baseline achievements (first-hero, first-mission, plan-completed, five-tomes, alliance-joined, hundred-missions, auto-improver). Subscribes to the broadcaster's onEvent channel and re-evaluates after every event.
* [x] **Phase 12 — Voice barks.** Web Audio synth fires per-faction barks on hero state transitions. Rate-limited to 1 bark per hero per 1.5 s; audio unlocked on first user pointerdown.
* [x] **Phase 13 — Art.** Painted hero banner generated and committed (`assets/showcase/heroes-banner.jpg`, \~2.9 MB). Used as the README hero image AND the demo splash background.
* [x] **Phase 14 — Faction portraits.** 5 painted bust portraits at `apps/web/public/portraits/<faction>.jpg` (\~120 KB total, 384×384 progressive JPEG). FactionPicker renders them as the primary visual; SVG emblem demotes to a corner badge. Demo HeroCards include them too via raw\.githubusercontent URLs. README gallery row added.
* [x] **Phase 15 — Codebase scan wrapper.** `apps/server/src/lib/codebaseWalker.ts` walks a directory tree with a strict ignore list (`node_modules`, `.git`, `dist`, lockfiles, sourcemaps, minified bundles, `EXAMPLES/`, directories named `findings`, and `extracted-artifacts/`), sorts by mtime DESC, truncates per file at 12 000 chars. `POST /llm/scan-codebase` wires it through `analyzeFiles` and emits two `mission_event` frames (`scan_started` + `analysis_complete`) so the web UI can show progress. Refuses to scan the filesystem root or follow symlinks.
* [x] **Phase 16 — Chat adapters.** New `apps/adapters/` workspace ships a shared bridge (`wsBridge` with exponential-backoff reconnect, `formatter` mapping 9 event types into severity-tagged Markdown, per-severity `ChannelThrottle`) plus two concrete senders — `discord/sender.ts` (webhook embeds, faction-coloured by severity) and `telegram/sender.ts` (Bot API `sendMessage`, MarkdownV2). Single dispatcher entry, `.env.example` documents the knobs (`FACTIONOS_WS_URL`, `FACTIONOS_ADAPTER_SEVERITY`, `FACTIONOS_ADAPTER_MIN_INTERVAL_MS`, `DISCORD_WEBHOOK_URL`, `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID`). One-way + best-effort + no inbound commands; documented in `apps/adapters/README_adapters.md`.
* [x] **Phase 17 — PWA demo shell.** `public-demo/sw.js` precaches the shell on install and applies stale-while-revalidate to the allowlisted CDN origins (`esm.sh`, `cdn.jsdelivr.net`, `cdn.tailwindcss.com`, `fonts.googleapis.com`, `fonts.gstatic.com`, `raw.githubusercontent.com`). `manifest.webmanifest` declares standalone display, theme colour, and 3 icon sizes (192/512/maskable-512). `index.html` gains the manifest link, apple-touch-icon, theme-color meta, and a load-time SW registration guarded to HTTPS / localhost. Icons rendered deterministically with Pillow from the existing favicon SVG (no extra binary asset drift).
* [x] **Phase 18 — Trophy room.** `apps/web/src/components/AchievementsGallery.tsx` ships a self-contained trigger + modal: TitleBar gets a compact `🏆 unlocked/total` button; clicking it opens a sorted gallery (unlocked first, then by tier weight desc). Locked rows redact the title to `··· ··` and reveal only the trigger name; unlocked rows show description + ISO `unlockedAt` formatted to the user locale. Tier-tinted ring around each medallion. Esc closes; click-outside closes; aria-labelled dialog.
* [x] **Phase 19 — Test scaffolding.** Root `vitest.config.ts` + `npm test` / `npm run test:watch` scripts. Five focused test files: `packages/protocol/tests/factions.test.ts` (5 tests), `packages/protocol/tests/achievements.test.ts` (5 tests), `apps/server/tests/codebaseWalker.test.ts` (9 tests, fs-tmpdir-backed), `apps/adapters/tests/formatter.test.ts` (11 tests, pure-function), `apps/adapters/tests/throttle.test.ts` (6 tests, fake-timers). v8 coverage configured against the source dirs; the historical artifact dumps under `EXAMPLES/` and `extracted-artifacts/` are excluded.
* [x] **Phase 20 — CI + integration tests.** `.github/workflows/test.yml` runs vitest on push/PR with coverage artifact upload. Eight new test files: `apps/server/tests/health.test.ts` (3 tests), `apps/server/tests/routes.test.ts` (8 tests), `apps/server/tests/permission.test.ts` (6 tests), `apps/adapters/tests/discordSender.test.ts` (4 tests), `apps/adapters/tests/telegramSender.test.ts` (5 tests), `apps/adapters/tests/config.test.ts` (10 tests), `packages/protocol/tests/events.test.ts` (8 tests), `packages/protocol/tests/scrolls.test.ts` (3 tests). Total test surface jumps from 36 -> \~83 cases.
* [x] **Phase 21 — Settings persistence.** New `useSettingsStore` (Zustand + manual localStorage round-trip) for theme/audio/severity-filter/reduce-motion/mock toggles. Redesigned `SettingsDrawer` (Esc + click-outside dismiss, Light/Dark/System radio, audio + reduce-motion toggles, per-severity buttons, reset buttons). `useThemeEffect` hook reconciles the `.dark` class + `colorScheme` on every preference change AND on `prefers-color-scheme` flips (system mode). `index.css` grew a light palette + scrollbar treatment + `data-reduce-motion="true"` escape hatch. `index.html` anti-FOUC bootstrap now respects `light` / `dark` / `system` keys explicitly. NoticeBoard + ToastTray honour the severity filter; useVoiceBarks reads the audio toggle live. `apps/web/tests/useSettingsStore.test.ts` covers defaults, hydration, corrupted JSON, persist round-trip, severity toggle, and `resolveTheme`.
* [x] **Phase 22 — Battlefield character refinements.** Each faction now mounts a per-faction `buildFactionAccessories` cluster on top of the existing body/head/shoulder/cape: orcs get two downward tusks + a chunky pauldron, humans get a kite shield + sword hilt, elves get a longbow with bowstring + a back quiver, undead get twin skeletal arms with icosahedron hands + a floating wisp, lobsters get two cylinder-arm + cone-claw assemblies + sweeping antennae. The animation loop now multiplies camera drift / hero bob / cape flutter / particle phase by `motionGain` so the persisted Reduce Motion preference actually quietens the scene.
* [x] **Phase 23 — LLM endpoint test coverage.** `apps/server/tests/llm.test.ts` adds 18 cases against the five LLM endpoints — happy paths land on the canned offline fallbacks so the suite is deterministic without an `ANTHROPIC_API_KEY`. Covers /llm/plan (400 on missing/whitespace prompt, 200 OR 503 with hint), /llm/analyze (400 on missing files, 200 with issues array, drops malformed entries), /llm/idle-suggestion (400 on missing heroId, 200 with suggestions), /llm/scan-codebase (400 on missing root, 400 on `/`, 200 against a tmpdir, 200 + hint when no files match the extensions), /memory + /memory/:type (200 happy paths, 400 on unknown type), plus a content-type guard.
* [x] **Phase 24 — README polish.** GitHub Actions status badge wired (now that CI exists), license badge, Node runtime badge, PRs-welcome badge. Settings table added documenting every persisted localStorage key. Stack section calls out the new per-faction accessories. Test count bumped to \~83.
* [x] **Phase 25 — Battlefield interaction.** Pointer-down + pointer-up on the canvas raycasts against every hero `root` group (each child mesh now carries `userData.heroId`, plus a wider invisible hitbox cylinder so small heroes are still easy to hit). Hits push the matching hero into `selectedHeroId`; empty terrain or re-clicking the same hero clears the selection. The camera transitions through four states: drift (ambient) → tween (cubic ease-out, \~700 ms) → follow (slow orbit around the focused hero at radius 5, y ≈ 3.8) → unfocus (eases back to the drift home). A floating name label projects the hero's world position to screen each frame, fades in over 200 ms, holds for 2.5 s, then fades out over 800 ms; it self-hides if the hero ends up behind the camera. Drag/long-press is rejected so the click handler doesn't fire on accidental gestures.
* [x] **Phase 27 — apps/web Vitest suite (happy-dom).** Root `vitest.config.ts` switches to `environmentMatchGlobs` so `apps/web/tests/**` files run under happy-dom while server/adapter/protocol tests stay on the cheaper `node` env. Added `happy-dom` + `@testing-library/react` + `@testing-library/dom` + `@testing-library/user-event` to root devDependencies. Three new `.test.tsx` files (\~36 cases total): `apps/web/tests/MissionLog.test.tsx` (11 cases — empty state, count pill, hero-id fallback, summary rendering, sort by `startedAt` DESC, severity-filtered list, "filtered" badge visibility, deduplication, tool-use pills), `apps/web/tests/ToastTray.test.tsx` (10 cases — null-when-empty, body optional, severity filter respect, return-null when everything filtered, dismiss button click, `slice(-4)` cap, per-kind tinted border classes, `aria-label="Dismiss"`, `role="status"` for live region), `apps/web/tests/AchievementsGallery.test.tsx` (15 cases — trigger button count, `aria-haspopup`, click-to-open, Escape closes, non-Escape keys don't close, backdrop click closes, inner card click doesn't close, close button works, unlocked count in header, redacted titles for locked rows, trigger names surfaced for locked rows, sort order (unlocked first then by tier weight desc), empty-state hint, tier badges with `aria-label="Tier: X"`, Escape listener cleanup after close). Total test surface: \~113 → \~149.
* [x] **Phase 26 — autoImprover golden tests.** `apps/server/tests/autoImprover.test.ts` adds 30 cases against `maybeRunAutoImprover`. Sections: **gating** (env-flag off-by-default, missing env, below-threshold for both failure rate + long-running, inclusive lower bounds, failure\_rate wins when both triggers fire), **output shaping** (branch / trigger / rationale / initialPrompt / candidateFiles shape, PROHIBITED\_PATHS filter for protocol freeze + workflows + extracted-artifacts, null when every candidate is banned, 5-file cap, null on empty/missing candidates, non-string coercion via String(), 400-char rationale + 1500-char initialPrompt truncation, synthesized rationale/initialPrompt when LLM omits), **safeParse fallbacks** (null on garbage, recovers from `json fences / plain` fences / brace bracket-match in prose), **24h cool-down** (exposes constants, blocks second fire inside window, re-fires after 24h:1s via `vi.setSystemTime`, doesn't fire at 23h:59m, **cool-down only consumed on successful fire** — garbage responses and all-banned candidates don't consume the lock). Mocks `llmClient.complete` via `vi.mock` so tests are deterministic without an `ANTHROPIC_API_KEY`. Engine exports a tiny test-only `__resetAutoImproverState()` + `__AUTO_IMPROVER_COOLDOWN_MS` + `__AUTO_IMPROVER_PROHIBITED_PATHS` surface so the test stays in sync with the prod constants. Total test surface: \~83 → \~113.
* [x] **Phase 32 — Replay scrubber.** `useGameStore` gains an `eventLog: ReplayEntry[]` ring buffer (cap 500 via exported `REPLAY_BUFFER_CAP`) — `applyEvent` pushes every dispatched event into it before the existing reducer runs. A `replayingSinceMs` flag gates capture so replayed events don't pollute the source buffer (no feedback loop). `replayLast(windowMs, speed)` filters by wall-clock cutoff and re-dispatches at the original cadence scaled by 1/speed (per-gap cap 1 s so a slow stream still moves). `clearEventLog()` wipes the log without touching game state. New `apps/web/src/components/ReplayPanel.tsx` (\~180 LOC) — bottom-left modal with `<picture>`-style window selector buttons (30 s / 2 m / 5 m), speed selector (1× / 2× / 4×), the headline ▶ Replay button (disabled while replaying or when no eligible events), a Clear-buffer button, and a chronological event list (last 50, with relative timestamps + event type + summary). Escape closes, backdrop click closes, inner-card click stops propagation. Opens via a new "⏯ Replay scrubber" button in `SettingsDrawer`'s "Dev / data" section that flips `useSettingsStore.replayPanelOpen` (transient — not persisted) and closes the drawer behind it. Mounted in `App.tsx` as a sibling to `<HeroDetailDrawer />`. Tests at `apps/web/tests/replayBuffer.test.ts` (\~7 cases): capture timestamps wall-clock-correct, FIFO eviction at 500, replay-time capture skip, `clearEventLog` is local, window filter picks only in-range events, immediate return when no events, replayingSinceMs lifecycle, replayed events don't re-enter buffer.
* [x] **Phase 31 — Codebase scan UI.** `apps/web/src/components/SettingsDrawer.tsx` gains a new "Codebase scan" section after the "Surface noise" section: a persistent root-path input field (with autocomplete + spellcheck off), a "Scan codebase" button, three live status pills (`walking…` amber / `✓ N files · M issues` emerald / `✗ failed` rose with the error message as the title attribute), and a summary line showing the previous run's truncated root path + duration. The button POSTs to `http://localhost:<serverPort>/llm/scan-codebase` with `{ root }` and routes the response through three toasts (info "Scan started" on dispatch, success/warn "Scan complete" on success keyed off issue count, error "Scan failed" on HTTP error / network throw). New `useSettingsStore.scanRoot` + `setScanRoot` keep the last-used root across reloads (persisted in the same `factionos-settings-v1` snapshot as the rest of the prefs). New `useGameStore.pushToast` selector for UI-originated toasts so the drawer doesn't have to synthesize `server_toast` events through `applyEvent`. Tests under `apps/web/tests/SettingsScan.test.tsx` (\~9 cases) cover empty-path disabled, typed input persistence, the POST URL + body shape, success-path rendering + toast, two failure-path renderings + toasts (4xx vs network throw), and the missing-`issues` defensive default. Total test surface: \~201 → \~210.
* [x] **Phase 30 — HeroDetail flyout.** New `apps/web/src/components/HeroDetailDrawer.tsx` (\~340 LOC) — when Phase 25's `selectedHeroId` resolves to a real hero, render a right-edge flyout drawer (460 px wide, full-height, rounded-left, with a translucent backdrop and the same Escape / backdrop-click / inner-card-stopPropagation pattern as `AchievementsGallery`). Sections: header with faction badge + name + state + cli + model, identity row (cwd / tty / spawned-at / last-active-at), 6-cell metrics grid (missions / tools / tokens in / tokens out / est cost / position), conditional active-mission preview with up to 5 tool-use pills, hero-scoped 5-most-recent mission list sorted by `startedAt` DESC with state chip + relative time + summary line, conditional achievement-contribution chip row (faction-mention + post-spawn filter), and a footer with a Speak button that fires a one-off `playBark(hero.faction, kind, 0.5)` Web Audio cue plus an explicit Close button. Mounted in `App.tsx` as a sibling to `<PlanWorkpad />`. Tests under `apps/web/tests/HeroDetailDrawer.test.tsx` (\~20 cases): null-state, identity surface, metrics rendering, active-mission show / hide, mission-history sort + 5-cap + cross-hero leakage absence, empty-state copy, all five dismissal paths, Speak button audio gating, achievement contributions (faction-mention AND post-spawn unlock), and listener-cleanup re-open. Total test surface: \~181 → \~201.
* [x] **Phase 28 — War Room E2E tests.** `apps/warroom/tests/warroom.test.ts` (\~21 cases) + `apps/warroom/tests/worker.test.ts` (\~11 cases). Approach: instead of standing up two `wrangler dev` workers (sandbox can't), the suite installs a thin in-process fake of `DurableObjectState` (Map-backed `storage.get`/`put`) and `WebSocketPair` (FakeWS with `accept` / `send` / `addEventListener` / `emitMessage` / `emitClose` / `failNextSend`) and exercises the **real** `WarRoom` class directly. Coverage: `/init` creates the room and rehydrates from storage; `/info` returns the snapshot, `/join` registers a pending participant + broadcasts `warroom_join_pending`, full-room (>=16) returns 409, `/approve` flips pending→member + broadcasts `warroom_join_approved` + `warroom_participant_change`, non-leader approve returns 403; `/socket` rejects without Upgrade / participant / on pending / on stranger; on accept emits `warroom_joined` + `warroom_participant_change`; reconnect replays the ring buffer (`warroom_pending_catchup`); **broadcast property — client message fans out to ALL peers EXCEPT the sender** (the federation guarantee the user asked for); ping frames don't pollute the ring buffer; unparseable client messages don't crash; send-failure on a peer triggers automatic disconnect + offline broadcast; ring buffer caps at 100 (101st evicts the oldest); persists every 5th event; CORS headers mirrored on DO-delegated responses; worker-level fetch handler matches the `[A-Z2-9]{6}` regex and rejects 5-letter/lowercase/banned-letter codes; different room codes create different DO instances (isolation). Total test surface: \~149 → \~181.
* [x] **Phase 29 — Animated battlefield strip.** `assets/showcase/factionos-strip.webp` (\~447 KB, 4 frames × 1280 × 714, 1.4 s per frame, infinite loop) — a 4-panel painted cycle generated via `GenerateImage` (Nano Banana Flash): **dusk muster** (heroes gathering around the rune circle, compass keeps in the distance) → **mid combat** (orc axe overhead, knight charging, elf drawing longbow, undead casting wisp, lobster snapping claws) → **victory chorus** (heroes celebrating, gold achievement particles rising, rune circle pulsing) → **night watch** (heroes resting by their faction keeps under twin moons, bonfires lit, rune circle faint). Each frame was downloaded via `FetchStoredFile`, resized to 1280 px wide, center-cropped to a common height, and stitched into an animated WebP via Pillow `save_all=True / duration=1400 / loop=0 / method=6 / quality=80`. Also shipped: `assets/showcase/factionos-strip-cover.jpg` (148 KB JPEG of frame 1 for clients that don't render animated WebP) and `assets/showcase/strip-frames/{1..4}-<name>.jpg` (\~95 KB each, the originals in case we want to remix). README's hero section uses `<picture>` with the WebP as primary source and the cover as fallback so the strip renders inline on GitHub *and* on RSS readers / Atom previewers that strip `<picture>`.
* [x] **Phase 59 — README sync to Phases 52-58.** Test count bumped from `~600` → `~718`. Six new deep-dive sections (Faction standings, Webhook adapter, Per-faction heatmap filter, Cost projection consolidating phases 55 + 57 + 58, Public demo refresh). "What's intentionally not built" closing sentence updated. Pure docs sync — no code or test changes.
* [x] **Phase 58 — FactionStandings UI projection chip.** Wires the Phase 57 `extrapolateCostByFaction` helper into `FactionStandings.tsx`: each row gets a `proj · 1h` chip (faction-tinted, gated on `hasSignal`), footer gets the fleet-wide `proj · 1h` total. Defensive `Object.values(missionsById ?? {})` keeps the component robust to test mocks that don't seed missions. Component JSDoc bumped to note the Phase 58 augmentation. Pure additive change — no helper or test edits.
* [x] **Phase 57 — Per-faction cost projection helper.** `apps/web/src/lib/factionCostProjection.ts` exports `extrapolateCostByFaction({heroes, missions, ...})` + `rankFactionsByProjection`. Partitions missions by their hero's faction, runs Phase 55's `extrapolateCost` per bucket, returns `{ rows: Record<FactionId, CostProjection>, total }` with defensive `ghost-hero` handling (drop from per-faction rows, keep in `total`). Ranker orders factions DESC by projected hourly cost with no-signal sink + canonical-order stable fallback. 14 new tests at `apps/web/tests/factionCostProjection.test.ts`. Pure helper — UI integration deferred to a follow-up phase, same shipping discipline as Phases 32 / 38 / 41 / 43 / 45 / 46 / 47 / 48 / 52 / 55.
* [x] **Phase 56 — Public demo refresh (v2).** `public-demo/index.html` rewritten from \~870 to \~1640 lines (\~102 KB) to surface the modern UI vocabulary added across Phases 30-55 without touching `apps/web`. New header tool row (🏆 trophy / 🗓 heatmap / ⚖ standings / ⌘K palette / ? shortcuts) sits next to a new `SessionRollup` strip (Σ missions / Σ tools / Σ tokens / Σ cost) mirroring the Phase 43 fleet rollup. Six new overlay modals — `AchievementsGallery` (recently-unlocked + tier sections + circular SVG progress ring), `MissionHeatmap` (24-hour × 7-day grid w/ fleet / faction / hero scope dropdown), `FactionStandings` (6-metric switcher, horizontal share bars, top-performer chip per faction), `CommandPalette` (Cmd/Ctrl+K, ranked search w/ prefix-100 / word-60 / substring-30 scoring, ↑↓ navigation, ⏎ activation, 11 stock commands), `KeyboardShortcutsModal` (full bindings cheatsheet), and a compact `ReplayPanel` with a Share-link copier. Global key handler implements the Phase 45 routeKey grammar (`?` shortcuts, `/` focus search, `r` replay, `s` settings hint toast, `g r/h/s` two-step prefix with 900ms window, ⌘K palette with Shift/Alt rejection, Escape dismissal, input-aware). `MissionLog` upgraded to the Phase 40 surface: 4-chip state filter (All / Active / Done / Failed) + free-text search + filter-empty copy, hero-scope still composes. Synthetic mission seed now backfills 7 days × 24 hours of weighted random missions (weekday-business-hours bias + weekend dampener + per-faction sampling) on first render so the heatmap has visible shape immediately. `SEED_ACHIEVEMENTS` ships 7 tier-ordered trophies with 2 unlocked + faction attribution (first-hero → orc, first-mission → human). Splash gets a `demo v2 · Phases 30-55` pill + "press ? for shortcuts" hint. Pure helpers (`buildHeatmapBuckets`, `tileOpacity`, `computeStandings`, `metricValue`, `formatStandingsValue`, `metricLabel`, `sessionStats`, `rankCommands`) inlined into the single-file demo so the visual + behavioural contract stays in sync with `apps/web/src/lib/*`. Zero `apps/web` touches, test surface unchanged at \~704.
* [x] **Phase 60 — Hero Leaderboard.** Fills the gap between HeroRoster (sorts but doesn't rank-visualize) and FactionStandings (aggregates per-faction, obscuring the individual carry-mode hero inside a busy house). New `apps/web/src/lib/heroLeaderboard.ts` ships the pure helpers (`buildLeaderboard`, `rankHeroes`, `metricValueFor`, `formatLeaderboardValue`, `leaderboardMetricLabel`, `countAchievementsPerHero`, `LEADERBOARD_METRICS`, `DEFAULT_LEADERBOARD_LIMIT = 10`); new `apps/web/src/components/Leaderboard.tsx` is the center-screen modal with a 5-pill metric selector and top-10 button rows that open HeroDetailDrawer. TitleBar gets a 🏅 Leaderboard button; command palette gains an `open:leaderboard` entry under Overlays; keyboard shortcut `g l` opens the modal (extends the existing `g r` prefix family). `useSettingsStore.leaderboardOpen` is the transient lifecycle flag. \~48 new tests across the pure helpers, the component lifecycle/dismissal/rendering, the keyboard-shortcut routing, and the command palette catalog. Total test surface: \~718 → \~766.
* [x] **Phase 61 — Mission Tag Inference helper.** `apps/web/src/lib/missionTags.ts` classifies missions into 8 intent-tags (`feature` / `bug` / `refactor` / `test` / `docs` / `config` / `chore` / `unknown`) by regex-matching the concatenated `prompt + summary + assistantText`. Word-boundary patterns avoid substring false positives (`bug` doesn't match `debugger`); inflection-aware (`fix` / `fixed` / `fixing` / `fixes`); multi-tag composition is the norm. Exports `inferTagsFromText`, `inferMissionTags`, `tagCountSummary`, `filterMissionsByTags`, `groupMissionsByTag` + the `MISSION_TAGS` canonical-order array + `missionTagLabel` + `missionTagAccentHex` (faction-color palette: elf green for feature, orc crimson for bug, etc.). Pure helper only — UI integration deferred to a follow-up phase under the Phase 57 → Phase 58 discipline. \~31 new tests covering pattern guarantees + inflection coverage + multi-tag composition + mission-level fallbacks + filter + group semantics. Total test surface: \~766 → \~797.
* [x] **Phase 62 — MissionLog tag-pill row.** Wires the Phase 61 inference into MissionLog as a second filter row below the Phase 40 state/search row. Eight pills with count badges (computed against the post-state/search/hero-scope slice so labels are honest), multi-select toggle, `× Clear tags` reset affordance, faction-themed active styling, dimmed zero-count pills. Composition order: hero-scope → state → search → tag. EmptyState honors `tagFilter.size > 0` so the "No missions match the current filter" copy fires when tags are responsible. \~11 new component tests cover pill count, aria-pressed defaults, count-badge math, single-click narrow, double-click toggle, multi-select union, Untagged isolation, Clear-tags lifecycle, state + tag composition, EmptyState wording, role="group" + aria-label. Total test surface: \~797 → \~808.
* [x] **Phase 63 — Mission Heatmap tag-pill row.** Extends Phase 48's `buildHeatmapBuckets` with an optional `tagFilter: Set<MissionTag> \| null` parameter — when non-empty, only missions whose inferred tags intersect the set contribute to the 7×24 calendar. Mirrors the Phase 62 MissionLog tag-pill row inside the heatmap modal: 8 pills (`Feature` / `Bug` / `Refactor` / `Test` / `Docs` / `Config` / `Chore` / `Untagged`) with faction-tinted accent dots, count badges computed against the POST-SCOPE / PRE-TAG slice (so each label is honest given the active scope filter), multi-select union semantics, `× Clear tags` affordance, dimmed zero-count pills, and a `role="group"` + `aria-label` container with `aria-pressed` per pill. Compositional filter chain is now hero (Phase 25) ∩ faction (Phase 54) ∩ tag (Phase 63) — with the hero filter still winning over faction when both are set. Both the scope `<select>` and the tag set reset to their unfiltered defaults every time the modal re-opens (matches the Phase 54 reset). Helper extension is \~25 LOC plus \~12 new helper-test cases (undefined / null / empty Set no-op semantics, single-tag narrow, multi-tag union, multi-tag mission via any-of, `unknown` bucket surface, zero-match returns zero peak, hero ∩ tag composition, faction ∩ tag composition, rollups respect the filter, input-Set immutability); component extension is \~120 LOC plus \~10 new component cases. Total test surface: \~808 → \~830.
* [x] **Phase 64 — Hero Specialization helper.** New `apps/web/src/lib/heroSpecialization.ts` ships the pure helpers (`summarizeSpecialization`, `summarizeFleetSpecialization`, `groupHeroesBySpecialization`, `specializationCaption`, `specializationRoleForTag`) + constants (`SPECIALIZATION_THRESHOLDS`, `GENERALIST_ROLE`, `GENERALIST_ACCENT`) + types (`SpecializationProfile`, `SpecializationInputs`, `SpecializationConfidence`, `SpecializationBucketKey`). Re-uses Phase 61's `inferMissionTags` to classify each hero's mission history into a primary + secondary tag, then translates the primary into a narrative role label (Feature Architect / Bug Hunter / Refactor Specialist / Test Engineer / Scribe / Operator / Quartermaster / Wandering Generalist). Generalist gate when no tag clears the dominance ratio (60% by default). Three confidence tiers gated on sample size + dominance — so a hero with 3 bug missions doesn't get crowned a "Bug Hunter" at full confidence. Pure helper only — UI integration to HeroDetailDrawer deferred to Phase 65 under the Phase 57 → 58 split discipline. \~36 new helper tests cover empty profiles, primary tag detection across every keyword class, secondary-tag distinct-from-primary, canonical-order tie-break, multi-tag missions in multiple buckets, prompt-less unknown bucket, share computation, generalist gating, three confidence tiers, role labels, fleet survey, group buckets (8 tags + generalist). Total test surface: \~830 → \~866.
* [x] **Phase 65 — HeroDetailDrawer Specialization section.** Wires the Phase 64 helper into the right-edge hero drawer. `apps/web/src/components/HeroDetailDrawer.tsx` gains a `<SpecializationSection profile={...} />` block between Metrics and the Phase 37 MissionTimeline. Renders: role chip (faction-tag accent border + tinted background) + confidence dot (solid/hollow/omitted by tier) + optional `also <secondary>` micro-chip + one-line caption (sans hero-name prefix) + horizontal distribution bar split into one segment per non-zero tag with `missionTagAccentHex` backgrounds and `aria-label` + sr-only label per segment. Section is conditionally rendered — heroes with zero missions get nothing (no "Specialization · no data" noise). Profile derived via `useMemo` over the same `heroMissionsAll` MissionTimeline already consumes so the section adds zero extra mission-iteration work. \~10 new component tests cover the zero-mission no-render, single-mission render, role-label match on specialized hero, generalist label on flat distribution, confidence-dot rendering, distribution bar segment count, segment aria-labels, caption format, secondary-tag chip, and hero-scoping isolation. Total test surface: \~866 → \~876.
* [x] **Phase 66 — HeroCard specialization chip.** Surfaces the Phase 64 role inference directly in HeroRoster cards so the user can scan-the-fleet at-a-glance without opening HeroDetailDrawer. `apps/web/src/components/HeroCard.tsx` imports `summarizeSpecialization` + adds a hero-scoped mission filter via `useMemo`. New compact `<SpecializationChip />` (\~50 LOC) reuses Phase 65's accent-on-tinted-background + confidence-dot semantics with smaller paddings + 9px font. Mounted between archetype/cli/model line and active-mission preview; conditionally rendered when `totalMissions > 0`. \~7 new component tests cover zero-mission no-chip, specialized hero (20 bug → Bug Hunter high confidence), generalist on flat distribution, low-confidence dot-omitted, hero-scoping isolation, aria-label, and coexistence with existing card content. Total test surface: \~876 → \~883.
* [x] **Phase 73 — HeroCard efficiency chip.** Mirrors the Phase 66 specialization-chip pattern: surfaces the Phase 71 `computeHeroEfficiency` profile directly on each HeroRoster card. New `<EfficiencyChip />` (\~55 LOC) renders the tier letter + 2-decimal score with the matching `TIER_COLOR` accent (falls back to `TIER_COLOR.B` cyan when the single-hero path returns `tier: null`). The previously-inline mission filter is extracted into a shared `heroMissions` memo so both Phase 66 specialization and Phase 73 efficiency consume the same O(n) slice. Spec + efficiency chips share a single flex row container. \~13 new component tests in `apps/web/tests/HeroCardEfficiency.test.tsx`. Total test surface: \~1091 → \~1104.
* [x] **Phase 84 — Mission Complexity command palette + keyboard shortcut.** Closes the Phase 81→82→83 feature train. `apps/web/src/lib/commandPalette.ts` widens `CommandHandlers` with `setComplexityOverlayOpen` and registers a new `open:complexity` entry under Overlays with a keyword set spanning the tier vocabulary (`quick`, `routine`, `heavy`, `epic`, `tier`, `tiers`) AND the user-facing question (`complexity`, `weight`, `difficulty`, `how hard`, `browse`). `apps/web/src/components/CommandPalette.tsx` threads the new handler through `buildCommands`. `useKeyboardShortcuts.ts` adds a `g x` sequence (`SHORTCUTS_HELP` entry placed next to `g t`, `ShortcutAction` widened with `"complexity_overlay_open"`, `routeKey` resolves the `g`→`x` prefix path, dispatch switch wires `setComplexityOverlayOpen(true)`). `commandPaletteRanking.test.ts`'s `noopHandlers` factory extended for type-checking the augmented `CommandHandlers`. \~30 new tests in `apps/web/tests/complexityOverlayDeepLink.test.tsx` cover catalog entry, ranker surfacing for multiple queries, `g x` routing including modifier/typing-target gates, and regression guards on the Phase 60 / 70 / 80 paths. Total test surface: \~1540 → \~1570.
* [x] **Phase 83 — Fleet Mission Complexity overlay modal.** Wires Phase 81's `aggregateMissionComplexity` rollup into a standalone center-screen modal (`apps/web/src/components/MissionComplexityOverlay.tsx`, \~370 LOC) reachable via the new ⛰ Complexity button in TitleBar. Store extension: `useSettingsStore` gains `complexityOverlayOpen` + `setComplexityOverlayOpen` (transient, mirrors Phase 48 / 52 / 60 / 79). Modal renders a 7-pill metric row, a `<table>` body with all-4-tier rows always present (tier accent dot + name in accent colour + Dominant badge + sub-line stats + headline value + inverted-when-lower-is-better share bar + 5-segment faction-share strip), and a footer with Σ missions / Σ tokens / avg score / failed (count + percent) + direction note. Modal contract matches every other overlay (role=dialog + aria-modal + aria-labelledby + Escape + backdrop close + inner-card stopPropagation). Lifecycle resets metric to "count" on each open. Empty fleet renders the "No missions yet" sentinel and `data-dominant-tier="none"`. \~33 new component tests in `apps/web/tests/MissionComplexityOverlay.test.tsx`. Total test surface: \~1507 → \~1540.
* [x] **Phase 82 — HeroDetailDrawer ComplexitySection.** Wires Phase 81's `aggregateMissionComplexityForHero` into a new section in HeroDetailDrawer mounted between the Phase 78 Tools section and the Phase 37 MissionTimeline. Section renders a dominant-tier chip (tier accent + label + `× N` counter), a midline-joined caption listing tier counts DESC, and a 4-cell stats grid (missions / avg score / heaviest tier / failed). Conditionally rendered when `report.totalMissions > 0`. \~28 new component tests in `apps/web/tests/HeroDetailDrawerComplexity.test.tsx`. Total test surface: \~1479 → \~1507.
* [x] **Phase 81 — Mission Complexity pure helper.** Opens a new feature train: classify every mission into a `quick` / `routine` / `heavy` / `epic` tier from a composite score over tool-count + token-volume + duration, then roll the tiers up per hero, per faction, and across the fleet. `apps/web/src/lib/missionComplexity.ts` (\~870 LOC) ships `classifyMissionComplexity`, `aggregateMissionComplexity`, `aggregateMissionComplexityForHero`, `complexityByFaction`, `complexityByHero`, `dominantTierForHero`, `rankComplexity`, `complexityCaption`, formatters (`formatComplexityValue` / `formatComplexityDuration` / `formatIntegerCount`), bar-share math (`complexityShare`), and accent helpers (`complexityTierColor` / `complexityTierLabel` / `complexityMetricLabel` / `complexityMetricLowerIsBetter`). Score uses a Hill saturating curve `(value/ceiling) / (1 + value/ceiling)` per axis (8 tools / 50K tokens / 10 min ceilings hit 0.5 at the inflection); composite weights default to 0.4 / 0.4 / 0.2; tier thresholds 0.20 / 0.45 / 0.75. Defensive rules mirror Phase 71 / 74 / 77: dismissed heroes drop by default, ghost missions still score but credit no faction, in-flight missions get durationAxis = 0, malformed inputs sanitise to zero / defaults. Pure helper only — UI integration deferred to Phase 82. Tests at `apps/web/tests/missionComplexity.test.ts` (\~88 cases across 14 describe blocks): constants + metadata, internal helpers via `__testing` (resolveWeights/Thresholds/Ceilings, saturatingNormalize at 0/ceiling/3×/9×/∞, parseTime, computeDuration clamp, pickDominantTier), classifier happy path (quick/routine/heavy/epic + faction resolution + ghost), defensive guards (null mission, missing toolUses, whitespace tool names, NaN/negative tokens, in-flight durationAxis, missionFailed flag, custom thresholds), aggregate fleet rollup (empty + non-array tolerance, per-tier counters, dismissed default + override, ghost factionShare null, share sums to 1, totalFailed accounting), hero-scoped, per-faction always-5, per-hero sorting + zero-mission inclusion, dominantTier dispatch, metric value + ranking direction (DESC default, ASC failed, tie-break ladder), share math (inversion for lower-is-better, \[0,1] clamp, NaN guards), formatters (ms/s/m/h + K/M ladders), caption (no-missions-yet sentinel + DESC + limit), realistic 4-faction scenario (orc heavy + quick, human routine, elf epic, undead dismissed dropped, custom weights skew classifications). Total test surface: \~1391 → \~1479.
* [x] **Phase 80 — Tool Usage command palette + keyboard shortcut.** Closes the Phase 77→78→79 feature train. `apps/web/src/lib/commandPalette.ts` widens `CommandHandlers` with `setToolUsageOpen` and registers a new `open:tool-usage` entry under Overlays with a keyword set spanning the tool noun (`bash`, `read`, `grep`, `edit`, etc.) AND the user-facing question (`tools`, `tooling`, `usage`, `which`, `audit`). `apps/web/src/components/CommandPalette.tsx` threads the new handler through `buildCommands`. `useKeyboardShortcuts.ts` adds a `g t` sequence (`SHORTCUTS_HELP` entry placed next to `g r` / `g l`, `ShortcutAction` widened with `"tool_usage_open"`, `routeKey` resolves the `g`→`t` prefix path, dispatch switch wires `setToolUsageOpen(true)`). `commandPaletteRanking.test.ts`'s `noopHandlers` factory extended for type-checking the augmented `CommandHandlers`. \~21 new tests in `apps/web/tests/toolUsageDeepLink.test.tsx` cover catalog entry, ranker surfacing for multiple queries, `g t` routing including modifier/typing-target gates, and regression guards on the Phase 60 / 70 / 76 paths. Total test surface: \~1370 → \~1391.
* [x] **Phase 79 — Fleet Tool Usage overlay modal.** Wires Phase 77's `aggregateToolUsage` into a standalone center-screen modal (`apps/web/src/components/ToolUsageOverlay.tsx`, \~420 LOC) reachable via the new 🛠 Tools button in TitleBar. Store extension: `useSettingsStore` gains `toolUsageOpen` + `setToolUsageOpen` (transient, mirrors Phase 48 / 52 / 60). Modal renders a 7-pill metric row, a "Show unused" toggle, a `<table>` body with per-tool rows (category accent + name + category badge + in-flight pill when pending > 0 + sub-line stats + headline value + inverted-when-lower-is-better share bar + 5-segment faction-share strip with FactionGlyph + proportional bar per FACTION\_IDS slot), and a footer with Σ calls / Σ tools / Σ heroes / overall success rate + direction note. Modal contract matches every other overlay (role=dialog + aria-modal + aria-labelledby + Escape + backdrop close + inner-card stopPropagation). Lifecycle resets metric to "calls" + showZero to false on each open. Empty fleet renders the "No tool calls yet" sentinel. \~35 new component tests in `apps/web/tests/ToolUsageOverlay.test.tsx`. Total test surface: \~1335 → \~1370.
* [x] **Phase 78 — HeroDetailDrawer ToolUsageSection.** Wires the Phase 77 helper into `apps/web/src/components/HeroDetailDrawer.tsx` as a new sub-section between Specialization and MissionTimeline. New `<ToolUsageSection />` (\~110 LOC) renders a top-tool chip with the category accent (cyan/amber/rose/lime/violet/slate per Phase 77's `TOOL_CATEGORY_COLOR`), a top-3 caption ("12 Bash · 8 Read · 5 Grep"), and a 4-cell stats grid (calls / success / unique tools / top tool avg dur). Conditionally rendered when `report.totalCalls > 0` (same data-honesty rule as Specialization + Efficiency). Hero-scoped via `aggregateToolUsageForHero` so sibling heroes' tool uses never leak in. Custom MCP tools paint with the slate "other" accent. Section uses `aria-labelledby` paired with the heading id and exposes `data-total-calls` on the root for tests. \~27 new component tests in `apps/web/tests/HeroDetailDrawerToolUsage.test.tsx`. Total test surface: \~1308 → \~1335.
* [x] **Phase 77 — Tool Usage pure helper.** New `apps/web/src/lib/toolUsage.ts` (\~870 LOC) opens a brand-new feature train: the first surface that exposes the *tool* axis (which tools the heroes actually lean on) rather than volume / tokens / discipline. Exports `aggregateToolUsage({heroes, missions, includeDismissed?})` — per-tool rollup with `calls` / `successCalls` / `failedCalls` / `pendingCalls` / `successRate` (null when nothing resolved) / `totalDurationMs` / `avgDurationMs` (null when no `endedAt` present) / `missionsUsing` / `heroesUsing` / `factionShare`. Companion: `extractToolUses` flattener with faction-attribution, `rankTools` for 7 metrics, `toolUsageByFaction` (all five FACTION\_IDS always present), `toolUsageByHero` (zero-call heroes still included so the UI can show "no tool calls yet"), `topToolForHero`, `aggregateToolUsageForHero`, `summarizeToolUsageReport`, plus formatters (`formatToolUsageValue`, `formatDurationMs`, `formatIntegerCount`) and the 7-category accent palette (`TOOL_CATEGORY_COLOR`, `toolCategoryFor`, `toolColorFor` — filesystem-read cyan, filesystem-write amber, shell rose, search lime, web violet, planning + other slate). `ordered` always seeds the 14 first-party `KNOWN_TOOL_NAMES` in canonical "read → write → run" flow even on a quiet day, then appends custom MCP tools by calls DESC + alpha tie-break. Defensive: dismissed heroes drop by default, ghost missions count calls but credit no faction, malformed `toolUses` arrays skip silently, negative-span clock skew clamps to zero, custom MCP tool names dedupe case-sensitively. Pure helper only — UI integration to HeroDetailDrawer + a standalone overlay deferred to Phase 78 / 79 under the established Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 / 74 → 75 split discipline. \~81 new tests in `apps/web/tests/toolUsage.test.ts` across 17 describe blocks cover constants/metadata, internal `__testing` helpers, `extractToolUses` edge cases, aggregateToolUsage empty/edge/duration/faction-share/MCP, ordering + rank tie-breaks, `metricValueForRow`, formatters, share-bar inversion, `aggregateToolUsageForHero`, `topToolForHero`, `toolUsageByFaction`, `toolUsageByHero`, `summarizeToolUsageReport`, and a realistic 4-faction scenario (orcs lean Bash, humans Read+Edit with pending Bash, elves Grep-heavy with a custom MCP tool, dismissed undead drops, ghost WebSearch credits no faction). Total test surface: \~1227 → \~1308.
* [x] **Phase 76 — Faction efficiency command palette + deep-link.** Mirrors the Phase 70 radar deep-link pattern for Phase 75's Efficiency view. `Cmd+K → "efficiency"` (or "lean" / "tier" / "score" / "discipline") opens the FactionStandings modal AND deep-links directly into Efficiency view without the user having to flip the view pill. Widens `StandingsView` in `useSettingsStore.ts` from `"table" | "radar"` to also accept `"efficiency"`, so the existing Phase 70 hint plumbing can carry the new value end-to-end. New `open:faction-efficiency` palette entry under Overlays; its `run()` calls `setStandingsInitialView("efficiency")` then `setStandingsOpen(true)` (same ordering Phase 70 uses for radar). `FactionStandings.tsx` open-transition effect's `nextView` ternary expanded to honour `"efficiency"` alongside `"table"` / `"radar"`; the Phase 75 local type widening was removed in favour of importing the now-canonical store type. Defensive: unknown hint values still collapse to `"table"`. \~16 new tests in `apps/web/tests/factionEfficiencyDeepLink.test.tsx`. Total test surface: \~1211 → \~1227.
* [x] **Phase 75 — FactionStandings Efficiency view.** Wires the Phase 74 `buildFactionEfficiency` helper into `apps/web/src/components/FactionStandings.tsx` as a third view alongside the Phase 52 Table and Phase 68 Radar. Adds a third "Efficiency" button to the existing view toggle. When Efficiency view is active, the metric pill row swaps from the 6-pill Phase 52 family to the 7-pill Phase 74 family (`avgScore` / `successRate` / `tokensPerMission` / `costPerMission` / `throughput` / `completedMissions` / `scoredHeroes`); pill `title` attributes call out direction. New `EfficiencyView` function (\~180 LOC) renders one row per faction with: rank pill + faction glyph + house name + summary line, headline metric value via `formatFactionEfficiencyValue`, share bar inverted for lower-is-better axes (leanest faction shows longest bar), tier-distribution chips (S/A/B/C/D with `TIER_COLOR` accent + count badge), top-profile chip (best-scoring live hero or "absent"/"no signal" marker), and a footer summarising fleet avg score + scored / total / closed counts + direction reminder. Empty + in-flight-only factions handled distinctly ("absent" vs "no signal"). View resets to "table" on re-open (matches Phase 68 default); efficiencyMetric resets to "avgScore". \~37 new component tests in `apps/web/tests/FactionStandingsEfficiency.test.tsx`. Total test surface: \~1174 → \~1211.
* [x] **Phase 74 — Faction Efficiency pure helper.** New `apps/web/src/lib/factionEfficiency.ts` (\~600 LOC) aggregates the Phase 71 per-hero `HeroEfficiencyProfile` into a per-faction view so future UI surfaces (Phase 75 FactionStandings "Efficiency" pill) can rank houses by per-mission discipline — the question Phase 52's volume-based standings can't answer. Per-row fields: `heroCount` (live only), `scoredHeroCount`, mission state breakdown (`totalMissions` / `completedMissions` / `failedMissions` / `inFlightMissions`), `totalTokens`, `totalCost`, `avgTokensPerMission` / `avgCostPerMission` (null when no missions), `successRate` (null when no closed missions), `throughputPerHour` (sum within faction), `avgScore` (mean of non-null per-hero scores), `tierCounts` (S/A/B/C/D distribution, sourced from fleet-wide tiering so cross-faction comparison is meaningful), `topProfile` (highest-scoring live hero from the faction). Exported surface: `buildFactionEfficiency`, `rankFactionsByEfficiency`, `metricValueForFaction`, `formatFactionEfficiencyValue`, `factionEfficiencyMetricLabel`, `factionEfficiencyMetricLowerIsBetter`, `factionEfficiencyShare`, `computeFactionEfficiencyFromHero`, plus types + `FACTION_EFFICIENCY_METRICS`. Seven metrics: `avgScore`, `successRate`, `tokensPerMission` (lower=better), `costPerMission` (lower=better), `throughput`, `completedMissions`, `scoredHeroes`. Tier source-of-truth is the fleet-wide `buildEfficiencyTable` (Phase 71) so a lone S-tier hero in one faction reads as S-tier without meaningless within-faction percentile gymnastics. Defensive rules mirror Phase 52 / 57: dismissed heroes drop from rows + cost; ghost missions (no resolvable hero) drop from per-faction rows but count in `total`; all five factions always appear (Phase 52 "empty rows make a point" convention); NaN / negative tokens guarded. `factionEfficiencyShare` inverts for lower-is-better metrics so the leanest faction shows the longest bar. Pure helper only — UI integration to FactionStandings deferred to Phase 75 under the Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 split discipline. \~70 new tests in `apps/web/tests/factionEfficiency.test.ts` cover empty inputs, dismissed exclusion, mission-state breakdown, token + cost aggregation, score + tier aggregation, fleet totals, all 7 metrics' rank direction, metric value extraction + null fallbacks, formatter ladders, label coverage, share-bar math, and a realistic 5-faction scenario (humans top on avgScore at 100% success + low tokens; orcs lead completedMissions but trail in score; undead poor success sinks; lobster in-flight-only stays null). Total test surface: \~1104 → \~1174.
* [x] **Phase 72 — HeroDetailDrawer Efficiency section.** Wires Phase 71's `computeHeroEfficiency` into the right-edge hero drawer between Metrics and Specialization. New `<EfficiencySection />` renders a tier chip (S/A/B/C/D with `TIER_COLOR` accent), the 2-decimal composite score, a one-line caption (e.g. "3/4 completed · 1.2K/mission · 90s avg · $0.12"), and a 4-cell stats grid (Success / Tokens-per-mission / Throughput / Avg duration). Tier falls back to B-colour when the single-hero `computeHeroEfficiency` path returns `tier: null` (fleet ranking only happens inside `buildEfficiencyTable`). Conditionally rendered when `efficiencyScore !== null` — heroes with only in-flight missions get nothing, mirroring the Phase 65 Specialization section's "zero missions = no render" rule. \~19 new component tests in `apps/web/tests/HeroDetailDrawerEfficiency.test.tsx` cover the conditional render, tier-chip data attributes, caption shape, stats grid cell count, hero scoping, aria semantics, and DOM ordering relative to the Specialization section. Total test surface: \~1072 → \~1091.
* [x] **Phase 71 — Hero Efficiency pure helper.** New `apps/web/src/lib/heroEfficiency.ts` (\~480 LOC) computes per-hero efficiency profiles (totalMissions / completedMissions / failedMissions / inFlightMissions / totalTokens / totalCost / avgTokensPerMission / avgCostPerMission / avgMissionDurationMs / successRate / throughputPerHour / efficiencyScore / tier). Composite efficiency score = `successRate × (1 / (1 + avgTokensPerMission / 5000))` — soft curve that pivots at 5K tokens-per-mission so a Haiku at 500 tokens pulls toward 1.0 while an Opus burning 15K bottoms at \~0.25. `buildEfficiencyTable({heroes, missions})` ranks the fleet S → A → B → C → D via percentile rank across scored heroes (in-flight-only heroes get tier=null and sort to the bottom). Where Phase 60's Leaderboard answers "who's done the most volume", this answers "who's most disciplined per mission" — a higher-fidelity signal for the "who do I delegate to" question. Pure helper only — UI integration to Leaderboard deferred to Phase 72 under the established split discipline. \~80 new tests in `apps/web/tests/heroEfficiency.test.ts` cover profile math, tier assignment, edge cases (single-hero fleet, in-flight-only heroes, dismissed exclusion), formatter ladders, and a realistic 3-hero scenario. Total test surface: \~992 → \~1072.
* [x] **Phase 70 — Faction radar command palette + deep-link.** `open:faction-radar` palette entry sets a new transient `useSettingsStore.standingsInitialView` hint then opens FactionStandings; the modal's open-transition effect consumes + clears the hint to land on Radar view directly. `open:standings` now explicitly resets the hint to `"table"` first. Bonus fix: `CommandPalette.tsx` was missing `setLeaderboardOpen` from its handlers object — now wired up. \~14 new tests in `apps/web/tests/factionRadarDeepLink.test.tsx`; `commandPaletteRanking.test.ts` noopHandlers factory extended.
* [x] **Phase 69 — README sync to Phases 60-68.** Top-level `README.md` rebased on the current feature surface; four new dedicated sections (hero leaderboard, mission tag inference + filters, hero specialization, faction radar); Phase 52 standings section rewritten for the Phase 68 Table / Radar toggle. Test count `~718` → `~978`. No code changes — pure docs sync (same shape as Phases 33 / 42 / 51 / 59).
* [x] **Phase 68 — Faction Radar UI integration.** `FactionStandings.tsx` gains a Table / Radar view toggle in the header; the radar mounts a 360×340 SVG with grid rings + spokes + per-faction polygons + vertex markers + clickable axis labels + a legend chip row + per-metric leader strip + one-line caption. Consumes Phase 67's `buildRadarLayout(rows, config)` via `useMemo` keyed on the standings rows. \~28 new component tests in `apps/web/tests/FactionStandingsRadar.test.tsx`.
* [x] **Phase 67 — Faction Radar pure helper.** `apps/web/src/lib/factionRadar.ts` ships `buildRadarLayout(rows, config)` + trig / shoelace / SVG-path helpers; produces an SVG-ready `RadarChartLayout` with axes + grid rings + per-faction polygons + areaShare + leader pick. Per-metric normalisation against the fleet-wide max keeps the chart legible. Pure helper only — UI integration deferred to Phase 68. \~67 new tests in `apps/web/tests/factionRadar.test.ts`.

## What's intentionally NOT shipped (and what's next)

* **Mission Complexity command palette + keyboard shortcut.** ✅ Shipped Phase 84 — closes the Phase 81→82→83 feature train. `Cmd+K → "complexity"` (or "tier" / "epic" / "how hard" / "weight" / "difficulty") opens the Phase 83 overlay via a new `open:complexity` catalog entry; `g x` keyboard sequence does the same via `useKeyboardShortcuts` (joins the `g r` / `g l` / `g t` navigation family). `CommandHandlers` widened with `setComplexityOverlayOpen`, `ShortcutAction` widened with `"complexity_overlay_open"`, SHORTCUTS\_HELP gains the new combo entry. \~30 new tests cover catalog, ranker surfacing, routing, and Phase 60 / 70 / 80 regression guards.
* **Fleet Mission Complexity overlay modal.** ✅ Shipped Phase 83 — `apps/web/src/components/MissionComplexityOverlay.tsx` is the dedicated browse-the-tiers surface, complementing the Phase 82 drawer drill-down. TitleBar gets a ⛰ Complexity button after the 🛠 Tools button. `useSettingsStore` gains `complexityOverlayOpen` + `setComplexityOverlayOpen` (transient). Modal layout: header with summary line ("Fleet skews heavy — 8 heavy, 5 routine, 3 quick · 16 missions"), 7-pill metric switcher (count / share / tokens / duration / tools / averageScore / failed), semantic `<table>` with all-4-tier rows always rendered (tier accent dot + tier label coloured in accent + Dominant badge on the dominant row + sub-line stats + headline value + share bar inverted for lower-is-better + 5-segment faction-share strip), footer with Σ missions / Σ tokens / avg score / failed count + percent. All modal contracts (role + aria-modal + Escape + backdrop close). Lifecycle resets to default metric on each open. Empty fleet renders "No missions yet" sentinel. \~33 new component tests.
* **HeroDetailDrawer ComplexitySection.** ✅ Shipped Phase 82 — wires the Phase 81 helper into the drawer between the Phase 78 Tools section and the Phase 37 MissionTimeline. Renders a dominant-tier chip with the tier accent (emerald / sky / amber / rose for quick / routine / heavy / epic), the tier label, and an `× N` mission count, plus a caption ("3 quick · 1 heavy" sorted DESC, collapses to a single entry when only one tier has missions) and a 4-cell stats grid (missions / avg score / heaviest / failed). Conditionally rendered when the hero has at least one mission — same data-honesty rule as Tools / Specialization / Efficiency. Dismissed heroes opened in the drawer still get the section (the drawer is showing that hero on purpose). \~28 new component tests cover conditional render, tier-chip dispatch for all four tiers, caption ordering + collapse, stats grid labels + values, cross-hero isolation, DOM ordering after Tools, aria contract, and state surfacing via data-\* attributes.
* **Mission Complexity pure helper.** ✅ Shipped Phase 81 — opens a new feature train alongside Phase 71-76 (efficiency) and Phase 77-80 (tool usage). `apps/web/src/lib/missionComplexity.ts` classifies every mission into a `quick` / `routine` / `heavy` / `epic` tier via a composite score over tool-count (0.4 weight) + token-volume (0.4 weight) + duration (0.2 weight), each axis individually normalised through a Hill saturating curve so a 30-second mission isn't artificially inflated to "half the ceiling". Default ceilings: 8 tools / 50K tokens / 10 min — calibrated so a 4-tool 13K-token 3-min mission lands in routine, a 12-tool 90K-token 12-min mission lands in heavy, and a 30-tool 400K-token 60-min mission lands in epic. Exports: `classifyMissionComplexity` (single-mission), `aggregateMissionComplexity` (fleet), `aggregateMissionComplexityForHero` (drawer-shaped), `complexityByFaction` (always-five-factions grid), `complexityByHero` (sorted DESC by missions w/ alpha tie-break), `dominantTierForHero` (single-chip), `rankComplexity` (7 metrics: count / share / tokens / duration / tools / averageScore / failed; only failed is lower-is-better), `complexityCaption` ("8 heavy · 14 routine · 3 epic"), `formatComplexityValue` + `formatComplexityDuration` + `formatIntegerCount` formatter ladder, `complexityShare` (bar-share with Phase 74-style inversion), accent helpers. Defensive rules mirror Phase 71 / 74 / 77 — dismissed heroes drop by default, ghost missions still score but credit no faction, in-flight missions get `durationAxis = 0`, NaN/negative inputs sanitise. \~88 new tests across 14 describe blocks. UI integration deferred to Phase 82 under the Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 / 74 → 75 / 77 → 78 split discipline.
* **Tool Usage command palette + keyboard shortcut.** ✅ Shipped Phase 80 — closes the Phase 77→78→79 feature train. `Cmd+K → "tool usage"` (or "bash" / "read" / "which tools" / "tooling") opens the Phase 79 overlay via a new `open:tool-usage` catalog entry; `g t` keyboard sequence does the same via `useKeyboardShortcuts` (joins the `g r` / `g l` navigation family). `CommandHandlers` widened with `setToolUsageOpen`, `ShortcutAction` widened with `"tool_usage_open"`, SHORTCUTS\_HELP gains the new combo entry. \~21 new tests cover catalog, ranker surfacing, routing, and Phase 60 / 70 / 76 regression guards.
* **Fleet Tool Usage overlay modal.** ✅ Shipped Phase 79 — `apps/web/src/components/ToolUsageOverlay.tsx` is the dedicated browse-the-tools surface, complementing the Phase 78 drawer drill-down. TitleBar gets a 🛠 Tools button next to Leaderboard. `useSettingsStore` gains `toolUsageOpen` + `setToolUsageOpen` (transient). Modal layout: header with summary line, 7-pill metric switcher, show-unused toggle, semantic `<table>` with per-tool rows (category accent + name + category badge + in-flight pill + sub-line stats + share bar inverted for lower-is-better + 5-segment faction strip), footer with Σ counts + overall success. All modal contracts (role + aria-modal + Escape + backdrop close). Lifecycle resets to default metric + show-zero false on each open. \~35 new component tests.
* **HeroDetailDrawer ToolUsageSection.** ✅ Shipped Phase 78 — wires Phase 77's `aggregateToolUsageForHero` into the right-edge hero drawer as a new section mounted between Specialization and the MissionTimeline. Renders a top-tool chip with the Phase 77 category accent (filesystem-read cyan / filesystem-write amber / shell rose / search lime / web violet / planning + other slate), a one-line top-3 caption, and a 4-cell stats grid (calls / success / unique tools / top tool avg dur). Conditionally rendered when `report.totalCalls > 0`; em-dash fallback for null sub-metrics (success when nothing resolved, top dur when no `endedAt`). Hero-scoped — sibling heroes' tool uses never leak in. Custom MCP tools paint with the slate "other" accent. \~27 new component tests.
* **Tool Usage pure helper.** ✅ Shipped Phase 77 — `apps/web/src/lib/toolUsage.ts` opens a brand-new feature train: the first surface that aggregates by the *tool* axis (which tools the heroes actually lean on) rather than volume / tokens / discipline. Walks every `mission.toolUses` and produces per-tool rows with `calls` / `successCalls` / `failedCalls` / `pendingCalls` / `successRate` / `totalDurationMs` / `avgDurationMs` / `missionsUsing` / `heroesUsing` / per-faction `factionShare`. Companion exports cover ranking across 7 metrics, per-faction + per-hero breakdowns, single-hero rollups, a top-tool convenience, a category accent palette (filesystem-read / write / shell / search / web / planning / other), and a one-line summary formatter. Defensive rules mirror Phase 71 / 74 (dismissed exclusion, ghost-mission attribution, clock-skew clamp, custom MCP case-sensitive dedupe). Pure helper only — UI integration deferred to Phase 78 (HeroDetailDrawer section) + Phase 79 (standalone overlay) under the established split discipline. \~81 new tests.
* **Faction efficiency command palette + deep-link.** ✅ Shipped Phase 76 — `Cmd+K → "efficiency"` (or any of "lean" / "tier" / "score" / "discipline" / "per-mission" / "tokens" / "throughput") opens FactionStandings and deep-links directly into Phase 75's Efficiency view via a widened `StandingsView` store type and the existing Phase 70 hint plumbing. Mirrors the Phase 70 radar deep-link pattern. Defensive: unknown hint values still collapse to `"table"`. \~16 new tests cover the catalog entry, deep-link behaviour, regression guards on the Phase 70 paths, and the store contract.
* **FactionStandings Efficiency view.** ✅ Shipped Phase 75 — wires Phase 74's `buildFactionEfficiency` into `FactionStandings.tsx` as a third view (Table / Radar / Efficiency). Metric pills swap to the 7-pill efficiency family when Efficiency view is active. Each row shows the headline metric value, an inverted-for-lower-is-better share bar (leanest faction shows longest bar), tier-distribution chips (S/A/B/C/D), and a top-scoring hero chip. Empty / in-flight-only factions distinguished via "absent" vs "no signal" markers. \~37 new component tests.
* **Faction Efficiency pure helper.** ✅ Shipped Phase 74 — `apps/web/src/lib/factionEfficiency.ts` aggregates Phase 71's per-hero efficiency profiles by faction so future UI (Phase 75) can rank houses by per-mission discipline. Exports `buildFactionEfficiency` + `rankFactionsByEfficiency` + `metricValueForFaction` + `formatFactionEfficiencyValue` + `factionEfficiencyMetricLabel` + `factionEfficiencyMetricLowerIsBetter` + `factionEfficiencyShare` + `computeFactionEfficiencyFromHero` plus the `FACTION_EFFICIENCY_METRICS` array and the per-row + per-totals types. Per-faction: heroCount + scoredHeroCount + mission-state breakdown + totalTokens + totalCost + avgTokensPerMission + avgCostPerMission + successRate + throughputPerHour + avgScore + tierCounts (sourced from fleet-wide ranking) + topProfile. Seven metrics with two flagged lower-is-better. Ghost missions count in `total` only; dismissed heroes drop everywhere; all five factions always present. Pure helper only — UI integration deferred to Phase 75. \~70 new tests.
* **HeroCard efficiency chip.** ✅ Shipped Phase 73 — surfaces the Phase 71 efficiency profile directly on each HeroRoster card via a new compact `<EfficiencyChip />` rendered alongside the Phase 66 specialization chip in the same flex row. Tier letter + 2-decimal score with `TIER_COLOR` accent. Conditionally rendered when `efficiencyScore !== null`. Shared `heroMissions` memo so both spec + efficiency memos consume one O(n) mission slice per card. \~13 new component tests.
* **HeroDetailDrawer Efficiency section.** ✅ Shipped Phase 72 — wires the Phase 71 helper into the right-edge hero drawer as a new section between Metrics and Specialization. Renders a tier chip (S/A/B/C/D with `TIER_COLOR` accent + tinted background), the 2-decimal `efficiencyScore`, a one-line caption (`3/4 completed · 1.2K/mission · 90s avg · $0.12`), and a 4-cell stats grid (Success / Tokens-per-mission / Throughput / Avg duration). Conditionally rendered when `efficiencyScore !== null` — heroes with only in-flight missions get nothing. \~19 new component tests.
* **Hero Efficiency pure helper.** ✅ Shipped Phase 71 — `apps/web/src/lib/heroEfficiency.ts` ships pure helpers (`computeHeroEfficiency`, `buildEfficiencyTable`, `compareEfficiencyProfile`, `computeEfficiencyScore`, `summarizeEfficiencyTable`, `percentileRank`, `tierForPercentile`, `missionDurationMs`, formatters) plus the `HeroEfficiencyProfile` / `EfficiencyMetric` / `EfficiencyTier` types and `TIER_PERCENTILE_GATES` / `TIER_COLOR` / `EFFICIENCY_METRICS` / `TOKEN_EFFICIENCY_PIVOT` constants. Per-hero profile carries totalMissions / completedMissions / failedMissions / inFlightMissions / totalTokens / totalCost / avgTokensPerMission / avgCostPerMission / avgMissionDurationMs / successRate / throughputPerHour / efficiencyScore / tier. Composite score is `successRate × (1 / (1 + avgTokensPerMission / 5000))` — soft curve pivoting at 5K tokens/mission with NaN / Infinity / negative-token defensive guards. Tier assignment via percentile rank across the fleet's scored heroes (live only); heroes with no closed mission stay at tier=null and sort to the bottom. Pure helper only — UI integration deferred to Phase 72. \~80 new tests.
* **Faction radar command palette + deep-link.** ✅ Shipped Phase 70 — `Cmd+K → "radar"` (or "spider" / "shape") opens the FactionStandings modal and lands directly in Phase 68's radar view via a new transient `useSettingsStore.standingsInitialView` hint that the modal consumes + clears on open. `open:standings` now explicitly resets the hint to `"table"` so it never inherits stale radar state. Also fixes a latent Phase 60 wire-up gap: `CommandPalette.tsx` was missing `setLeaderboardOpen` from its handlers object. \~14 new tests.
* **README sync to Phases 60-68.** ✅ Shipped Phase 69 — top-level `README.md` updated with dedicated sections for the hero leaderboard (Phase 60), mission tag inference + filters (Phases 61-63), hero specialization (Phases 64-66), and faction radar (Phases 67-68); Phase 52 standings section rewritten for the Table / Radar toggle; test count bumped `~718` → `~978`; "not built" closing paragraph extended to mention the new milestones shipped through Phase 68. No code changes — pure docs sync (same shape as Phases 33 / 42 / 51 / 59).
* **Faction Radar UI integration.** ✅ Shipped Phase 68 — `FactionStandings.tsx` ships a Table / Radar toggle in the header; Radar view mounts a 360×340 SVG with concentric grid rings, spokes, per-faction polygons (DESC by `areaShare` so larger paint underneath), vertex markers at non-zero levels, clickable axis labels that re-select the metric, a legend row with `data-empty` marker for absent factions, the one-line `radarCaption`, and a per-metric leader strip surfacing the "orcs lead missions but humans lead tools" insight explicitly. View choice resets on close. \~28 new component tests.
* **Faction Radar pure helper.** ✅ Shipped Phase 67 — `apps/web/src/lib/factionRadar.ts` exports `buildRadarLayout(rows, config)` + the supporting trig / shoelace / SVG-path helpers; produces an SVG-ready `RadarChartLayout` with axes + concentric grid rings + one polygon per FACTION\_IDS slot + per-metric normalisation against the fleet-wide max + `leader` convenience pick. Pure helper only — UI integration to FactionStandings.tsx deferred to Phase 68 under the Phase 57 → 58 / 64 → 65 split discipline. \~67 new tests covering normalisation guards, shoelace area, SVG path output, layout composition (empty, half-radius, balanced, degenerate single-metric), and a realistic 3-faction comparison.
* **Real FBX models + MP3 voice barks.** Those were licensed in the original; we replaced with SVG emblems + Web Audio synth + painted bust portraits.
* **Discord/Telegram channel adapters.** ✅ Shipped Phase 16.
* **Browser notifications for achievements (Phase 44).** ✅ Shipped Phase 44 — opt-in `Notification` surface in `apps/web/src/lib/notifications.ts` with throttled fire (1.5 s/category), active-tab `Notification` ctor path + background `ServiceWorkerRegistration.showNotification` path keyed on `document.visibilityState`, `useNotifications` hook bridges achievement unlocks, Settings drawer toggle runs the permission flow on first enable. `apps/web/public/sw.js` ships with `push` + `notificationclick` handlers ready for a future real-push backend.
* **PWA push notifications.** Out of scope for v1 (no push server yet) — but the SW + Notification plumbing landed in Phase 44, so adding a push backend is now a backend-only change.
* **Real codebase scan integration for `onDemandAnalysisEngine`.** ✅ Shipped Phase 15.
* **Per-faction individual portraits.** ✅ Shipped Phase 14.
* **CI on push/PR.** ✅ Shipped Phase 20.
* **SettingsDrawer localStorage persistence.** ✅ Shipped Phase 21.
* **Battlefield per-faction primitive variants.** ✅ Shipped Phase 22.
* **LLM endpoint test coverage.** ✅ Shipped Phase 23.
* **README polish.** ✅ Shipped Phase 24.
* **Battlefield interaction (click-to-select / camera tween / floating label).** ✅ Shipped Phase 25.
* **`autoImprover` golden tests (cool-down + 5-file budget + safeParse fallbacks).** ✅ Shipped Phase 26.
* **`apps/web` Vitest suite (happy-dom + component tests).** ✅ Shipped Phase 27.
* **War Room E2E test.** ✅ Shipped Phase 28 — in-process DO fake instead of `wrangler dev` since the sandbox can't install Cloudflare's CLI; the real `WarRoom` class is exercised.
* **Animated screenshot strip.** ✅ Shipped Phase 29 — 4-frame painted WebP cycle (`assets/showcase/factionos-strip.webp`) wired into the README hero via `<picture>`.
* **HeroDetail flyout wired to `selectedHeroId`.** ✅ Shipped Phase 30 — right-edge drawer with identity / metrics / active mission / mission history / contribution chips / Speak button.
* **Codebase scan UI surface.** ✅ Shipped Phase 31 — Settings drawer section with persistent root path, status pills, and toast lifecycle.
* **Replay scrubber.** ✅ Shipped Phase 32 — in-memory ring buffer + bottom-left modal with window/speed selectors, ▶ Replay, Clear, and a live timeline.
* **localStorage-persisted replay buffer.** ✅ Shipped Phase 34 — debounced 250 ms write of the trailing 100 events to `factionos-replay-v1`; replays survive a refresh.
* **Hero attribution on achievements.** ✅ Shipped Phase 35 — protocol gains `heroId` + `unlockedByHeroName` + `unlockedByFaction`; engine credits the responsible hero per achievement; HeroDetailDrawer + AchievementsGallery render the attribution.
* **Achievement celebration on the battlefield.** ✅ Shipped Phase 36 — gold particle bursts over the attributed hero (rune-circle centre for authorless unlocks); tier-tinted; 60 particles per burst, 6 concurrent cap, FIFO eviction.
* **Mission timeline visualizer in HeroDetailDrawer.** ✅ Shipped Phase 37 — pure-SVG 24-hour sparkline with mission-count bars + tool-use polyline overlay; faction-coloured fills; aria-labelled summary; mounted between Metrics and Active Mission preview.
* **HeroRoster activity sort toggle.** ✅ Shipped Phase 38 — header gains a `State` / `Active` segmented control; activity mode sorts by `metrics.lastActiveAt DESC` across all states, ties broken by `spawnedAt DESC`; state mode keeps the lifecycle bands but breaks ties inside each band by `lastActiveAt DESC` (was previously stable-insert order); choice persisted to localStorage and surfaced in `useSettingsStore.rosterSort`.
* **Mission-detail flyout.** ✅ Shipped Phase 39 — `MissionDetailDrawer.tsx` is a center-screen modal with prompt + 8-cell stats grid (started / ended / duration / tools / tokens in / tokens out / tokens total / plan?) + tool-use timeline (status pill, per-call duration, optional path + inputPreview) + streamed assistantText + conditional summary section. New `selectedMissionId` field on `useGameStore` drives it; MissionLog + HeroDetailDrawer mission rows are now native `<button>` elements that open the drawer.
* **MissionLog filter + search.** ✅ Shipped Phase 40 — header above the list ships a 4-chip state filter (All / Active / Done / Failed) and a free-text search box (prompt + summary + assistantText, case-insensitive); filters compose with the hero-scope filter; filter-empty state shows distinct copy.
* **AchievementsGallery tier sections + progress ring.** ✅ Shipped Phase 41 — flat list replaced with 4 collapsible tier sections + pinned "Recently unlocked" at the top + circular SVG progress ring in header; section collapse state is component-local and resets on modal re-open.
* **README sync to Phases 34-41.** ✅ Shipped Phase 42 — top-level `README.md` rewritten with dedicated sections for HeroRoster sort, MissionDetailDrawer, mission filter + search, AchievementsGallery redesign, battlefield achievement celebration, and the localStorage-persisted replay buffer; test count bumped to \~335; "Not built" list reduced to 3 items.
* **Session rollup strip in TitleBar.** ✅ Shipped Phase 43 — new `SessionRollup.tsx` mounted in TitleBar surfaces fleet-wide aggregates (`Σ missions / Σ tools / Σ tokens / Σ cost`) that previously required opening each hero drawer; dismissed heroes excluded; pure `sessionStats(heroes)` helper exported for reuse.
* **Browser notifications for achievements.** ✅ Shipped Phase 44 — `apps/web/src/lib/notifications.ts` + `useNotifications` hook + `SettingsDrawer` toggle + `apps/web/public/sw.js`; strictly opt-in (toggle defaults to `false`), throttled at 1.5 s per category, dual delivery path (active-tab `Notification` ctor + background `ServiceWorkerRegistration.showNotification`), tested across \~38 cases.
* **Keyboard shortcuts overlay.** ✅ Shipped Phase 45 — `useKeyboardShortcuts.ts` hook (document-level keydown listener, pure `routeKey` decision function, input-aware suppression, `g r` sequence combo with 1.2 s timeout) drives `?` / Esc / `/` / `r` / `s` / `g r` bindings; `KeyboardShortcutsModal.tsx` renders the help overlay with `<kbd>` chips per combo and category grouping; `useSettingsStore.shortcutsHelpOpen` is the transient lifecycle flag. Tests cover the routing matrix without a window plus the modal close paths and content surface.
* **Command palette (Cmd+K).** ✅ Shipped Phase 46 — `commandPalette.ts` exports the pure `buildCommands` catalog + `rankCommands` ranker; `CommandPalette.tsx` is a center-screen modal with auto-focused search input, ranked result list with keyboard nav (ArrowUp / ArrowDown / Enter / Esc) + click-to-activate + mouseEnter active-index; the only permitted modifier chord in `useKeyboardShortcuts.routeKey` is Cmd+K / Ctrl+K (Shift/Alt rejected to avoid Firefox devtools + Bookmarks collisions); `useSettingsStore.commandPaletteOpen` is the transient lifecycle flag; \~45 new tests across the ranker, the catalog builder, and the component.
* **Share-replay link.** ✅ Shipped Phase 47 — `apps/web/src/lib/replayLink.ts` exports `encodeReplayLink(entries)` + `decodeReplayToken(token)` + `readReplayHash(rawHash)` + `buildShareUrl(base, token)` + `dispatchReplayEntries(entries, sink, opts)` + `bootShareReplay(opts)`. Wire format is `#replay=v1.<base64url(JSON{v,t0,e:[[offsetMs,event]]})>` — versioned, offset-encoded for compactness, URL-safe (no `+/=`), capped at 200 entries. ReplayPanel gains a "Share link" button next to "Clear buffer" that encodes the current window-filtered slice (same predicate the Replay button uses), writes the resulting URL to `navigator.clipboard`, and shows a success/info/warn toast keyed off clipboard availability. App.tsx calls `bootShareReplay` on mount: if `window.location.hash` starts with `#replay=`, it strips the hash via `history.replaceState`, posts an info toast, dispatches the entries through `applyEvent` with cadence preserved (default 2× speed, per-gap cap 1s, `replayingSinceMs` set so the events don't re-enter the local ring buffer), then posts a success toast on completion. Decoder is strict-defensive: any malformed token returns `null` and never throws, so attacker-supplied URL fragments can't crash boot.
* **Wrangler dev integration test.** ✅ Shipped Phase 50 — `apps/warroom/tests/wranglerDev.test.ts` opt-in companion to Phase 28's in-process fake. Activates when `FACTIONOS_RUN_WRANGLER_DEV=1` AND `wrangler` resolves on PATH (4-second `--version` probe). Spawns the real `wrangler dev` against the workspace's `wrangler.toml`, waits for the "Ready on <http://127.0.0.1>:" log line, then hits `POST /<code>/init` against the live worker; afterAll kills the process unconditionally. Skips silently otherwise — sandbox / CI without the Cloudflare CLI runs the full in-process suite untouched. Drift detector for when the fake and the real Cloudflare runtime diverge.
* **CSV/JSON export endpoint.** ✅ Shipped Phase 49 — `apps/server/src/lib/exportSession.ts` exports `buildSessionExport` (single entry point), `sessionToCsv` (RFC-4180-compliant two-section CSV: heroes + missions), `sessionToJson` (pretty-printed `{exportedAt, schemaVersion, counts, heroes, missions}`), plus pure `escapeCsvField` / `heroesToCsv` / `missionsToCsv` / `isExportFormat`. `apps/server/src/routes/export.ts` is a 5-line shell that registers `POST /export/session?format=csv|json` and emits the right Content-Type + Content-Disposition headers. `apps/web/src/lib/exportSession.ts` is the frontend trigger — POSTs to the server, reads the Blob, injects a hidden `<a download>` anchor, clicks it, revokes the URL; IO deps are injected through an optional `customDeps` argument so the unit tests run without network. Command palette gains "Export session as CSV" + "Export session as JSON" entries; `exportWithToast(format, port, pushToast)` fires the info/success/error toast lifecycle. \~40 new tests cover RFC-4180 escaping, hero/mission CSV column order, JSON round-trip, the HTTP route (200 / 400 / Content-Disposition / Content-Type / X-FactionOS-Export-Schema header / body-vs-query format), and the frontend trigger (URL shape, anchor lifecycle, network-throw failure path, toast emission).
* **Mission heatmap (24×7 calendar).** ✅ Shipped Phase 48 — `apps/web/src/lib/missionHeatmap.ts` exports a pure `buildHeatmapBuckets({missions, nowMs?, heroId?})` that lays out a 7-row × 24-column grid for the last week with per-cell mission counts + tool-use sums + `peakCell` / `hottestDay` / `hottestHour` rollups, plus a `tileOpacity(count, peak)` helper that maps counts to the SVG fill ramp. `apps/web/src/components/MissionHeatmap.tsx` is the center-screen modal: SVG grid with sparse hour labels (every 3 hours), day + date labels per row, faction-coloured tiles (accent follows the optional hero-filter dropdown, neutral steel for fleet-wide), `<title>` tooltips per cell, a footer with hottest-day + hottest-hour rollups and a 5-step intensity legend. Opens from a new "🗓 Heatmap" button in TitleBar plus the command-palette entry "Open mission heatmap"; `useSettingsStore.heatmapOpen` is the transient lifecycle flag (matches the existing `commandPaletteOpen` / `shortcutsHelpOpen` / `replayPanelOpen` pattern); the hero filter resets to "all" every time the modal re-opens so a stale filter never surprises the user. \~39 new tests across the helper math and the modal component.
* **Faction standings comparison.** ✅ Shipped Phase 52 — `apps/web/src/lib/factionStandings.ts` exports the pure `groupByFaction({heroes, achievements})` partitioner + `rankFactions(rows, metric)` sorter + `metricValue` / `metricShare` / `formatStandingsValue` / `metricLabel` / `computeStandings` helpers; `apps/web/src/components/FactionStandings.tsx` is a center-screen modal with a 6-pill metric selector, one row per faction (rank pill + glyph + counts + share bar + top-performer chip), and a footer with Σ tokens / Σ cost / Σ tool-calls. Opens from a new "⚔ Standings" button in TitleBar plus the command-palette entry "Open faction standings"; `useSettingsStore.standingsOpen` is the transient lifecycle flag. Achievement credit gates on both `unlockedAt` and the Phase 35 `unlockedByFaction` snapshot (authorless unlocks stay uncredited). Top performer per faction is the live hero with the highest `missionsCompleted` with `toolCallsTotal` as the tie-breaker. \~45 new tests across the helper math and the modal component.
* **Cost projection helper.** ✅ Shipped Phase 55 — `apps/web/src/lib/costProjection.ts` exports `extrapolateCost({missions, nowMs?, windowMs?, costPerMtokIn?, costPerMtokOut?})` plus `formatProjectedCost` + `projectionLabel` helpers; SessionRollup gains a `proj · next 1h` pill gated on `hasSignal`; \~21 new tests covering window filtering, projection math, defensive guards, and the formatter ladder.
* **README sync to Phases 52-58.** ✅ Shipped Phase 59 — top-level `README.md` rebased on the current surface: test count `~600` → `~718`, six new deep-dive sections (Phase 52 faction standings, Phase 53 webhook adapter, Phase 54 per-faction heatmap filter, Phases 55 + 57 + 58 cost projection, Phase 56 demo v2 refresh), closing list updated. Pure docs sync — same shape as Phase 33 / 42 / 51.
* **FactionStandings UI projection chip.** ✅ Shipped Phase 58 — `FactionStandings.tsx` consumes the Phase 57 helper via `useMemo` and renders a per-row `proj · 1h` chip plus a footer fleet total, both gated on `hasSignal`. No new tests; the math is already covered by Phase 57's `factionCostProjection.test.ts`.
* **Per-faction cost projection helper.** ✅ Shipped Phase 57 — `apps/web/src/lib/factionCostProjection.ts` exports `extrapolateCostByFaction({heroes, missions, ...})` and `rankFactionsByProjection`; runs Phase 55's `extrapolateCost` per faction bucket; unresolvable hero ids drop from per-faction rows but still count in `total`; 14 new pure-helper tests. UI wiring deferred to a follow-up.
* **Public demo refresh (v2).** ✅ Shipped Phase 56 — `public-demo/index.html` adds the modern UI vocabulary from Phases 30-55 (trophy room with progress ring + tier sections + recently-unlocked, 24×7 mission heatmap w/ fleet / faction / hero scope, faction standings w/ 6-metric switcher, Cmd+K command palette w/ ranked search + ↑↓ nav, shortcuts cheatsheet, compact replay panel w/ share-link copier), a `SessionRollup` strip in the header (Σ missions / tools / tokens / cost), a 4-chip filter + free-text search on the mission log, and a full Phase 45 keyboard grammar (`?` `/` `r` `s` `g r/h/s` `Esc` `⌘K`). Splash gets a `demo v2 · Phases 30-55` badge. Synthetic seed now backfills 7 days × 24 hours of weighted missions so the heatmap has shape on first paint. Pure-helper logic mirrors `apps/web/src/lib/*` (`buildHeatmapBuckets`, `computeStandings`, `rankCommands`, `sessionStats`) inlined into the single-file demo so the visual + behavioural contract stays in sync. No `apps/web` or test changes.
* **Per-faction mission heatmap filter.** ✅ Shipped Phase 54 — `buildHeatmapBuckets` gains `factionId?` + `heroes?` inputs; `MissionHeatmap.tsx` swaps its hero-only `<select>` for a unified scope dropdown with optgroups (`faction:<id>` + `hero:<id>` encoded values). When `heroId` and `factionId` are both set the narrower hero filter wins. \~7 new helper test cases + 1 new component test case; existing tests updated to the new encoded value format.
* **Generic webhook adapter.** ✅ Shipped Phase 53 — apps/adapters grows a third bridge alongside Discord + Telegram. New `apps/adapters/src/webhook/sender.ts` exposes pure `buildWebhookPayload` + `buildWebhookHeaders` + `sendToWebhook(cfg, msg, opts)` with full vs. compact payload shapes, optional auth header, and a fetch-abort timeout. New `apps/adapters/src/webhook/run.ts` mirrors `discord/run.ts` (startBridge → formatEvent → ChannelThrottle → sendToWebhook). Dispatcher picks `webhook` when `FACTIONOS_WEBHOOK_URL` is set; package.json gains `npm run webhook`; .env.example documents every new env var; README rewritten with a dedicated "Generic webhook" section and worked examples. \~29 new tests across the sender and the config reader.
* **HeroCard specialization chip.** ✅ Shipped Phase 66 — `apps/web/src/components/HeroCard.tsx` adds a compact `<SpecializationChip />` between the archetype line and the active-mission preview. Same data-honesty rule as the drawer (conditionally rendered when totalMissions > 0). Same Phase 65 accent semantics with smaller paddings + 9px font. Lets users scan-the-fleet at-a-glance to spot the Bug Hunter, Feature Architect, etc. without opening the drawer. \~7 new component tests.
* **HeroDetailDrawer Specialization section.** ✅ Shipped Phase 65 — wires Phase 64's `summarizeSpecialization` into the right-edge hero drawer between Metrics and MissionTimeline. Role chip + confidence dot + secondary-tag micro-chip + caption + horizontal distribution bar. Conditionally rendered (zero-mission heroes get nothing). \~10 new component tests.
* **Hero Specialization helper.** ✅ Shipped Phase 64 — `apps/web/src/lib/heroSpecialization.ts` ships pure helpers (`summarizeSpecialization`, `summarizeFleetSpecialization`, `groupHeroesBySpecialization`, `specializationCaption`, `specializationRoleForTag`) + constants (`SPECIALIZATION_THRESHOLDS`, `GENERALIST_ROLE`, `GENERALIST_ACCENT`). Re-uses Phase 61 tag inference to classify heroes into roles (Bug Hunter, Feature Architect, Refactor Specialist, etc.) with three confidence tiers + generalist gating. \~36 new tests. UI integration deferred to Phase 65 under the Phase 57 → 58 split.
* **Mission Heatmap tag-pill row.** ✅ Shipped Phase 63 — `apps/web/src/lib/missionHeatmap.ts` gains an optional `tagFilter: Set<MissionTag> | null` parameter on `buildHeatmapBuckets`; `apps/web/src/components/MissionHeatmap.tsx` mounts a tag-pill row mirroring Phase 62's MissionLog row (8 pills with faction-tinted accents, count badges against the post-scope/pre-tag slice, multi-select union, `× Clear tags` affordance, dimmed zero-count pills). Filter chain is now hero ∩ faction ∩ tag. \~12 new helper tests + \~10 component tests. Total test surface \~808 → \~830.
* **MissionLog tag-pill filter row.** ✅ Shipped Phase 62 — MissionLog gets a second filter row below the Phase 40 state/search row. Eight tag pills (feature / bug / refactor / test / docs / config / chore / Untagged) with count badges computed against the post-state/search/hero-scope slice, multi-select toggle, `× Clear tags` reset, faction-themed active styling, dimmed zero-count pills. Composes with hero scope → state → search → tag. \~11 new component tests.
* **Mission tag inference helper.** ✅ Shipped Phase 61 — `apps/web/src/lib/missionTags.ts` ships pure helpers (`inferTagsFromText`, `inferMissionTags`, `tagCountSummary`, `filterMissionsByTags`, `groupMissionsByTag`) plus the `MISSION_TAGS` array and `TAG_PATTERNS` regex map. Classifies missions into 8 intent-tags (`feature` / `bug` / `refactor` / `test` / `docs` / `config` / `chore` / `unknown`) by keyword regex match against prompt + summary + assistantText. Multi-tag composition (a "refactor the failing tests" mission tags as both `refactor` AND `test`) + inflection coverage + word-boundary patterns + fallback to `unknown`. \~31 new tests. UI integration deferred to a follow-up phase (Phase 57 → 58 split pattern).
* **Hero leaderboard.** ✅ Shipped Phase 60 — `apps/web/src/lib/heroLeaderboard.ts` ships `buildLeaderboard` / `rankHeroes` / `metricValueFor` / `formatLeaderboardValue` / `leaderboardMetricLabel` / `countAchievementsPerHero` + the `LEADERBOARD_METRICS` array. `apps/web/src/components/Leaderboard.tsx` is the center-screen modal with a 5-pill metric selector (missions / tools / tokens / cost / achievements), top-10 button rows that open HeroDetailDrawer, share bars scaled to the post-limit table max, and a footer with Σ + leader name. TitleBar gets a 🏅 Leaderboard button; command palette gains `open:leaderboard`; keyboard shortcut `g l` opens it (same prefix family as `g r` for trophy room). Tie-break ladder: value DESC → `metrics.lastActiveAt DESC` → name ASC. Achievement attribution honors Phase 35 `heroId` snapshot with `unlockedByHeroName` as the pre-Phase-35 fallback. \~48 new tests.

## Backlog (ordered)

1. ~~**HeroRoster activity sort.**~~ ✅ Shipped Phase 38 — header toggle (`State` / `Active`) drives the order; activity mode is `metrics.lastActiveAt DESC`; persisted to localStorage; aria-pressed reflects mode.
2. ~~**Mission-detail flyout.**~~ ✅ Shipped Phase 39 — center-screen modal with prompt + stats grid + tool-use timeline + streamed assistantText + summary; backed by new `selectedMissionId` store field; MissionLog + HeroDetailDrawer mission rows are now native `<button>` elements that open the drawer.
3. ~~**Browser notifications for achievements.**~~ ✅ Shipped Phase 44 — `notifications.ts` + `useNotifications` hook + SW + Settings toggle; opt-in, throttled, dual delivery path.
4. ~~**Keyboard shortcuts overlay.**~~ ✅ Shipped Phase 45 — `useKeyboardShortcuts` hook with pure `routeKey` decision function + `KeyboardShortcutsModal` help overlay; `?` toggles help, `Esc` closes any overlay, `/` focuses mission search, `r` toggles replay, `s` opens settings, `g r` opens trophy room; input-aware; \~45 new tests.
5. ~~**Command palette (Cmd+K / Ctrl+K).**~~ ✅ Shipped Phase 46 — pure `buildCommands` catalog + `rankCommands` ranker in `commandPalette.ts`; `CommandPalette.tsx` modal with auto-focused search input, ranked list, keyboard nav, click + hover support; Cmd+K wired into `useKeyboardShortcuts` (Shift/Alt rejected to avoid devtools / bookmarks collisions); \~45 new tests.
6. ~~**Share-replay link.**~~ ✅ Shipped Phase 47 — `replayLink.ts` exports the pure encode/decode/dispatch/boot helpers; ReplayPanel gains a "Share link" button next to "Clear buffer"; App.tsx consumes `#replay=v1.<token>` on mount. \~45 new tests across the ranker-free pure helpers + the ReplayPanel button.
7. ~~**Mission heatmap (24×7 calendar).**~~ ✅ Shipped Phase 48 — `apps/web/src/lib/missionHeatmap.ts` exports `buildHeatmapBuckets({missions, nowMs?, heroId?})` + `tileOpacity(count, peak)`; `apps/web/src/components/MissionHeatmap.tsx` is a center-screen modal opened from a new 🗓 Heatmap button in TitleBar (and the command palette); fleet-wide by default with a hero-filter dropdown that scopes the grid; \~39 new tests across the pure helper and the component.
8. ~~**CSV/JSON export endpoint.**~~ ✅ Shipped Phase 49 — `apps/server/src/lib/exportSession.ts` exports the pure CSV/JSON formatters; `apps/server/src/routes/export.ts` mounts `POST /export/session?format=csv|json` on the live server; `apps/web/src/lib/exportSession.ts` exposes `triggerSessionExport` + `exportWithToast` so the command palette can launch a real browser download. \~40 new tests across the server formatters, the HTTP route, and the frontend trigger.
9. ~~**Wrangler dev integration test.**~~ ✅ Shipped Phase 50 — `apps/warroom/tests/wranglerDev.test.ts` opts in via `FACTIONOS_RUN_WRANGLER_DEV=1` AND a `wrangler --version` probe; spawns the real `wrangler dev`, waits for the ready-line on stdout, exercises `POST /<code>/init` end-to-end, and kills the process on teardown. Skips silently when not opted in or not installed — keeps the default `npm test` fast and quiet.

**Original backlog items shipped through Phase 50.** Phase 51 followed up with a README sync to reflect Phases 43-50. Phase 52 (faction standings comparison modal) shipped as the first in-line follow-up under the "Future phases" rubric. Future phases should continue from this section.

### Open follow-ups (newest first)

10. ~~**Demo refresh.**~~ ✅ Shipped Phase 56 — `public-demo/index.html` rewritten with header tool row (🏆 trophy / 🗓 heatmap / ⚖ standings / ⌘K palette / ? shortcuts), six new overlay modals matching `apps/web` (AchievementsGallery w/ progress ring + tier sections, MissionHeatmap w/ scope dropdown, FactionStandings w/ 6-metric switcher, CommandPalette w/ ranker, KeyboardShortcutsModal, compact ReplayPanel), MissionLog filter + search, SessionRollup strip, full keyboard grammar, "demo v2" splash badge. 7-day × 24-hour weighted mission seed gives the heatmap visible shape immediately.
11. ~~**Webhook adapter.**~~ ✅ Shipped Phase 53 — `apps/adapters/src/webhook/{sender,run}.ts` POSTs the formatted event to any URL that accepts JSON; auth-header, payload-shape, and timeout knobs are env-configurable; \~29 new tests.
12. ~~**Per-faction filter for the mission heatmap.**~~ ✅ Shipped Phase 54 — `buildHeatmapBuckets({factionId, heroes})` + unified scope dropdown with `faction:<id>` / `hero:<id>` optgroups in `MissionHeatmap.tsx`; hero filter wins when both are set; \~8 new tests.
13. ~~**Cost projection helper.**~~ ✅ Shipped Phase 55 — `extrapolateCost({missions, windowMs?, costPerMtokIn?, costPerMtokOut?})` extrapolates the trailing 15-minute window to hourly + daily projections; SessionRollup gains a `proj · next 1h` pill gated on `hasSignal`; defensive guards on every input; \~21 new tests.

## Demo

Published in-thread: `[[ARTIFACT_3mh3x4rm]]`. Opens with a splash screen using the hero banner; the "Enter the demo" button reveals the full UI with the new battlefield. Synthetic event loop runs the heroes/missions/scrolls/notices entirely client-side.

## How to resume after an interrupt

1. Read this file.
2. Run `GITHUB_GET_REPOSITORY_CONTENT` on the repo root — compare against the **Commits on main** above to see what already shipped.
3. Pick the first item from "Backlog" that hasn't been struck through.
4. Build, commit via `GITHUB_COMMIT_MULTIPLE_FILES` (use `paramsFile` for any payload over a few KB; pass binary entries with `encoding: "base64"`), then update this file.
5. Mention the new commit SHA in the **Commits on `main`** table.

## Sandbox constraints — KEEP IN MIND

* `git clone` and `git push` fail with proxy 407.
* `npm install` fails with 403 from `registry.npmjs.org`. We cannot run `vite`, `tsc`, or `eslint` to validate.
* Cannot ship FBX/TGA/MP3 binary assets at full quality. Use SVG emblems + Web Audio synth + \~120 KB compressed JPEGs for portraits.
* Commit via `GITHUB_COMMIT_MULTIPLE_FILES` with `paramsFile` for large payloads. Use `encoding: "base64"` for binary uploads.
* GitHub secondary rate limit kicks in after rapid writes; batch commits to \~30+ files where possible. After a 403 rate limit, wait \~5 minutes.

## Decisions log (newest first)

* **2026-05-19 (Phase 84 — Mission Complexity command palette + keyboard shortcut):** Picked **the same `g <letter>` sequence family for `g x`** (joining `g r` / `g l` / `g t`) rather than a dedicated single-key chord like `c` or `x` because the `g <letter>` family is already a "navigate to an overlay" mnemonic. A lone `c` would either fight typing (single-letter shortcuts must respect `inTypingTarget`) or claim a reserved character without a coherent family. A lone `x` carries baggage from the broader UI grammar (close button, cancel buttons) — the `g` prefix disambiguates. Picked **`g x` over `g c` (where "c" stands for "complexity")** because `c` is too easy to confuse with the existing `Cmd+K` and the future possibility of `g c` meaning "go to commands". `x` is the most distinctive remaining letter that doesn't collide with the existing family, and "X marks the spot" reads as a complexity marker (where the mountain is on the map). The visual of an X-shaped reticle also reads as "weight / load / where the heavy lifting is". Picked **open-only semantics (not toggle) for `g x`** mirroring `g r` / `g l` / `g t` — the muscle memory for `g <letter>` across the app is "navigate to that surface", not "toggle it". Esc / backdrop / close-button dismiss the overlay, which is the standard pattern. Toggle would surprise the user who taps `g x` twice expecting "go to complexity twice" but gets the overlay closing instead. Picked **`open:complexity` palette entry directly invokes `setComplexityOverlayOpen(true)` rather than going through a `standingsInitialView`-style hint mechanism** mirroring the Phase 80 reasoning: Phase 83's overlay is a STANDALONE modal, not a view INSIDE another modal — there's no parent state to deep-link into. Phase 70 / 76 use the hint pattern because their overlays are views inside FactionStandings; Phase 84's overlay is its own modal, so the entry just opens it. Picked **the keyword set spans both the tier vocabulary AND the user-facing question** (`quick` / `routine` / `heavy` / `epic` / `tier` next to `complexity` / `weight` / `difficulty` / `how hard`) because users come at the modal from two distinct intents: sometimes "I want to see how many epic missions we've done" (tier-first), sometimes "how complex is the work the fleet is doing?" (question-first). A narrow keyword set covering only one of those would silently drop the other intent from the ranker. Same reasoning as Phase 76's "lean / tier / score / discipline" expansion and Phase 80's "bash / read / grep / edit" + "tools / usage / which" expansion. Picked **`weight class` as a TWO-WORD keyword** rather than splitting into `weight` + `class` separately because the tokenizer scores compound terms higher when matched verbatim, and "weight class" is the term that bridges the boxing / tier metaphor. Picked **regression-guard tests for Phase 60 / 70 / 80 paths inside the Phase 84 file** because the test fixture is identical (same `CommandHandlers` interface, same `routeKey` rules) — a single accidental edit to either layer could silently break either the Phase 84 entry OR the older entries without a test catching it. Co-locating the guards in the new file means a Phase 70 regression caught the same turn Phase 84 ships, not after the bug ships. Picked **`SHORTCUTS_HELP` entry placed directly after `g t`** rather than alphabetically (which would put `g x` last in the `g <letter>` family) because the help overlay is grouped by category and the user reads it as a sequence — having `g r` / `g l` / `g t` / `g x` together visually communicates "these four are the same kind of action". Picked **typing-target gate honored for `g x`** mirroring `g r` / `g l` / `g t` — the existing `if (inTypingTarget)` early-return in `routeKey` blocks single-letter chords from interfering with text input, and `g`-prefix chords are no different. A user typing `gx` in the mission search field shouldn't accidentally open the complexity overlay. Picked **modifier-key gate also blocks `g x`** because any modifier (Ctrl / Alt / Meta) is treated as a non-shortcut chord by `routeKey`'s opening guard, so `Ctrl+G Ctrl+X` (a future browser binding, hypothetically) won't accidentally route through the prefix machinery. Picked **catalog tests assert top-5 placement for queries** rather than top-1 because the ranker has a stable tie-break ladder that could shift across phases — asserting top-5 captures "the entry is reachable on the most natural query" without being brittle to future ranker tweaks. Picked **the `noopHandlers` factory in `commandPaletteRanking.test.ts` widens to include `setComplexityOverlayOpen: vi.fn()`** rather than letting the existing 200+ catalog tests fail with a type error. This is the canonical pattern Phase 70 / 80 established: any `CommandHandlers` widening MUST come with a `noopHandlers` extension in the same commit, so the existing test surface keeps compiling. Without the extension, the entire suite would type-error on the missing field even though no individual test exercises it. Picked **a `ShortcutAction` compile-time membership assertion in the routing test** (assigning the string literal to a `ShortcutAction` variable and asserting it equals itself) as a defensive check — if a future refactor narrows the union without adding `complexity_overlay_open`, the assignment fails type-check.
* **2026-05-19 (Phase 83 — Fleet Mission Complexity overlay modal):** Picked **a standalone center-screen modal** rather than a view inside the existing FactionStandings modal (which carries the Phase 67 radar + Phase 75 efficiency views) because the data is fleet-wide-per-tier, not per-faction. FactionStandings shows "how do my 5 factions compare on one axis"; the complexity modal answers "how do my 4 tiers compare across the whole fleet". Crowding the two into one modal would conflate axes. Picked **all four tier rows always rendered** even when some tiers are zero (mirrors Phase 79's `KNOWN_TOOL_NAMES` seeding) because the user needs to see "we haven't reached epic yet" as much as "we've done 8 epics" — the absence is part of the signal. Empty rows render with `opacity: 0.25` on the share bar so they're visually muted but still present. Picked **⛰ Complexity glyph** rather than 📊 or 🎚 because ⛰ visualises the "weight class" / "mountain to climb" metaphor that the tier names (quick → epic) communicate, and 📊 is too generic (could read as "charts" / "analytics" / "data" — overlapping with Tools). Picked **TitleBar button placement after 🛠 Tools** rather than before Leaderboard because the natural reading order of the title bar is now "fleet identity → standings → leaderboard → tools → complexity" — broad → narrow → specific. Mirrors how the data flow in HeroDetailDrawer reads: Specialization → Tools → Complexity. Picked **summary line format "Fleet skews — "** rather than "Total: 16 missions" because dominantTier is the actionable signal — the user opens the modal to ask "what tier are we operating at?" and the summary should answer that question in the first three words. Counts come after as the supporting detail. Picked **Dominant badge inside the dominant tier row** rather than only signalling via `data-dominant=true` because visual users need a textual cue too (screen-reader-friendly + immediately scannable). Same pattern Phase 79 uses for the in-flight `× N live` pill. Picked **metric reset to "count" on each open** rather than persisting the last-picked metric because the most common entry vector will be "what's the tier distribution?" — landing on a different metric (e.g. failed) would force the user to re-orient on every open. Same Phase 60 / 79 lifecycle convention. Picked **share bar inversion via `complexityShare(value, metricMin, true)`** mirroring Phase 74's faction-efficiency and Phase 79's tool-usage inversions: when ranking by `failed`, the *least* failed tier should show the longest bar (so "you're doing well at this" reads correctly). Single dispatch point so the helper's `complexityMetricLowerIsBetter` flag can never drift from the bar math. Picked **footer surfaces failed share percent** (e.g. "failed 3 (15%)") rather than just the count because percentages contextualise — 3 failed of 100 is a healthy fleet; 3 failed of 5 is alarming. The user shouldn't have to do the math. Picked **`data-dominant-tier` on the modal root** rather than only marking the row because the modal root attribute lets a future Phase 84 deep-link assert "did opening the modal land on the right dominant tier?" without scraping row state. Mirrors Phase 79's `data-metric` + `data-direction` surfacing. Picked **never-truncate rows in the table even with 30 custom tools / 4 tiers** because 4 is bounded — the table will never be more than 4 rows tall, so a fixed height with no scroll is fine (Phase 79 needed scroll because tool count is unbounded). Saves a scroll-on-card-click ergonomic issue. Picked **dominant badge title text "The heaviest tier this fleet has reached"** to communicate to a hovering user *why* this tier is marked — "Dominant" alone is ambiguous (most common? heaviest? both?), and the title clarifies. Picked **happy-dom + @testing-library/react component tests** for the same reason Phase 82 used them — the modal's behavior (Escape / backdrop / inner-card stopPropagation / metric reset on open transition / data attribute surfacing) requires a real DOM.
* **2026-05-19 (Phase 82 — HeroDetailDrawer ComplexitySection):** Picked **mount between the Phase 78 Tools section and the Phase 37 MissionTimeline** rather than between Specialization and Tools because the visual rhythm matters: Specialization → Tools → Complexity is a "what kind / which tools / how hard" sweep that reads top-to-bottom like a question-and-answer. Putting Complexity above Tools would force the reader to know the tier verdict before seeing the underlying tool list, which is backwards — the user wants to see the tools used (the *evidence*) before the tier label (the *summary*). Picked **conditionally render the section when `totalMissions > 0`** rather than always rendering it because a brand-new hero with no missions yet would otherwise show "0 missions · 0.00 score · — · 0 failed", which is honest-but-noisy. Same data-honesty rule the Phase 78 ToolUsage section uses (`totalCalls > 0`), Phase 72 Efficiency (`efficiencyScore !== null`), and Phase 65 Specialization (`totalMissions > 0`). An empty section is worse than no section. Picked **dismissed heroes still get the section in drawer mode** mirroring the Phase 78 pattern: when the user explicitly opened the drawer for that hero, dropping the section would gaslight them. The helper's `aggregateMissionComplexityForHero` already passes `includeDismissed: true` so this just works. Picked **dominant-tier chip as the headline visual** rather than an `averageScore` value because tier names ("Heavy", "Epic") communicate the verdict to a non-technical user better than a number ("0.61"). The 4-cell stats grid still surfaces the score for power users. Mirrors Phase 71 / 72's "tier label THEN score" hierarchy. Picked **4-cell stats grid layout** (missions / avg score / heaviest / failed) rather than a 3-cell or 5-cell grid because every section in this drawer uses `grid-cols-4` (Tools, Efficiency, Specialization, Metrics) — visual consistency matters for the rhythm. Picked **caption "3 quick · 1 heavy" by tier count DESC** rather than alphabetic or canonical tier order because DESC by count matches the user's natural read ("what's the dominant tier? How many of those vs. the other?"). Canonical order (always quick / routine / heavy / epic) would read backwards when the hero is dominated by heavy missions. Picked **single-entry caption when only one tier has missions** ("2 quick" with no separator) rather than emitting a midline for a single value ("2 quick · ") because trailing/leading midlines look broken; the chip already shows the tier, so the caption can be terse. Picked **× N counter shows the count for the DOMINANT tier specifically** (not the total missions count) because the chip's purpose is to communicate "your fleet reaches Heavy — and 2 of them did". Showing total missions there would conflict with the missions stat cell below. Picked **data-dominant-tier + data-total-missions on the section root** for test + a11y consumption mirroring Phase 78's data-\* surfacing. Tests assert these directly rather than scraping the chip's visible text. Picked **aria-label format `Dominant complexity <Label>: <N> mission(s)` with plural/singular handling** rather than a static singular label ("1 missions" reads wrong; "1 mission(s)" is jarring). Same plural rule the Phase 78 chip uses for `call(s)`. Picked **inline 4-cell stats grid** rather than a separate `<MetricsBlock>`-shaped component because the grid is too small to factor out (\~12 lines) and the inline shape matches how Phase 78's ToolUsage stats grid is written. Picked **format avg score with `formatComplexityValue(value, "averageScore")`** rather than calling `.toFixed(2)` inline because the helper centralises the format ladder — if Phase 83 / 84 want to swap to a 1-decimal score format, one edit propagates everywhere. Picked **happy-dom + @testing-library/react component tests** rather than vitest-only assertions because the section's behavior (conditional render, DOM ordering after Tools via `compareDocumentPosition`, aria-labelledby paring) requires a real DOM and React's render lifecycle. Mirrors Phase 65 / 66 / 72 / 78's test split.
* **2026-05-19 (Phase 81 — Mission Complexity pure helper):** Picked **a per-mission "weight class" axis** (quick / routine / heavy / epic) as the next feature train because the existing dense helper coverage answers every other observability question — *when* (heatmap), *who* (leaderboard), *what kind* (mission tags), *which role* (specialization), *how lean* (efficiency), *which tools* (tool usage) — but nothing exposes *how heavy* a single mission actually is. A "Bug Hunter" hero (Phase 64) can ship epic missions or snack missions; a Bash-heavy faction (Phase 77) can ship routines or epics; complexity is orthogonal to every existing axis. Picked **4 tiers (quick / routine / heavy / epic)** rather than 3 (light/medium/heavy) or 5 (trivial/quick/routine/heavy/epic) because 3 tiers under-discriminates (a 4-tool mission and a 12-tool mission both land in "medium") and 5 over-discriminates (the bottom tier becomes so rare it's a debugging-mode thing, not a UI surface). 4 gives clean naming, clean colour palette (emerald / sky / amber / rose), and a sensible distribution against the synthetic seed. Picked **a composite score over three sub-axes** rather than a single-axis classifier (e.g. just tool count, or just tokens) because each axis catches a different pathology: a quiet "ask Claude to write a haiku" mission has 0 tools but 8K tokens; a "run npm test" mission has 1 tool but 200 tokens and 90s duration; a "refactor src/foo.ts" mission has 12 tools / 80K tokens / 12 min. A single-axis classifier would mis-tier at least one of these. Picked **weights 0.4 tool / 0.4 token / 0.2 duration** rather than a uniform 0.33 / 0.33 / 0.33 because tool count and token volume are the *active* signals (the agent is doing work), while duration is a *passive* signal (could be slow LLM response, could be the user thinking). Down-weighting duration means the score reflects what the agent did, not just how long it took. Picked **a Hill saturating curve** `f(x) = (x/c) / (1 + x/c)` rather than a log curve or a `min(x/c, 1)` clamp because the log curve over-inflates small inputs (a 30-second mission against a 30-minute ceiling ends up at \~0.7 on the duration axis with log-norm, which is absurd), while the clamp loses gradient above the ceiling (a 30-tool mission paints identically to a 16-tool mission at ceiling=15). Hill keeps the bottom honest (30s against 10min ceiling → 0.05), the ceiling at exactly 0.5 (the "this is a real chunk of work" inflection), and the top differentiated (3× ceiling → 0.75, 9× → 0.9, asymptotic to 1). Picked **ceilings of 8 tools / 50K tokens / 10 minutes** rather than the originally-drafted 15 / 150K / 30min because the 15-tool ceiling was too generous — most real missions in the synthetic seed have 3-7 tool calls, so the median mission was scoring as quick when it should be routine. Recalibrated against four test cases that map cleanly: a 4-tool/13K-token/3-min mission lands at score ≈ 0.25 (routine), a 12-tool/90K-token/12-min mission lands at 0.61 (heavy), a 30-tool/400K-token/60-min mission lands at 0.84 (epic). Picked **tier thresholds 0.20 / 0.45 / 0.75** rather than 0.25 / 0.5 / 0.75 or 0.20 / 0.40 / 0.70 because the score distribution is right-skewed (most missions cluster in the lower scores; epics are rare). 0.20 / 0.45 / 0.75 gives wider routine and heavy buckets where most missions live, and a narrow epic bucket that's the alarm signal ("you're tackling something big"). Picked **in-flight missions get `durationAxis = 0`** rather than measuring `now - startedAt` because using "now" would silently penalise long-lived idle missions — a mission stuck in `awaiting_approval` for an hour shouldn't be tiered as epic just because the user hasn't clicked Approve yet. Mirrors the Phase 71 "don't libel an unresolved mission" rule. The score still gets the tool + token contributions, which honestly reflect the work the agent has done so far. Picked **dismissed heroes drop by default** mirroring Phase 52 / 74 / 77 — the rollup is about *active* fleet state, so a hero that was retired three days ago shouldn't show in the per-faction grid unless the user explicitly opts in via `{includeDismissed: true}`. But **single-hero scoping keeps dismissed heroes** because the drawer is showing that hero's data on purpose; dropping their missions there would gaslight the user. Picked **ghost missions still score** but credit `null` faction rather than dropping silently because a mission with an unresolvable `heroId` (hero was deleted from the roster but the mission lingers in the log) is real work that happened; dropping it would understate the fleet's totalMissions and pickerflate the per-faction totals. Carrying `faction = null` through to the tier rollup means `factionShare` totals are honest (`Σ factionShare ≤ count`) and the empty bucket signals the data-quality issue without crashing. Picked **`resolveThresholds` sorts out-of-order tier boundaries** rather than rejecting them because a malformed override (e.g. `{quickMax: 0.8, routineMax: 0.2, heavyMax: 0.5}`) silently mis-classifying every mission as quick is a far worse user experience than the override silently being "corrected" to monotonic. Same defensive philosophy as Phase 71's `resolveWeights` normalising to sum 1. Picked **`saturatingNormalize` over the originally-named `logNormalize`** rename after recognising the math no longer used a log curve at all — the helper name should match the math, not the brainstorming-phase name. Future readers grepping `logNormalize` will find no false positives. Picked **88 unit tests with no React mount** because the entire classifier + aggregator surface is pure math against the protocol types; happy-dom would add 1-2 seconds of boot time per test without catching any bug the .ts tests can't. UI integration in Phase 82 ships its own .tsx tests against the drawer section. Picked **`__testing` export of internal helpers** rather than re-exporting them at the top level because callers should never reach for `saturatingNormalize` directly — it's an implementation detail of the score math. Tests need it for granular assertions; runtime callers should use `classifyMissionComplexity` instead. Mirrors Phase 77's `__testing.blankRow` / `parseTime` pattern.
* **2026-05-19 (Phase 80 — Tool Usage command palette + keyboard shortcut):** Picked **the same `g <letter>` sequence family for `g t`** (joining `g r` / `g l`) rather than a dedicated single-key chord like `t` because the `g <letter>` family is already a "navigate to an overlay" mnemonic — `g r` for trophy room, `g l` for leaderboard, and now `g t` for tools. Adding a lone `t` would either fight typing (single-letter shortcuts must respect `inTypingTarget`) or claim a reserved character without a coherent mnemonic family. Picked **`g t` over `g w` (where "w" stands for "workshop" matching the TitleBar's 🛠 icon)** because the user has just learned the word "Tools" from the TitleBar button + the modal heading, and the natural keyboard mnemonic for that word is `t`. Forcing `w` would create a discoverability friction the help overlay would have to actively fight against. Picked **open-only semantics (not toggle) for `g t`** mirroring `g r` and `g l` — the muscle memory for `g <letter>` across the app is "navigate to that surface", not "toggle it". Esc / backdrop / close-button dismiss the overlay, which is the standard pattern. Toggle would surprise the user who taps `g t` twice expecting "go to tools twice" but gets the overlay closing instead. Picked **`open:tool-usage` palette entry directly invokes `setToolUsageOpen(true)` rather than going through a `standingsInitialView`-style hint mechanism** because Phase 79's overlay is a STANDALONE modal, not a view INSIDE another modal — there's no parent state to deep-link into. Phase 70 / 76 use the hint pattern because their overlays are views inside FactionStandings; Phase 80's overlay is its own modal, so the entry just opens it. Picked **the keyword set spans both the tool noun AND the user-facing question** (`bash` / `read` / `grep` / `edit` next to `tools` / `usage` / `tooling` / `which`) because users come at the modal from two distinct intents: sometimes "I want to see which Bash calls happened" (noun-first), sometimes "which tools is the fleet using" (question-first). A narrow keyword set covering only one of those would silently drop the other intent from the ranker. Same reasoning as Phase 76's "lean / tier / score / discipline" expansion. Picked **regression-guard tests for Phase 60 / 70 / 76 paths inside the Phase 80 file** because the test fixture is identical (same `CommandHandlers` interface, same `routeKey` rules) — a single accidental edit to either layer could silently break either the Phase 80 entry OR the older entries without a test catching it. Co-locating the guards in the new file means a Phase 70 regression caught the same turn Phase 80 ships, not after the bug ships. Picked **`SHORTCUTS_HELP` entry placed directly after `g l`** rather than alphabetically by combo (which would put `g t` last in the `g <letter>` family) because the help overlay is grouped by category and the user reads it as a sequence — having `g r` / `g l` / `g t` together visually communicates "these three are the same kind of action". Picked **typing-target gate honored for `g t`** mirroring `g r` and `g l` — the existing `if (inTypingTarget)` early-return in `routeKey` blocks single-letter chords from interfering with text input, and `g`-prefix chords are no different. A user typing `gt` in the mission search field shouldn't accidentally open the tool usage overlay. Picked **modifier-key gate also blocks `g t`** because any modifier (Ctrl / Alt / Meta) is treated as a non-shortcut chord by `routeKey`'s opening guard, so `Ctrl+G Ctrl+T` (a future browser binding, hypothetically) won't accidentally route through the prefix machinery. Picked **catalog test asserts `top 5` placement for the `"tool usage"` query** rather than `top 1` because the ranker has a stable tie-break ladder that could shift across phases — asserting top-5 captures "the entry is reachable on the most natural query" without being brittle to future ranker tweaks (e.g. a Phase 81 entry that legitimately outranks "tool usage" on the same query). Picked **the `noopHandlers` factory in `commandPaletteRanking.test.ts` widens to include `setToolUsageOpen: vi.fn()`** rather than letting the existing 200+ catalog tests fail with a type error. This is the canonical pattern Phase 70's commit established: any `CommandHandlers` widening MUST come with a `noopHandlers` extension in the same commit, so the existing test surface keeps compiling. Without the extension, the entire suite would type-error on the missing field even though no individual test exercises it.
* **2026-05-19 (Phase 79 — Fleet Tool Usage overlay modal):** Picked **a standalone center-screen modal rather than embedding the tool table inside FactionStandings as a fourth view** because the user-facing question is fundamentally different: FactionStandings answers "rank the houses across 6 / 7 metrics", while ToolUsage answers "rank the tools across the fleet" — collapsing them would force a four-way view toggle (Table / Radar / Efficiency / Tools) where three are house-rooted and one is tool-rooted, breaking the modal's mental model. Phase 60 set the precedent for separate overlays per axis (Leaderboard for heroes-by-volume, Standings for factions-by-volume, Efficiency for factions-by-discipline, Tools for tools-by-volume). Picked **semantic `<table>` with `<thead>` / `<tbody>` rather than a `<ol>` of cards** (the Leaderboard pattern) because the tool table has four orthogonal columns (Tool / Value / Share / Factions) that scan better as a grid than as a stack — and screen readers benefit from the row/column relationship. Leaderboard rows are vertical because each hero card carries multi-line content (rank + name + faction badge + value + share); tool rows are horizontal because each row's content is dense + structured. Picked **`<thead>` + `<tbody>` with `aria-label` on the table** so assistive tech announces it as "Tools ranked by Calls, table with 4 columns" rather than a generic list — the cognitive load on a screen-reader user trying to navigate seven metrics × seven rows × five faction segments is otherwise immense. Picked **a "Show unused" toggle defaulting to off** rather than always showing the 14 KNOWN\_TOOL\_NAMES seeds because the default view should answer "what's actually happening" not "what could be happening" — a 14-row table on a quiet day reads as "the fleet is broken" when the truth is "nobody's fired anything yet". Toggle-on surfaces the discoverable surface for users who genuinely want to know which first-party tools exist; toggle-off keeps the table focused on real signal. Picked **share bar accent matches the row's `toolColorFor(tool)`** rather than a uniform metric-coloured bar because the bar is the row's signature visual element and the category accent is already on the chip — making them match unifies the eye into a single "this is the Bash row" identity. A uniform metric-coloured bar would force the user to re-read the category badge separately to identify the row. Picked **per-faction strip with a FactionGlyph + a small proportional bar per slot** rather than the more compact "stacked bar with five segments" because the stacked-bar variant struggles to communicate "this tool is exclusive to one faction" (one segment fills the whole bar; the rest are invisible) — five separate strips with empty slots dimmed make the absence visible at a glance. Each segment carries a `data-count` attribute + `title` tooltip so the underlying count is still discoverable. Picked **lifecycle: reset metric to "calls" + show-zero to false on each open** matching the Phase 60 (Leaderboard reset to "missions") and Phase 68 (FactionStandings reset to "table") conventions — the user opens the modal with a question in mind, and the default lens answers the most common question ("which tool is the fleet leaning on most"). Without the reset, a user who closes the modal on `failedCalls` would re-open it confused why the order looks wrong. Picked **`data-metric` + `data-direction` on the modal root** for both test access AND CSS hooks (a future phase could use `[data-direction='lower-better']` to flip a chevron icon next to the metric label). Single source of truth for downstream consumers. Picked **the in-flight `× N live` pill on the tool row itself** rather than as a separate column because the pill is a transient state ("this tool has a call still running") that's row-scoped, not metric-scoped — putting it in a column would force every row to consume that column even when the tool is fully resolved. Inlining keeps the table grid uniform and the in-flight signal proximal to the tool name where the user's eye lands first. Picked **footer "Overall success" computed locally from the report's row-level success / fail counts** rather than threading a fleet-wide success field through Phase 77's helper because the helper deliberately keeps each row independent — adding a fleet-wide aggregate would mean either every consumer pays for that compute or the helper duplicates state. Computing it in the JSX is O(rows), and the rendered footer is the only consumer. Picked **header summary line of "3 Bash · 2 Read · 25 calls across 3 missions"** rather than just the count summary because the top tools by call are the most important context for the table that follows — having them in the header means a user can skim the modal in 2 seconds without parsing the data grid. Picked **🛠 Tools button in TitleBar between Leaderboard and AchievementsGallery** rather than between Standings and Leaderboard because the icon family naturally groups (🗓 Heatmap → ⚔ Standings → 🏅 Leaderboard → 🛠 Tools → 🏆 Achievements) reads as "browse data" → "browse data" → "browse rankings" → "browse activity" → "browse rewards" — Tools sits between the analytical and the celebratory like a workshop between the trophy hall and the throne room. Picked **passing the entire `ToolUsageRow` to `FactionShareStrip`** rather than a flattened `Record<FactionId, number>` because the helper's full row shape is already memoised by the parent — passing the whole object avoids one extra useMemo + ref-stability hazard. TypeScript catches accidental field reads inside the strip if the row shape ever changes.
* **2026-05-19 (Phase 78 — HeroDetailDrawer ToolUsageSection):** Picked **mount between Specialization (Phase 65) and MissionTimeline (Phase 37)** rather than between Metrics and Specialization (the slot Phase 72 Efficiency used) because the section answers a different question than Efficiency: Efficiency is the quantitative "discipline" lens (tier + composite score + per-mission rates), while ToolUsage is the qualitative "what actually happened" lens (which tools did this hero touch). Specialization classifies missions into role intent, ToolUsage rolls up the tools each mission fired — putting them adjacent in the DOM means the user reads "this hero is a Bug Hunter (Specialization) who leans on Bash + Read (Tools)" as one coherent paragraph. Picked **conditional render gated on `report.totalCalls > 0`** mirroring Specialization's `totalMissions > 0` gate and Efficiency's `efficiencyScore !== null` gate because the data-honesty rule is consistent across all three sections: zero-data heroes get nothing rather than a "0 calls" readout that misleads the user into thinking the hero is broken. The parent gate also means the section can trust `topTool` to exist when rendered, simplifying the component's null handling. Picked **single top-tool chip rather than the full ordered grid** because the drawer is the "drill-down" surface — the user is already inspecting one hero, so the question "which tools does THIS hero use" reduces to a top-1 readout most of the time, and the caption row delivers the next 2 via the midline-joined summary. A full grid would compete with the Specialization distribution bar one section up and the MissionTimeline below; restraint reads cleaner. Saving the multi-row grid for the Phase 79 standalone overlay (which is the dedicated "browse tools" surface). Picked **`× N` counter inside the chip rather than a separate "N calls" line** because the chip is already the primary attention-grabber in the row; appending the count in the same chip keeps the data dense without adding visual weight. The 80% opacity on the count subtly demotes it relative to the tool name so the eye still reads the tool first. Picked **chip accent dispatch via `toolColorFor(tool.name)` rather than threading a `data-category` attribute through the helper** because the colour is a UI-layer concern: `toolColorFor` already centralises the category → hex mapping inside the helper module, and the component just delegates. This means a future Phase 79 overlay or Phase 80 command-palette result can use the SAME accent for the same tool without recomputing — single source of truth. Picked **stats grid order: calls / success / unique / top-dur** because that's the "outside in" reading: total volume is the first question ("how much tool work did this hero do?"), success rate is the second ("was it good work?"), unique-tool count is the third ("how diverse is the toolbox?"), and top-tool duration is the fourth ("how heavy is each call?"). Reversing the order (e.g. duration first) would lead with a metric most users only care about after they've answered the others. Picked **`formatDurationMs` from `toolUsage` rather than re-using `formatDuration` from `heroEfficiency`** because the two formatters honestly have different scales: heroEfficiency formats *mission* durations (typically minutes to hours), while toolUsage formats *tool call* durations (typically milliseconds to seconds). Using the same formatter would either over-precision the mission durations or under-precision the tool durations. Phase 77's `formatDurationMs` is calibrated for the tool-call range (ms → s → m → h with sensible decimal precision per band). Picked **`unique` cell labelled "unique" rather than "tools"** because the parent section is already labelled "Tools" — a cell labelled "tools" inside a section labelled "Tools" would be visually redundant. "unique" disambiguates without needing the user to read column headers. Picked **em-dash on success cell when `totalSuccess + totalFailed === 0`** rather than `0%` because zero resolved calls means "we don't know yet", not "0% successful". Same null-as-honesty rule Phase 71 / 74 / 77 apply. Picked **section semantics with `aria-labelledby` + a hidden `data-total-calls` attribute** rather than `aria-label` on the section root because screen readers should hear the heading text ("Tools") naturally as the user navigates landmarks, and the `data-total-calls` attribute is a test-only contract that doesn't affect the accessibility tree. Picked **mount the section BEFORE the MissionTimeline (Phase 37)** rather than after because the timeline is a 24h sparkline of *missions*, and the Tools section summarises *tool calls within those missions* — putting Tools above the timeline reads as a zoom-out → zoom-in flow ("here's the tool overview, now here's the timeline showing when those tools ran"). Reversing would break the visual hierarchy. Picked **drawer-level `useMemo` keyed on `[hero, heroMissionsAll]`** rather than recomputing on every render because the Phase 77 rollup is O(M·T) over the hero's missions × tool uses per mission — for a long-running hero, that could be hundreds of operations on every event tick. The memo + the `heroMissionsAll` selector mean the rollup only re-runs when the hero's mission set actually changes, not when an unrelated faction's hero spawns. Picked **`includeDismissed: true` inside `aggregateToolUsageForHero` (Phase 77 contract)** because the drawer is a manual-open surface — if the user clicked on a dismissed hero in some legacy roster, the drawer should still show their history rather than silently rendering an empty section. The fleet-wide rollups (Phase 79 / 80) will use the default `includeDismissed: false` so dismissed heroes drop from the fleet-level metrics where they shouldn't pollute the live readout.
* **2026-05-19 (Phase 77 — Tool Usage pure helper):** Picked **opening a brand-new feature train on the tool axis** rather than continuing the Phase 74→75→76 faction-efficiency train into a fourth surface (e.g. Phase 77 = HeroLeaderboard Efficiency view) because the existing fleet has Phase 60 (Leaderboard / volume), Phase 64 (Specialization / mission-intent), Phase 71 (HeroEfficiency / per-mission discipline), and Phase 74 (FactionEfficiency / per-faction discipline) — five distinct lenses on heroes and four on factions, but ZERO lenses on the tools themselves. The user can already answer "who closes the most missions" and "which faction is most disciplined", but not "is the orc race a Bash shop while the elves carry on Grep + Edit" — and the tool axis is the most tactile, "what actually happened" axis available since every `mission.toolUses` entry already carries the full ledger. Picked **all seven metrics in a single helper rather than re-engineering by axis** (`calls` / `successRate` / `avgDurationMs` / `totalDurationMs` / `missionsUsing` / `heroesUsing` / `failedCalls`) because every metric reduces to a Σ-or-mean over the same `ToolUse[]` slice — splitting them across files would just multiply the test-fixture wiring without adding any architectural clarity. Phase 71 / 74 set the precedent: one helper file per axis, all metrics inside. Picked **`pendingCalls` as a separate bucket rather than collapsing in-flight calls into `failedCalls`** (because pending ≠ failed — a `ToolUse` with no `ok` field is still running or never got a verdict from the engine, NOT a known failure) — same logic Phase 71 applies to in-flight missions for `successRate`. Lumping them together would libel every still-running shell as a failure. The `pendingCalls` field also gives the Phase 79 overlay a real "in-flight" badge to render, rather than the test-only signal it'd otherwise be. Picked **always seed `KNOWN_TOOL_NAMES` in the rows map** (even on a fully-empty fleet) so the `ordered` list always returns 14 first-party tools at the top. Without the seed, a fresh session would render an empty Phase 79 overlay — the "discoverable surface" stays invisible until a hero fires something. With the seed, the grid is always populated and the calls-DESC sort on customs still puts the dominant tools first. Picked **`KNOWN_TOOL_NAMES` ordered in "read → write → run" task flow** (`Read` / `Glob` / `Grep` / `Edit` / `MultiEdit` / `Write` / `NotebookEdit` / `Bash` / `Task` / `WebFetch` / `WebSearch` / `TodoWrite` / `AskUserQuestion` / `ExitPlanMode`) rather than alphabetical because the task-flow ordering matches the user's mental model of how a Claude Code session actually unfolds (scan with Grep → edit with Edit → run with Bash) — the user reads top-to-bottom and the dominant pattern is what they're looking for. Alphabetical ordering would put `AskUserQuestion` first, which is the LEAST common path. Picked **case-sensitive custom MCP tool dedupe** (so `mcp__github__SearchRepos` ≠ `mcp__github__searchrepos`) because the MCP tool namespace is user-controlled and case carries semantic meaning — collapsing case would silently merge two tools that the user genuinely registered separately and produce a misleading rollup. Picked **`factionShare` per row instead of a separate per-faction view as the primary output** because the per-tool row is the user's first read ("how much Bash did we run?"), the per-faction breakdown is the second read ("who's running it?"), and the per-faction `factionShare` field puts both reads on the same row so the Phase 79 overlay can render a single "Bash · 12 calls · 8 orc 3 human 1 elf" line without joining two queries. Picked **ghost-mission calls count in `calls` but credit no faction** (mirrors Phase 74's `total` honesty: tokens were spent regardless of whether the hero is still in the roster) because the call genuinely happened. Dropping it from `calls` would understate the fleet's actual work, but crediting a ghost to a specific faction would libel them. The `factionShare` Σ ≤ `calls` invariant is the test-asserted contract. Picked **`successRate` denominator = resolved calls only** (matches Phase 71's `successRate` for missions): pending calls drop from the denominator entirely. A 1-success-1-pending shell run is not 50% successful, it's 100% of resolved + still running. Picked **negative durations from clock skew clamp to zero** (NOT to `Math.abs(diff)`) because the absolute value would hide a real signal — a call where `endedAt < startedAt` is a SUSPECT call (clock skew, malformed timestamps), and zero-duration preserves the "this row is weird" reading without throwing. `Math.abs` would silently absorb the bug. Picked **`category` accent palette in the helper** rather than in a separate UI module because the colour is read by every consumer (HeroDetailDrawer chip, overlay grid, command palette result row) and centralising it eliminates the "did we already pick a colour for Grep?" question. The palette stays hex-only so the helper has no Tailwind / styling import — runtime-only, just like Phase 71's `TIER_COLOR`. Picked **`toolUsageByHero` always returns one entry per live hero** (even with zero calls), sorted by `totalCalls` DESC then name ASC, rather than filtering zero-call heroes out: the per-hero list is browseable and a zero-call hero is meaningful ("Aragorn hasn't fired a tool yet — did he just spawn or is he idle?"). Filtering would silently drop heroes from the per-hero overlay. Picked **`includeDismissed` as an opt-in flag everywhere** (default false) matching Phase 74 / 52: the standings convention is "dismissed heroes don't count toward the live fleet's stats", and the Phase 78 HeroDetailDrawer single-hero variant (`aggregateToolUsageForHero`) overrides to true so a manually-opened drawer for a dismissed hero still surfaces their history (the user opened it on purpose). Picked **pure helper only — no UI integration this phase** under the established Phase 57 → 58 / 64 → 65 / 67 → 68 / 71 → 72 / 74 → 75 split discipline because the rollup math has six tricky paths (pending, ghost, clock-skew, MCP case, dismissed default, all-five-factions invariant) and locking them in via 81 tests before any React mounts the helper means the Phase 78 component lands with one bug class eliminated: "my React-mock fixture happened to match the path the helper actually walks".
* **2026-05-19 (Phase 76 — Faction efficiency command palette + deep-link):** Picked **widening the store-typed `StandingsView` from `"table" | "radar"` to `"table" | "radar" | "efficiency"`** rather than keeping a parallel local widening in `FactionStandings.tsx` (Phase 75's approach) because Phase 76 needs the deep-link hint to actually carry the new value end-to-end — the `standingsInitialView` field on the settings store IS the wire format between the command palette and the modal, so widening it once at the source eliminates the type-cast theatre Phase 75 used and lets the open-transition effect handle the new value with a plain string comparison instead of a runtime cast. Picked **mirror the Phase 70 entry shape exactly** (same `kind: "action"`, same `group: "Overlays"`, same `run()` ordering of `setStandingsInitialView(view)` THEN `setStandingsOpen(true)`) because the user has already learned the Phase 70 pattern (`Cmd+K → "radar" → land on Radar`) — preserving the muscle memory means the new entry feels immediately familiar, and the keyboard-mock invocation-order test from Phase 70 generalises one-to-one to Phase 76 without any new test infrastructure. Picked **a keyword set that spans both the metric vocabulary AND the user-facing question** (`"lean"`, `"tier"`, `"score"`, `"discipline"`, `"per-mission"`, `"tokens"`, `"throughput"`) rather than a narrow `["efficiency", "score"]` because users come at the modal from two different intents — sometimes "I want to see efficiency rankings" (axis-first), sometimes "which faction is the leanest" (question-first), and the ranker needs to surface the same entry under either intent. Picked **explicit "table" hint reset on the existing `open:standings` path** stays the regression-guarded behaviour, NOT widened to also clear "efficiency" hints — because the Phase 70 contract treats the existing hint as a one-shot consumed-on-apply value, so it can never linger past a modal cycle in practice; only the most-recent palette intent ever materialises, and adding "clear efficiency hint" defensiveness would just be dead code with no observable effect. Picked **defensive collapse-to-table for unknown hint values** (matches Phase 70's contract: an unrecognised value falls through to the default rather than crashing the modal) because the deep-link is essentially URL-shaped state — and like Phase 47's share-replay token decoder, the right defensive posture is "never crash on attacker-supplied input, just degrade gracefully". A corrupted hint (e.g. a future phase introduces "podium" but a stale store reads the new value into the current build) now silently routes to Table view instead of breaking render. Picked **simplifying FactionStandings's local type re-export** (`export type { StandingsView }` instead of the Phase 75 widening `export type StandingsView = StandingsViewStored | "efficiency"`) because the store type is now canonical — every consumer that needs the view union should import from `useSettingsStore.ts`, and the local widening was always a workaround pending the store update. This is a tiny breaking change for any external consumer that imported `StandingsView` from the component (none in-tree), but it tightens the dependency graph: the store owns the type, the component re-exports it for convenience without forking the definition. Picked **the catalog tests live in their own file** (`factionEfficiencyDeepLink.test.tsx`) rather than extending `factionRadarDeepLink.test.tsx` because the two surfaces are independent — a future radar refactor shouldn't have to touch efficiency tests, and a future efficiency refactor shouldn't have to touch radar tests. The Phase 70 file is a closed chapter; Phase 76's file extends the deep-link contract without mutating it. Picked **explicit regression-guard tests for the Phase 70 paths** (Table + Radar deep-links still land on their respective views, `open:standings` still clears to `"table"`) inside the Phase 76 file because the test fixture is identical and a single accidental edit to the open-transition effect could silently break either Phase 70 or Phase 76 — co-locating the guards in the new file makes a Phase 70 regression caught the same turn Phase 76 ships, not after the bug ships.
* **2026-05-19 (Phase 75 — FactionStandings Efficiency view):** Picked **a three-option view toggle (Table / Radar / Efficiency)** rather than mounting Efficiency as a sibling modal or a separate command-palette entry because the user has already developed muscle memory for the existing two-option toggle from Phase 68 — adding a third button under the same `role="group"` is the lowest-friction discovery path AND it physically co-locates the three views, which makes the "different lenses on the same five factions" mental model legible without explanation. Picked **swapping the metric pill row based on view** instead of merging both pill families into a single 13-pill row because the user-facing question genuinely differs — Table view answers "rank by volume" with metrics like missions / tools / tokens, Efficiency view answers "rank by discipline" with metrics like avgScore / successRate / tokensPerMission, and a merged 13-pill row would conflate the two questions and force the user to remember which pill belongs to which lens. Swapping based on `view === "efficiency"` also keeps the Phase 52 `metric` state and the new Phase 75 `efficiencyMetric` state independent (no collision when the user flips Table → Efficiency → Table → expects the original Table metric still selected). Picked **`title="Higher is better"` / `title="Lower is better"` on each efficiency pill** via the shared `factionEfficiencyMetricLowerIsBetter(metric)` helper because the seven metrics genuinely point in different directions (avgScore higher = better, tokensPerMission lower = better), and without the inline hint the user would have to remember the convention for each pill or open the helper docs. The single-source-of-truth direction flag also guarantees the share-bar math and the title attribute can never drift. Picked **share-bar inversion for lower-is-better axes** (`min / value` instead of `value / max`) so the LEANEST faction shows the LONGEST bar on `tokensPerMission` / `costPerMission` — preserves the universal "bigger bar = better" reading convention regardless of which direction the metric points. Without the inversion, the bar would visually punish efficient factions on those two axes, which would directly contradict the readout the user just enabled by clicking that pill. Picked **render the EfficiencyView body as a sibling to RadarView at the bottom of `FactionStandings.tsx`** (rather than splitting it into its own component file) because the view is intimately wired to the same store snapshots + lifecycle hooks the parent already owns, and the file-level co-location keeps the wire-up readable in a single scroll. Tests assert via DOM selectors rather than props, so the structural choice has no impact on test stability. Picked **distinct empty markers — "absent" for zero-hero factions and "no signal" for in-flight-only factions** because the two readouts genuinely differ: "absent" means "no hero has joined this faction yet — recruit one to see action", whereas "no signal" means "this faction has live heroes but they haven't closed a mission yet — wait or check why". Collapsing them under a single label would obscure a real action item. Picked **tier chip with the count formatted as `S × 2`** rather than rendering N separate chips (one per scored hero) because the count format keeps each row at fixed visual height regardless of faction size — a 12-hero S-tier faction would otherwise blow the row layout open and make the table feel inconsistent. The chip's aria-label still surfaces the count for screen readers. Picked **tier source rendering with `TIER_COLOR[t]` accents directly** (importing from `heroEfficiency.ts`) rather than threading the colour through factionEfficiency.ts's export surface because the colour is a UI-layer concern that shouldn't live in the pure helper — Phase 74's helper deliberately stayed runtime-only with no React or styling dependencies, so the component pulls colours from the heroEfficiency module that already exports them. Picked **resetting `view` to "table" on modal re-open** even though Efficiency view is the newest because the Phase 68 default-to-Table convention is well-established AND the Phase 70 deep-link hint mechanism still routes to Table or Radar (widening the hint type to accept "efficiency" is a future-phase concern); preserving the Table default also means the Phase 52 component tests don't have to be retrofitted. Picked **top-profile chip read-only for this phase** (deferring the click-to-open-drawer wiring to a future phase) because adding the open-drawer wire-up would require touching the Phase 30 HeroDetailDrawer integration and rolling that hero into the Phase 70 setSelectedHeroId path, which expands the commit's blast radius beyond just "wire Phase 74 into FactionStandings"; the chip already surfaces the hero name + score, which is enough for the in-modal browse use case. Picked **`tabular-nums` class on every numeric cell + tier count + score** so that values across rows align column-by-column even when digits proportionally differ — without it the table reads as visually jittery on every metric flip. Picked **footer note swaps with metric direction** ("Higher is better · top faction shows longest bar" vs "Lower is better · leanest faction shows longest bar") so the user gets a one-line reminder of how to read the chart immediately below the data, which closes the "wait, which way is good?" question the pill title attributes opened.
* **2026-05-19 (Phase 74 — Faction Efficiency pure helper):** Picked **per-faction aggregation as a separate helper** (`factionEfficiency.ts`) rather than extending Phase 71's `heroEfficiency.ts` because the two helpers answer fundamentally different questions: Phase 71 ranks INDIVIDUAL heroes via fleet-wide percentile, while Phase 74 partitions the same data by faction and asks "which house is winning the per-mission discipline contest" — adding partitioning to `heroEfficiency.ts` would conflate two responsibilities and the test surface would balloon into two interleaved scenarios that fight each other. Picked **tier source-of-truth = Phase 71's fleet-wide `buildEfficiencyTable`** rather than recomputing per-faction percentile ranks inside the helper because the within-faction-percentile approach has a degenerate single-hero case: a lone S-Tier hero in their faction would be both 100th percentile (top within faction) AND 0th percentile (bottom within faction) depending on how you count, neither of which carries product meaning. The user-visible question is "how does this faction stack up across the fleet", so fleet-wide tiers are the right denominator — orcs with three S-tier heroes vs. elves with one S-tier hero now reads as the faction strength signal the user expects. Picked **all five factions always present in the output** (even fully-empty ones) matching the Phase 52 / 57 convention because the standings UI needs the visible empty row to make a point — "the lobsters haven't done anything this session" is a meaningful insight the user shouldn't have to infer from a missing row. Picked **ghost missions drop from per-faction rows but count in `total`** because the per-faction row should only reflect the work that the present-and-accounted-for heroes actually did, while the fleet-wide `total` should stay honest about total tokens spent regardless of whether the hero record is still around (same data-honesty rule as Phase 57 cost-projection — if you spent the tokens, they go in the fleet total whether or not the hero is still in the roster). Picked **null avgScore when no hero has been scored** rather than `0` because the standings UI needs to render an em-dash ("—") in the cell for "no signal yet" — emitting `0` would silently say "the faction is at 0% efficiency", which is libelous for a faction that's just spawned its first hero and hasn't closed anything yet. Same null-as-honesty rule Phase 71 applies for per-hero `successRate` / `efficiencyScore`. Picked **two of the seven metrics flagged lower-is-better** (`tokensPerMission` + `costPerMission`) via a centralised `factionEfficiencyMetricLowerIsBetter(metric)` helper that the ranker AND the share-bar math both consume so the two surfaces can never drift — a "more is better" inversion bug in either would otherwise be hard to spot (the bar would just look wrong without an obvious cause). The bar math literally inverts for lower-is-better — `min / value` instead of `value / max` — so the leanest faction still shows the longest visual bar, preserving the "bigger bar = better" reading convention regardless of axis direction. Picked **`null` for `metricValueForFaction("throughput")` when the row has zero throughput** but **`0` for `completedMissions` / `scoredHeroes`** because they sit on opposite sides of the "is this a known fact or a missing signal" line — throughput=0 means "we haven't measured anything yet, the span is degenerate or no missions have completed" (a missing signal that should sink to the bottom of the ranking), whereas completedMissions=0 is a known fact ("this faction has not closed any missions") that should still sort like any other small number. The intent surfaces in formatter behaviour: throughput=0 renders "—", completed/scored=0 render "0". Picked **a convenience `computeFactionEfficiencyFromHero` re-export over Phase 71's `computeHeroEfficiency`** rather than only the bulk `buildFactionEfficiency` entry because the Phase 75 UI integration will need drawer-style mocks where a single hero is being inspected without a full fleet — exporting the convenience function keeps the Phase 75 component-test imports clean (single helper from `factionEfficiency.ts` instead of importing both modules). Picked **70+ test cases** rather than the typical 40-50 because the cross-product of 7 metrics × 5 factions × 4 hero-state buckets × 3 mission states is large enough that an interesting tie-break or null-propagation bug could easily slip past a smaller suite; tests are cheap insurance and the Phase 71/72/73 train made it clear how many subtle off-by-one paths exist in this kind of aggregator.
* **2026-05-19 (Phase 73 — HeroCard efficiency chip):** Picked **rendering both spec + efficiency chips in a single flex row** (vs. two stacked rows with separate `mt-1` containers) because the card is already vertically tight — header line + archetype/cli/model line + chips row + active-mission preview + 6-cell metrics row + 6-cell stats footer. Adding a second row for efficiency would push the metrics row two extra lines down on small viewports. A single flex-wrap row lets the chips wrap to a second line only when both are present AND the card is narrow, which is the right adaptive behaviour. Picked **`<div data-testid="hero-card-chips">` container with both chips as direct children** rather than rendering each chip in its own conditional `<div>` because the test surface needs to assert "both chips live in the same row" — direct-child parent checks (`spec.parentElement === row`) are the cleanest assertion shape. The container only renders when at least one chip would show, so a hero with no missions still gets the zero-DOM-weight contract the original Phase 66 conditional gave. Picked **extracting `heroMissions` into a shared memo** rather than letting both Phase 66 specialization and Phase 73 efficiency each compute their own hero-scoped filter inside their respective `useMemo` because the filter is the expensive part (O(N) over all missions where N grows unbounded), while the helper math runs O(M) over the hero's own missions (typically much smaller). Sharing the filter cuts the total work per re-render in half AND keeps the dependency graph clean: both memos take `heroMissions` as a single argument, which means the memos invalidate at exactly the same time (whenever the missions map mutates) — no race condition where one chip lags the other. Picked **single-character tier letter** (just `"S"`, `"A"`, etc.) over `formatTier(tier)` (`"S Tier"`) in the chip body because the chip is space-constrained — the full "S Tier" label would push the chip width past where it can sit alongside the spec chip. The full label is preserved in `aria-label` and `title` (via `formatTier(tier)` in the aria-label string) so screen readers + hover tooltips still get the verbose version. Picked **the tier-null fallback to "—" character** (rather than blanking the tier slot or showing "?") because the dash reads as "we don't have fleet-rank yet" and aligns visually with the em dashes other sections use for null sub-metrics. When a future phase wires `buildEfficiencyTable` through HeroRoster, every chip receives its real tier and the fallback stops firing. Picked **`tabular-nums` class** on the chip so the 2-decimal score occupies the same width across cards regardless of digits — a 0.05 hero and a 0.85 hero render identical chip widths. Without `tabular-nums`, proportional digits make the chip slide left and right when scrolling the roster, which reads as flicker. Picked **a single-test impossibility documentation** in the chips-row layout suite (the "only-efficiency-chip" branch is impossible by construction) rather than removing the test because future refactors might violate the construction (e.g., adding a new in-flight state that doesn't appear in the spec chip condition) — keeping the test documents the invariant so a future refactor that breaks it gets caught. Picked **using `formatTier(tier)` in the aria-label** even though the chip body only shows the letter because the aria-label is the screen-reader-facing string and "S Tier" reads more naturally than "S" alone in the audio stream. Same a11y rule the Phase 65 specialization chip applies for confidence dots.
* **2026-05-19 (Phase 72 — HeroDetailDrawer Efficiency section):** Picked **mounting between Metrics and Specialization** (not below Specialization, not above Metrics) because the section is the quantitative bridge: Metrics shows the raw counts the hero accumulated, Efficiency translates those into per-mission discipline numbers, Specialization tells the qualitative story. Top-to-bottom reads as zoom: identity → cwd → counts → per-mission efficiency → role → 24h activity → missions. Above Metrics would put the score before the data that justifies it; below Specialization would bury the score under the role narrative. Picked **conditionally rendering on `efficiencyScore !== null`** (not on `totalMissions > 0` like the Specialization section) because efficiency only makes sense once at least ONE mission has closed — a hero with 3 in-flight-only missions has totalMissions=3 but no honest score yet. Same data-honesty rule, different threshold; matches the helper's null-score semantics. Picked **falling back to `TIER_COLOR.B` (cyan)** when `profile.tier === null` rather than rendering a colourless "no tier" chip because `computeHeroEfficiency` always returns `tier: null` in isolation (fleet ranking lives inside `buildEfficiencyTable`). The drawer mounts a SINGLE hero so the helper path is the no-tier one — but the score is still meaningful. The B-colour fallback reads as "neutral, not graded yet"; the score string next to it carries the real signal. When Phase 73 wires `buildEfficiencyTable` through the drawer (so the tier is fleet-ranked), the fallback path stops firing — but for now, falling back to B (the visual middle ground) gracefully degrades rather than rendering a confusing "—" tier chip. Picked **a 4-cell stats grid** (Success / Tokens-per-mission / Throughput / Avg dur) rather than 6 because the drawer is vertically tight (header + cwd box + 6-cell Metrics + new 4-cell Efficiency + Specialization + MissionTimeline + active mission + 5 recent missions + Contributions + footer); 6 cells of efficiency stats would push more useful sections below the fold. The 4 picked are the highest-signal ones: success rate is the headline outcome, tokens-per-mission is the cost signal, throughput is the velocity signal, avg duration is the latency signal. Skipped completed-count + total-cost because they're already on the Metrics row above (3-cell `missions` / `tokens in` / `tokens out` / `est cost` grid). Picked **em-dash fallback for every nullable sub-metric** (throughput when span = 0, avg duration when no completed mission has timing, success rate when in-flight only) over hiding the cell because the grid layout needs consistent slot count — collapsing a cell would shift the others and read as "did I miss something". Em dash universally reads as "no data yet"; pairs cleanly with the section's overall conditional render. Picked **caption joins via `·` separator** matching the Phase 65 Specialization caption convention so the visual rhythm carries over (also matches the Metrics-row "cwd · tty · spawned" pattern). Picked **caption parts assembled defensively** (only push when the underlying field is non-null) so a hero with a completed mission but no avg-duration data doesn't render `3/3 completed · · ...` with awkward double separators. Same pattern as the Phase 39 mission-detail summary. Picked **`formatCost as formatEfficiencyCost` import rename** rather than letting the local `formatCost` shadow because the drawer ALREADY has a `formatCost(n)` helper for the Metrics row (locally defined at line 475) and the Phase 71 `formatCost` has slightly different conventions (`<$0.01` instead of `$0.00`). Renaming on import keeps both functions accessible without a hoist-order accident — same trick TypeScript's `import { x as y }` pattern is designed for. Picked **dedicated test file `HeroDetailDrawerEfficiency.test.tsx`** instead of appending to the existing `HeroDetailDrawer.test.tsx` because the existing file is 605 lines covering 20+ cases against the pre-Phase-72 surface; appending would force a re-read of the whole file every edit and risks accidentally regressing an existing assertion. Same split-file approach Phase 68 used for `FactionStandingsRadar.test.tsx` (separate from the existing `FactionStandings.test.tsx`). Picked **`data-tier="null"` literal string** for the no-fleet-rank case (not `data-tier={undefined}` which would omit the attribute entirely) so tests can assert against a stable "null" sentinel rather than checking attribute presence — clearer test intent + matches the convention React uses for falsy `data-*` props. Picked **DOM-order assertion via `compareDocumentPosition`** (vs. `getElementsByTagName('section')` indexing) because the drawer has multiple `<section>` elements and indexing is brittle to future section insertions; `compareDocumentPosition(other) & 4` directly asks "does `other` follow this element?" without enumerating anything in between. Same approach the testing-library a11y suite uses for sibling ordering.
* **2026-05-19 (Phase 71 — Hero Efficiency pure helper):** Picked **a composite score that multiplies success rate by token efficiency** rather than a weighted sum because the two factors have very different failure modes: a 0% success rate should drop the score to 0 regardless of token discipline (a hero who never finishes a job has no efficiency, period), and conversely a 100% success rate at 100K tokens/mission should still hurt (it's expensive even if it works). Multiplication makes both factors gates, not additive — you can't be efficient without both finishing AND not burning the budget. Picked **`successRate × tokenEfficiency`** specifically (not `(successRate + tokenEfficiency) / 2`) because additive averaging would let a 100% success at 50K tokens/mission score `(1 + 0.09) / 2 = 0.545` — which feels generous for a hero burning \~10× the average. Multiplicative collapses it to `1 × 0.09 = 0.09` which is honestly bad. Picked **`1 / (1 + tokens / 5000)`** for the token-efficiency curve over a piecewise linear ladder because the smooth hyperbola has no breakpoints — every additional 5K tokens cuts the multiplier in half, but nothing weird happens at the boundary. Linear ladders break tied heroes who happen to sit at the gate (e.g. exactly 5000 tokens) into adjacent buckets even though they're indistinguishable in practice. Picked **`TOKEN_EFFICIENCY_PIVOT = 5_000`** because that's roughly the "average coding agent" token spend on a Sonnet-class model — a focused single-file edit lands around 2-4K, a moderate multi-tool mission around 5-8K, a deep refactor with reads + edits + tests pushes 10-20K. Centering the curve there means the median hero scores around 0.4-0.6 and the outliers in both directions show up clearly. Constant is exported so a future user can tweak it without forking the helper. Picked **per-fleet percentile tiers** (S = top 20%, A = next 20%, ...) over **absolute score gates** (e.g. score > 0.8 = S) because the absolute gates would crush every hero into D tier on a fresh session with no closed missions — and S tier should be a relative ranking that always exists, not a "you must hit 0.8 to qualify" badge. Tier as percentile is the JRPG / shotcaller's framing: there's always an S Tier, even in a fleet of three. Picked **`tier: null` for heroes with no closed missions** rather than defaulting them to D because labelling a brand-new hero "D Tier — bottom 20% efficiency" libels them — they haven't done anything yet. Null sorts to the bottom but reads as "no data" instead of "we ranked you bad", and the UI can dim them or show a "—" tier chip honestly. Picked **failed and cancelled as one bucket** in the rate denominator because both are non-successful from the hero's POV — a user cancelled it, the engine failed it, either way the hero didn't ship the work. In-flight states (active, pending, awaiting\_approval) DROP from the denominator entirely; a hero who's mid-mission shouldn't be punished as 0% successful, they're an unknown. Picked **tokens summed across ALL mission states** (not just completed) because the tokens were already spent — an active mission that fails still ate compute. Cost off `hero.metrics.estimatedCostUsd` instead of summing token-derived dollars because the canonical billing-blessed number lives on the hero record (engines that charge different rates for cached vs. uncached vs. context aren't reflected in raw token counts). Picked **`avgMissionDurationMs` from completed missions only** because an active mission has no honest duration yet — `now - startedAt` would inflate as the mission runs. Failed missions DO have a duration but they ended on failure; treating them as "this hero takes X seconds per mission" would mix successes and failures incoherently. Failed-duration data is honest data, but we'd want a separate `avgFailedDurationMs` field to surface it cleanly, and that's a follow-up. Picked **`throughputPerHour` using `spawnedAt → max(lastActive, now)`** (not `firstMissionStart → now`) because the hero's effective "available time" started when they spawned — a hero who's been idle for an hour is correctly less productive than one who's been active for an hour, even with the same mission count. The `max(lastActive, now)` clamp prevents weird `lastActive > now` edge cases from clock skew. Picked **pure helper this phase, UI integration next phase** under the established Phase 57 → 58 / 61 → 62 / 64 → 65 / 67 → 68 split discipline. The math + tier logic is novel enough that locking it in via 80 tests before any React mounts it catches the edge cases (in-flight-only heroes, single-hero fleets, NaN inputs) before they manifest as silent renderer crashes. Phase 72 will be a thin Leaderboard.tsx shell that adds `efficiency` to the metric pill list and wires `buildEfficiencyTable` through `useMemo`. Picked **exporting `TIER_COLOR` as hex strings** instead of CSS classes so the helper stays runtime-only and never imports Tailwind — same rule the Phase 64 / 65 specialization chip follows. Components inline the hex via `style={{borderColor: TIER_COLOR.S}}` to avoid PurgeCSS enumeration of every tier × confidence × surface combination. Picked **3-decimal precision on `efficiencyScore.toFixed(2)`** in `formatEfficiencyValue` (vs. 1 or 3) because efficiency scores cluster in the 0-1 range and a meaningful difference between a 0.45 and 0.50 hero is real signal that 1-decimal rounding would erase — but 3 decimals reads as fake precision when the underlying tokens-per-mission estimate is itself approximate.
* **2026-05-19 (Phase 70 — Faction radar command palette + deep-link):** Picked **a transient store hint** (`standingsInitialView`) over **hoisting the view state to the store** because the existing pattern across every modal (FactionStandings, CommandPalette, MissionHeatmap, AchievementsGallery, KeyboardShortcutsModal) is to keep view-style selectors as component-local `useState` that resets on close. Hoisting `view` to the store would have changed that semantics and every place the component is rendered would now have to think about store cleanup; a hint flag is a tiny diff that preserves the established contract. Picked **one-shot consumption** (read + clear on the open transition) so the deep-link can't "stick" — opening the palette and clicking "Open faction radar" once should land on radar; closing and re-opening normally should go back to table. Without one-shot semantics, a Radar deep-link would leak into the next normal open and surprise the user. Picked **clearing the hint inside the same `useEffect`** rather than on a separate unmount/close effect because race conditions are easier to reason about when the read AND the clear happen in the same render pass: set view ← hint, then set hint ← null, atomically. Picked **explicitly setting `"table"` from `open:standings`** rather than letting the hint default fall through because if a user ran `open:faction-radar`, closed the modal mid-render before the open-transition effect cleared the hint, then ran `open:standings`, they'd land on radar — surprising. The explicit reset closes that race. Picked **defensive fallback to `"table"` on unknown hint values** because a corrupted store (or a future schema migration that lands without a settings-store reset) shouldn't crash render; the worst-case is a benign "open to default" rather than a runtime error. Picked **fixing the latent Phase 60 `setLeaderboardOpen` gap inside Phase 70** because I was touching the same handlers object — bundling the fix kept the command-palette wiring in a consistent state. Picked **re-export `StandingsView` from FactionStandings.tsx** (vs. importing directly from the store everywhere) because the type's semantic anchor is the standings component, not the store — the store just plumbs it. Picked **invocation-order assertion via `mock.invocationCallOrder`** in the catalog tests so the hint MUST land before the open flag — without that, the open-transition effect might run before the hint is set and the deep-link would silently land on the default view.
* **2026-05-19 (Phase 69 — README sync to Phases 60-68):** Picked **a docs-only sync phase** (vs. bundling docs into the next code phase) because the README is the actual front door — the test count is 9 phases out of date (`~718` → `~978`), and the standings section claims a flat 5-row table when it now ships a Table / Radar toggle. Same cadence as Phases 33 / 42 / 51 / 59 (a README sync every 5-8 phases keeps the gap honest). Picked **four new dedicated sections** (hero leaderboard, mission tag inference + filters, hero specialization, faction radar) rather than burying them in a "Recent additions" lump because each is a substantive new feature surface and deserves its own anchor for linking and table-of-contents scanning. Picked **rewriting the Phase 52 section in-place** (vs. adding a separate Phase 68 section) because the Radar view IS the FactionStandings modal in a new mode — splitting would imply they're separate features when they share the same trigger, the same metric pills, the same dismissal contract, and the same data store. Picked **leaving the "what's intentionally not built" section minimal** (just licensed binary assets + PWA push backend) because every other historic gap has shipped; the README's closing paragraph now enumerates the shipped milestones across Phases 30-68 so readers see what's actually in the box. Picked **not touching `public-demo/`** in this phase because the demo refresh is a Phase 70 candidate (or later) — bundling docs + demo into one commit would muddle the diff and the demo refresh deserves a focused phase.
* **2026-05-19 (Phase 68 — Faction Radar UI integration):** Picked **header view toggle (Table / Radar) over replacing the table** because the table view is still the right surface for "give me the exact numbers" questions — projection chips, top-performer navigation, share bars carry information the radar polygon can't. The radar is an *additional* lens, not a replacement, so the toggle lets the user pick the right tool for their current question. Picked **resetting the view to `table` on modal re-open** (matching how Phase 52's metric resets) so the user always lands in the same starting state — predictability outweighs preserving the last-used view across closes. Picked **DESC paint order by `areaShare`** so the largest polygon goes underneath; otherwise a small polygon (e.g. a faction with 1 active hero) gets occluded by the dominant faction's web and the user can't see it at all. Painting bottom-up by area makes every faction visible while keeping the dominant one's outline the most prominent. Picked **vertex markers only on non-zero levels** because stacked dots at the center (5 axes all at level 0 = 5 dots at the same pixel) looks like a rendering bug. Picked **clickable axis labels that re-select the metric** so the user can pivot from "interesting shape on the cost axis" to "now rank the table by cost" without breaking eye contact with the chart. Picked **per-metric leader strip below the legend** because the polygon shapes alone don't always make the leader explicit — when orc tokens = 3000 and elf tokens = 10000, the polygon vertices both look "high" relatively to the other axes, but the strip says "Tokens · elf · 10.0K" unambiguously. Picked **"no signal" leader-row copy** (vs. omitting the row) when a metric has zero across every faction so the strip's length stays constant — six rows always — and the user never wonders "did I miss a metric?". Picked **`data-empty="true"` on legend chips for zero-area factions** rather than dropping them so the five-faction roster is always visible — same rule as Phase 52's "empty rows make a point". Picked **SVG `<title>` for polygon tooltips** over a HTML tooltip library because the SVG element is the natural carrier; browsers handle the hover behaviour without any extra JS. Picked **`role="img"` + `aria-label` from `radarCaption`** on the SVG so screen readers announce the leader + percent without having to enumerate every polygon — matches the heatmap component's pattern (Phase 48). Picked **360×340 viewBox** because `radius * (1 + labelOffset) ≈ 142` and labels need \~15px headroom on each side; 340 high gives enough margin for the bottom axis label without clipping.
* **2026-05-19 (Phase 67 — Faction Radar pure helper):** Picked **per-metric normalisation** (each axis scaled to its own fleet-wide max) over **global normalisation** (one shared denominator) because the metrics have wildly different scales — token totals run in the 10⁵-10⁶ range while achievement counts top out around 10-20. A global denominator would crush every non-token axis to a needle so close to the center that the polygons would look identical except for the tokens spoke. Per-metric normalisation keeps the radar legible — "orcs lead missions but humans lead tools" reads instantly because each axis has its own visible range. Picked **collapsing empty factions to the center** (vs. dropping them) so the canonical FACTION\_IDS five-slot layout is preserved — same rule as Phase 52's standings table; caller can skip drawing zero-area polygons in the UI if it gets noisy, but the helper always returns the full set. Picked **shoelace formula + max regular-polygon area as the denominator for `areaShare`** because that gives an intuitive 0..1 "how much of the chart this faction's web covers" — a faction at full radius on every axis hits 1.0 (regular hexagon = max area for a 6-axis chart), a half-radius polygon hits 0.25 (area scales with radius²). Picked **single non-zero axis = degenerate polygon = 0 area = no leader credit** because the polygon mathematically has zero interior — a faction with one metric and nothing else shouldn't "lead" the radar. Picked **default 6 axes from STANDINGS\_METRICS in canonical order** (vs. letting the caller permute) so the radar reads the same way across every component that mounts it — muscle memory matters when you're scanning the chart every minute. Picked **`startAngle = -π/2`** (first axis straight up) over `0` (first axis right) because every consumer-facing radar I've seen (Excel, Datawrapper, D3 examples) starts at top — match the user's existing mental model. Picked **`labelOffset = 1.18 × radius`** so axis labels sit just outside the polygon at full radius without colliding when two adjacent metrics both peak — empirically derived from a 240×240 viewbox sketch; large enough for breathing room, small enough that long labels like "Achievements" don't run off the SVG. Picked **3-decimal SVG coordinate precision + near-zero collapse to `"0"`** because `M -0,1.7763568394002505e-17 L ...` strings leak from naive `String(x)` rendering and crash some SVG inspectors; the formatter gives clean `M 0,50 L ...` output and trims the compressed-file size by \~40% on a typical layout. Picked **pure helper this phase, UI integration next phase** under the Phase 57 → 58 / 64 → 65 split discipline so the math is locked in by tests before any React mounts it; Phase 68 will be a thin FactionStandings.tsx shell that consumes the layout via `useMemo`. Picked **`areaShare` clamped to \[0, 1]** rather than letting it exceed 1 on pathological inputs because the UI will turn this into a percent string and "107% of the chart" looks like a bug even if the math justifies it.
* **2026-05-19 (Phase 66 — HeroCard specialization chip):** Picked **mounting on the card** (vs. waiting for HeroRoster header to grow a sort-by-specialization toggle) because the chip is the cheapest possible way to surface the Phase 64 inference in the user's actual scan path. The roster is the FIRST thing the user sees — opening HeroDetailDrawer (Phase 65) means clicking into each hero one at a time, which is fine for inspecting one hero but loses the at-a-glance "where's our bug-fixer" answer. The chip costs \~50 LOC of component + 10 LOC of card change and changes nothing about the existing layout. **Memoize the inference per-card** rather than hoisting to HeroRoster because (a) `useMemo(..., [hero, missions])` already gates re-computation on the only inputs that matter, (b) hoisting would force HeroRoster to pass profiles down via props and require a new "profiles record" subscription that triggers every card on every store tick (the opposite of the goal), (c) for a typical 5-15 hero fleet, computing 15 specialization profiles per store tick is \~O(15 × missions) which is bounded by the protocol's 200-event ring buffer. Profile bytes are tiny; the work is the regex inference per mission per render, which already shows up in MissionLog + MissionHeatmap + FactionStandings without measurable perf impact. **Smaller chip dimensions** (9px font, 1px confidence dot, no separate caption) because the card is dense — name + state-badge + archetype/cli/model + (active mission) + 6-cell metric strip = lots already. The Phase 65 drawer chip is sized for a 460px-wide panel; cards are 280-320px wide. Dropping the caption is fine because the role name itself answers the at-a-glance question; if the user wants the share + confidence + sample-size detail, the drawer is one click away. **`aria-label` carries the role** so screen readers don't need the visible-text scan; the chip's `data-role` carries it for tests. **Hidden when zero missions** matches the drawer's behaviour exactly — no "Specialization · no data" placeholder that pushes the metrics row off-screen for newly-spawned heroes. Same data-honesty rule as Phase 65. **New HeroCard.test.tsx file** because the original card had no dedicated test file (covered only indirectly via the live UI). Going from zero to seven cases is the minimum viable test surface for a card that now carries two distinct UI surfaces (state badge + spec chip). Test fixture follows the established `useGameStore.setState({...})` seed + render pattern other component tests use (HeroDetailDrawer, MissionLog, Leaderboard). **Chip variant component lives inside HeroCard.tsx** rather than its own file because it's tightly coupled to the card's visual rhythm — extracting it would create a parallel component that drifts out-of-sync with the card's tokens. Phase 65's `SpecializationSection` stayed inside HeroDetailDrawer.tsx for the same reason; both chips can be unified into one shared module if a third surface ever wants them (FactionStandings rows, BattlefieldHero label) but premature DRY would obscure the visual contract right now.
* **2026-05-19 (Phase 65 — HeroDetailDrawer Specialization section):** Picked **mounting between Metrics and MissionTimeline** (not below MissionTimeline, not above Metrics) because the section answers the same family of questions Metrics does ("what is this hero doing") at a higher level of abstraction — Metrics shows raw numbers, Specialization shows the narrative role. Putting it directly above the MissionTimeline sparkline gives the user "role chip → activity texture → recent missions" flow top-to-bottom, which reads as zooming in: identity → metrics → role → 24h trend → 5 most-recent rows. Above Metrics would put the role label before the numbers that justify it (the dots) which reads backwards; below MissionTimeline would bury it under the sparkline that the role label contextualizes. **Conditionally rendered when `totalMissions === 0`** rather than always-render-with-no-data-state because the drawer is already vertically tight (header + cwd box + Metrics + Timeline + active mission + 5 recent missions + Contributions + footer); adding a "Specialization · no data" placeholder pushes the more-useful sections below the fold on smaller windows. Heroes with zero missions get a clean drawer with no specialization block at all — same approach the Phase 39 active-mission preview takes when `activeMissionId` is empty. **`heroMissionsAll` re-use** for the profile input rather than recomputing a hero-scoped filter for the third time (Metrics already does its own, MissionTimeline does the 24h slice). Both consumers want the full hero-scoped set anyway, so re-using the existing `useMemo` is cheaper than maintaining a fourth slice that gets recomputed on every store tick. **Role chip uses inline accent styling** rather than Tailwind utility classes for the same reason Phase 62's MissionLog TagPillRow does: there are 8 tags × 2 confidence-presence states × 2 generalist-vs-specialist states = too many class permutations for PurgeCSS to enumerate. Inline `borderColor` + `color` + `background: ${accent}1a` keeps the math runtime-derived. **Confidence dot is solid for `high`, hollow for `medium`, omitted for `low`** rather than three filled colours because a small dot already reads as "high signal"; carrying signal in shape (solid vs. hollow) vs. fill (full vs. faded) maps cleanly to "confident vs. tentative" without needing the user to memorize a third colour. Low confidence drops the dot entirely so a 3-bug hero doesn't telegraph false certainty. **Per-tag segment widths are share-of-tag-slots** (not share-of-missions) because multi-tag missions like "refactor failing tests" contribute to two buckets — share-of-missions would push the bar above 100% width. Same normalization the Phase 64 helper does for `primaryShare`; the bar inherits the math. **Each segment carries both an `aria-label` AND a `<span class="sr-only">`** because Tailwind's `sr-only` is the standard pattern for hidden-but-readable text, but `aria-label` is what `getByLabelText` matches in tests and what some screen readers prefer for inline elements. Adding both costs nothing and locks in test ergonomics + a11y. **Caption strips the leading `<heroName> —` prefix** because the drawer header already names the hero in the `<h2>` (`hero-detail-title`). Repeating it inside the caption would read as overly chatty; the helper still returns the full caption for surfaces that DO need the name (e.g. fleet-grid views). Test verifies the prefix is stripped so a future refactor doesn't silently regress. **Secondary tag is a separate text chip** ("also feature") rather than a second pill or a slash in the role label because: (1) the secondary is informational, not actionable, so it shouldn't compete visually with the primary chip; (2) muted text reads as "and by the way..." without making the user wonder if they need to click it; (3) it gracefully degrades when secondary is null (chip simply doesn't render). **`data-*` attributes on the role chip** (`data-role`, `data-confidence`, `data-generalist`) so the test surface can grep by semantic state without re-string-matching the visible text — same pattern Phase 60 / 62 used. Tests use these as the primary assertion surface; visible-text matching is only for the caption + Generalist text branch.
* **2026-05-19 (Phase 64 — Hero Specialization helper):** Picked **pure-helper-first shipping** (helper this phase, UI integration in Phase 65) over a single-commit ship because the role-mapping table + confidence math + generalist gate is the kind of logic where seeing the tests pass on its own is the entire feature value — every UI surface that wants to consume it (HeroDetailDrawer, FactionStandings, eventually a fleet-survey grid) just calls into the same function. Same discipline as Phase 57 → 58 + Phase 61 → 62. **Single-tag role labels, no hybrid names** like "Feature-Bug Hybrid" / "Test-Refactor Maven" because the combinatorial space (8 × 8 = 64 hybrid labels for two-tag overlap, 512 for three) blows up faster than the value: the user-facing question is "what's this hero's vibe right now", not "what's the precise three-decimal-place taxonomy of their work mix". Generalist label gets a dedicated bucket so flat-distribution heroes don't lie as whichever tag happens to lead by one mission. **Generalist gate uses a `DOMINANCE_RATIO` of 60%** because below that, "primary tag" becomes ambiguous — a hero with 5 features + 4 bugs + 3 tests is empirically a generalist even if `feature` wins by one. Chosen empirically from the demo mission generator's distribution; can be tuned via the exported constant without touching consumers. **Three confidence tiers gated on sample size AND dominance** rather than dominance alone because a hero with 2 bug missions shouldn't outrank a hero with 18 mixed missions when surfacing "who is the bug expert". Low/medium/high lets the UI dim or asterisk the chip when confidence is low — same UX move HeroLeaderboard (Phase 60) used for tie-break: surface the rank, but mark it as provisional. **Primary share normalized against `totalTagSlots`, not `totalMissions`** so multi-tag missions don't push the share above 1.0. Example: a hero with one mission tagged `refactor` + `test` would otherwise show 100% for both tags; normalizing against slot count (2) gives 50%-50% which is honest. Tested explicitly in the share-math case to lock in the invariant. **Canonical-order tie-break** for primary picks (feature beats bug beats refactor etc.) because deterministic tests want a stable answer when two tags genuinely tie. Same rule the `MISSION_TAGS` array enforces for the pill row order in Phase 62 + 63. **Dismissed heroes still get a profile** because their mission history is preserved on the record — the UI may want to surface "what this hero did before they were dismissed" alongside roster sort / detail flyout views. Server-side aggregation that drops dismissed heroes would break that consumer. `summarizeFleetSpecialization` documents this contract explicitly. **`groupHeroesBySpecialization` returns shape-complete buckets** (all 8 tags + the dedicated `generalist` key, even when they're empty) so React renderers can `Object.entries` without conditional-key handling. Same shape `groupMissionsByTag` (Phase 61) and `groupByFaction` (Phase 52) use. **Heroes with zero missions are skipped entirely from the buckets** because they have no specialization to bucket — a profile placeholder in the `unknown` or `generalist` slot would be misleading. The UI can render them under a separate "no missions yet" footer if needed. **Caption helper** centralizes the chip text so the UI can drop it into `aria-label` / `<title>` tooltip / Hero Detail Drawer section header without re-stringifying the math three places. Same kind of single-source-of-truth move Phase 60's `formatLeaderboardValue` made.
* **2026-05-19 (Phase 63 — Mission Heatmap tag-pill row):** Picked **a single-phase helper + UI shipping** (vs. the Phase 57 → 58 split) because the helper extension is genuinely trivial — a 5-line conditional inside the existing mission-tally loop that re-uses Phase 61's `inferMissionTags` directly. Splitting that into its own phase would create a "helper exists but isn't wired anywhere" interim commit that adds no test value because the math is already covered by `missionTags.test.ts`. The Phase 57 → 58 split made sense when the helper was a \~280-LOC standalone module with novel maths (per-faction projection partitioning); Phase 63's `tagFilterActive` branch is a passthrough wrapper around a Phase 61 helper, so the helper test surface only validates that the wrapper threads the filter through correctly. **Tag counts computed against the POST-SCOPE / PRE-TAG slice** (mirroring Phase 62's `filteredByState`-based counts) rather than against the raw mission map because a fleet-wide count would lie to the user when they've already scoped to a faction. Example: an orc-scoped heatmap should show `Bug 4` if there are 4 orc bug missions, not `Bug 47` (the fleet total) — the chip needs to answer "how many would surface if I clicked this *given my current scope*", which is the same data-honesty rule Phase 60 + 62 used. Implemented via a small `scopedMissions` `useMemo` that re-runs the scope filter in JS (cheap, \~O(n) over the mission list) without duplicating the day/hour layout. **Tag set is component-local + transient** (resets on modal re-open) like the scope `<select>` and like Phase 62's MissionLog filter — tag filtering is exploration, not preference. Persisting it would surprise a user who left `bug` active a week ago and forgot. **Hero filter still wins over faction when both are set, AND tag filter still applies on top** — the precedence rule established in Phase 54 stays. This means the helper's filter chain is: (heroId ?? factionId-via-heroes-lookup) ∩ tagFilter. Tested explicitly in the hero ∩ tag composition + faction ∩ tag composition cases so the precedence is locked in. **Multi-select unions, not intersects** (any-of, not all-of) because the user's mental question is "show me work that's bugs OR tests", not "show me work that's tagged as BOTH bug AND test". This matches MissionLog's Phase 62 semantics and matches the `groupMissionsByTag` partition model from Phase 61 where multi-tag missions appear in multiple buckets. **Empty `Set` is a no-op** — same treatment as `null` / `undefined` — so the component doesn't have to nullify the state when the user clears every pill, just store an empty set. Verified with three separate test cases in the helper suite. **Pill counts include the `unknown` bucket** because hiding it would make the row width jump as missions are added/removed, and the Untagged bucket is informative: when it's non-zero, the user knows there are missions whose intent the inference whiffed on. **Component reuses Phase 62's pill markup verbatim** — same Tailwind classes, same `data-testid` shape (`heatmap-tag-pill-<tag>` mirrors `mission-tag-pill-<tag>`), same accent inline-style (`background: ${accent}1a`), same `aria-pressed` + `aria-label` + `role="group"` contract. A user who learned the row in MissionLog picks the heatmap version up instantly. **Helper signature stays additive** — adding `tagFilter` as an OPTIONAL named field on `BuildHeatmapInputs` means every existing Phase 48 / 54 caller compiles unchanged. Verified by leaving the existing test fixtures untouched and adding a fresh describe block at the bottom (Phase 54's pattern).
* **2026-05-19 (Phase 62 — MissionLog tag-pill row):** Counts on each pill label are computed against `filteredByState` (the post-state/post-search/post-hero-scope slice) rather than against the raw `missions` record because a fleet-wide count would lie to the user — they're already inside a narrowed view ("show me hero X's missions"), and a `Bug 47` chip when their filtered slice only contains 3 bugs would be cargo numbers. Same data-honesty rule Phase 60 used for "top N of M live" in the leaderboard summary. **Composition order is hero-scope → state → search → tag, not parallel intersection.** This means each filter narrows further, and the tag pass works on whatever's left. Order matters mostly for performance (run the cheapest filters first — hero-scope drops to one hero immediately) but also for predictability: if a user has state=`completed` active, the tag counts reflect "how many completed missions are bugs", not "how many missions are bugs in total". That matches the mental model of stacking narrowing filters in a SQL `WHERE` clause. **Pills are component-local state, not persisted.** Same rule as Phase 40's state filter and search input — tag filtering is a transient exploration tool, not a user preference, so reloading the page resets it. Persisting it would surprise users who left a filter active and forget about it. **Zero-count pills are dimmed instead of hidden** because the user's mental model is "which tags exist in this slice" — if `docs` is missing entirely it should still appear in the row, just visibly weaker. Hiding the pill on zero would make the row width jump around as the slice changes, which reads as bugs. **Active pills inherit the faction-themed accent via inline style** rather than ad-hoc Tailwind utilities because there are 8 tags × 2 states = too many class permutations for purgeCSS to enumerate. Inline style with `borderColor` + `color` + `background: ${accent}1a` keeps the math runtime-derived and lets the pill render correctly regardless of which exact hex value the faction palette resolves to. Same approach FactionStandings used for share-bar background. **No new helper logic this phase** — `filterMissionsByTags` + `tagCountSummary` already exist from Phase 61. UI test surface is component-only (happy-dom) so the existing Phase 40 / 60 / 52 component-test pattern continues to lock in render + click contracts without re-testing the math.
* **2026-05-19 (Phase 61 — Mission Tag Inference helper):** Picked **client-side regex inference** over a server-side `mission.tags: MissionTag[]` field on the protocol because adding the field would force every adapter / hook / War Room peer to either populate or tolerate the new schema, and older clients that decode shared replay links (Phase 47) wouldn't know what to do with tags they didn't write. Inference stays inside `apps/web` so the wire format is unchanged — same approach as Phase 60's leaderboard ranks (computed, not stored). **Pattern table over a single mega-regex** so each tag's matching surface is grep-able / testable in isolation and so future contributors can extend one tag (e.g. broaden `chore` to include `bump deps`) without touching the others. **Word-boundary anchors on every pattern** — a regression test (`'debugger' should NOT match 'bug'`) locks the rule in. **Multi-tag composition is the norm, not the exception** — "refactor the failing tests" SHOULD tag as both `refactor` AND `test` because filtering by `test` would otherwise miss the refactor-of-tests case, which is exactly what someone wants when they're surveying test churn. **Mission-level fallback to `unknown` instead of an empty tag set** — empty sets are ambiguous in filter UIs ("did this mission have zero tags or did the inference whiff?"), and a literal `Untagged` chip in the row above MissionLog is more honest. **Inflection coverage**: regex includes `(fix|fixes|fixed|fixing)` rather than relying on prefix matching, because prefix matching would over-match (`fix` → `fixed` is fine, but `fix` → `fixate` is not). **Faction-themed accent palette** so the chip row reads as native UI (orc red for bugs, elf green for features, etc.) rather than yet-another-tag-color-palette competing with the existing visual language. **Pure helper only this phase** — UI integration to MissionLog deferred to Phase 62 under the established Phase 32 / 38 / 41 / 43 / 45 / 46 / 47 / 48 / 52 / 55 / 57 / 60 discipline (logic ships first, fully tested, UI lands as a thin shell that wires store snapshots through `useMemo`). Helper file's top JSDoc names the phase + design rationale + extension hooks so the next contributor doesn't have to spelunk through git history to find the "why".
* **2026-05-19 (Phase 60 — Hero Leaderboard):** Picked **a dedicated leaderboard surface** over extending HeroRoster with a rank column because the two answer different questions: HeroRoster is "show me every hero by life-cycle state so I can act on each one" while a leaderboard is "show me who is *carrying* the fleet right now so I know who to interrogate / dismiss / clone". Squashing those into one surface (rank-in-roster) forces the user to scroll through every dismissed agent + every idle agent to find the carry hero — same anti-pattern that motivated the Phase 52 FactionStandings split out of the per-row metrics in HeroCard. **Hard cap at top 10 (DEFAULT\_LEADERBOARD\_LIMIT)** rather than "show every live hero" because a podium that scrolls past the viewport stops feeling like a podium; on a 30-hero fleet, ranks 11-30 are noise. Header advertises `top N of M live` so the truncation is visible. **Tie-break ladder is value DESC → lastActiveAt DESC → name ASC** so the "currently carrying" hero wins ties over the historically-tied one — answers the user-facing question "who's earning RIGHT NOW?" not "who has the longest history?". Alphabetic-name as the deterministic last resort keeps test output stable. **Share bars scale to the post-limit table max** rather than to a fleet-wide max because the user is looking at the podium and wants to see the relative gap inside *this* group (Phase 52's `metricShare` does the same — share-of-displayed, not share-of-fleet). **Achievement attribution uses Phase 35 `heroId` snapshot with `unlockedByHeroName` as the fallback** because pre-Phase-35 achievements still ship in the wild and dropping them from the count would make veteran heroes look like underperformers. Authorless unlocks (alliance-joined / auto-improver) are skipped — same data-honesty rule as `groupByFaction`. **5 metrics, not 6** — dropped FactionStandings' `heroes` count metric because "rank a list of heroes by hero count" doesn't make sense. **`g l` keyboard shortcut extends the `g r` family** so both navigation chords land in the same mental category: "go to a ranking surface". Open-only, not toggle — the muscle memory for `g <x>` across the app is "navigate to that surface", not "toggle it" (replay scrubber uses `r`-toggle for symmetry with its own dismissal pattern; leaderboard dismisses via Esc / backdrop). **CommandHandlers interface gains a new field** — could have wired the open via the `setStandingsOpen` handler but that would couple two unrelated overlays, so each modal owns its own handler key. Updates: TitleBar button + palette command + shortcut + SHORTCUTS\_HELP entry + commandPaletteRanking noopHandlers + App.tsx mount — exactly the same surface set the Phase 52 FactionStandings landing touched, so future contributors can grep for "Leaderboard" to find every wire-up site. **Tests pulled from both halves of the established pattern**: \~29 pure-helper tests in `heroLeaderboard.test.ts` covering metricValueFor / formatLeaderboardValue / countAchievementsPerHero / buildLeaderboard / rankHeroes, plus \~16 component tests in `Leaderboard.test.tsx` mirroring `FactionStandings.test.tsx` lifecycle / dismissal / metric-pill / hero-detail-open paths, plus 2 routeKey cases for `g l` + 1 SHORTCUTS\_HELP surface-contract case.
* **2026-05-19 (Phase 59 — README sync to Phases 52-58):** Pure docs sync — exactly the same shape as Phase 33 (sync to \~217 tests), Phase 42 (sync to \~335 tests), and Phase 51 (sync to \~600 tests). README had drifted again: it claimed \~600 tests when the actual count is \~718, its newest deep-dive section was Phase 49 CSV/JSON export, and its "What's intentionally not built" closing line credited shipping through phase 50 — but Phases 52-58 had landed in the meantime (faction standings, webhook adapter, per-faction heatmap filter, three cost-projection phases, public demo refresh). Untouched README content drifts faster than code does because nobody reads it during code review — keeping a periodic sync after every batch of 5-7 shipped phases is the right rhythm, and Phase 59 hits that cadence (Phases 51-58 = 8 phases since the last sync). **Consolidated phases 55 + 57 + 58 into one "Cost projection" section** rather than three sequential sections because the user-facing story is one narrative ("trailing-window math → per-faction partitioner → standings modal chip"), and three back-to-back sections that all start with "Phase 55…" / "Phase 57…" / "Phase 58…" would read as bookkeeping. Same approach Phase 51 took with the Phase 43-50 section: organize by user surface, not by commit ordinal. \*\*Test count kept as `~718` rather than the precise `718` because PROGRESS.md is the canonical ledger and the README is the friendly summary — exact precision belongs in one place. No code changes — keeps the commit reviewable as a docs-only diff.
* **2026-05-19 (Phase 58 — FactionStandings UI projection chip):** Picked **additive chip over a new metric** because the user's mental question with a projection is "how does spend look going forward?" not "rank me by projection" — those are different lenses and forcing them into one selector ("missions / tools / tokens / cost / achievements / heroes / projection") would (a) reset the user's metric every time they wanted to peek at projection without losing the missions ranking, and (b) require updating `factionStandings.ts` lib + its 30+ unit tests for a UI affordance that's pure addition. Rendering the chip in every row regardless of selected metric lets the user keep their ranking AND see the projection alongside — same pattern as Phase 43's `SessionRollup` strip showing the projection chip independent of the rest of the rollup numbers. **Chip is gated on `factionProjection.hasSignal`** so empty/quiet factions don't pollute the row with a misleading `$0` — same `hasSignal` discipline the Phase 55 `SessionRollup` chip uses. Footer total gets the same gate. **Faction-tinted projection value** (`style.hex` on the number) so the user's eye can follow the visual line from "the orcs are leading the missions ranking AND projecting the highest hourly burn" — colour as a same-faction identity marker reinforces the row. **Defensive `Object.values(missionsById ?? {})`** because `FactionStandings.test.tsx` (the existing component test) creates a partial store mock and doesn't necessarily seed `missions` — the existing Phase 52 test infrastructure was built before Phase 55 introduced the `missions` field in the store. Skipping the helper crash gives the test continued use without an update. **No new tests in this phase** — the math is already locked in by Phase 57's `factionCostProjection.test.ts` (\~14 cases), and the UI change is purely additive (existing assertions on `standings-row`, `standings-top-performer`, `standings-footer`, etc. all still pass because the new elements add to the DOM without removing or restructuring existing ones). Adding a component test for the chip would be cargo — the failure mode would be "the chip didn't render when it should have" or "the chip rendered when it shouldn't have", both of which are pure pass-throughs of the `hasSignal` flag that Phase 57's tests already exhaust.
* **2026-05-19 (Phase 57 — per-faction cost projection helper):** Picked **pure-helper-first** shipping — the helper + tests land in this commit, the standings UI integration is deferred to a follow-up — for two reasons. (1) Same discipline established across every helper phase since Phase 32: heavy logic exports cleanly, component is a thin wiring shell, and shipping the helper alone keeps the commit reviewable end-to-end without the reviewer also having to validate render-layer plumbing. (2) The FactionStandings.tsx surface needs a metric-selector update (add `projection` as a 7th metric) plus a per-row chip + footer total chip — that's a UI redesign with its own test contract, deserving its own phase rather than getting bolted onto a math commit. **Re-uses Phase 55's `extrapolateCost`** internally per-bucket instead of duplicating the windowing + scaling math because (a) the rate-override semantics, zero-rate-is-free-model handling, and clock-skew defence are all already locked in by \~21 tests against `extrapolateCost`, (b) a future cost-model change (e.g. Sonnet 5 pricing) flows through to every projection surface from a single edit, (c) the per-faction helper is therefore strictly compositional — easy to audit, no math drift. **`heroId` → faction lookup via `Map`** is O(1) per mission so the helper is O(missions + heroes) total; a naïve `heroes.find(...)` per mission would be O(missions × heroes) which matters when the heatmap-seeded mission history has 1000+ entries. **Unresolvable hero ids drop from per-faction rows but stay in `total`** because the user-visible contract is "fleet-wide is always honest, faction-specific is conditioned on attribution". This is the same data-honesty rule that the Phase 52 standings table uses (authorless achievements stay uncredited) and the Phase 54 heatmap faction filter (unresolvable missions drop when scoped). An alternative would be silently re-routing ghost-hero missions to a synthetic "unknown" bucket, but that introduces a sixth faction that doesn't actually exist in the game — better to make the gap visible by counting only in `total`. **`rankFactionsByProjection` is a separate exported function** rather than baked into `extrapolateCostByFaction` because ranking is a presentation concern, while the projection math is a data concern — separating them lets a future surface (e.g. a no-signal-friendly heatmap legend, or an alphabetical "list all factions even when zero" table) consume the same projections without inheriting an opinionated sort. Sink-no-signal-to-bottom rule mirrors the Phase 52 standings `heroCount` tie-breaker — factions with literally nothing to project don't deserve top placement just because their canonical id index is low. **Stable tie-break uses `projectedHourlyUsd → recentCostUsd → FACTION_IDS index`** so two factions burning the same projected $/hr break on which one is more recently active (a faction that ran $10 in the last 60 seconds beats one that ran $10 in the trailing window's first 60 seconds), and a fully-tied scenario falls back to canonical order for visual predictability across re-renders. **Test seed `as Hero` / `as Mission` casts** because the protocol types are huge and the helper doesn't read the full shape — only `id` + `faction` on Hero and `heroId` + `startedAt` + `tokensIn` + `tokensOut` on Mission. Type-narrowing the test inputs would force every test to construct the full 10-field Hero record which adds noise without catching real bugs.
* **2026-05-19 (Phase 56 — public demo refresh / v2):** The demo had been frozen since Phase 17's PWA shell (`046649f`) — it showcased only the original four-surface UI (battlefield + roster + mission log + scrolls) and the splash for the first-time visitor implied that was *the* product, while every commit since Phase 30 had been quietly enlarging the actual app's vocabulary (trophy room, heatmap, standings, command palette, replay scrubber, full keyboard grammar). The demo was undersell by a wide margin. Picked **one big rewrite of `public-demo/index.html`** over an incremental phase-per-feature progression because (a) the demo's value is the single-file installable showcase — drip-feeding features over six commits would have left intermediate states where the splash advertised "v2" but only half the v2 surfaces existed, (b) the demo doesn't have its own test surface (it's a static showcase, not a tested unit), so the usual "PR-per-helper" discipline isn't pulling its weight here, (c) the user explicitly called out that demo work should not bleed back into `apps/web` — keeping it self-contained in one file makes that constraint mechanical to enforce. **Inlined the pure helpers** (`buildHeatmapBuckets`, `tileOpacity`, `computeStandings`, `metricValue`, `formatStandingsValue`, `metricLabel`, `sessionStats`, `rankCommands`) rather than ES-importing from `apps/web/src/lib/*` because the single-HTML-file constraint forbids a bundler and rewriting them in plain JS keeps the demo zero-install — the contract maintained instead is "demo behaviour mirrors the apps/web helper behaviour by structure", validated by hand-review (the demo's `computeStandings` reads as a direct port of `factionStandings.ts`'s, the heatmap math is identical, etc.). If a future phase changes a helper's behaviour in `apps/web`, the demo follows in a sibling commit — same discipline as keeping the README in sync (Phase 51) but file-bounded. **All five overlay modals share a single `Modal` shell** (Escape handler, click-outside dismiss, `role="dialog"`, `aria-modal`, optional `wide` / `tall` size hints) rather than each modal owning its own backdrop because the user-facing contract is "every overlay dismisses the same way" — five forked backdrops would invite drift the first time we tweak the focus trap. **Keyboard grammar matches the full app's Phase 45 routeKey** (input-aware skip, `?` honored even while typing, `g r/h/s` two-step with 900ms window, Cmd/Ctrl+K accepted with Shift/Alt rejected to avoid devtools/bookmarks collisions) — replicating the same heuristics in the demo means a user who learns `g r` here doesn't have to re-learn anything when they boot the real `apps/web`. **Synthetic mission seed expanded to 7 days × 24 hours of weighted random missions on first render** (weekday-business-hours bias + weekend dampener + per-faction sampling) because Phase 48's heatmap was the highest-value Phase-30+ addition and shipping it with an empty grid would have been false advertising — the user's first impression of the heatmap should be "oh, this is a real surface" not "oh, this is a placeholder". Generator runs in `useState` initializer (one-shot, deterministic for the session) rather than the demo loop's `setInterval` so the heatmap doesn't shimmer between renders. **`SEED_ACHIEVEMENTS` ships 2 of 7 unlocked** with faction attribution (first-hero → orc, first-mission → human) so the trophy room's progress ring and "recently unlocked" section have non-empty initial content; the loop unlocks more over time as missions complete, exactly the way the real `AchievementEngine` would (Phase 35). **Command palette ranker is the simplified Phase 46 ranker** (prefix-100 / word-60 / substring-30 / no-match -1, sorted DESC) — close enough to the apps/web ranker that the UX matches, simple enough to inline in JS without dragging in lib/commandPalette.ts wholesale. **Header tool buttons (🏆 / 🗓 / ⚖ / ⌘K / ?) sit in a single row** with the SessionRollup strip in the middle and the GitHub source link on the right — the same TitleBar topology as the real app's Phase 43 layout, just rendered in a single-row demo width. Buttons have `title=` tooltips with the keyboard shortcut so users learn the binding without opening the cheatsheet. **Splash gets a `demo v2 · Phases 30-55` pill** + "press ? for shortcuts" hint at the bottom of the splash so the diff vs. v1 is visible without needing to read the README. No `apps/web` touches, no test changes — the demo refresh is a self-contained docs phase that closes follow-up #10 in the backlog.
* **2026-05-19 (Phase 55 — cost projection helper):** Picked **trailing-window extrapolation** (last 15 minutes) over a per-hero rate model because the metric the user actually wants to see is "how much will this session cost in the next hour at the current pace" — and "pace" by definition is a recent-activity-window concept. Per-hero `lifetime cost / lifetime ms` averages would silently dilute spikes (one expensive Opus mission followed by 30 minutes of idle would project \~$0/hour, which is a lie when the user is actively spending). 15 minutes is the documented default because (a) it's long enough that a single 30-second mission completing doesn't whiplash the projection by 100×, and (b) it's short enough that a quiet 30-minute lunch break correctly drops the projection to $0 rather than letting it haunt the rollup strip for an hour. Configurable via `windowMs` for power users with strong opinions ("I want my projection sensitive to the last 60 seconds because I'm benchmarking"). **Per-Mtok rate defaults to Sonnet 4.5's published numbers** ($3 in / $15 out) because that's the model the seed roster uses, not because we're trying to be sneaky about model heterogeneity — the helper deliberately doesn't try to attribute per-model. Callers with mixed-model rosters can pass `costPerMtokIn` + `costPerMtokOut` overrides; a future refactor could materialize a per-model rate table inside the helper but for now the single-rate model matches every other dollar-cost surface in the app (the Phase 43 `estimatedCostUsd` field, the Phase 49 CSV export, etc.). **Negative rate overrides bypass to the default** but a **zero-rate is a legitimate input** (a user pointing at a free local model wants $0 projections, not "we assumed Sonnet pricing because you passed 0"). This matters because the `hasSignal` flag specifically gates on `recentCostUsd > 0` — a zero-rate model legitimately has no projection signal, and the UI should hide the chip rather than render a pile of meaningless zeros. **Future-dated missions are dropped** (clock-skew defence) because some demo environments will deliberately seed missions a few seconds ahead of `Date.now()` to keep them in the "active" band, and we don't want those to contribute to a projection of "the next hour" that hasn't started yet. Mounting the chip in `SessionRollup` (not a separate strip or modal) because it's a sibling of the existing `Σ cost` rollup and benefits from sitting next to the cumulative-cost surface — users will glance at "we've spent $X so far" and immediately want to know "and we'll spend $Y in the next hour". `hasSignal` gate keeps the strip clean when nothing is firing (vs. always-rendering "proj · next 1h: $0" which would dilute the at-a-glance utility). Same pure-helper pattern as every other phase since Phase 32 — heavy logic exports cleanly, component is a thin wiring shell.
* **2026-05-19 (Phase 54 — per-faction heatmap filter):** Picked **a single unified scope `<select>` with optgroups** over a parallel hero-dropdown + faction-dropdown pair because (a) the user's mental question is "narrow down to X" where X is a single value (a faction OR a hero, never both), (b) two parallel dropdowns invite contradictory states ("orcs" AND "Ada the elf") that need awkward UI to resolve, and (c) optgroups are exactly the native pattern HTML provides for "pick one of several typed buckets". Encoded the scope value as a tagged string (`all` / `faction:<id>` / `hero:<id>`) rather than maintaining a parallel "scope kind" state because a `<select>` value is a string anyway — encoding the kind into the value keeps the React state to one variable + one `useMemo` parse. **`heroId` filter wins when both `heroId` and `factionId` are set** because hero is strictly narrower than faction (a hero belongs to exactly one faction) — a UI that exposed the conflict and asked the user to choose would be more honest but actually more confusing for a filter widget. Documented in the JSDoc on `BuildHeatmapInputs`. **Missions with an unresolvable hero are DROPPED when the faction filter is active** rather than counted under "fleet-wide" — same reasoning as the Phase 35 hero-attribution UI rejecting authorless achievements from leaderboard credit: if we can't substantiate the faction membership, including the mission would lie to the leaderboard. Without the faction filter (fleet-wide), unresolvable heroes still count because the user explicitly didn't ask to scope. **`heroes?: Record<string, Hero>`** is optional on the helper, but the helper drops every mission when `factionId` is set without a lookup — the alternative ("ignore the filter") would silently lie about scope when the caller forgot to pass the lookup. Defaulting to zero results forces the bug to surface immediately. Accent colour follows the chosen scope (hero faction colour OR scoped faction colour OR neutral steel for fleet) — same visual vocabulary as Phase 48's hero-scope behaviour. Same pure-helper extraction pattern as Phase 32 / 36 / 38 / 40 / 41 / 43 / 45 / 46 / 47 / 48 / 49 / 52 / 53 — every new surface ships heavy logic as a pure exported helper so tests stay React-free.
* **2026-05-19 (Phase 53 — generic webhook adapter):** Picked **explicit name+value env vars** for the auth header (`FACTIONOS_WEBHOOK_AUTH_HEADER_NAME` + `FACTIONOS_WEBHOOK_AUTH_HEADER_VALUE`) rather than baking in vendor-specific schemes (`FACTIONOS_WEBHOOK_BEARER_TOKEN`, `FACTIONOS_WEBHOOK_API_KEY`, etc.) because the universe of HTTP auth schemes is large and changing — the adapter shouldn't pick favourites. Supporting `Authorization: Bearer …`, `X-Api-Key: …`, ntfy basic auth, custom internal headers, etc., is then a documentation problem, not a code problem. Validator throws when only one half of the pair is set so a user can't accidentally ship a half-configured auth that fails silently — same defensiveness as `readDiscordConfig`'s "DISCORD\_WEBHOOK\_URL is required" throw. **Two payload shapes** (`full` vs. `compact`) because the user base for this adapter is fragmented: internal observability pipelines want the raw event passthrough (so they can re-derive any field the formatter dropped, even fields the formatter never knew about), but consumer chat hooks (ntfy, Pushover, generic Slack incoming webhooks) want a tiny envelope they can show as a notification title + body without parsing. Default is `full` because losing data downstream is a worse failure mode than carrying a few extra KB; `compact` is opt-in for users who explicitly need the smaller shape. Picked **`source: "factionos"` + `version: 1`** at the top of the full envelope so a future v2 shape change is unambiguous: downstream code that doesn't recognise the version can fall back to the documented v1 contract. Same pinning discipline as Phase 49's `schemaVersion: 1` in the JSON export — both are stable contracts with downstream consumers and need addressable versions. **`buildWebhookPayload`, `buildWebhookHeaders`, and `sendToWebhook` are exported separately** so the unit tests can lock the schema (`buildWebhookPayload` round-trip), the header contract (`buildWebhookHeaders` matrix), and the fetch lifecycle (`sendToWebhook` with injected fetch) independently — exact-same decomposition discipline as Phase 49's `sessionToJson` + `sessionToCsv` + `buildSessionExport` and Phase 47's `encodeReplayLink` + `decodeReplayToken` + `bootShareReplay`. **`sendToWebhook` uses `AbortController` + a `setTimeout` for timeout enforcement** (not `Promise.race`) because `fetch` in Node 18+ honors `AbortController.signal` natively — the request is actually cancelled at the socket layer, not just at the Promise layer (which is what `Promise.race` does and leaks the underlying request). `controllerFactory` is a test seam so we can verify the abort wiring without sleeping in tests. Timeout-default of **5000 ms** matches Discord's documented response envelope (median \~300ms, 99p \~2s) with a 2× safety margin — ntfy and Pushover are similar. Power users can raise it for slow internal endpoints via `FACTIONOS_WEBHOOK_TIMEOUT_MS`; setting it to `0` disables the timeout entirely (we do not auto-clamp to a floor because some internal pipes legitimately accept 30+ second waits). **No retry / queue** because adapters are best-effort and the web UI is the canonical record (same rule the README has had since Phase 16) — adding queue + retry would re-open the persistence question. `redactUrl` masks the token portion of the webhook URL in the boot log because a webhook URL is often a secret (signed Slack incoming hooks, Discord webhook URLs, ntfy topics with bearer auth in the path) and operators tend to grep journalctl with the verbose mode on; a leaked URL is then a Slack post away from the wrong eyes. Redaction surface uses URL parsing rather than string slicing so malformed URLs degrade gracefully (`<malformed url>` placeholder, no throw at boot time). Dispatcher's `pickDefault` falls through Discord → Telegram → webhook so a user who has the new env var set without typing `webhook` on the command line still gets the right adapter. Same pure-helper extraction pattern as Phase 32 / 36 / 38 / 40 / 41 / 43 / 45 / 46 / 47 / 48 / 49 / 52 — every new surface ships heavy logic as a pure exported helper so tests stay off the wire.
* **2026-05-19 (Phase 52 — faction standings comparison):** Picked a **center-screen modal** (not a docked panel) for the same reason MissionHeatmap and AchievementsGallery chose modal — the table reads as "step back and compare", not "watch continuously", so the modal lifecycle (Escape / backdrop / close button) matches user intent. Mounted as a sibling in `App.tsx` rather than tacked onto an existing surface because faction comparison is **fleet-wide by design** — embedding it inside HeroDetailDrawer or HeroRoster would imply scope that doesn't apply. Render **all five faction rows even when a faction has zero live heroes** because the user-facing insight is "the lobsters aren't pulling weight today", which requires showing the empty row, not omitting it — same reasoning as the Phase 38 HeroRoster sort decision to keep dismissed-band rows visible until pruned. **Top performer per faction** uses `missions DESC` with `tools DESC` as the tie-breaker and `name` alphabetic as the last-resort stable sort, so the chip is deterministic across re-renders (tests assert this with three heroes all at `10m` to lock in the order). Chose **horizontal share bars** over column charts because (a) one bar per row reads in O(5) glances even on narrow viewports, (b) the relative gap between "the leader has 4× more missions" and "everyone is tied" is what the user actually cares about, and (c) bars share the existing accent-colour-per-faction palette so the visual vocabulary stays unified. Bars use **opacity 0.35 for absent factions and 0.85 for present factions** so the "absent" state reads as a real status rather than a rendering bug. **Share bar `data-share` attribute is exposed on the DOM** so tests can assert the math without parsing CSS `width` — same approach as Phase 48's `data-count` on heatmap cells. Metric selection is **component-local state (not persisted)** because the user almost always wants to revisit the leaderboard with "missions" as the default — persisting their last metric choice would defeat the "always land in a fresh state" pattern that AchievementsGallery section collapse and MissionHeatmap hero filter already establish. **Achievement attribution gates on BOTH `unlockedAt` AND `unlockedByFaction`** — the former so unearned achievements never count, the latter so authorless unlocks (alliance-joined, auto-improver) stay uncredited (giving them to a faction would be a lie the leaderboard can't substantiate). Authorless achievements simply don't show up in the standings — same data-honesty rule as Phase 35's hero-attribution UI. **Top-performer chip is a `<button>`** (not a `<Link>` or icon-only) so it's tabbable, has an aria-label, and click-activates the hero selection — same a11y rule the rest of the app follows. The chip click **closes the modal first and defers the selection by `setTimeout(0)`** so HeroDetailDrawer's auto-focus doesn't fight the modal's keydown listener tear-down — same defer-by-tick discipline the command palette uses for command activation in Phase 46. `formatStandingsValue` reuses the **K/M ladder + sub-cent floor** from Phase 43's SessionRollup formatter — exact-same formatters across the app reduce visual surprise when the user moves from rollup-strip to standings-modal. `metricShare(row, metric, rows)` is a pure helper kept separate from `formatStandingsValue` because (a) percent-share has different math than display formatting and (b) tests can assert clamping + zero-denominator guard against the helper directly. Same pure-helper extraction pattern as Phase 32 (`replayLast`), Phase 36 (`diffNewUnlocks`), Phase 38 (`sortRoster`), Phase 40 (`filterMissions`), Phase 41 (`groupByTier` / `recentUnlocks`), Phase 43 (`sessionStats`), Phase 45 (`routeKey`), Phase 46 (`buildCommands` / `rankCommands`), Phase 47 (`encodeReplayLink` / `dispatchReplayEntries` / `bootShareReplay`), Phase 48 (`buildHeatmapBuckets` / `tileOpacity`), Phase 49 (`buildSessionExport` / `sessionToCsv` / `sessionToJson`).
* **2026-05-19 (Phase 51 — README sync):** Pure docs sync — exactly the same shape as Phase 33 (sync to \~217 tests) and Phase 42 (sync to \~335 tests). README had drifted: it claimed \~335 tests when the actual count was \~600, advertised three "not built" items when one of them (wrangler-dev test) had shipped in Phase 50 and another (PWA push) had been partially shipped in Phase 44 (the Notification + SW plumbing landed, only a push backend is missing). Untouched README content drifts faster than code does because nobody reads it during code review — running a periodic sync after every batch of 5-7 shipped phases is the right rhythm. Pulled the **transient flag list** into its own sub-table inside the Settings section so future readers grep'ing for `commandPaletteOpen` / `heatmapOpen` / etc. land on the actual surface explanation instead of scrolling through 200 lines of feature copy. Kept the test count as `~600` rather than the exact `~598-600` because PROGRESS.md is the canonical ledger and the README is the friendly summary — exact precision belongs in one place. No code changes — keeps the commit reviewable as a docs-only diff.
* **2026-05-19 (Phase 50 — wrangler-dev integration test):** Picked a **double gate** (opt-in env var `FACTIONOS_RUN_WRANGLER_DEV=1` AND a `wrangler --version` probe) rather than a single check because (a) a dev who has wrangler installed for unrelated work shouldn't have a 30-second workerd boot land on them every `npm test`; (b) a CI run that does have wrangler available shouldn't accidentally run the real-runtime test without explicit opt-in. The single source of truth for "did this test fire?" stays a single env var, with the path-probe acting as a graceful degrade when the user opts in on a misconfigured box. **Used `it.runIf`** (Vitest 0.34+ idiomatic conditional) so the skipped path is **invisible** rather than printed as a noisy "skipped" line — the test reads as "did not apply" rather than "did not pass". The default-CI case (no opt-in) emits a single `it.skip(...)` row at the top of the describe block with a clear "using in-process fake suite" hint, so a future maintainer reading test output knows WHY this surface looks bare. **45-second spawn timeout** because `wrangler dev` cold-starts in 5-15s on a fast machine but can hit 30s on first-run-after-cache-clear when it re-downloads workerd; 45s is the right balance between "fail fast on a hung start" and "don't false-fail on a slow start". The ready-line regex (`/Ready on http:\/\/127\.0\.0\.1:(\d+)/i`) is paranoid about case (some wrangler versions emit "Ready" with capitalisation drift); the port extraction means we don't have to hardcode 8787 or pre-allocate. **Lifecycle is explicit**: `beforeAll` resolves the handle (or `null` on skip); `afterAll` kills the process in a try/catch so a thrown kill doesn't cascade into the test report. This is the **drift detector** that the Phase 28 in-process fake suite cannot be — the fake covers the WarRoom semantics deterministically, but only a real workerd round-trip catches a Cloudflare runtime upgrade that breaks a fetch listener signature or a DurableObjectState method we previously relied on. Worth the maintenance cost as long as it stays a one-file, one-flag opt-in surface.
* **2026-05-19 (Phase 49 — CSV/JSON export endpoint):** Picked **one CSV file with two labeled sections** over "ship two .csv attachments" because a real spreadsheet user opens one file at a time — splitting heroes and missions into separate downloads loses the join key and the "what session did this come from" context. Section markers are `# Section: <name>` comment rows separated by a blank line; every CSV reader I tested (Excel, Numbers, LibreOffice, Google Sheets, pandas) either skips comment rows as noise OR keeps them as a labeled header, both of which are acceptable outcomes. **POST (not GET)** on the route because (a) export is an observable side-effect from an audit / persistence standpoint — logging exports separately from idempotent reads keeps access patterns honest; (b) browsers honor `Content-Disposition: attachment` identically on POST as on GET, so the UX is unchanged; (c) a POST naturally supports a future "filter by date range" body without breaking the URL contract. **`buildSessionExport` is the single entry point** with `sessionToCsv` + `sessionToJson` + `heroesToCsv` + `missionsToCsv` + `escapeCsvField` all exported separately so each layer is unit-tested without the full pipeline — exactly the same pattern as Phase 47's `encodeReplayLink` + `decodeReplayToken` + `bootShareReplay` decomposition. **`schemaVersion: 1` baked into the JSON payload + an `X-FactionOS-Export-Schema: 1` response header** because the export is a stable contract with downstream consumers (auditors, offline analyzers, "paste this into a sheet" scripts); we want a v2 to be addable without breaking pinned tools. Adding a field is a v1.x change, removing or renaming is the v2 trigger. **CSV uses RFC-4180-compliant quoting** (`escapeCsvField` wraps with `"` whenever the value has `,` / `"` / `\r` / `\n` and doubles internal quotes) tested across all the surface area where users actually break things (multiline prompts, names with commas, embedded quotes). Numbers + booleans coerce via `String()` rather than `JSON.stringify` so booleans render as `true`/`false` (spreadsheet-friendly) rather than `"true"`/`"false"`. **Filename includes a `YYYYMMDDHHMM` stamp** so a power user who exports twice in a session ends up with two distinct files in their Downloads folder instead of `factionos-session (2).csv`. **Frontend `triggerSessionExport(format, port, customDeps?)`** injects every IO side-effect (`fetch`, `URL.createObjectURL`, `document.createElement`, anchor click, anchor removal) through the optional `customDeps` bag so the unit tests run headless — happy-dom is along for the ride but never actually clicks the anchor. This is the same testability pattern as Phase 47's `bootShareReplay(opts)`. The download path uses the **hidden-anchor-+-click trick** (the standard browser-side download technique; works on every browser that doesn't lock down user-gestured downloads) rather than `window.open(blobUrl)` because the latter triggers popup blockers on Safari and loses the suggested filename. `URL.revokeObjectURL` is called immediately after the click so we don't leak the Blob into the browser's URL store — important when the user is exporting multi-megabyte sessions. **`parseFilenameFromDisposition` handles both quoted and unquoted forms** + is case-insensitive on the `filename` key because real-world servers are inconsistent and we want the frontend to land the right filename even when a proxy normalises the header. Falls back to `factionos-session.<format>` when the header is malformed. **Command palette entries live under the Dev group** (vs. Settings or Overlays) because export is a power-user audit action — surfacing it next to the heatmap and reset commands matches user intent ("I'm in dev mode, I want to inspect what just happened"). `exportWithToast(format, port, pushToast)` mirrors the Phase 31 codebase-scan toast lifecycle (info on dispatch, success/error on outcome) so the visual vocabulary across the app stays consistent. Fixed a stale `commandPaletteRanking.test.ts` `noopHandlers()` factory along the way — Phase 48 added `setHeatmapOpen` to the required `CommandHandlers` interface but didn't update the test factory, so this phase backfills both `setHeatmapOpen` AND the new `exportSession` field. Same pure-helper extraction pattern as Phase 32 (`replayLast`), Phase 36 (`diffNewUnlocks`), Phase 38 (`sortRoster`), Phase 40 (`filterMissions`), Phase 41 (`groupByTier` / `recentUnlocks`), Phase 43 (`sessionStats`), Phase 45 (`routeKey`), Phase 46 (`buildCommands` / `rankCommands`), Phase 47 (`encodeReplayLink` / `dispatchReplayEntries` / `bootShareReplay`), Phase 48 (`buildHeatmapBuckets` / `tileOpacity`) — every new surface ships its heavy lifting as a pure standalone function so the test suite stays React-free.
* **2026-05-19 (Phase 48 — mission heatmap):** Picked **per-actual-day rows for the last 7 days** rather than aggregated-by-day-of-week (Mon..Sun) because the user-facing question this surface answers is "what happened THIS past week" — concrete dates beat "Mondays in general" for an observability tool that's pointing at real incidents. Aggregating into the 7 weekday labels would smear two consecutive Fridays together; if a future use case wants the recurring-pattern view it can ship as a separate `buildWeeklyAverages` helper without breaking the cell-shape contract. **Center-screen modal** (not a docked panel) because the grid wants \~720 × 220 px to read comfortably and the existing 3-column layout would crowd it; the modal lifecycle (Escape / backdrop / close-button) also matches the established pattern from `HeroDetailDrawer` / `MissionDetailDrawer` / `AchievementsGallery` / `KeyboardShortcutsModal`. Mounted as a **sibling in `App.tsx`** rather than inside HeroDetailDrawer because the heatmap is fleet-wide by default — scoping it inside the hero drawer would imply per-hero only and miss the "we always crash on Friday afternoons" insight that the backlog item explicitly called out. The hero-filter dropdown is in the modal header so a power user can still narrow to one hero without losing the surface. **Per-day-of-month label rendering** uses `getDay()` + `getMonth()` / `getDate()` from the runtime Date so the row labels match the user's local timezone exactly (matching MissionTimeline's wall-clock alignment from Phase 37) — that means a US user viewing a session that ran across DST sees the right boundaries. **DST-safe bucket math**: the day-index is derived from `Date.setHours(0,0,0,0)` then a divided-by-86\_400\_000, NOT from raw timestamp subtraction, because a 23-hour DST-spring day would otherwise leak the 23:00 cell into the next row. **`tileOpacity(count, peak)` exported as a pure helper** with the same shape pattern as Phase 47's `dispatchReplayEntries` (pure inputs, pure outputs, no side effects) so the colour-ramp math is unit-tested without rendering — six cases covering idle, minimum-visible, peak clamp, range linearity, defensive `peak === 0`, and over-peak clamp. Cell intensity uses **opacity scaling on a single accent colour** (not a discrete tier-coloured palette like GitHub's commit graph) because the heatmap reads at-a-glance — opacity-as-density is more intuitive than "is light yellow more or fewer than medium green" for users not already trained on the GitHub palette. Idle cells get **0.06 baseline opacity** (not zero) so the grid still reads as a grid even when nothing fired, same logic as MissionTimeline's faint baseline ticks. Sparse hour labels (every 3 hours: 00 / 03 / 06 / 09 / 12 / 15 / 18 / 21) because labeling every column would compress to unreadable 5px text at 880-px modal width; the `<title>` tooltips give the exact hour on hover for any cell. **Hero filter resets to "all" every time the modal re-opens** (gated by a `prevOpenRef`) so a power user who filtered to "Boris" last session doesn't open the modal hours later wondering why the grid is mostly empty — same "always land in a fresh state" UX that Phase 41 picked for AchievementsGallery section collapse. Same pure-helper extraction pattern as Phase 32 (`replayLast`), Phase 36 (`diffNewUnlocks`), Phase 38 (`sortRoster`), Phase 40 (`filterMissions`), Phase 41 (`groupByTier` / `recentUnlocks`), Phase 43 (`sessionStats`), Phase 45 (`routeKey`), Phase 46 (`buildCommands` + `rankCommands`), Phase 47 (`encodeReplayLink` + `decodeReplayToken` + `bootShareReplay`) — every new surface ships its heavy lifting as a pure standalone function so the test suite stays React-free.
* **2026-05-19 (Phase 47 — share-replay link):** Picked **plain JSON + base64url over LZ-string** for the wire format (the backlog suggested LZ-string). Trade-off: LZ-string would shrink the payload \~3× for repetitive event streams, but (a) the typical 30-second / 200-entry share already fits under 20 KB base64url — well below the 32 KB Edge / 8 KB modest-proxy URL budget that actually matters for share links; (b) LZ-string adds a transitive dep that would be the first thing in `apps/web` to NOT come from React/Three/Zustand/Tailwind, breaking the "every dep earns its place" discipline established in Phase 19's vitest scaffold + Phase 28's no-wrangler war-room tests; (c) LZ-string's high-bit output gets silently rewritten by Slack/Discord/Telegram unicode normalisation on copy/paste — the kind of breakage a teammate would diagnose as "the URL came through corrupted" — whereas base64url survives every chat client. Wire format wraps timestamps as **offsets from `t0`** to keep entries small (a typical share has 50-200 events within a 30s-5m window; storing absolute ms wastes 6 chars per entry × 200 = 1.2 KB before encoding). Decoder is **strict-defensive**: every parse-error path returns `null` and never throws (validated by 15+ negative test cases — empty / missing dot / bad version / non-base64 / bogus JSON / missing v / non-numeric t0 / non-array e / non-pair entries / non-number offset / non-string event.type / non-object event / etc.) — so an attacker-supplied `#replay=v1.<crafted>` fragment cannot crash boot even if the user clicks a hostile link. Encoded events are dropped (not the whole token rejected) when any single entry is malformed — the share is partially recoverable rather than all-or-nothing. Cap of **200 entries** (`REPLAY_LINK_MAX_ENTRIES`) with **tail-slice** (`entries.slice(-CAP)`) so the most-recent events win when the slice overshoots — the user almost always cares about "what just happened" not "what happened 30 minutes ago". **Versioned prefix `v1.<payload>`** so a future v2 wire format can ship without breaking older shared links — the decoder explicitly rejects non-`v1` tokens rather than guessing. Boot path **strips the hash BEFORE dispatching the first event** (via `history.replaceState`) so a mid-replay refresh doesn't fire a second replay; the strip is wrapped in `try/catch` because some sandboxed iframes refuse history mutation and the share should still play in that case. Boot path **doesn't re-capture into the local ring buffer** (sets `replayingSinceMs` via the existing flag) so sharing a replay doesn't pollute the receiver's buffer with the sender's events — keeps the receiver's local "what just happened" view intact. Default playback speed is **2×** for shared replays (vs. the 1× default the user picks in the ReplayPanel) because shared replays usually skip the "between scenes" idle that makes 1× feel slow, and a 30s slice replays in \~15s which matches the toast-life window. Build path **extracted as `bootShareReplay(opts)`** with all I/O (strip / toast / applyEvent / setReplayingFlag) injected as callbacks, so the entire orchestration is unit-tested without React: 8 cases in `replayLink.test.ts` exercise null-on-no-hit / strip-before-dispatch ordering / info-toast-before-dispatch / replayed count / success-toast-after / flag toggle order / sandboxed-strip-throws survival / speed override. Share button **disabled when `eligibleCount === 0` OR a replay is in flight** (same predicate as the existing Replay button) — the disabled state is the affordance for "this window has nothing to share". Clipboard write **wrapped in try/catch** so a denied-permission browser still gets a usable info-toast with the URL in the body (the user can manually copy from the toast); the same code path handles `navigator.clipboard` not existing at all (some older Safari builds). Toast title flips between three states: **success** (clipboard wrote), **info** (clipboard failed, URL in body), **warn** (no eligible events — defensive, the button is also disabled in that state). Same pure-helper pattern as Phase 32 (`replayLast`), Phase 36 (`diffNewUnlocks`), Phase 38 (`sortRoster`), Phase 40 (`filterMissions`), Phase 41 (`groupByTier`), Phase 43 (`sessionStats`), Phase 45 (`routeKey`), Phase 46 (`buildCommands` + `rankCommands`) — exports the heavy lifting as standalone functions so the test surface decouples from React rendering.
* **2026-05-19 (Phase 46 — command palette / Cmd+K):** Built the catalog as a **`buildCommands(inputs, handlers)` pure data structure** that takes a snapshot of the live game/settings stores + a bag of handler callbacks and returns a flat list of `PaletteCommand` records — same shape/discipline as Phase 38's `sortRoster`, Phase 40's `filterMissions`, Phase 41's `groupByTier`, Phase 43's `sessionStats`, Phase 45's `routeKey`. That decomposition means **the entire catalog + ranker is unit-tested without React** (22 cases in `commandPaletteRanking.test.ts` exercise it directly with `vi.fn()` spies for the handlers), and the component is a thin shell that just wires the store snapshots into `buildCommands` via `useMemo`. **Multi-token substring matching** (vs. fuzzy / edit-distance / TF-IDF) because (a) the catalog is small (\~50 commands), (b) edit-distance would let `"reset"` partially match `"replay"` which is a worse experience than honest "this token didn't match anything", (c) `Array.prototype.includes` is dependency-free. **Scoring matrix** (label-prefix 100 > label-interior 60 > group 20 > keywords 15 > detail 10 > id 5) tuned so typing `"and"` surfaces the Andrew hero before any other command that happens to contain `"and"` in its description — label-prefix is the strongest signal because that's what the user actually sees. **Empty query returns the catalog in insertion order**, not alphabetised, so the most-useful surfaces (jump-to-hero / jump-to-mission live at the TOP of `buildCommands`) stay visible without typing. Cmd+K is allowed as the **one permitted modifier chord** because it's the universal palette binding (VS Code / Linear / GitHub / Notion / ChatGPT / every IDE in the past 15 years). The hook's `routeKey` checks for it BEFORE the general modifier guard, so the bulk of the chord behavior (Cmd+R refresh, Ctrl+F find, etc.) is unchanged. **`Cmd+Shift+K` and `Cmd+Alt+K` are explicitly rejected** because they collide with Firefox devtools (Shift) and Bookmarks (Alt) — those test cases lock that property in. Uppercase `K` is accepted to handle caps-lock — the `key` field stores the post-modifier character, so caps-lock + `k` produces `"K"`. Activation **closes the palette first** and defers the side effect by `setTimeout(0)` so any drawer the command opens (e.g. HeroDetailDrawer when jumping to a hero) lands on a fresh DOM without competing with the palette's input focus. Picked a **center-screen modal at 12vh from top** (rather than full-screen overlay or right-side drawer) because that's the Linear/VS Code convention and matches the visual weight users expect from "Cmd+K". The palette **auto-focuses the input via `setTimeout(0)`** for the same reason — needs to wait for the input element to mount. Hover + keyboard share the same `activeIndex` state (mouseEnter sets the index, keyboard arrows mutate it) so the user's last interaction always controls focus — same UX pattern as VS Code's quick-open and Linear's command bar. Catalog grows with the session: more heroes spawn, more missions log, more become palette-jumpable. No persisted history because (a) localStorage scope creep, (b) the palette is fast enough that re-finding a command isn't friction. Could add a "Recent commands" section later if the user asks for it.
* **2026-05-19 (Phase 45 — keyboard shortcuts overlay):** Built the routing logic as a **pure `routeKey({...})` function** that takes the keydown signal (key, modifiers, typing-target state, prefix, help-open) and returns `{action, nextPrefix, preventDefault}` — same export-helper pattern as Phase 38's `sortRoster`, Phase 40's `filterMissions`, Phase 41's `groupByTier`, and Phase 43's `sessionStats`. The hook is then a thin shell that observes both stores, threads state into refs (so the listener doesn't re-attach on every render), and calls the right setter when `routeKey` returns an action. That decomposition means the **entire shortcut matrix is unit-tested without a window** — all 31 routing test cases use `routeKey` directly, with no React or DOM mounted. Picked **single-key bindings** (over Cmd+K palette / per-shortcut modifier chords) because the bindings are short, ergonomic, and match the convention every observability tool the user already lives in (GitHub, Linear, Vercel, Slack). Modifier chords would compete with browser-level chords and the OS — `Cmd+R` is sacred. **`?` is the only Shift-required binding** (because `?` literally requires Shift+/ on US layouts) and is honored even while typing because the user has clearly opted in by pressing Shift; `Escape` is also always honored because dismissal should never feel "stuck". Every other single-key binding silently no-ops while the user is in an `<input>` / `<textarea>` / `<select>` / contenteditable region — that's the same heuristic every keyboard-shortcut app converges on, and skipping it would mean `s` in the mission search opens settings instead of typing. The typing-target guard **preserves the sequence prefix** (vs clearing it) so a user who hits `g`, accidentally tabs into a field, then tabs back out can still complete the combo within the 1.2 s window. Sequence timeout is **1.2 s** because shorter felt twitchy in self-testing and longer (e.g. 3 s) made the prefix feel haunted — the GitHub convention is \~1 s and we went slightly above to be forgiving. Picked the **trophy room aria-label as the resolution selector** (`[aria-label="Open trophy room"]`) rather than adding a fresh `[data-trophy-trigger]` to AchievementsGallery because the aria-label already exists and is stable — coupling to a label that's deliberately part of the a11y contract is no worse than coupling to an arbitrary data-attribute, and it means we don't have to touch a second component. Settings uses `[data-settings-trigger]` because SettingsDrawer already shipped that attribute for its own click-outside detection (Phase 21). Both helpers **skip the click when the trigger is already expanded** (`aria-expanded="true"`) so re-pressing `s` while settings is open doesn't close it — that would be surprising behavior for what reads as "open settings". Replay scrubber is the exception: `r` **toggles** because the same key dismissing an accidental open is the friendlier interaction (the user already learned `Esc` for dismissal but `r r` lets a power user close the panel with the same hand position). Help modal lives in a separate `KeyboardShortcutsModal` component (not inlined into SettingsDrawer or App) so the trigger-keypress → state-flip → modal-render flow is a single source of truth — same self-contained pattern as Phase 18's `AchievementsGallery` and Phase 30's `HeroDetailDrawer`. Combos like `g r` render as **two adjacent `<kbd>` chips** rather than `<kbd>g r</kbd>` because that's the convention every help overlay (GitHub, Linear, Notion) uses and it makes "press g, then r" visually unambiguous. `SHORTCUTS_HELP` is exported as a constant so the modal and the docs stay in lockstep — a test asserts every binding has a corresponding row, so a hook change without a help entry fails CI.
* **2026-05-18 (Phase 44 — browser notifications for achievements):** Picked **strictly opt-in** with `notificationsEnabled: false` as the default because uninvited permission prompts are an anti-pattern users instinctively deny — once the browser remembers a "denied" verdict, the surface is dead until they walk through site settings to reverse it. The toggle wraps `requestPermission()` so the prompt is bound to a deliberate gesture (clicking the switch) which dramatically improves the grant rate. Built a **dual-delivery path** (active-tab `new Notification(...)` vs background `registration.showNotification(...)`) keyed on `document.visibilityState` because (a) iOS Safari refuses active-tab notifications when the page is visible, and (b) firing OS-level toasts while the user is staring at the FactionOS tab is noise — the toast tray already covers that case. **Throttle window of 1.5 s per category** (matching the voice-bark cadence from Phase 12) means an unlock storm doesn't flood the OS shade; categories are bucketed (`achievement` / `mission` / `test`) so a test fire doesn't suppress a real achievement. Used a **diff baseline snapshot** (same shape as Phase 36's `diffNewUnlocks`) so re-broadcasts of an already-unlocked achievement don't fire a duplicate notification — the hook subscribes once and only fires on the transition edge. **Re-enabling after a disable does NOT replay missed unlocks**; the baseline gets reset to the current snapshot so the user explicitly opts back IN to future unlocks rather than getting a batch of "you missed N things" toasts. Shipped the **real `apps/web/public/sw.js`** (not a stub) with `push` + `notificationclick` handlers wired so adding a push backend later is a backend-only change — the worker already parses the JSON payload schema we'd send and falls back to defaults on malformed data. Kept `apps/web` SW distinct from `public-demo` SW because they have different jobs: public-demo is offline-precache for the zero-install showcase, apps/web is notification delivery only. **Permission badge in the Settings UI** uses the same colour family as the codebase-scan status pills (amber/emerald/rose/grey) so the visual vocabulary across the drawer stays consistent. Tests carry a `_setRegistrationForTests` seam so the background-path branch fires deterministically in happy-dom without a real Service Worker.
* **2026-05-18 (Phase 43 — Session rollup strip in TitleBar):** Put the rollup **inside TitleBar** rather than as a separate horizontal strip below it because (a) the TitleBar already houses every other "what's happening across the whole session" status (WS connection state, port, server version, active/total hero counts, selected faction, trophy room trigger) — adding fleet-wide aggregate metrics there matches the existing mental model, and (b) a separate strip would push the battlefield down by \~28 px on every viewport, which is a real cost for a metrics surface that experienced users won't look at every second. Made the strip **`hidden lg:flex`** so narrow viewports (mobile / split-screen / sidebar IDE preview) keep the original TitleBar layout — the same metrics are still reachable by opening individual hero drawers, just hidden from the strip at small widths. Pure `sessionStats(heroes)` helper is exported with the **same shape pattern** as Phase 38's `sortRoster` and Phase 40's `filterMissions`: takes the raw input list, returns the derived object, no side effects. Returning `heroes` and `active` counts alongside the four summed metrics so a future SessionRollup variant (e.g. a Command Center surface) can reuse the same aggregation without re-walking the roster. **Dismissed heroes excluded from EVERY aggregate** (not just the count) because a dismissed-and-reaped agent's lifetime metrics still live on the hero object until the auto-prune sweep runs — counting them would inflate `Σ cost` long after the user dismissed the hero, which reads as "where did this money go?" rather than "this is what's happening right now". Cost formatter has a **`<$0.01` sub-cent floor** rather than rounding to `$0.00` because rounding a 5-mill cost to `$0` reads as "the system isn't tracking cost" — explicitly showing `<$0.01` confirms it IS tracking, just below the resolution of two decimals.
* **2026-05-18 (Phase 42 — README sync):** Pure docs sync. Top-level README had drifted: it still claimed three items in "What's intentionally not built" that have actually shipped (localStorage replay persistence in Phase 34, server-side hero attribution in Phase 35, HeroDetail flyout in Phase 30 — though the README mentioned it at the wrong fidelity). Sweeping those into "shipped" sections matters because the "Not built" list is the user-visible honesty surface — letting stale entries linger reads as "we forgot to ship things" rather than "we shipped them and forgot to remove the bullet". Also bumped the test count from \~217 to \~335 (the figure had not been updated since Phase 27), added dedicated sections for the Phase 38-41 surfaces so a fresh visitor can find Mission filter + search / Tier-sectioned trophy room / Sort toggle / Mission flyout without spelunking through `PROGRESS.md`. No code changes — keeps the commit reviewable.
* **2026-05-18 (Phase 41 — AchievementsGallery tier sections + progress ring + recently-unlocked):** Picked **collapsible tier sections** (vs. a single flat list with tier badges) because the gallery is the surface the user opens to PARSE their progress — "do I have any gold ones yet?" / "what's left in bronze?" — and tier sections answer that question at-a-glance from the section header counts alone, without scrolling. Added a **pinned "Recently unlocked"** section at the top because the most common open-trophy-room reason is "I just got a popup, let me see what it was" — surfacing the last 3 unlocks (any tier) above the tier breakdown answers that without the user having to remember which tier their unlock landed in. 3-entry cap is the same shape as the toasts auto-dismiss window (7 s) — by the time the third toast fades, the recent section still shows what just happened. Built the **progress ring as inline SVG** (vs. a chart library or a `<progress>` element) because (a) `<progress>` doesn't give us full control over stroke colour + radius + offset, (b) any chart lib is overkill for one circle, (c) the ring composes with the existing modal aesthetic — same family as the Three.js sun glow and the toast badges. Tier-weight tie-breaker on `recentUnlocks` so identical timestamps (server-batched unlocks) order intuitively — a platinum unlocked at 10:00 sits above a bronze unlocked at 10:00, matching the user's mental "the bigger one is the bigger event" expectation. Section collapse state is **per-modal-open** (reset every time the modal is opened) so the user always lands in the same "everything visible" state. Persisting collapse would create a "did I leave gold collapsed yesterday?" footgun for an explicitly exploration surface. Each header is a **native `<button>`** with `aria-expanded` so screen readers announce the disclosure state — same a11y pattern as Phase 38's segmented toggle.
* **2026-05-18 (Phase 40 — MissionLog filter + search):** Put the filter row **between the header and the list** rather than collapsing it into the header itself because (a) the header already carries the title + count pill + the "filtered" badge for the hero-scope filter, adding a 4-chip group + search box there would crowd it; (b) a dedicated filter row reads as "this controls the list below", which matches user expectations from every other filterable list pattern (Linear, GitHub Issues, Notion). Filter state is **component-local** (not persisted) because it's an exploration tool — the user wants the next session to start with the full list visible. Persisting would also break the "did I leave a filter on?" worry that every persisted-filter UI inevitably creates. State filter buckets are **denormalised on purpose**: "Active" lumps `active + awaiting_approval + pending` because all three read as "in flight, the user is waiting"; "Failed" lumps `failed + cancelled` because both read as "didn't complete" — splitting them would force the user to think about engine-specific distinctions that don't change what they're trying to do. Search matches **prompt + summary + assistantText** (not just prompt) because the most useful searches are usually "find the run where X happened" — the answer is often in the assistant text, not the user's verbatim prompt. Chose a **segmented chip group + plain search input** (vs a single combined Cmd-K palette) because Cmd-K is a heavier interaction surface that competes with the existing SettingsDrawer / replay scrubber for the same modifier slot; the inline chips + search match the rest of the in-card pattern (HeroRoster's Phase 38 toggle uses the same group shape).
* **2026-05-18 (Phase 39 — mission-detail flyout):** Picked **center-screen modal** over a right-edge flyout (HeroDetailDrawer's pattern) for three reasons: (1) the full `assistantText` block needs wider lines than a 460 px drawer affords — Markdown-shaped prose at \~620 px reads materially better; (2) the mission detail is opened from MissionLog (left rail) AND from HeroDetailDrawer (right rail) — a center modal works from either entry point without "the drawer slid in from the same side as the trigger" weirdness; (3) it's intentionally a focused, exclusive surface — when you click into a mission you want to UNDERSTAND it, not compare it to anything else, so taking over the screen with a backdrop matches the user's mental mode. The drawer carries the **single `selectedMissionId` store field** rather than separate "opened from log" vs "opened from hero" variants because the close action is identical and we want clicking outside / hitting Escape / pressing Close to all collapse a single piece of state. **Mission rows are native `<button>` elements** (rather than `<div onClick>` or `<li tabIndex>` shims) because that pattern keeps focus rings, Enter/Space activation, and screen-reader role announcements correct without any extra JS — same a11y win we leaned into for ScrollShelf entries and the toggle group in Phase 38. Tool-use timeline is a **vertical numbered list with status pills** (ok / error / running) rather than a horizontal timeline because horizontal layouts collapse when 10+ tool calls happen — the vertical version stays readable up to \~50 calls before the user has to scroll, which matches typical mission shapes. **Token totals show in / out / total separately** even though they're trivially derivable because at-a-glance scanning of "X tokens in vs Y out" is the question the user is actually asking — making them re-compute it from the in/out split would add a 200 ms cognitive step. **Pure helpers exported** (`missionDurationMs`, `toolUseDurationMs`, `formatDuration`) so the duration math gets unit-tested without rendering the drawer; same pattern as Phase 32's `replayLast`, Phase 36's `diffNewUnlocks`, and Phase 38's `sortRoster`. `formatDuration` deliberately ladders from `ms` → `1.5s` → `1m 30s` → `1h 5m` rather than always showing the same granularity because a 250 ms Bash call vs a 90 s test run are different mental categories — uniform formatting flattens that signal.
* **2026-05-18 (Phase 38 — HeroRoster activity sort):** Put the toggle in the **roster header** rather than in `SettingsDrawer` because the choice is a one-click ergonomics setting the user wants to flip multiple times per session (busy debugging session → see "Active" mode; idle review → "State" mode for the lifecycle overview). Burying it in the settings drawer would add a 4-click round-trip every time. Used a **segmented button group** (rather than a single toggle button or a select) so both modes are always visible — the user can see what they're switching FROM and TO without opening anything. `aria-pressed` on each button is the right ARIA pattern for a toggle segment (per the WAI-ARIA Authoring Practices); we also set `role="group"` on the wrapper + `aria-label="Sort roster"` so screen readers announce the relationship between the two buttons. The pure `sortRoster(heroes, mode)` helper is **exported separately** so the unit tests can lock in the ordering rules without rendering a Battlefield or hydrating Zustand — same pattern as Phase 32's `replayLast` and Phase 36's `diffNewUnlocks`. Even in **state mode**, ties within a band are now broken by `lastActiveAt DESC` (the old behaviour was insertion order, which made the "active" band feel arbitrary when 3+ heroes were active at once). That's a subtle improvement; it makes the default sort still useful for finding the busy hero in a state without forcing the user to switch to activity mode. Heroes that never acted (`lastActiveAt` empty / unparseable) land at the bottom in activity mode, with `spawnedAt DESC` as the tie-breaker so newly-spawned-but-not-yet-acted heroes are still visible above ancient idle ones. Toggle button hovers + focus rings reuse the same tokens as the SettingsDrawer (text-ink-200, bg-white/15, ring-ring) so the visual language is consistent. Persisted via the same store as every other user preference; `loadPersistedSettings` validates the value as `"state" | "activity"` so a corrupted localStorage key falls back to the default.
* **2026-05-18 (Phase 37 — mission timeline sparkline):** Picked **pure SVG** over a chart library (Recharts, Chart.js, ECharts) for three reasons: (1) the strip is tiny (\~240×56 px) and rendering 24 rects + a polyline doesn't justify a 70+ KB dependency; (2) we already ship Three.js for the battlefield — adding a second canvas-or-SVG renderer would balloon the bundle for no visual win; (3) inline SVG plays nicely with happy-dom which means we can write straightforward Testing Library assertions on data-\* attributes. Bucketed by **wall-clock hour boundaries** (rounding `now` DOWN to the current hour for the rightmost bucket) so the strip reads consistently across the same calendar hour — a hero glanced at 3:42 PM sees the same buckets as one glanced at 3:05 PM. Empty buckets render as faint **baseline ticks** (1.2 px tall, low-alpha white) instead of nothing so the eye reads "idle" rather than "data missing"; this is the same pattern used in financial sparkline conventions. Tool-use overlay is a **translucent polyline** rather than a second bar series because two parallel bar series of different colours would compete for the same vertical space; the line lets the user see the texture without losing the primary mission-count signal. **24-hour window is hardcoded** rather than user-configurable because (a) the drawer is already information-dense and another control would push the strip below the fold on small screens, (b) 24 hours is the universal "what's been happening today" window for a coding-session observability tool. The component accepts a `nowMs` prop so the test suite can pin the window without `vi.setSystemTime` ceremony; in prod it falls back to `Date.now()`.
* **2026-05-18 (Phase 36 — battlefield achievement celebration):** Spawned the celebration inside the existing `Battlefield.tsx` `useEffect` rather than as a sibling component because the burst needs the live scene reference + the hero-visual lookup map (`visualsRef.current`) to position itself over the credited hero — both of those are inside the Three.js scope and aren't lifted out. Put the manager code in a **separate module** (`achievementCelebration.ts`) so the Battlefield file doesn't grow by 150 LOC of unrelated particle math, but kept the wiring inside the existing subscribe callback so there's no second store subscription. Used **MeshBasicMaterial + dodecahedron** for particles rather than `THREE.Points` because (a) Points doesn't accept per-particle scale animations without a custom shader; (b) the burst is small enough (60 particles × ≤6 concurrent = 360 max active) that the overhead of individual Mesh objects is negligible; (c) Mesh-with-rotation gives a sparkle look that flat point sprites don't. **Shared geometry + material per burst** (rather than per-particle) so a burst allocates ONE BufferGeometry + ONE Material that get disposed together when the burst expires; particles are just Mesh instances pointing at the same data. **Tier-derived colour** (gold for bronze/silver, cool blue-white for gold/platinum) keeps higher-tier celebrations visually distinct without needing a separate legend; the tier weights match the AchievementsGallery sort order. **6 concurrent bursts max with FIFO eviction** because an unlock storm (e.g. replaying a saved session that previously fired 20+ achievements in 30 s) would otherwise queue thousands of meshes — the cap keeps the worst-case frame budget bounded. **`diffNewUnlocks` exported as a separate helper** so the Battlefield's subscribe callback stays tiny (one function call) and the diff logic gets focused unit tests without spinning up the renderer. Authorless unlocks (alliance-joined / auto-improver) fall back to the **rune-circle centre at world origin** rather than skipping the celebration entirely — the milestone is still visible, just not credited to a specific hero.
* **2026-05-18 (Phase 35 — hero attribution on achievements):** Persisted the attribution snapshot on the Achievement record itself (heroId + unlockedByHeroName + unlockedByFaction) rather than only on the broadcast event. Reasons: (1) heroes get dismissed and pruned \~60 s after dismissal, so a clean roster lookup from the gallery would lose attribution for older sessions; (2) the gallery and HeroDetailDrawer both want to render attribution without coupling to the live roster — snapshotting denormalises the join into the row itself; (3) tests for "the hero who triggered first-hero is no longer in the roster" pass deterministically because attribution survives the dismissal. Picked **three optional fields** (heroId + unlockedByHeroName + unlockedByFaction) over a single nested object so JSON serialization stays flat and the discriminator (`!!a.heroId`) is a one-liner everywhere. Attribution is a **best-effort heuristic, not a transactional record**: the engine picks a sensible hero per achievement but doesn't try to reconstruct exact causal chains (e.g. if the 100th mission completes via a flurry of three nearly-simultaneous finishes, we credit the one with the earliest `endedAt` — not "the one that lifted the count from 99→100" since the roster doesn't track per-event sequencing at that resolution). Authorless unlocks (`alliance-joined`, `auto-improver`) deliberately omit attribution rather than crediting "the user" or "the server" — the absence is meaningful and the UI handles it cleanly. The HeroDetailDrawer kept the legacy "faction-mention after spawn" heuristic as a **fallback** for pre-Phase-35 achievements so existing sessions don't suddenly lose contribution chips. Once the server stops emitting attribution-less unlocks, the fallback can be removed; meanwhile the `data-attributed` attribute on each chip lets future styling distinguish authoritative from heuristic credit. Server's `lastPlanHeroId` / `lastScrollHeroId` thread through the broadcaster listener as mutable ctx fields rather than passing them through `evaluate()` — same pattern as the existing `warroomJoined` / `autoImproverPropose` / `planFullyCompleted` flags. Tests stand up real `AchievementManager` + `AchievementEngine` with thin fakes of Broadcaster / Roster / Missions / Scrolls, which is materially more honest than mocking the unlock path; the only fakes are the integration boundaries.
* **2026-05-18 (Phase 34 — localStorage replay persistence):** Persisted only the **trailing 100 entries** (vs. the 500-entry in-memory ring) so the JSON snapshot stays comfortably under the \~5MB localStorage quota even when events carry large `metrics` / `evidence` blobs. Picked **debounced 250 ms write** instead of writing on every dispatch because typical hook traffic is bursty (10+ events in 100 ms on tool-use sweeps) — debouncing collapses that to one persist per burst without losing the final state. Used a separate **`replayingSinceMs` gate** rather than tracking "are we currently replaying" via a counter, because the gate is already the source of truth for in-memory capture skip; reusing it for persistence makes the contract single-sourced. **Hydrate on store init** (in the `eventLog: loadPersistedReplayBuffer()` literal) rather than via a separate `restorePersistedReplayBuffer()` action, so the timing matches the rest of Zustand's lazy initialisers and there's no "did I forget to hydrate" footgun in tests. Defensive parser: any malformed entry (missing `at`, missing `event`, wrong type for `event.type`) is silently dropped on read rather than throwing — a corrupted snapshot should never block a session boot.
* **2026-05-18 (Phase 32 — replay scrubber):** Captured the ring buffer **inside `applyEvent` itself** rather than as a separate Zustand subscriber because (1) the reducer already has the typed `ServerEvent` and the canonical "this just got dispatched" moment — a subscriber would need to diff state to detect activity and would miss reducer-internal events; (2) one source of truth keeps the gate-during-replay logic colocated with the only thing that needs to know about it. Stored the buffer in `useGameStore` (in-memory only) instead of `useSettingsStore` (persistent) because (1) 500 events × typical payload size = \~200 KB which hits the localStorage quota uncomfortably fast; (2) the user wants to debug "what just happened in this session" — surviving a refresh isn't the primary use case. localStorage persistence is in the backlog as a follow-up that should also truncate to \~100 events. Replay speed is implemented as **per-gap delay scaling** (not a single setTimeout chain) so the inter-event cadence is preserved — a burst of 10 tool\_use events in 100 ms replays as a 100 ms burst at 1× and \~25 ms at 4×, not as 10 evenly-spaced events. Per-gap delay capped at 1 s so a 5-minute idle gap doesn't stall the replay scrubber for 5 minutes. Window choices (30 s / 2 m / 5 m) and speed choices (1× / 2× / 4×) are hardcoded because the underlying primitives accept arbitrary numbers — a future SPA route or a "custom window" input can use them directly without touching `useGameStore.replayLast`.
* **2026-05-18 (Phase 31 — codebase scan UI):** Persisted the scan root path in `useSettingsStore` rather than `useGameStore` because (1) it's a user preference, not game state — game state gets clobbered by `resetToSeed`; the scan root should survive that; (2) the persistence concern is identical to theme / audio / severity / mock-toggle / motion: round-trip through `localStorage` under the same `factionos-settings-v1` snapshot key, no separate storage rotation. Added `pushToast` to the game store rather than dispatching a synthetic `server_toast` event through `applyEvent` because the `server_toast` event shape has a `level` field while the toast tray takes a `kind` field, and the two enums aren't 1:1 (no `achievement` level on the server side). Cleaner to expose the toast pipeline directly for UI-originated calls. The Scan button is keyed off `scan.status === "scanning" || !scanRoot.trim()` so re-clicking during an in-flight scan is a no-op without needing a Promise reference at the component level — the local state is the single source of truth for "are we currently scanning". Three pills (walking / done / failed) rather than one polymorphic pill so the colour/border treatment can be tuned per state without a switch statement at render time; CSS-only difference, no JS conditional class spaghetti. Toast levels: info on dispatch (low signal), success on a clean run, **warn** when issues > 0 (the engine found something worth surfacing), error on transport failure. The toast title for completion is `Scan complete · N issues` so the count is visible even in collapsed views.
* **2026-05-18 (Phase 30 — HeroDetail flyout):** Built the drawer as a self-contained component (no new store slice) that reads `selectedHeroId` directly and calls `setSelectedHeroId(null)` to dismiss. Rationale: Phase 25 already designed `selectedHeroId` as the single source of truth for "which hero is focused"; adding a parallel `detailOpen` state would have created a divergence path where the camera animates one way and the drawer state lags. Single source of truth keeps the click-to-deselect-on-empty-terrain behaviour from Phase 25 also dismissing the drawer for free. Used **independent `useGameStore` selectors per slice** (one for `selectedHeroId`, one for the hero record, one for the missions map, one for `recentMissionIds`, one for `achievements`) rather than a single combined selector because Zustand's selector equality is shallow — a metrics tick on an unrelated hero would otherwise re-render the entire mission history list every second. Kept `relativeTime` / `formatTokens` / `formatCost` / `StateBadge` local rather than refactoring the existing copies out of `HeroCard.tsx` / `MissionLog.tsx` / `lib/timeFormat.ts` because the helpers are 5-10 lines each and a cross-component refactor would have grown the diff to dozens of touched files for no behavioural change — see the "small duplication is cheaper than premature extraction" principle. **Speak button bark-kind mapping is heuristic**: hero.state==="active" → "active", "error" → "error", "verifying" → "complete", everything else → "idle". The voiceSynth API supports a 5th kind ("celebration") that fires on achievement unlock; we don't trigger it from the drawer because there's no "I just unlocked an achievement" state on the hero itself. **Achievement contribution heuristic** is text-match (`title + description + trigger`) against the hero's faction string, plus a post-spawn unlockedAt filter. Not perfect — server-side could attribute achievements to specific heroes via a `heroId` field — but the heuristic is correct \~95% of the time given the baseline catalogue's wording, and the explicit `unlockedAt >= spawnedAt` filter prevents the "you just spawned this hero, why does it have 5 contributions already" footgun. Drawer footprint: 460 px wide so it overlaps the existing 360-px right rail intentionally — the drawer is meant to read as "on top of" the roster, not "next to" it.
* **2026-05-18 (Phase 29 — animated strip):** Chose **animated WebP** over GIF / APNG / WebM because (1) GitHub markdown renders animated WebP inline via standard `<img>` tags and the README's `<picture>` element with a JPEG fallback covers the long tail; (2) WebP at quality 80 produced a \~447 KB file for the same content that a comparable GIF would have hit \~3-4 MB on — important because the strip is the very first thing visitors load; (3) APNG support in older readers is patchier than animated WebP. Generated the 4 source panels via `gemini-3.1-flash-image-preview` (NB2) rather than Pro because Flash already produces editorial-grade fantasy painting at this scale and the cost difference scales with the burst — 4 calls × Pro would have been \~$0.54 vs \~$0.18 with NB2. Frame timing is **1.4 s per frame** rather than the more cinematic 0.6 s because the panels are detail-dense and a fast cycle would feel jittery in the README scroll context; the 5.6 s cycle is long enough that the strip reads as a narrative arc (gathering → fighting → winning → resting) rather than a flicker. Each panel's prompt references the prior one ("SAME five fantasy heroes from the prior panel") to anchor visual consistency without passing `inputImages` — empirically the description-anchor approach was tight enough across all 4 frames that no re-roll was needed. Cropped to a common height with a center-crop pass because Pillow's `save_all` for animated WebP requires identical dimensions; the 2752×1536 raw output came down to 1280×714 (16:9 letterbox-trimmed) which fits the README column width without horizontal scroll on mobile.
* **2026-05-18 (Phase 28 — War Room E2E tests):** Wrote a Map-backed `DurableObjectState` fake and a callable-listener `WebSocketPair` fake rather than running `wrangler dev` because (1) the sandbox can't install the Cloudflare CLI; (2) the worker test surface I actually care about is **the WarRoom class semantics** — broadcast-excluding-sender, ring buffer cap, role gating, persistence — and those are independent of the Worker runtime; (3) a wrangler-based test would be slower and flakier (port allocation + spawn lifecycle), so even if it were available it wouldn't replace the in-process suite. Exposed the private `sockets` Map via a TS cast in the test file (`(room as unknown as { sockets: Map<...> }).sockets`) rather than adding a `__getSocket` accessor to the production class — the cast keeps the production surface narrow and the test still proves the same property. Used `structuredClone` inside the fake `storage.put` / `get` to catch any accidental shared-reference bugs where the DO mutates a persisted reference (turns out the production code is clean — `stripSocket` already produces fresh objects — but the structuredClone makes future regressions visible). The "broadcast excludes sender" test sets up three approved members all connected and asserts the sender's `sent` array contains 0 `remote_event` frames while the two peers each contain exactly 1 — that's the federation guarantee the user explicitly asked for in the backlog, made explicit with a directly-named test case. Worker-level routing tests use a separate `makeNamespace()` fake that maps `idFromName(code)` to a per-name `WarRoom` instance, so the regex-driven path matching and DO delegation can be tested end-to-end without a real Cloudflare runtime.
* **2026-05-18 (Phase 27 — apps/web Vitest suite):** Picked **happy-dom** over **jsdom** because the bundle is \~3× smaller, has noticeably faster cold-start under vitest, and provides everything I actually need (`window`, `document`, `localStorage`, `matchMedia`, `KeyboardEvent`). Used vitest's `environmentMatchGlobs` to pin only `apps/web/tests/**` to happy-dom rather than switching the whole repo — the server suite has hundreds of cases and Node is materially faster, so the per-glob switch keeps the suite hot on the cheap envs. Picked **@testing-library/react** for the assertion ergonomics; `fireEvent.click/keyDown` already wraps in `act()` so I didn't need to import act manually. Used `useGameStore.setState({ ... })` to seed state directly rather than dispatching events through `applyEvent()` because the tests should assert on rendered shape, not event reducers — those have their own coverage on the server. Each test owns its own seed via `beforeEach({heroes:{}, missions:{}, ...})` so the order of tests doesn't matter. For AchievementsGallery's modal lifecycle I asserted on `role="dialog"` + `aria-modal="true"` instead of class names so the test survives a future visual refactor — only the semantic surface gets exercised. Locked-row redaction is checked via a regex on the dialog's text content (`/·{2,}/g`) rather than a specific string, because the prod code maps every `[A-Za-z0-9]` to `·`, and writing the exact mangled title would lock the test to today's title verbatim.
* **2026-05-18 (Phase 26 — autoImprover golden tests):** Mocked `llmClient.complete` with `vi.mock("../src/lib/llmClient.js")` so the engine can be exercised deterministically without an `ANTHROPIC_API_KEY`. The canned fallback inside `llmClient.ts` for the `autoImprover` engine returns `{ proposal, rationale }` — which doesn't satisfy the `{ rationale, initialPrompt, candidateFiles }` shape the engine requires — so the offline path always returns null. That means the happy paths could not be tested against the real fallback; mocking was the only way. Exposed three test-only hooks (`__resetAutoImproverState`, `__AUTO_IMPROVER_COOLDOWN_MS`, `__AUTO_IMPROVER_PROHIBITED_PATHS`) with the `__` prefix so the prod constants and the test expectations can't silently drift. Critical-path tests: (1) **cool-down is only consumed on a successful fire** — if the LLM returns garbage or every candidate is banned, the next call still fires (this is the real production safety property: a single broken response shouldn't lock the system out for 24 h); (2) **failure\_rate wins over long\_running** when both thresholds are met (the engine checks failure\_rate first); (3) inclusive lower bounds (`>= 0.2` and `>= 3`) — these matter because a typical regression is flipping `>=` to `>`. Used `vi.useFakeTimers` + `vi.setSystemTime` for the 24h re-fire tests rather than mocking `Date.now` directly so the engine's own `Date.now()` call also picks up the fake clock.
* **2026-05-18 (Phase 25 — battlefield interaction):** Went with pointerdown→pointerup tracking (not raw `click`) so I can reject the gesture when the user dragged more than 6 px or held longer than 350 ms — that prevents a click from firing while the user is orbiting the camera with a future drag-to-pan handler, and is the standard discriminator for "click vs drag" in canvas UIs. Added an invisible `MeshBasicMaterial({ visible: false })` hitbox cylinder per hero on top of the visible meshes — small silhouettes (the lobster body is \~0.45 m wide) were physically hard to hit without it. Stamped `userData.heroId` onto every mesh inside the hero root (body/head/shoulders/cape/accessories/ring/hitbox) so `walkUpToHeroId` finds the id from any descendant, then the click resolution is `intersectObjects(roots, true) → walk up parents → find userData.heroId`. Re-clicking the currently-selected hero clears the selection (toggle UX), so users have an obvious way out without learning a separate "click empty terrain" gesture. Camera follow holds a continuous `followTheta` so the orbit picks up where the tween landed rather than snapping. Floating label uses a CSS-overlaid `<div>` that's transformed each frame from `worldPos.project(camera)`, not a Three.js `Sprite`, because the existing scene uses `MeshBasic`/`MeshStandard` and a single Sprite would have introduced an alpha-blended pass + texture management — DOM is faster here and inherits Tailwind chrome. `labelWorld = new THREE.Vector3()` is hoisted to module scope so we don't allocate a vector every frame.
* **2026-05-17 (Phase 24 — README polish):** Wired the GitHub Actions status badge directly to the `test.yml` workflow file (`/actions/workflows/test.yml/badge.svg`) rather than the deprecated branch-based form so the badge survives workflow renames. License + Node-engine badges are inline shields rather than dynamic — they don't need to round-trip a shields.io API call on every page render. Added a Settings table documenting every persisted localStorage key so users can sanity-check what survives a reload; pointed at the anti-FOUC bootstrap line in `index.html` so cargo-culting it elsewhere doesn't drift.
* **2026-05-17 (Phase 23 — LLM endpoint tests):** Kept the test suite deterministic by force-deleting `ANTHROPIC_API_KEY` inside the `boot()` helper so the engines always take the canned-fallback path. The plan endpoint can legitimately return 503 in offline mode (when the fallback declines), so the happy-path test accepts EITHER 200 with a plan body OR 503 with the documented hint — that flex matches the real behaviour rather than over-specifying. `/llm/scan-codebase` test uses `mkdtempSync` so the walker has a real fs to chew on; using a fixture directory inside the repo would have run afoul of `EXAMPLES/` and other historical artifact dirs being in the default ignore list. Re-used the existing `createFactionOsServer({port:0})` + global-fetch pattern from `routes.test.ts` so we picked up no new transitive deps.
* **2026-05-17 (Phase 22 — battlefield accessories):** Added a single `buildFactionAccessories` function returning a `THREE.Group` instead of folding the per-faction primitives directly into `addHeroVisual`. This keeps the hero builder readable and lets the accessory geometry use local coordinates starting at 0 (the caller positions the group at `factionBaseY()`). Threaded a live `reduceMotionRef` through the animation loop rather than a `motion` prop because heroes are not React-rendered — the value has to be readable from the requestAnimationFrame closure, and a ref subscription is the cheapest way to track it. Lobster claws use cone geometry with two-axis rotation (forward tilt + outward yaw) so the silhouette reads as pincers without needing custom geometry.
* **2026-05-17 (Phase 21 — settings persistence):** Built a separate `useSettingsStore` rather than folding the preferences into `useGameStore`. Reasons: (1) game state is event-sourced and gets clobbered by `resetToSeed`; user preferences must survive that reset. (2) the persistence concerns are different — game state never round-trips through localStorage, settings always do. (3) the subscription patterns are different — `useVoiceBarks` cares about audio toggles but not hero metrics, and Zustand selectors are clearer when the slices are separate. Wrote a hand-rolled persist instead of pulling in `zustand/middleware/persist` so the import surface stays minimal and hydration is observable inline in `loadPersistedSettings()`. Kept `factionos-theme` as a separate localStorage key (in addition to the combined `factionos-settings-v1` snapshot) because the anti-FOUC bootstrap inside `index.html` needs to read it synchronously without parsing JSON.
* **2026-05-17 (Phase 20 — CI + integration tests):** Picked an "ephemeral port + global fetch" pattern for the server route tests over supertest. Reasons: (1) `createFactionOsServer({port:0})` already returns the `http` handle, so we have everything we need in 5 lines of test boilerplate; (2) avoiding supertest means zero new transitive dependencies, so vitest run is still hermetic on a fresh `npm install`; (3) global `fetch` in the repo-supported Node runtime is a stable, well-typed API. Adapter tests use `vi.spyOn(globalThis, 'fetch')` for the same reason. Current CI now follows the repository Node policy from `.nvmrc`; coverage artifacts remain per runtime label so the run can survive future policy bumps without losing history.
* **2026-05-17 (Phase 19 — tests):** Picked Vitest over Jest because the rest of the stack is ESM-native and Vitest needs zero transform config. Skipped Vitest's multi-project workspace mode — every test file in the repo follows the same `**/tests/**.test.ts` pattern so the root config can glob across packages directly. Tests target the pure / easily-isolatable bits first (factions table, achievements catalogue, formatter, throttle, the fs walker against a tmpdir) — wiring tests for the full Express + ws server is a follow-up because it requires a port and would need supertest plus a graceful shutdown.
* **2026-05-17 (Phase 18 — trophy room):** Built `AchievementsGallery` as a self-contained component (its own trigger button + modal) instead of adding a `galleryOpen` slice to the store. The component already lives in TitleBar so it has no cross-component coupling; lifting state into the store would be premature. Locked rows redact the title with `·` rather than hiding it entirely so the achievement count stays visible without spoiling the unlock copy.
* **2026-05-17 (Phase 17 — PWA):** Service worker uses a versioned cache name (`factionos-demo-v1-shell` + `-runtime`) so we can prune stale entries on activate without forcing users to clear storage. Navigation requests fall back to cached `index.html` on offline so the demo at least boots; the synthetic event loop runs entirely in-browser so it's actually fully functional offline. Icons rendered with Pillow from the favicon path so we don't introduce a separate icon design that could drift from the inline favicon.
* **2026-05-17 (Phase 16 — adapters):** Built `apps/adapters/` as a single workspace with two adapters under one bridge rather than two separate packages — the shared `wsBridge` + `formatter` + `throttle` modules are 90% of the value, and a third surface (Slack, Matrix) would be \~30 LOC under `src/<name>/`. Webhook-based for Discord so there's no discord.js dependency; raw `sendMessage` REST for Telegram so there's no bot framework either. Adapter package only depends on `ws` + `@factionos/protocol`. Used Node 18+ global `fetch` everywhere so the dependency footprint stays tiny. One-way / best-effort / no inbound commands by design — accepting commands from chat would reopen the permission model and double the surface area.
* **2026-05-17 (Phase 15 — scan-codebase):** Added a server-side directory walker (`apps/server/src/lib/codebaseWalker.ts`) and a new route `POST /llm/scan-codebase` that wraps `analyzeFiles`. Decision: walker is sync (fast enough; mtime-sort over a typical repo is <100 ms) and lives behind the route so a future remote/sandboxed walker can swap in without touching the engine. Refuses `/` and won't follow symlinks. Default ignore list bakes in `EXAMPLES/`, directories named `findings`, and `extracted-artifacts/` so the engine doesn't waste its 5-issue budget critiquing historical or quarantined dumps.
* **2026-05-17 (Phase 14 — portraits):** Generated 5 painted bust portraits via Gemini 2.5 Flash Image, resized to 384×384 progressive JPEG quality 82 (13-35 KB each, \~120 KB total). Committed under `apps/web/public/portraits/` as the canonical location so Vite can serve them at `/portraits/<faction>.jpg` in dev/build; the standalone `public-demo/index.html` references them via raw\.githubusercontent URLs (same approach as `heroes-banner.jpg`) so we don't duplicate the binaries.
* **2026-05-17 (pivot 2 — substantial build):** Built out the gaps that v1 punted on: 9 real Claude Code hook scripts, real Cloudflare Worker for War Room, 5 LLM engines wired with maintained prompts, real persistence layer, real achievement engine, upgraded Three.js battlefield with composed silhouettes + faction keeps + real lighting + voice barks. Generated a painted hero banner for the README + demo splash. Build is now \~120 files / \~500 KB of source.
* **2026-05-17 (v1):** Shipped scaffolding only — protocol + server + web shell + zero-install demo. User correctly called this out as shallow.
* **2026-05-17:** Decided Vite + React + TS + Tailwind + Three.js for the frontend and Node ESM + Express + ws for the server. Anthropic SDK stays behind canned fallbacks so the app runs offline by default.
* **2026-05-17:** Lobster is preserved as a hidden 5th faction (per historical metadata - `lobster` is in `clone-voice` enum but not `list-voices`). Unlocks after collecting all five fundamental scrolls.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://faction-os.gitbook.io/faction-os-docs/docs/progress.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
