# NousResearch/hermes-agent · Terminal light background readability and theme detection

## Open now

- 5 issues, 3 PRs open on GitHub.
- #31143: Remote/Docker deployments: skin selection should auto-set TUI light/dark mode base
- #47210: \[Feature\]: Hermes CLI does not auto-detect light terminal backgrounds in Ghostty, breaks dark-themed skins
- #58038: \[Feature\]: QR code color-inversion detection for dark-mode terminals (WhatsApp pairing)
- #59751: \[Bug\]: Hermes .app TUI agent reply text unreadable on MacBook Air M2 15.3" Retina
- #61536: \[Bug\]: Zed dark themes: \`/skin default\` is rendered as light-mode colors because Hermes trusts inherited COLORFGBG
- PR #58545: fix(qr): use invert=False for QR code terminal rendering (preserve quiet zone)
- PR #60972: fix(cli): improve light terminal body contrast
- PR #61600: fix(theme): ignore inherited COLORFGBG in Zed

These issues and pull requests concern Hermes TUI and CLI text being unreadable on light or cream terminal backgrounds, and efforts to improve auto-detection of light terminals, fix unreliable COLORFGBG inheritance (especially in Zed), and add light-mode skins and color adjustments.

## TL;DR

- 32 member(s): 5 open issue(s), 6 closed issue(s), 3 open PR(s), 5 merged PR(s), 13 PR(s) closed without merging.
- A fix was merged to main in PR #9461. Verify it addresses your case.
- **Recommended:** read PR #9461 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-13103`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.06`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #31143: Remote/Docker deployments: skin selection should auto-set TUI light/dark mode base
- #47210: \[Feature\]: Hermes CLI does not auto-detect light terminal backgrounds in Ghostty, breaks dark-themed skins
- #58038: \[Feature\]: QR code color-inversion detection for dark-mode terminals (WhatsApp pairing)
- #59751: \[Bug\]: Hermes .app TUI agent reply text unreadable on MacBook Air M2 15.3" Retina
- #61536: \[Bug\]: Zed dark themes: \`/skin default\` is rendered as light-mode colors because Hermes trusts inherited COLORFGBG

## Closed / duplicate issues

- #4807: \[Bug\]: CLI is unreadable on light/cream terminal backgrounds — no light mode support
- #8526: \[Bug\]:  \[color\] default color is broken, when launching hermes via terminal, not being readable
- #11300: \[Bug\]: TUI hardcodes bright yellow colors, unreadable on white terminal backgrounds
- #12330: \[Bug\]: The text of Hermes is hard to read on a white terminal background.
- #19039: \[Bug\]: CLI teminal interface hard to read in day/light background.
- #60141: \[Bug\]: Hermes TUI assistant reply text remains low-contrast on macOS Terminal.app light profile

## Prior & competing PRs

- **#9461: feat(skin): add light-mode skins + skin-aware completion menus**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `docs/skins/example-skin.yaml`, `hermes_cli/skin_engine.py`, `tests/hermes_cli/test_skin_engine.py`, `website/docs/user-guide/features/skins.md`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -988,19 +988,19 @@ def _prune_orphaned_branches(repo_root: str) -> None:
     # ANSI building blocks for conversation display
     _ACCENT_ANSI_DEFAULT = "\033[1;38;2;255;215;0m"  # True-color #FFD700 bold — fallback
     _BOLD = "\033[1m"
    -_DIM = "\033[2m"
     _RST = "\033[0m"
     
     
    -def _hex_to_ansi_bold(hex_color: str) -> str:
    -    """Convert a hex color like '#268bd2' to a bold true-color ANSI escape."""
    +def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:
    +    """Convert a hex color like '#268bd2' to a true-color ANSI escape."""
         try:
             r = int(hex_color[1:3], 16)
             g = int(hex_color[3:5], 16)
             b = int(hex_color[5:7], 16)
    -        return f"\033[1;38;2;{r};{g};{b}m"
    +        prefix = "1;" if bold else ""
    +        return f"\033[{prefix}38;2;{r};{g};{b}m"
         except (ValueError, IndexError):
    -        return _ACCENT_ANSI_DEFAULT
    +        return _ACCENT_ANSI_DEFAULT if bold else "\033[38;2;184;134;11m"
     
     
     class _SkinAwareAnsi:
    ```

    ```diff
    @@ -1010,20 +1010,22 @@ class _SkinAwareAnsi:
         force re-resolution after a ``/skin`` switch.
         """
     
    -    def __init__(self, skin_key: str, fallback_hex: str = "#FFD700"):
    +    def __init__(self, skin_key: str, fallback_hex: str = "#FFD700", *, bold: bool = False):
             self._skin_key = skin_key
             self._fallback_hex = fallback_hex
    +        self._bold = bold
             self._cached: str | None = None
     
         def __str__(self) -> str:
             if self._cached is None:
                 try:
                     from hermes_cli.skin_engine import get_active_skin
    -                self._cached = _hex_to_ansi_bold(
    -                    get_active_skin().get_color(self._skin_key, self._fallback_hex)
    +                self._cached = _hex_to_ansi(
    +                    get_active_skin().get_color(self._skin_key, self._fallback_hex),
    +                    bold=self._bold,
                     )
                 except Exception:
    -                self._cached = _hex_to_ansi_bold(self._fallback_hex)
    +                self._cached = _hex_to_ansi(self._fallback_hex, bold=self._bold)
             return self._cached
     
         def __add__(self, other: str) -> str:
    ```

    ```diff
    @@ -1037,7 +1039,8 @@ def reset(self) -> None:
             self._cached = None
     
     
    -_ACCENT = _SkinAwareAnsi("response_border", "#FFD700")
    +_ACCENT = _SkinAwareAnsi("response_border", "#FFD700", bold=True)
    +_DIM = _SkinAwareAnsi("banner_dim", "#B8860B")
     
     
     def _accent_hex() -> str:
    ```

    ```diff
    @@ -6156,6 +6159,7 @@ def _handle_skin_command(self, cmd: str):
     
             set_active_skin(new_skin)
             _ACCENT.reset()  # Re-resolve ANSI color for the new skin
    +        _DIM.reset()     # Re-resolve dim/secondary ANSI color for the new skin
             if save_config_value("display.skin", new_skin):
                 print(f"  Skin set to: {new_skin} (saved)")
             else:
    ```
  - `docs/skins/example-skin.yaml`

    ```diff
    @@ -41,6 +41,14 @@ colors:
       session_label: "#DAA520"        # Session label
       session_border: "#8B8682"       # Session ID dim color
     
    +  # TUI surfaces
    +  status_bar_bg: "#1a1a2e"              # Status / usage bar background
    +  voice_status_bg: "#1a1a2e"            # Voice-mode badge background
    +  completion_menu_bg: "#1a1a2e"         # Completion list background
    +  completion_menu_current_bg: "#333355" # Active completion row background
    +  completion_menu_meta_bg: "#1a1a2e"    # Completion meta column background
    +  completion_menu_meta_current_bg: "#333355"  # Active completion meta background
    +
     # ── Spinner ─────────────────────────────────────────────────────────────────
     # Customize the animated spinner shown during API calls and tool execution.
     spinner:
    ```
  - `hermes_cli/skin_engine.py`

    ```diff
    @@ -32,6 +32,12 @@
           response_border: "#FFD700"         # Response box border (ANSI)
           session_label: "#DAA520"           # Session label color
           session_border: "#8B8682"          # Session ID dim color
    +      status_bar_bg: "#1a1a2e"          # TUI status/usage bar background
    +      voice_status_bg: "#1a1a2e"        # TUI voice status background
    +      completion_menu_bg: "#1a1a2e"      # Completion menu background
    +      completion_menu_current_bg: "#333355"  # Active completion row background
    +      completion_menu_meta_bg: "#1a1a2e"     # Completion meta column background
    +      completion_menu_meta_current_bg: "#333355"  # Active completion meta background
     
         # Spinner: customize the animated spinner during API calls
         spinner:
    ```

    ```diff
    @@ -87,6 +93,8 @@
     - ``ares``    — Crimson/bronze war-god theme with custom spinner wings
     - ``mono``    — Clean grayscale monochrome
     - ``slate``   — Cool blue developer-focused theme
    +- ``daylight`` — Light background theme with dark text and blue accents
    +- ``warm-lightmode`` — Warm brown/gold text for light terminal backgrounds
     
     USER SKINS
     ==========
    ```

    ```diff
    @@ -304,6 +312,80 @@ def get_branding(self, key: str, fallback: str = "") -> str:
             },
             "tool_prefix": "┊",
         },
    +    "daylight": {
    +        "name": "daylight",
    +        "description": "Light theme for bright terminals with dark text and cool blue accents",
    +        "colors": {
    +            "banner_border": "#2563EB",
    +            "banner_title": "#0F172A",
    +            "banner_accent": "#1D4ED8",
    +            "banner_dim": "#475569",
    +            "banner_text": "#111827",
    +            "ui_accent": "#2563EB",
    +            "ui_label": "#0F766E",
    +            "ui_ok": "#15803D",
    +            "ui_error": "#B91C1C",
    +            "ui_warn": "#B45309",
    +            "prompt": "#111827",
    +            "input_rule": "#93C5FD",
    +            "response_border": "#2563EB",
    +            "session_label": "#1D4ED8",
    +            "session_border": "#64748B",
    +            "status_bar_bg": "#E5EDF8",
    +            "voice_status_bg": "#E5EDF8",
    +            "completion_menu_bg": "#F8FAFC",
    +            "completion_menu_current_bg": "#DBEAFE",
    +            "completion_menu_meta_bg": "#EEF2FF",
    +            "completion_menu_meta_current_bg": "#BFDBFE",
    +        },
    +        "spinner": {},
    +        "branding": {
    +            "agent_name": "Hermes Agent",
    +            "welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
    +            "goodbye": "Goodbye! ⚕",
    +            "response_label": " ⚕ Hermes ",
    +            "prompt_symbol": "❯ ",
    +            "help_header": "[?] Available Commands",
    +        },
    +        "tool_prefix": "│",
    +    },
    +    "warm-lightmode": {
    +        "name": "warm-lightmode",
    +        "description": "Warm light mode — dark brown/gold text for light terminal backgrounds",
    +        "colors": {
    +            "banner_border": "#8B6914",
    +            "banner_title": "#5C3D11",
    +            "banner_accent": "#8B4513",
    +            "banner_dim": "#8B7355",
    +            "banner_text": "#2C1810",
    +            "ui_accent": "#8B4513",
    +            "ui_label": "#5C3D11",
    +            "ui_ok": "#2E7D32",
    +            "ui_error": "#C62828",
    +            "ui_warn": "#E65100",
    +            "prompt": "#2C1810",
    +            "input_rule": "#8B6914",
    +            "response_border": "#8B6914",
    +            "session_label": "#5C3D11",
    +            "session_border": "#A0845C",
    +            "status_bar_bg": "#F5F0E8",
    +            "voice_status_bg": "#F5F0E8",
    +            "completion_menu_bg": "#F5EFE0",
    +            "completion_menu_current_bg": "#E8DCC8",
    +            "completion_menu_meta_bg": "#F0E8D8",
    +            "completion_menu_meta_current_bg": "#DFCFB0",
    +        },
    +        "spinner": {},
    +        "branding": {
    +            "agent_name": "Hermes Agent",
    +            "welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
    +            "goodbye": "Goodbye! \u2695",
    +            "response_label": " \u2695 Hermes ",
    +            "prompt_symbol": "\u276f ",
    +            "help_header": "(^_^)? Available Commands",
    +        },
    +        "tool_prefix": "\u250a",
    +    },
         "poseidon": {
             "name": "poseidon",
             "description": "Ocean-god theme — deep blue and seafoam",
    ```

    ```diff
    @@ -685,6 +767,12 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
         label = skin.get_color("ui_label", title)
         warn = skin.get_color("ui_warn", "#FF8C00")
         error = skin.get_color("ui_error", "#FF6B6B")
    +    status_bg = skin.get_color("status_bar_bg", "#1a1a2e")
    +    voice_bg = skin.get_color("voice_status_bg", status_bg)
    +    menu_bg = skin.get_color("completion_menu_bg", "#1a1a2e")
    +    menu_current_bg = skin.get_color("completion_menu_current_bg", "#333355")
    +    menu_meta_bg = skin.get_color("completion_menu_meta_bg", menu_bg)
    +    menu_meta_current_bg = skin.get_color("completion_menu_meta_current_bg", menu_current_bg)
     
         return {
             "input-area": prompt,
    ```

    ```diff
    @@ -692,13 +780,20 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
             "prompt": prompt,
             "prompt-working": f"{dim} italic",
             "hint": f"{dim} italic",
    +        "status-bar": f"bg:{status_bg} {text}",
    +        "status-bar-strong": f"bg:{status_bg} {title} bold",
    +        "status-bar-dim": f"bg:{status_bg} {dim}",
    +        "status-bar-good": f"bg:{status_bg} {skin.get_color('ui_ok', '#8FBC8F')} bold",
    +        "status-bar-warn": f"bg:{status_bg} {warn} bold",
    +        "status-bar-bad": f"bg:{status_bg} {skin.get_color('banner_accent', warn)} bold",
    +        "status-bar-critical": f"bg:{status_bg} {error} bold",
             "input-rule": input_rule,
             "image-badge": f"{label} bold",
    -        "completion-menu": f"bg:#1a1a2e {text}",
    -        "completion-menu.completion": f"bg:#1a1a2e {text}",
    -        "completion-menu.completion.current": f"bg:#333355 {title}",
    -        "completion-menu.meta.completion": f"bg:#1a1a2e {dim}",
    -        "completion-menu.meta.completion.current": f"bg:#333355 {label}",
    +        "completion-menu": f"bg:{menu_bg} {text}",
    +        "completion-menu.completion": f"bg:{menu_bg} {text}",
    +        "completion-menu.completion.current": f"bg:{menu_current_bg} {title}",
    +        "completion-menu.meta.completion": f"bg:{menu_meta_bg} {dim}",
    +        "completion-menu.meta.completion.current": f"bg:{menu_meta_current_bg} {label}",
             "clarify-border": input_rule,
             "clarify-title": f"{title} bold",
             "clarify-question": f"{text} bold",
    ```

    ```diff
    @@ -716,4 +811,6 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
             "approval-cmd": f"{dim} italic",
             "approval-choice": dim,
             "approval-selected": f"{title} bold",
    +        "voice-status": f"bg:{voice_bg} {label}",
    +        "voice-status-recording": f"bg:{voice_bg} {error} bold",
         }
    ```
  - `tests/hermes_cli/test_skin_engine.py`

    ```diff
    @@ -78,6 +78,28 @@ def test_slate_skin_loads(self):
             assert skin.name == "slate"
             assert skin.get_color("banner_title") == "#7eb8f6"
     
    +    def test_daylight_skin_loads(self):
    +        from hermes_cli.skin_engine import load_skin
    +
    +        skin = load_skin("daylight")
    +        assert skin.name == "daylight"
    +        assert skin.tool_prefix == "│"
    +        assert skin.get_color("banner_title") == "#0F172A"
    +        assert skin.get_color("status_bar_bg") == "#E5EDF8"
    +        assert skin.get_color("voice_status_bg") == "#E5EDF8"
    +        assert skin.get_color("completion_menu_bg") == "#F8FAFC"
    +        assert skin.get_color("completion_menu_current_bg") == "#DBEAFE"
    +        assert skin.get_color("completion_menu_meta_bg") == "#EEF2FF"
    +        assert skin.get_color("completion_menu_meta_current_bg") == "#BFDBFE"
    +
    +    def test_warm_lightmode_skin_loads(self):
    +        from hermes_cli.skin_engine import load_skin
    +
    +        skin = load_skin("warm-lightmode")
    +        assert skin.name == "warm-lightmode"
    +        assert skin.get_color("banner_text") == "#2C1810"
    +        assert skin.get_color("completion_menu_bg") == "#F5EFE0"
    +
         def test_unknown_skin_falls_back_to_default(self):
             from hermes_cli.skin_engine import load_skin
             skin = load_skin("nonexistent_skin_xyz")
    ```

    ```diff
    @@ -114,6 +136,8 @@ def test_list_skins_includes_builtins(self):
             assert "ares" in names
             assert "mono" in names
             assert "slate" in names
    +        assert "daylight" in names
    +        assert "warm-lightmode" in names
             for s in skins:
                 assert "source" in s
                 assert s["source"] == "builtin"
    ```

    ```diff
    @@ -242,6 +266,15 @@ def test_prompt_toolkit_style_overrides_cover_tui_classes(self):
                 "completion-menu.completion.current",
                 "completion-menu.meta.completion",
                 "completion-menu.meta.completion.current",
    +            "status-bar",
    +            "status-bar-strong",
    +            "status-bar-dim",
    +            "status-bar-good",
    +            "status-bar-warn",
    +            "status-bar-bad",
    +            "status-bar-critical",
    +            "voice-status",
    +            "voice-status-recording",
                 "clarify-border",
                 "clarify-title",
                 "clarify-question",
    ```

    ```diff
    @@ -277,3 +310,9 @@ def test_prompt_toolkit_style_overrides_use_skin_colors(self):
             assert overrides["clarify-title"] == f"{skin.get_color('banner_title')} bold"
             assert overrides["sudo-prompt"] == f"{skin.get_color('ui_error')} bold"
             assert overrides["approval-title"] == f"{skin.get_color('ui_warn')} bold"
    +
    +        set_active_skin("daylight")
    +        skin = get_active_skin()
    +        overrides = get_prompt_toolkit_style_overrides()
    +        assert overrides["status-bar"] == f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('banner_text')}"
    +        assert overrides["voice-status"] == f"bg:{skin.get_color('voice_status_bg')} {skin.get_color('ui_label')}"
    ```
  - `website/docs/user-guide/features/skins.md`

    ```diff
    @@ -36,6 +36,8 @@ display:
     | `ares` | War-god theme — crimson and bronze | `Ares Agent` | Deep crimson borders with bronze accents. Aggressive spinner verbs ("forging", "marching", "tempering steel"). Custom sword-and-shield ASCII art banner. |
     | `mono` | Monochrome — clean grayscale | `Hermes Agent` | All grays — no color. Borders are `#555555`, text is `#c9d1d9`. Ideal for minimal terminal setups or screen recordings. |
     | `slate` | Cool blue — developer-focused | `Hermes Agent` | Royal blue borders (`#4169e1`), soft blue text. Calm and professional. No custom spinner — uses default faces. |
    +| `daylight` | Light theme for bright terminals with dark text and cool blue accents | `Hermes Agent` | Designed for white or bright terminals. Dark slate text with blue borders, pale status surfaces, and a light completion menu that stays readable in light terminal profiles. |
    +| `warm-lightmode` | Warm brown/gold text for light terminal backgrounds | `Hermes Agent` | Warm parchment tones for light terminals. Dark brown text with saddle-brown accents, cream-colored status surfaces. An earthy alternative to the cooler daylight theme. |
     | `poseidon` | Ocean-god theme — deep blue and seafoam | `Poseidon Agent` | Deep blue to seafoam gradient. Ocean-themed spinners ("charting currents", "sounding the depth"). Trident ASCII art banner. |
     | `sisyphus` | Sisyphean theme — austere grayscale with persistence | `Sisyphus Agent` | Light grays with stark contrast. Boulder-themed spinners ("pushing uphill", "resetting the boulder", "enduring the loop"). Boulder-and-hill ASCII art banner. |
     | `charizard` | Volcanic theme — burnt orange and ember | `Charizard Agent` | Warm burnt orange to ember gradient. Fire-themed spinners ("banking into the draft", "measuring burn"). Dragon-silhouette ASCII art banner. |
    ```

    ```diff
    @@ -63,6 +65,12 @@ Controls all color values throughout the CLI. Values are hex color strings.
     | `response_border` | Border around the agent's response box (ANSI escape) | `#FFD700` |
     | `session_label` | Session label color | `#DAA520` |
     | `session_border` | Session ID dim border color | `#8B8682` |
    +| `status_bar_bg` | Background color for the TUI status / usage bar | `#1a1a2e` |
    +| `voice_status_bg` | Background color for the voice-mode status badge | `#1a1a2e` |
    +| `completion_menu_bg` | Background color for the completion menu list | `#1a1a2e` |
    +| `completion_menu_current_bg` | Background color for the active completion row | `#333355` |
    +| `completion_menu_meta_bg` | Background color for the completion meta column | `#1a1a2e` |
    +| `completion_menu_meta_current_bg` | Background color for the active completion meta column | `#333355` |
     
     ### Spinner (`spinner:`)
     
    ```

    ```diff
    @@ -129,6 +137,12 @@ colors:
       response_border: "#FFD700"
       session_label: "#DAA520"
       session_border: "#8B8682"
    +  status_bar_bg: "#1a1a2e"
    +  voice_status_bg: "#1a1a2e"
    +  completion_menu_bg: "#1a1a2e"
    +  completion_menu_current_bg: "#333355"
    +  completion_menu_meta_bg: "#1a1a2e"
    +  completion_menu_meta_current_bg: "#333355"
     
     spinner:
       waiting_faces:
    ```
- **#13103: fix(tui): theme-driven update-behind banner + auto-detect light terminals (#11300)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `ui-tui/src/__tests__/theme.test.ts`, `ui-tui/src/components/branding.tsx`, `ui-tui/src/theme.ts`
  - fix hunks:
  - `ui-tui/src/__tests__/theme.test.ts`

    ```diff
    @@ -1,6 +1,6 @@
     import { describe, expect, it } from 'vitest'
     
    -import { DARK_THEME, DEFAULT_THEME, fromSkin, LIGHT_THEME } from '../theme.js'
    +import { DARK_THEME, DEFAULT_THEME, detectLightMode, fromSkin, LIGHT_THEME } from '../theme.js'
     
     describe('DEFAULT_THEME', () => {
       it('has brand defaults', () => {
    ```

    ```diff
    @@ -30,11 +30,37 @@ describe('LIGHT_THEME', () => {
     })
     
     describe('DEFAULT_THEME aliasing', () => {
    -  it('defaults to DARK_THEME when HERMES_TUI_LIGHT is unset', () => {
    +  it('defaults to DARK_THEME when nothing signals light', () => {
         expect(DEFAULT_THEME).toBe(DARK_THEME)
       })
     })
     
    +describe('detectLightMode', () => {
    +  it('returns false on empty env', () => {
    +    expect(detectLightMode({})).toBe(false)
    +  })
    +
    +  it('honors HERMES_TUI_LIGHT on/off', () => {
    +    expect(detectLightMode({ HERMES_TUI_LIGHT: '1' })).toBe(true)
    +    expect(detectLightMode({ HERMES_TUI_LIGHT: 'true' })).toBe(true)
    +    expect(detectLightMode({ HERMES_TUI_LIGHT: 'on' })).toBe(true)
    +    expect(detectLightMode({ HERMES_TUI_LIGHT: '0' })).toBe(false)
    +    expect(detectLightMode({ HERMES_TUI_LIGHT: 'off' })).toBe(false)
    +  })
    +
    +  it('sniffs COLORFGBG bg slots 7 and 15 as light (#11300)', () => {
    +    expect(detectLightMode({ COLORFGBG: '0;15' })).toBe(true)
    +    expect(detectLightMode({ COLORFGBG: '0;default;15' })).toBe(true)
    +    expect(detectLightMode({ COLORFGBG: '0;7' })).toBe(true)
    +    expect(detectLightMode({ COLORFGBG: '15;0' })).toBe(false)
    +    expect(detectLightMode({ COLORFGBG: '7;default;0' })).toBe(false)
    +  })
    +
    +  it('lets HERMES_TUI_LIGHT=0 override a light COLORFGBG', () => {
    +    expect(detectLightMode({ COLORFGBG: '0;15', HERMES_TUI_LIGHT: '0' })).toBe(false)
    +  })
    +})
    +
     describe('fromSkin', () => {
       it('overrides banner colors', () => {
         expect(fromSkin({ banner_title: '#FF0000' }, {}).color.gold).toBe('#FF0000')
    ```
  - `ui-tui/src/components/branding.tsx`

    ```diff
    @@ -161,16 +161,16 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
             </Text>
     
             {typeof info.update_behind === 'number' && info.update_behind > 0 && (
    -          <Text bold color="yellow">
    +          <Text bold color={t.color.warn}>
                 ! {info.update_behind} {info.update_behind === 1 ? 'commit' : 'commits'} behind
    -            <Text bold={false} color="yellow" dimColor>
    +            <Text bold={false} color={t.color.warn} dimColor>
                   {' '}
                   - run{' '}
                 </Text>
    -            <Text bold color="yellow">
    +            <Text bold color={t.color.warn}>
                   {info.update_command || 'hermes update'}
                 </Text>
    -            <Text bold={false} color="yellow" dimColor>
    +            <Text bold={false} color={t.color.warn} dimColor>
                   {' '}
                   to update
                 </Text>
    ```
  - `ui-tui/src/theme.ts`

    ```diff
    @@ -171,9 +171,26 @@ export const LIGHT_THEME: Theme = {
       bannerHero: ''
     }
     
    -const LIGHT_MODE = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_LIGHT ?? '').trim())
    +// Pick light vs dark. Explicit `HERMES_TUI_LIGHT` wins; otherwise sniff
    +// `COLORFGBG` (set by XFCE Terminal, rxvt, Terminal.app, etc.) — last field is the
    +// background ANSI index; 7/15 are the "white" slots most light themes emit (#11300).
    +export function detectLightMode(env: NodeJS.ProcessEnv = process.env): boolean {
    +  const explicit = (env.HERMES_TUI_LIGHT ?? '').trim().toLowerCase()
    +
    +  if (/^(?:1|true|yes|on)$/.test(explicit)) {
    +    return true
    +  }
    +
    +  if (/^(?:0|false|no|off)$/.test(explicit)) {
    +    return false
    +  }
    +
    +  const bg = Number((env.COLORFGBG ?? '').trim().split(';').at(-1))
    +
    +  return bg === 7 || bg === 15
    +}
     
    -export const DEFAULT_THEME: Theme = LIGHT_MODE ? LIGHT_THEME : DARK_THEME
    +export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME
     
     // ── Skin → Theme ─────────────────────────────────────────────────────
     
    ```
- **#17113: feat(tui): expand light-terminal auto-detection (HERMES_TUI_THEME, background hex)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `ui-tui/src/__tests__/theme.test.ts`, `ui-tui/src/theme.ts`
  - fix hunks:
  - `ui-tui/src/__tests__/theme.test.ts`

    ```diff
    @@ -1,46 +1,90 @@
    -import { describe, expect, it } from 'vitest'
    -
    -import { DARK_THEME, DEFAULT_THEME, detectLightMode, fromSkin, LIGHT_THEME } from '../theme.js'
    +import { afterEach, describe, expect, it, vi } from 'vitest'
    +
    +// `theme.js` reads `process.env` at module-load to compute DEFAULT_THEME,
    +// and `fromSkin` closes over DEFAULT_THEME.  A developer shell with
    +// HERMES_TUI_THEME=light (or HERMES_TUI_BACKGROUND set to something
    +// bright) would flip the base and turn these assertions into a local-
    +// only failure.  We sterilize the relevant env vars + dynamically
    +// import the module fresh so EVERY symbol that closes over the env
    +// (DEFAULT_THEME, DARK_THEME, LIGHT_THEME, fromSkin) is loaded against
    +// a known-empty environment.
    +//
    +// `detectLightMode` takes env as an explicit arg, so it's safe to import
    +// statically — but we stay consistent and dynamic-import it too.
    +const RELEVANT_ENV = [
    +  'HERMES_TUI_LIGHT',
    +  'HERMES_TUI_THEME',
    +  'HERMES_TUI_BACKGROUND',
    +  'COLORFGBG',
    +  'TERM_PROGRAM',
    +] as const
    +
    +async function importThemeWithCleanEnv() {
    +  for (const key of RELEVANT_ENV) {
    +    vi.stubEnv(key, '')
    +  }
    +  vi.resetModules()
    +  return import('../theme.js')
    +}
    +
    +afterEach(() => {
    +  vi.unstubAllEnvs()
    +  vi.resetModules()
    +})
     
     describe('DEFAULT_THEME', () => {
    -  it('has brand defaults', () => {
    +  it('has brand defaults', async () => {
    +    const { DEFAULT_THEME } = await importThemeWithCleanEnv()
    +
         expect(DEFAULT_THEME.brand.name).toBe('Hermes Agent')
         expect(DEFAULT_THEME.brand.prompt).toBe('❯')
         expect(DEFAULT_THEME.brand.tool).toBe('┊')
       })
     
    -  it('has color palette', () => {
    +  it('has color palette', async () => {
    +    const { DEFAULT_THEME } = await importThemeWithCleanEnv()
    +
         expect(DEFAULT_THEME.color.gold).toBe('#FFD700')
         expect(DEFAULT_THEME.color.error).toBe('#ef5350')
       })
     })
     
     describe('LIGHT_THEME', () => {
    -  it('avoids bright-yellow accents unreadable on white backgrounds (#11300)', () => {
    +  it('avoids bright-yellow accents unreadable on white backgrounds (#11300)', async () => {
    +    const { LIGHT_THEME } = await importThemeWithCleanEnv()
    +
         expect(LIGHT_THEME.color.gold).not.toBe('#FFD700')
         expect(LIGHT_THEME.color.amber).not.toBe('#FFBF00')
         expect(LIGHT_THEME.color.dim).not.toBe('#B8860B')
         expect(LIGHT_THEME.color.statusWarn).not.toBe('#FFD700')
       })
     
    -  it('keeps the same shape as DARK_THEME', () => {
    +  it('keeps the same shape as DARK_THEME', async () => {
    +    const { DARK_THEME, LIGHT_THEME } = await importThemeWithCleanEnv()
    +
         expect(Object.keys(LIGHT_THEME.color).sort()).toEqual(Object.keys(DARK_THEME.color).sort())
         expect(LIGHT_THEME.brand).toEqual(DARK_THEME.brand)
       })
     })
     
     describe('DEFAULT_THEME aliasing', () => {
    -  it('defaults to DARK_THEME when nothing signals light', () => {
    -    expect(DEFAULT_THEME).toBe(DARK_THEME)
    +  it('defaults to DARK_THEME when nothing signals light', async () => {
    +    const { DEFAULT_THEME, DARK_THEME: DARK } = await importThemeWithCleanEnv()
    +
    +    expect(DEFAULT_THEME).toBe(DARK)
       })
     })
     
     describe('detectLightMode', () => {
    -  it('returns false on empty env', () => {
    +  it('returns false on empty env', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
         expect(detectLightMode({})).toBe(false)
       })
     
    -  it('honors HERMES_TUI_LIGHT on/off', () => {
    +  it('honors HERMES_TUI_LIGHT on/off', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
         expect(detectLightMode({ HERMES_TUI_LIGHT: '1' })).toBe(true)
         expect(detectLightMode({ HERMES_TUI_LIGHT: 'true' })).toBe(true)
         expect(detectLightMode({ HERMES_TUI_LIGHT: 'on' })).toBe(true)
    ```

    ```diff
    @@ -48,7 +92,9 @@ describe('detectLightMode', () => {
         expect(detectLightMode({ HERMES_TUI_LIGHT: 'off' })).toBe(false)
       })
     
    -  it('sniffs COLORFGBG bg slots 7 and 15 as light (#11300)', () => {
    +  it('sniffs COLORFGBG bg slots 7 and 15 as light (#11300)', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
         expect(detectLightMode({ COLORFGBG: '0;15' })).toBe(true)
         expect(detectLightMode({ COLORFGBG: '0;default;15' })).toBe(true)
         expect(detectLightMode({ COLORFGBG: '0;7' })).toBe(true)
    ```

    ```diff
    @@ -56,38 +102,119 @@ describe('detectLightMode', () => {
         expect(detectLightMode({ COLORFGBG: '7;default;0' })).toBe(false)
       })
     
    -  it('lets HERMES_TUI_LIGHT=0 override a light COLORFGBG', () => {
    +  it('falls through on malformed COLORFGBG with empty/non-numeric trailing field', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +    // `Number('')` is 0, so `'15;'` would have been read as bg=0
    +    // (authoritative dark) and incorrectly blocked TERM_PROGRAM.
    +    // The strict /^\d+$/ guard makes these fall through instead.
    +    const allowList = new Set(['Apple_Terminal'])
    +
    +    expect(detectLightMode({ COLORFGBG: '15;', TERM_PROGRAM: 'Apple_Terminal' }, allowList)).toBe(true)
    +    expect(detectLightMode({ COLORFGBG: 'default;default', TERM_PROGRAM: 'Apple_Terminal' }, allowList)).toBe(true)
    +    // Without an allow-list match, fall-through still defaults to dark.
    +    expect(detectLightMode({ COLORFGBG: '15;' })).toBe(false)
    +  })
    +
    +  it('lets HERMES_TUI_LIGHT=0 override a light COLORFGBG', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
         expect(detectLightMode({ COLORFGBG: '0;15', HERMES_TUI_LIGHT: '0' })).toBe(false)
       })
    +
    +  it('honors HERMES_TUI_THEME=light/dark as a symmetric explicit override', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
    +    expect(detectLightMode({ HERMES_TUI_THEME: 'light' })).toBe(true)
    +    expect(detectLightMode({ HERMES_TUI_THEME: 'dark' })).toBe(false)
    +    expect(detectLightMode({ COLORFGBG: '0;15', HERMES_TUI_THEME: 'dark' })).toBe(false)
    +    expect(detectLightMode({ COLORFGBG: '15;0', HERMES_TUI_THEME: 'light' })).toBe(true)
    +  })
    +
    +  it('uses HERMES_TUI_BACKGROUND luminance when COLORFGBG is missing', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#ffffff' })).toBe(true)
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#000000' })).toBe(false)
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#1e1e1e' })).toBe(false)
    +    // Three-char hex normalises like CSS.
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fff' })).toBe(true)
    +    // Garbage falls through to the default-dark path.
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: 'not-a-colour' })).toBe(false)
    +  })
    +
    +  it('rejects partially-invalid hex instead of silently truncating', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +    // `parseInt('fffgff'.slice(2,4), 16)` would return 15 — the strict
    +    // regex must reject these inputs so they fall through to default-
    +    // dark instead of producing a false-positive light reading.
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fffgff' })).toBe(false)
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: 'ffggff' })).toBe(false)
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#xyz' })).toBe(false)
    +    // Wrong length also rejected (no implicit padding/truncation).
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fffff' })).toBe(false)
    +    expect(detectLightMode({ HERMES_TUI_BACKGROUND: '#fffffff' })).toBe(false)
    +  })
    +
    +  it('treats COLORFGBG as authoritative when present so it dominates the TERM_PROGRAM allow-list', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +    // Inject a light-default allow-list so the precedence test is
    +    // meaningful even though the production allow-list is empty.
    +    const allowList = new Set(['Apple_Terminal'])
    +
    +    // Sanity: the allow-list alone WOULD turn this terminal light.
    +    expect(detectLightMode({ TERM_PROGRAM: 'Apple_Terminal' }, allowList)).toBe(true)
    +
    +    // Dark COLORFGBG must beat the allow-list.
    +    expect(
    +      detectLightMode({ COLORFGBG: '15;0', TERM_PROGRAM: 'Apple_Terminal' }, allowList),
    +    ).toBe(false)
    +  })
     })
     
     describe('fromSkin', () => {
    -  it('overrides banner colors', () => {
    +  // `fromSkin` closes over DEFAULT_THEME (which is env-derived), so we
    +  // must dynamic-import it after sterilizing env — otherwise an ambient
    +  // HERMES_TUI_THEME=light would flip the base palette and make these
    +  // assertions order-dependent on the developer's shell.
    +
    +  it('overrides banner colors', async () => {
    +    const { fromSkin } = await importThemeWithCleanEnv()
    +
         expect(fromSkin({ banner_title: '#FF0000' }, {}).color.gold).toBe('#FF0000')
       })
     
    -  it('preserves unset colors', () => {
    +  it('preserves unset colors', async () => {
    +    const { DEFAULT_THEME, fromSkin } = await importThemeWithCleanEnv()
    +
         expect(fromSkin({ banner_title: '#FF0000' }, {}).color.amber).toBe(DEFAULT_THEME.color.amber)
       })
     
    -  it('overrides branding', () => {
    +  it('overrides branding', async () => {
    +    const { fromSkin } = await importThemeWithCleanEnv()
         const { brand } = fromSkin({}, { agent_name: 'TestBot', prompt_symbol: '$' })
    +
         expect(brand.name).toBe('TestBot')
         expect(brand.prompt).toBe('$')
       })
     
    -  it('defaults for empty skin', () => {
    +  it('defaults for empty skin', async () => {
    +    const { DEFAULT_THEME, fromSkin } = await importThemeWithCleanEnv()
    +
         expect(fromSkin({}, {}).color).toEqual(DEFAULT_THEME.color)
         expect(fromSkin({}, {}).brand.icon).toBe(DEFAULT_THEME.brand.icon)
       })
     
    -  it('passes banner logo/hero', () => {
    +  it('passes banner logo/hero', async () => {
    +    const { fromSkin } = await importThemeWithCleanEnv()
    +
         expect(fromSkin({}, {}, 'LOGO', 'HERO').bannerLogo).toBe('LOGO')
         expect(fromSkin({}, {}, 'LOGO', 'HERO').bannerHero).toBe('HERO')
       })
     
    -  it('maps ui_ color keys + cascades to status', () => {
    +  it('maps ui_ color keys + cascades to status', async () => {
    +    const { fromSkin } = await importThemeWithCleanEnv()
         const { color } = fromSkin({ ui_ok: '#008000' }, {})
    +
         expect(color.ok).toBe('#008000')
         expect(color.statusGood).toBe('#008000')
       })
    ```
  - `ui-tui/src/theme.ts`

    ```diff
    @@ -179,23 +179,129 @@ export const LIGHT_THEME: Theme = {
       bannerHero: ''
     }
     
    -// Pick light vs dark. Explicit `HERMES_TUI_LIGHT` wins; otherwise sniff
    -// `COLORFGBG` (set by XFCE Terminal, rxvt, Terminal.app, etc.) — last field is the
    -// background ANSI index; 7/15 are the "white" slots most light themes emit (#11300).
    -export function detectLightMode(env: NodeJS.ProcessEnv = process.env): boolean {
    -  const explicit = (env.HERMES_TUI_LIGHT ?? '').trim().toLowerCase()
    +const TRUE_RE = /^(?:1|true|yes|on)$/
    +const FALSE_RE = /^(?:0|false|no|off)$/
    +
    +// Reserved for future TERM_PROGRAM-based heuristics.  Empty by default:
    +// most modern terminals (Ghostty, Warp, iTerm2, Apple_Terminal) ship a
    +// dark profile out of the box, so guessing wrong here is more annoying
    +// than missing a light user — light users can always set
    +// `HERMES_TUI_LIGHT=1` or `HERMES_TUI_THEME=light`.
    +const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>()
    +
    +// Best-effort RGB → luminance check.  Currently only accepts a 3- or
    +// 6-digit hex value (with or without a leading `#`); the env var name
    +// `HERMES_TUI_BACKGROUND` is intentionally generic so a future OSC11
    +// query helper can cache its answer there too, but additional formats
    +// (rgb()/hsl()/named colours) would need explicit parsing here first.
    +const LUMA_LIGHT_THRESHOLD = 0.6
    +
    +// Strict allow-list: parseInt(..., 16) silently truncates at the first
    +// non-hex character (e.g. `fffgff` would parse as `fff` and yield a
    +// false-positive "white" reading), so reject anything that doesn't match
    +// the canonical 3- or 6-digit shape up front.
    +const HEX_3_RE = /^[0-9a-f]{3}$/
    +const HEX_6_RE = /^[0-9a-f]{6}$/
    +
    +function backgroundLuminance(raw: string): null | number {
    +  const v = raw.trim().toLowerCase()
    +
    +  if (!v) {
    +    return null
    +  }
    +
    +  const hex = v.startsWith('#') ? v.slice(1) : v
    +  const rgb = HEX_6_RE.test(hex)
    +    ? [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]
    +    : HEX_3_RE.test(hex)
    +      ? [parseInt(hex[0]! + hex[0]!, 16), parseInt(hex[1]! + hex[1]!, 16), parseInt(hex[2]! + hex[2]!, 16)]
    +      : null
    +
    +  if (!rgb) {
    +    return null
    +  }
    +
    +  // Rec. 709 luma — close enough for "is this background bright".
    +  return (0.2126 * rgb[0]! + 0.7152 * rgb[1]! + 0.0722 * rgb[2]!) / 255
    +}
    +
    +// Pick light vs dark with ordered, explainable signals (#11300):
    +//
    +//   1. `HERMES_TUI_LIGHT` boolean — `1`/`true`/`yes`/`on` → light;
    +//      `0`/`false`/`no`/`off` → dark.  Either explicit value wins
    +//      regardless of any later signal.
    +//   2. `HERMES_TUI_THEME` named override — `light` / `dark` win over
    +//      every signal below.
    +//   3. `HERMES_TUI_BACKGROUND` hex hint (3- or 6-digit) — luminance
    +//      ≥ LUMA_LIGHT_THRESHOLD → light.
    +//   4. `COLORFGBG` last field — XFCE / rxvt / Terminal.app emit
    +//      slot 7 or 15 on light profiles; 0–15 ranges are otherwise
    +//      treated as authoritatively dark so the TERM_PROGRAM
    +//      allow-list below cannot override an explicit dark profile.
    +//   5. `TERM_PROGRAM` light-default allow-list (currently empty).
    +//
    +// Anything we can't decide stays dark — the default Hermes palette
    +// is the dark one.
    +export function detectLightMode(
    +  env: NodeJS.ProcessEnv = process.env,
    +  // Injectable so tests can prove the COLORFGBG-over-TERM_PROGRAM
    +  // precedence rule even though the production allow-list is empty.
    +  lightDefaultTermPrograms: ReadonlySet<string> = LIGHT_DEFAULT_TERM_PROGRAMS,
    +): boolean {
    +  const lightFlag = (env.HERMES_TUI_LIGHT ?? '').trim().toLowerCase()
    +
    +  if (TRUE_RE.test(lightFlag)) {
    +    return true
    +  }
    +
    +  if (FALSE_RE.test(lightFlag)) {
    +    return false
    +  }
    +
    +  const themeFlag = (env.HERMES_TUI_THEME ?? '').trim().toLowerCase()
     
    -  if (/^(?:1|true|yes|on)$/.test(explicit)) {
    +  if (themeFlag === 'light') {
         return true
       }
     
    -  if (/^(?:0|false|no|off)$/.test(explicit)) {
    +  if (themeFlag === 'dark') {
         return false
       }
     
    -  const bg = Number((env.COLORFGBG ?? '').trim().split(';').at(-1))
    +  const bgHint = backgroundLuminance(env.HERMES_TUI_BACKGROUND ?? '')
    +
    +  if (bgHint !== null) {
    +    return bgHint >= LUMA_LIGHT_THRESHOLD
    +  }
    +
    +  const colorfgbg = (env.COLORFGBG ?? '').trim()
    +
    +  if (colorfgbg) {
    +    // Validate as a decimal integer before coercing — `Number('')` is 0,
    +    // so a malformed `COLORFGBG='15;'` would otherwise look like an
    +    // authoritative dark slot and incorrectly block the TERM_PROGRAM
    +    // allow-list.  Anything that isn't pure digits falls through.
    +    const lastField = colorfgbg.split(';').at(-1) ?? ''
    +
    +    if (/^\d+$/.test(lastField)) {
    +      const bg = Number(lastField)
    +
    +      if (bg === 7 || bg === 15) {
    +        return true
    +      }
    +
    +      // Slots 0–6 and 8–14 are the dark half of the 0–15 ANSI range.
    +      // When COLORFGBG is set we trust it as authoritative — a non-light
    +      // value here shouldn't get overridden by the TERM_PROGRAM allow-list.
    +      if (bg >= 0 && bg < 16) {
    +        return false
    +      }
    +    }
    +  }
    +
    +  const termProgram = (env.TERM_PROGRAM ?? '').trim()
     
    -  return bg === 7 || bg === 15
    +  return lightDefaultTermPrograms.has(termProgram)
     }
     
     export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME
    ```
- **#17695: fix(tui): normalize legacy Terminal.app colors**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `ui-tui/packages/hermes-ink/src/ink/colorize.test.ts`, `ui-tui/packages/hermes-ink/src/ink/colorize.ts`, `ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts`, `ui-tui/packages/hermes-ink/src/ink/components/Text.tsx`, `ui-tui/src/__tests__/forceTruecolor.test.ts`, `ui-tui/src/__tests__/theme.test.ts`, `ui-tui/src/entry.tsx`, `ui-tui/src/lib/forceTruecolor.ts`, `ui-tui/src/theme.ts`
  - fix hunks:
  - `ui-tui/packages/hermes-ink/src/ink/colorize.test.ts`

    ```diff
    @@ -0,0 +1,60 @@
    +import { describe, expect, it } from 'vitest'
    +
    +import {
    +  CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE,
    +  richEightBitColorNumber,
    +  shouldUseRichEightBitDowngradeForLegacyAppleTerminal
    +} from './colorize.js'
    +
    +describe('shouldUseRichEightBitDowngradeForLegacyAppleTerminal', () => {
    +  it('memoizes the current process decision for render hot paths', () => {
    +    expect(typeof CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE).toBe('boolean')
    +  })
    +
    +  it('uses Rich-compatible 256-color downgrade on legacy Apple Terminal', () => {
    +    expect(
    +      shouldUseRichEightBitDowngradeForLegacyAppleTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv, 2)
    +    ).toBe(true)
    +  })
    +
    +  it('normalizes Apple Terminal names before matching', () => {
    +    expect(
    +      shouldUseRichEightBitDowngradeForLegacyAppleTerminal({ TERM_PROGRAM: ' Apple_Terminal ' } as NodeJS.ProcessEnv, 2)
    +    ).toBe(true)
    +  })
    +
    +  it('does not rewrite when Apple Terminal advertises truecolor', () => {
    +    expect(
    +      shouldUseRichEightBitDowngradeForLegacyAppleTerminal(
    +        { COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv,
    +        3
    +      )
    +    ).toBe(false)
    +  })
    +
    +  it('does not override explicit color environment choices', () => {
    +    expect(
    +      shouldUseRichEightBitDowngradeForLegacyAppleTerminal(
    +        { FORCE_COLOR: '2', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv,
    +        2
    +      )
    +    ).toBe(false)
    +    expect(
    +      shouldUseRichEightBitDowngradeForLegacyAppleTerminal(
    +        { HERMES_TUI_TRUECOLOR: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv,
    +        3
    +      )
    +    ).toBe(false)
    +  })
    +})
    +
    +describe('richEightBitColorNumber', () => {
    +  it('matches Rich downgrade output for default Hermes skin colors', () => {
    +    expect(richEightBitColorNumber(0xff, 0xd7, 0x00)).toBe(220)
    +    expect(richEightBitColorNumber(0xff, 0xbf, 0x00)).toBe(214)
    +    expect(richEightBitColorNumber(0xcd, 0x7f, 0x32)).toBe(173)
    +    expect(richEightBitColorNumber(0xb8, 0x86, 0x0b)).toBe(136)
    +    expect(richEightBitColorNumber(0xff, 0xf8, 0xdc)).toBe(230)
    +  })
    +})
    +
    ```
  - `ui-tui/packages/hermes-ink/src/ink/colorize.ts`

    ```diff
    @@ -28,6 +28,39 @@ function boostChalkLevelForXtermJs(): boolean {
       return false
     }
     
    +export function shouldUseRichEightBitDowngradeForLegacyAppleTerminal(
    +  env: NodeJS.ProcessEnv = process.env,
    +  level = chalk.level
    +): boolean {
    +  const termProgram = (env.TERM_PROGRAM ?? '').trim()
    +  const truecolorOverride = /^(?:1|true|yes|on)$/i.test((env.HERMES_TUI_TRUECOLOR ?? '').trim())
    +  const advertisesTruecolor = /^(?:truecolor|24bit)$/i.test((env.COLORTERM ?? '').trim())
    +
    +  return termProgram === 'Apple_Terminal' && !truecolorOverride && !advertisesTruecolor && !('FORCE_COLOR' in env) && level === 2
    +}
    +
    +export function richEightBitColorNumber(red: number, green: number, blue: number): number {
    +  const rn = red / 255
    +  const gn = green / 255
    +  const bn = blue / 255
    +  const max = Math.max(rn, gn, bn)
    +  const min = Math.min(rn, gn, bn)
    +  const lightness = (max + min) / 2
    +  const saturation = max === min ? 0 : lightness > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min)
    +
    +  if (saturation < 0.15) {
    +    const gray = Math.round(lightness * 25)
    +
    +    return gray === 0 ? 16 : gray === 25 ? 231 : 231 + gray
    +  }
    +
    +  const sixRed = red < 95 ? red / 95 : 1 + (red - 95) / 40
    +  const sixGreen = green < 95 ? green / 95 : 1 + (green - 95) / 40
    +  const sixBlue = blue < 95 ? blue / 95 : 1 + (blue - 95) / 40
    +
    +  return 16 + 36 * Math.round(sixRed) + 6 * Math.round(sixGreen) + Math.round(sixBlue)
    +}
    +
     /**
      * tmux parses truecolor SGR (\e[48;2;r;g;bm) into its cell buffer correctly,
      * but its client-side emitter only re-emits truecolor to the outer terminal if
    ```

    ```diff
    @@ -58,15 +91,17 @@ function clampChalkLevelForTmux(): boolean {
     }
     
     // Computed once at module load — terminal/tmux environment doesn't change mid-session.
    -// Order matters: boost first so the tmux clamp can re-clamp if tmux is running
    -// inside a VS Code terminal. Exported for debugging — tree-shaken if unused.
    +// Order matters: boost first; then tmux can still clamp RGB to 256.
    +// Exported for debugging — tree-shaken if unused.
     export const CHALK_BOOSTED_FOR_XTERMJS = boostChalkLevelForXtermJs()
     export const CHALK_CLAMPED_FOR_TMUX = clampChalkLevelForTmux()
    +export const CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE = shouldUseRichEightBitDowngradeForLegacyAppleTerminal()
     
     export type ColorType = 'foreground' | 'background'
     
     const RGB_REGEX = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/
     const ANSI_REGEX = /^ansi256\(\s?(\d+)\s?\)$/
    +const HEX_REGEX = /^#[0-9a-fA-F]{6}$/
     
     export const colorize = (str: string, color: string | undefined, type: ColorType): string => {
       if (!color) {
    ```

    ```diff
    @@ -128,6 +163,16 @@ export const colorize = (str: string, color: string | undefined, type: ColorType
       }
     
       if (color.startsWith('#')) {
    +    if (HEX_REGEX.test(color) && CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE) {
    +      const value = Number.parseInt(color.slice(1), 16)
    +      const red = (value >> 16) & 0xff
    +      const green = (value >> 8) & 0xff
    +      const blue = value & 0xff
    +      const ansi = richEightBitColorNumber(red, green, blue)
    +
    +      return type === 'foreground' ? chalk.ansi256(ansi)(str) : chalk.bgAnsi256(ansi)(str)
    +    }
    +
         return type === 'foreground' ? chalk.hex(color)(str) : chalk.bgHex(color)(str)
       }
     
    ```

    ```diff
    @@ -154,6 +199,12 @@ export const colorize = (str: string, color: string | undefined, type: ColorType
         const secondValue = Number(matches[2])
         const thirdValue = Number(matches[3])
     
    +    if (CHALK_USES_RICH_EIGHT_BIT_DOWNGRADE) {
    +      const ansi = richEightBitColorNumber(firstValue, secondValue, thirdValue)
    +
    +      return type === 'foreground' ? chalk.ansi256(ansi)(str) : chalk.bgAnsi256(ansi)(str)
    +    }
    +
         return type === 'foreground'
           ? chalk.rgb(firstValue, secondValue, thirdValue)(str)
           : chalk.bgRgb(firstValue, secondValue, thirdValue)(str)
    ```
  - `ui-tui/packages/hermes-ink/src/ink/components/Text.test.ts`

    ```diff
    @@ -1,18 +1,38 @@
     import { describe, expect, it } from 'vitest'
     
    -import { shouldUseAnsiDim } from './Text.js'
    +import { dimColorFallback, shouldUseAnsiDim } from './Text.js'
     
     describe('shouldUseAnsiDim', () => {
       it('disables ANSI dim on VTE terminals by default', () => {
         expect(shouldUseAnsiDim({ VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(false)
       })
     
    +  it('disables ANSI dim on Apple Terminal by default', () => {
    +    expect(shouldUseAnsiDim({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)
    +  })
    +
       it('keeps ANSI dim enabled elsewhere by default', () => {
         expect(shouldUseAnsiDim({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true)
       })
     
       it('honors explicit env override', () => {
         expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(true)
    +    expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(true)
         expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '0' } as NodeJS.ProcessEnv)).toBe(false)
       })
     })
    +
    +describe('dimColorFallback', () => {
    +  it('renders Apple Terminal dim as muted gray by default', () => {
    +    expect(dimColorFallback({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe('#6B7280')
    +  })
    +
    +  it('normalizes Apple Terminal names before matching', () => {
    +    expect(dimColorFallback({ TERM_PROGRAM: ' Apple_Terminal ' } as NodeJS.ProcessEnv)).toBe('#6B7280')
    +  })
    +
    +  it('does not apply when dim is explicitly configured', () => {
    +    expect(dimColorFallback({ HERMES_TUI_DIM: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBeUndefined()
    +    expect(dimColorFallback({ HERMES_TUI_DIM: '0', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBeUndefined()
    +  })
    +})
    ```
  - `ui-tui/packages/hermes-ink/src/ink/components/Text.tsx`

    ```diff
    @@ -6,6 +6,7 @@ import type { Color, Styles } from '../styles.js'
     
     const ENV_ON_RE = /^(?:1|true|yes|on)$/i
     const ENV_OFF_RE = /^(?:0|false|no|off)$/i
    +const LEGACY_APPLE_DIM_COLOR: Color = '#6B7280'
     type BaseProps = {
       /**
        * Change text color. Accepts a raw color value (rgb, hex, ansi).
    ```

    ```diff
    @@ -76,9 +77,23 @@ export function shouldUseAnsiDim(env: NodeJS.ProcessEnv = process.env): boolean
         return false
       }
     
    +  if ((env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal') {
    +    return false
    +  }
    +
       return !env.VTE_VERSION
     }
     
    +export function dimColorFallback(env: NodeJS.ProcessEnv = process.env): Color | undefined {
    +  const override = (env.HERMES_TUI_DIM ?? '').trim()
    +
    +  if (ENV_ON_RE.test(override) || ENV_OFF_RE.test(override)) {
    +    return undefined
    +  }
    +
    +  return (env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal' ? LEGACY_APPLE_DIM_COLOR : undefined
    +}
    +
     const memoizedStylesForWrap: Record<NonNullable<Styles['textWrap']>, Styles> = {
       wrap: {
         flexGrow: 0,
    ```

    ```diff
    @@ -161,6 +176,7 @@ export default function Text(t0: Props) {
       const inverse = t4 === undefined ? false : t4
       const wrap = t5 === undefined ? 'wrap' : t5
       const effectiveDim = dim && shouldUseAnsiDim()
    +  const effectiveColor = dim && !effectiveDim ? (color ?? dimColorFallback()) : color
     
       if (children === undefined || children === null) {
         return null
    ```

    ```diff
    @@ -168,11 +184,11 @@ export default function Text(t0: Props) {
     
       let t6
     
    -  if ($[0] !== color) {
    -    t6 = color && {
    -      color
    +  if ($[0] !== effectiveColor) {
    +    t6 = effectiveColor && {
    +      color: effectiveColor
         }
    -    $[0] = color
    +    $[0] = effectiveColor
         $[1] = t6
       } else {
         t6 = $[1]
    ```
  - `ui-tui/src/__tests__/forceTruecolor.test.ts`

    ```diff
    @@ -1,6 +1,7 @@
     import { describe, expect, it } from 'vitest'
     
    -const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR'] as const
    +const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR', 'TERM', 'TERM_PROGRAM'] as const
    +let importId = 0
     
     async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
       const saved: Record<string, string | undefined> = {}
    ```

    ```diff
    @@ -25,11 +26,39 @@ async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
     }
     
     describe('forceTruecolor', () => {
    -  it('sets COLORTERM=truecolor and FORCE_COLOR=3 when unset', async () => {
    +  it('does not force truecolor by default', async () => {
         await withCleanEnv(
           () => {},
           async () => {
    -        await import('../lib/forceTruecolor.js?t=' + Date.now())
    +        await import('../lib/forceTruecolor.js?t=default-' + importId++)
    +        expect(process.env.COLORTERM).toBeUndefined()
    +        expect(process.env.FORCE_COLOR).toBeUndefined()
    +      }
    +    )
    +  })
    +
    +  it('does not infer truecolor from Apple Terminal on pre-Tahoe macOS', async () => {
    +    await withCleanEnv(
    +      () => {
    +        process.env.TERM_PROGRAM = 'Apple_Terminal'
    +        process.env.TERM = 'xterm-256color'
    +      },
    +      async () => {
    +        const mod = await import('../lib/forceTruecolor.js?t=apple-' + importId++)
    +        expect(mod.shouldForceTruecolor({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false)
    +        expect(process.env.COLORTERM).toBeUndefined()
    +        expect(process.env.FORCE_COLOR).toBeUndefined()
    +      }
    +    )
    +  })
    +
    +  it('sets COLORTERM=truecolor and FORCE_COLOR=3 when explicitly enabled', async () => {
    +    await withCleanEnv(
    +      () => {
    +        process.env.HERMES_TUI_TRUECOLOR = '1'
    +      },
    +      async () => {
    +        await import('../lib/forceTruecolor.js?t=enabled-' + importId++)
             expect(process.env.COLORTERM).toBe('truecolor')
             expect(process.env.FORCE_COLOR).toBe('3')
           }
    ```

    ```diff
    @@ -40,9 +69,10 @@ describe('forceTruecolor', () => {
         await withCleanEnv(
           () => {
             process.env.HERMES_TUI_TRUECOLOR = '0'
    +        process.env.TERM_PROGRAM = 'Apple_Terminal'
           },
           async () => {
    -        await import('../lib/forceTruecolor.js?t=optout-' + Date.now())
    +        await import('../lib/forceTruecolor.js?t=optout-' + importId++)
             expect(process.env.COLORTERM).toBeUndefined()
             expect(process.env.FORCE_COLOR).toBeUndefined()
           }
    ```

    ```diff
    @@ -53,12 +83,41 @@ describe('forceTruecolor', () => {
         await withCleanEnv(
           () => {
             process.env.NO_COLOR = '1'
    +        process.env.HERMES_TUI_TRUECOLOR = '1'
           },
           async () => {
    -        await import('../lib/forceTruecolor.js?t=no-color-' + Date.now())
    +        await import('../lib/forceTruecolor.js?t=no-color-' + importId++)
             expect(process.env.COLORTERM).toBeUndefined()
             expect(process.env.FORCE_COLOR).toBeUndefined()
           }
         )
       })
    +
    +  it('respects existing FORCE_COLOR unless Hermes truecolor is explicit', async () => {
    +    await withCleanEnv(
    +      () => {
    +        process.env.FORCE_COLOR = ''
    +      },
    +      async () => {
    +        const mod = await import('../lib/forceTruecolor.js?t=force-color-' + importId++)
    +        expect(mod.shouldForceTruecolor(process.env)).toBe(false)
    +        expect(process.env.COLORTERM).toBeUndefined()
    +        expect(process.env.FORCE_COLOR).toBe('')
    +      }
    +    )
    +  })
    +
    +  it('lets explicit Hermes truecolor override existing FORCE_COLOR', async () => {
    +    await withCleanEnv(
    +      () => {
    +        process.env.FORCE_COLOR = '0'
    +        process.env.HERMES_TUI_TRUECOLOR = '1'
    +      },
    +      async () => {
    +        await import('../lib/forceTruecolor.js?t=explicit-force-' + importId++)
    +        expect(process.env.COLORTERM).toBe('truecolor')
    +        expect(process.env.FORCE_COLOR).toBe('3')
    +      }
    +    )
    +  })
     })
    ```
  - `ui-tui/src/__tests__/theme.test.ts`

    ```diff
    @@ -16,12 +16,13 @@ const RELEVANT_ENV = [
       'HERMES_TUI_THEME',
       'HERMES_TUI_BACKGROUND',
       'COLORFGBG',
    +  'COLORTERM',
       'TERM_PROGRAM'
     ] as const
     
    -async function importThemeWithCleanEnv() {
    +async function importThemeWithEnv(env: Partial<Record<(typeof RELEVANT_ENV)[number], string>> = {}) {
       for (const key of RELEVANT_ENV) {
    -    vi.stubEnv(key, '')
    +    vi.stubEnv(key, env[key] ?? '')
       }
     
       vi.resetModules()
    ```

    ```diff
    @@ -29,6 +30,10 @@ async function importThemeWithCleanEnv() {
       return import('../theme.js')
     }
     
    +async function importThemeWithCleanEnv() {
    +  return importThemeWithEnv()
    +}
    +
     afterEach(() => {
       vi.unstubAllEnvs()
       vi.resetModules()
    ```

    ```diff
    @@ -84,6 +89,12 @@ describe('detectLightMode', () => {
         expect(detectLightMode({})).toBe(false)
       })
     
    +  it('defaults Apple Terminal to light when no stronger signal is present', async () => {
    +    const { detectLightMode } = await importThemeWithCleanEnv()
    +
    +    expect(detectLightMode({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(true)
    +  })
    +
       it('honors HERMES_TUI_LIGHT on/off', async () => {
         const { detectLightMode } = await importThemeWithCleanEnv()
     
    ```

    ```diff
    @@ -159,8 +170,8 @@ describe('detectLightMode', () => {
     
       it('treats COLORFGBG as authoritative when present so it dominates the TERM_PROGRAM allow-list', async () => {
         const { detectLightMode } = await importThemeWithCleanEnv()
    -    // Inject a light-default allow-list so the precedence test is
    -    // meaningful even though the production allow-list is empty.
    +    // Injecting the allow-list keeps this precedence rule explicit even if
    +    // production defaults change.
         const allowList = new Set(['Apple_Terminal'])
     
         // Sanity: the allow-list alone WOULD turn this terminal light.
    ```

    ```diff
    @@ -221,6 +232,40 @@ describe('fromSkin', () => {
         expect(fromSkin({}, {}).brand.icon).toBe(DEFAULT_THEME.brand.icon)
       })
     
    +  it('normalizes non-banner foregrounds on light Apple Terminal', async () => {
    +    const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: 'Apple_Terminal' })
    +
    +    const theme = fromSkin({
    +      banner_accent: '#FFBF00',
    +      banner_border: '#CD7F32',
    +      banner_dim: '#B8860B',
    +      banner_text: '#FFF8DC',
    +      banner_title: '#FFD700',
    +      prompt: '#FFF8DC'
    +    }, {})
    +
    +    expect(theme.color.primary).toBe('#FFD700')
    +    expect(theme.color.accent).toBe('#FFBF00')
    +    expect(theme.color.border).toBe('#CD7F32')
    +    expect(theme.color.muted).toBe('ansi256(245)')
    +    expect(theme.color.text).toBe('ansi256(136)')
    +    expect(theme.color.prompt).toBe('ansi256(136)')
    +  })
    +
    +  it('does not normalize light Apple Terminal when truecolor is advertised', async () => {
    +    const { fromSkin } = await importThemeWithEnv({ COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' })
    +    const theme = fromSkin({ banner_text: '#FFF8DC' }, {})
    +
    +    expect(theme.color.text).toBe('#FFF8DC')
    +  })
    +
    +  it('normalizes Apple Terminal names before matching', async () => {
    +    const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: ' Apple_Terminal ' })
    +    const theme = fromSkin({ banner_text: '#FFF8DC' }, {})
    +
    +    expect(theme.color.text).toBe('ansi256(136)')
    +  })
    +
       it('passes banner logo/hero', async () => {
         const { fromSkin } = await importThemeWithCleanEnv()
     
    ```
  - `ui-tui/src/entry.tsx`

    ```diff
    @@ -1,8 +1,6 @@
     #!/usr/bin/env -S node --max-old-space-size=8192 --expose-gc
    -// Must be first import — mutates process.env.FORCE_COLOR / COLORTERM before
    -// any chalk / supports-color import so the banner gradient renders in
    -// truecolor instead of being downsampled to 256-color (which collapses
    -// gold #FFD700 and amber #FFBF00 to the same slot).
    +// Must be first import. If the user explicitly opts into truecolor, this
    +// nudges chalk / supports-color before either package is initialized.
     import './lib/forceTruecolor.js'
     
     import type { FrameEvent } from '@hermes/ink'
    ```
  - `ui-tui/src/lib/forceTruecolor.ts`

    ```diff
    @@ -1,27 +1,25 @@
     /**
    - * Force 24-bit truecolor output before any chalk / supports-color import.
    + * Targeted 24-bit truecolor override before chalk / supports-color imports.
      *
    - * Why this exists:
    - *   The base CLI (Python/Rich) emits banner colors as truecolor ANSI
    - *   (`\033[38;2;R;G;Bm`). The TUI renders through Ink → chalk, whose
    - *   supports-color auto-detection defaults to 256-color on macOS Terminal.app
    - *   and any terminal that does NOT set `COLORTERM=truecolor`. In 256-color
    - *   mode, chalk downsamples `#FFD700` (gold) and `#FFBF00` (amber) to the
    - *   *same* xterm-256 palette slot (220) — collapsing the banner gradient
    - *   into a single flat yellow band. The bronze and dim rows also lose
    - *   contrast against each other.
    - *
    - *   Terminal.app (macOS 12+), iTerm2, kitty, Alacritty, VS Code, Cursor,
    - *   and WezTerm all render truecolor correctly. The few that don't
    - *   (ancient xterm, some CI environments) can set `HERMES_TUI_TRUECOLOR=0`
    - *   to opt out.
    - *
    - * This MUST run before any `chalk` or `supports-color` import. supports-color
    - * caches its level on first load, so nudging env vars after that point has
    - * no effect.
    + * macOS Terminal.app before Tahoe 26 does not support RGB SGR, so do not
    + * infer truecolor from TERM_PROGRAM=Apple_Terminal. Users can still opt in
    + * explicitly on terminals that support RGB but do not advertise COLORTERM.
      */
     
    -if (process.env.HERMES_TUI_TRUECOLOR !== '0' && !process.env.NO_COLOR && !process.env.FORCE_COLOR) {
    +const TRUE_RE = /^(?:1|true|yes|on)$/i
    +const FALSE_RE = /^(?:0|false|no|off)$/i
    +
    +export function shouldForceTruecolor(env: NodeJS.ProcessEnv = process.env): boolean {
    +  const override = (env.HERMES_TUI_TRUECOLOR ?? '').trim()
    +
    +  if (FALSE_RE.test(override) || 'NO_COLOR' in env) {
    +    return false
    +  }
    +
    +  return TRUE_RE.test(override)
    +}
    +
    +if (shouldForceTruecolor()) {
       if (!process.env.COLORTERM) {
         process.env.COLORTERM = 'truecolor'
       }
    ```
  - `ui-tui/src/theme.ts`

    ```diff
    @@ -76,6 +76,162 @@ function mix(a: string, b: string, t: number) {
       return '#' + ((1 << 24) | (lerp(0) << 16) | (lerp(1) << 8) | lerp(2)).toString(16).slice(1)
     }
     
    +const XTERM_6_LEVELS = [0, 95, 135, 175, 215, 255] as const
    +const ANSI_LIGHT_MAX_LUMINANCE = 0.72
    +const ANSI_LIGHT_TARGET_LUMINANCE = 0.34
    +const ANSI_LIGHT_MIN_SATURATION = 0.22
    +const ANSI_MUTED_BUCKET = 245
    +
    +const ANSI_NORMALIZED_FOREGROUNDS: readonly (keyof ThemeColors)[] = [
    +  'text',
    +  'label',
    +  'ok',
    +  'error',
    +  'warn',
    +  'prompt',
    +  'statusFg',
    +  'statusGood',
    +  'statusWarn',
    +  'statusBad',
    +  'statusCritical',
    +  'shellDollar'
    +]
    +
    +const ANSI_MUTED_FOREGROUNDS: readonly (keyof ThemeColors)[] = ['muted', 'sessionLabel', 'sessionBorder']
    +
    +function xtermEightBitRgb(colorNumber: number): [number, number, number] {
    +  if (colorNumber >= 232) {
    +    const value = 8 + (colorNumber - 232) * 10
    +
    +    return [value, value, value]
    +  }
    +
    +  if (colorNumber >= 16) {
    +    const offset = colorNumber - 16
    +
    +    return [
    +      XTERM_6_LEVELS[Math.floor(offset / 36) % 6]!,
    +      XTERM_6_LEVELS[Math.floor(offset / 6) % 6]!,
    +      XTERM_6_LEVELS[offset % 6]!
    +    ]
    +  }
    +
    +  return [0, 0, 0]
    +}
    +
    +function channelLuminance(value: number): number {
    +  const normalized = value / 255
    +
    +  return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4
    +}
    +
    +function relativeLuminance(red: number, green: number, blue: number): number {
    +  return 0.2126 * channelLuminance(red) + 0.7152 * channelLuminance(green) + 0.0722 * channelLuminance(blue)
    +}
    +
    +function rgbToHsl(red: number, green: number, blue: number): [number, number, number] {
    +  const rn = red / 255
    +  const gn = green / 255
    +  const bn = blue / 255
    +  const max = Math.max(rn, gn, bn)
    +  const min = Math.min(rn, gn, bn)
    +  const lightness = (max + min) / 2
    +
    +  if (max === min) {
    +    return [0, 0, lightness]
    +  }
    +
    +  const delta = max - min
    +  const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min)
    +
    +  const hue =
    +    max === rn
    +      ? (gn - bn) / delta + (gn < bn ? 6 : 0)
    +      : max === gn
    +        ? (bn - rn) / delta + 2
    +        : (rn - gn) / delta + 4
    +
    +  return [hue / 6, saturation, lightness]
    +}
    +
    +function circularDistance(a: number, b: number): number {
    +  const distance = Math.abs(a - b)
    +
    +  return Math.min(distance, 1 - distance)
    +}
    +
    +// Mirrors @hermes/ink's colorize.ts. Keep local: app code compiles from
    +// ui-tui/src, while @hermes/ink is bundled separately from packages/.
    +function richEightBitColorNumber(red: number, green: number, blue: number): number {
    +  const [, saturation, lightness] = rgbToHsl(red, green, blue)
    +
    +  if (saturation < 0.15) {
    +    const gray = Math.round(lightness * 25)
    +
    +    return gray === 0 ? 16 : gray === 25 ? 231 : 231 + gray
    +  }
    +
    +  const sixRed = red < 95 ? red / 95 : 1 + (red - 95) / 40
    +  const sixGreen = green < 95 ? green / 95 : 1 + (green - 95) / 40
    +  const sixBlue = blue < 95 ? blue / 95 : 1 + (blue - 95) / 40
    +
    +  return 16 + 36 * Math.round(sixRed) + 6 * Math.round(sixGreen) + Math.round(sixBlue)
    +}
    +
    +function bestReadableAnsiColor(red: number, green: number, blue: number): number {
    +  const [hue, saturation, lightness] = rgbToHsl(red, green, blue)
    +  let bestColor = richEightBitColorNumber(red, green, blue)
    +  let bestScore = Number.POSITIVE_INFINITY
    +
    +  for (let colorNumber = 16; colorNumber <= 255; colorNumber += 1) {
    +    const [candidateRed, candidateGreen, candidateBlue] = xtermEightBitRgb(colorNumber)
    +    const candidateLuminance = relativeLuminance(candidateRed, candidateGreen, candidateBlue)
    +
    +    if (candidateLuminance > ANSI_LIGHT_MAX_LUMINANCE) {
    +      continue
    +    }
    +
    +    const [candidateHue, candidateSaturation, candidateLightness] = rgbToHsl(
    +      candidateRed,
    +      candidateGreen,
    +      candidateBlue
    +    )
    +
    +    const saturationFloorPenalty =
    +      candidateSaturation < ANSI_LIGHT_MIN_SATURATION ? (ANSI_LIGHT_MIN_SATURATION - candidateSaturation) * 3 : 0
    +
    +    const score =
    +      circularDistance(candidateHue, hue) * 4 +
    +      Math.abs(candidateSaturation - Math.max(ANSI_LIGHT_MIN_SATURATION, saturation)) * 0.8 +
    +      Math.abs(candidateLightness - Math.min(lightness, ANSI_LIGHT_TARGET_LUMINANCE)) * 2 +
    +      saturationFloorPenalty
    +
    +    if (score < bestScore) {
    +      bestColor = colorNumber
    +      bestScore = score
    +    }
    +  }
    +
    +  return bestColor
    +}
    +
    +function normalizeAnsiForeground(color: string): string {
    +  const rgb = parseHex(color)
    +
    +  if (!rgb) {
    +    return color
    +  }
    +
    +  const richAnsi = richEightBitColorNumber(rgb[0], rgb[1], rgb[2])
    +  const richRgb = xtermEightBitRgb(richAnsi)
    +
    +  const ansi = relativeLuminance(richRgb[0], richRgb[1], richRgb[2]) > ANSI_LIGHT_MAX_LUMINANCE
    +    ? bestReadableAnsiColor(rgb[0], rgb[1], rgb[2])
    +    : richAnsi
    +
    +  return `ansi256(${ansi})`
    +}
    +
     // ── Defaults ─────────────────────────────────────────────────────────
     
     const BRAND: ThemeBrand = {
    ```

    ```diff
    @@ -190,12 +346,11 @@ export const LIGHT_THEME: Theme = {
     const TRUE_RE = /^(?:1|true|yes|on)$/
     const FALSE_RE = /^(?:0|false|no|off)$/
     
    -// Reserved for future TERM_PROGRAM-based heuristics.  Empty by default:
    -// most modern terminals (Ghostty, Warp, iTerm2, Apple_Terminal) ship a
    -// dark profile out of the box, so guessing wrong here is more annoying
    -// than missing a light user — light users can always set
    -// `HERMES_TUI_LIGHT=1` or `HERMES_TUI_THEME=light`.
    -const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>()
    +// TERM_PROGRAM fallback allow-list for terminals whose default profile is
    +// light and which may not expose COLORFGBG. This currently includes Apple
    +// Terminal. Explicit HERMES_TUI_THEME / COLORFGBG signals above still win,
    +// so dark Apple Terminal profiles that advertise a dark background stay dark.
    +const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>(['Apple_Terminal'])
     
     // Best-effort RGB → luminance check.  Currently only accepts a 3- or
     // 6-digit hex value (with or without a leading `#`); the env var name
    ```

    ```diff
    @@ -247,7 +402,7 @@ function backgroundLuminance(raw: string): null | number {
     //      slot 7 or 15 on light profiles; 0–15 ranges are otherwise
     //      treated as authoritatively dark so the TERM_PROGRAM
     //      allow-list below cannot override an explicit dark profile.
    -//   5. `TERM_PROGRAM` light-default allow-list (currently empty).
    +//   5. `TERM_PROGRAM` light-default allow-list.
     //
     // Anything we can't decide stays dark — the default Hermes palette
     // is the dark one.
    ```

    ```diff
    @@ -313,7 +468,42 @@ export function detectLightMode(
       return lightDefaultTermPrograms.has(termProgram)
     }
     
    -export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME
    +function shouldNormalizeAnsiLightTheme(env: NodeJS.ProcessEnv = process.env, isLight = detectLightMode(env)): boolean {
    +  const colorTerm = (env.COLORTERM ?? '').trim().toLowerCase()
    +  const termProgram = (env.TERM_PROGRAM ?? '').trim()
    +
    +  return termProgram === 'Apple_Terminal' && colorTerm !== 'truecolor' && colorTerm !== '24bit' && isLight
    +}
    +
    +export function normalizeThemeForAnsiLightTerminal(
    +  theme: Theme,
    +  env: NodeJS.ProcessEnv = process.env,
    +  isLight = detectLightMode(env)
    +): Theme {
    +  if (!shouldNormalizeAnsiLightTheme(env, isLight)) {
    +    return theme
    +  }
    +
    +  const color = { ...theme.color }
    +
    +  for (const key of ANSI_NORMALIZED_FOREGROUNDS) {
    +    color[key] = normalizeAnsiForeground(color[key])
    +  }
    +
    +  for (const key of ANSI_MUTED_FOREGROUNDS) {
    +    color[key] = `ansi256(${ANSI_MUTED_BUCKET})`
    +  }
    +
    +  return { ...theme, color }
    +}
    +
    +const DEFAULT_LIGHT_MODE = detectLightMode()
    +
    +export const DEFAULT_THEME: Theme = normalizeThemeForAnsiLightTerminal(
    +  DEFAULT_LIGHT_MODE ? LIGHT_THEME : DARK_THEME,
    +  process.env,
    +  DEFAULT_LIGHT_MODE
    +)
     
     // ── Skin → Theme ─────────────────────────────────────────────────────
     
    ```

    ```diff
    @@ -333,7 +523,7 @@ export function fromSkin(
       const muted = c('banner_dim') ?? d.color.muted
       const completionBg = c('completion_menu_bg') ?? d.color.completionBg
     
    -  return {
    +  return normalizeThemeForAnsiLightTerminal({
         color: {
           primary: c('ui_primary') ?? c('banner_title') ?? d.color.primary,
           accent,
    ```

    ```diff
    @@ -379,5 +569,5 @@ export function fromSkin(
     
         bannerLogo,
         bannerHero
    -  }
    +  }, process.env, DEFAULT_LIGHT_MODE)
     }
    ```
- **#26137: fix(cli): kill resize scrollback duplication + light-mode visibility**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `hermes_cli/skin_engine.py`, `tests/cli/test_cli_light_mode.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -1242,7 +1242,13 @@ def _prune_orphaned_branches(repo_root: str) -> None:
     
     
     def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:
    -    """Convert a hex color like '#268bd2' to a true-color ANSI escape."""
    +    """Convert a hex color like '#268bd2' to a true-color ANSI escape.
    +
    +    Auto-remaps known dark-mode-tuned colors to readable light-mode
    +    equivalents when running on a light terminal (see
    +    _maybe_remap_for_light_mode + _LIGHT_MODE_REMAP).
    +    """
    +    hex_color = _maybe_remap_for_light_mode(hex_color)
         try:
             r = int(hex_color[1:3], 16)
             g = int(hex_color[3:5], 16)
    ```

    ```diff
    @@ -1253,6 +1259,250 @@ def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:
             return _ACCENT_ANSI_DEFAULT if bold else "\033[38;2;184;134;11m"
     
     
    +# ────────────────────────────────────────────────────────────────────────
    +# Light/dark terminal mode detection.
    +#
    +# Mirrors ui-tui/src/theme.ts detectLightMode().  Used to decide whether
    +# to remap "near-white" skin colors (e.g. #FFF8DC banner_text, #B8860B
    +# banner_dim) to darker equivalents that are readable on a light
    +# Terminal.app / iTerm2 background.
    +#
    +# Detection priority:
    +#   1. HERMES_LIGHT / HERMES_TUI_LIGHT env (true/false) — explicit override
    +#   2. HERMES_TUI_THEME=light|dark — explicit theme
    +#   3. HERMES_TUI_BACKGROUND=#RRGGBB — explicit bg hint
    +#   4. COLORFGBG env (set by xterm/Konsole/urxvt) — bg slot 7/15 = light
    +#   5. OSC 11 query (\x1b]11;?\x1b\\) — ask the terminal directly
    +#   6. Default: assume dark (matches the legacy Hermes assumption)
    +#
    +# Cached after first call so we don't query the terminal repeatedly.
    +_LIGHT_MODE_CACHE: bool | None = None
    +_TRUE_RE = re.compile(r"^(1|true|on|yes|y)$")
    +_FALSE_RE = re.compile(r"^(0|false|off|no|n)$")
    +_LIGHT_DEFAULT_TERM_PROGRAMS = frozenset()  # Apple_Terminal doesn't reliably indicate; require explicit
    +
    +
    +def _luminance_from_hex(hex_str: str) -> float | None:
    +    s = (hex_str or "").strip().lstrip("#")
    +    if len(s) == 3:
    +        s = "".join(c * 2 for c in s)
    +    if len(s) != 6 or not all(c in "0123456789abcdefABCDEF" for c in s):
    +        return None
    +    try:
    +        r, g, b = int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
    +    except ValueError:
    +        return None
    +    # Rec.709 luma
    +    return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0
    +
    +
    +def _query_osc11_background() -> str | None:
    +    """Ask the terminal for its background color via OSC 11.
    +
    +    Most modern terminals reply with \x1b]11;rgb:RRRR/GGGG/BBBB\x1b\\
    +    within a few ms.  We wait up to 100ms total before giving up.
    +    Returns "#RRGGBB" or None on timeout / non-tty.
    +    """
    +    if not sys.stdin.isatty() or not sys.stdout.isatty():
    +        return None
    +    try:
    +        import termios
    +        import tty
    +        fd = sys.stdin.fileno()
    +        old = termios.tcgetattr(fd)
    +    except Exception:
    +        return None
    +    try:
    +        try:
    +            tty.setcbreak(fd)
    +        except Exception:
    +            return None
    +        try:
    +            sys.stdout.write("\x1b]11;?\x1b\\")
    +            sys.stdout.flush()
    +        except Exception:
    +            return None
    +        # Read up to ~50ms for the response
    +        import select
    +        deadline = time.monotonic() + 0.1
    +        buf = b""
    +        while time.monotonic() < deadline:
    +            r, _, _ = select.select([fd], [], [], deadline - time.monotonic())
    +            if not r:
    +                continue
    +            try:
    +                chunk = os.read(fd, 64)
    +            except OSError:
    +                break
    +            if not chunk:
    +                break
    +            buf += chunk
    +            if b"\x1b\\" in buf or b"\x07" in buf:
    +                break
    +        # Parse: \x1b]11;rgb:RRRR/GGGG/BBBB\x1b\\
    +        m = re.search(rb"rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)", buf)
    +        if not m:
    +            return None
    +        # Each component is 1-4 hex digits — normalize to 8-bit
    +        def norm(h: bytes) -> int:
    +            v = int(h, 16)
    +            # Scale to 0-255 based on hex length
    +            bits = len(h) * 4
    +            return (v * 255) // ((1 << bits) - 1) if bits else 0
    +        r, g, b = norm(m.group(1)), norm(m.group(2)), norm(m.group(3))
    +        return f"#{r:02X}{g:02X}{b:02X}"
    +    finally:
    +        try:
    +            termios.tcsetattr(fd, termios.TCSANOW, old)
    +        except Exception:
    +            pass
    +
    +
    +def _detect_light_mode() -> bool:
    +    global _LIGHT_MODE_CACHE
    +    if _LIGHT_MODE_CACHE is not None:
    +        return _LIGHT_MODE_CACHE
    +    result = False
    +    try:
    +        # 1. Explicit env override
    +        for var in ("HERMES_LIGHT", "HERMES_TUI_LIGHT"):
    +            v = (os.environ.get(var) or "").strip().lower()
    +            if _TRUE_RE.match(v):
    +                result = True
    +                _LIGHT_MODE_CACHE = result
    +                return result
    +            if _FALSE_RE.match(v):
    +                _LIGHT_MODE_CACHE = result
    +                return result
    +        # 2. Theme hint
    +        theme = (os.environ.get("HERMES_TUI_THEME") or "").strip().lower()
    +        if theme == "light":
    +            result = True
    +            _LIGHT_MODE_CACHE = result
    +            return result
    +        if theme == "dark":
    +            _LIGHT_MODE_CACHE = result
    +            return result
    +        # 3. Explicit bg hex
    +        bg_hint = os.environ.get("HERMES_TUI_BACKGROUND") or ""
    +        bg_lum = _luminance_from_hex(bg_hint)
    +        if bg_lum is not None:
    +            result = bg_lum >= 0.5
    +            _LIGHT_MODE_CACHE = result
    +            return result
    +        # 4. COLORFGBG (xterm/Konsole/urxvt)
    +        cfgbg = (os.environ.get("COLORFGBG") or "").strip()
    +        if cfgbg:
    +            last = cfgbg.split(";")[-1] if ";" in cfgbg else cfgbg
    +            if last.isdigit():
    +                bg = int(last)
    +                if bg in (7, 15):
    +                    result = True
    +                    _LIGHT_MODE_CACHE = result
    +                    return result
    +                if 0 <= bg < 16:
    +                    _LIGHT_MODE_CACHE = result
    +                    return result
    +        # 5. OSC 11 query (best-effort, only when stdin/stdout are TTY)
    +        bg_color = _query_osc11_background()
    +        if bg_color:
    +            lum = _luminance_from_hex(bg_color)
    +            if lum is not None:
    +                result = lum >= 0.5
    +                _LIGHT_MODE_CACHE = result
    +                return result
    +        # 6. TERM_PROGRAM allow-list (currently empty)
    +        tp = (os.environ.get("TERM_PROGRAM") or "").strip()
    +        if tp in _LIGHT_DEFAULT_TERM_PROGRAMS:
    +            result = True
    +    except Exception:
    +        result = False
    +    _LIGHT_MODE_CACHE = result
    +    return result
    +
    +
    +# Light-mode equivalents of skin colors that are unreadable on cream
    +# Terminal.app backgrounds.  Used by _SkinAwareAnsi to remap colors
    +# at resolution time when light mode is detected.
    +#
    +# IMPORTANT: only remap colors that are used as STANDALONE foregrounds
    +# on the terminal's background.  Don't remap colors that are paired
    +# with a dark bg (e.g. status bar text on bg:#1a1a2e) — those would
    +# become invisible the OTHER direction (dark gray on dark navy).
    +_LIGHT_MODE_REMAP: dict[str, str] = {
    +    # Original (dark-mode) -> Light-mode replacement (darker, readable)
    +    "#FFF8DC": "#1A1A1A",   # cornsilk -> near-black
    +    "#FFD700": "#9A6B00",   # gold -> dark goldenrod (readable on cream)
    +    "#FFBF00": "#8A5A00",   # amber -> dark amber
    +    "#B8860B": "#5C4500",   # dark goldenrod -> deeper brown (more contrast)
    +    "#DAA520": "#6B4F00",   # goldenrod -> dark olive
    +    "#F1E6CF": "#1A1A1A",   # cream -> near-black
    +    "#c9d1d9": "#24292F",   # github-light fg
    +    "#EAF7FF": "#0F1B26",   # ice
    +    "#F5F5F5": "#1A1A1A",
    +    "#FFF0D4": "#1A1A1A",
    +    "#CD7F32": "#8A4F1A",   # bronze -> darker bronze
    +    "#FFEFB5": "#3A2A00",
    +    # NOTE: skipping #C0C0C0/#888888/#555555/#8B8682 — those are
    +    # status-bar foregrounds paired with dark navy bg, where dark
    +    # remap values would become invisible.
    +}
    +
    +
    +def _maybe_remap_for_light_mode(hex_color: str) -> str:
    +    """If we're in light mode, remap a dark-mode-tuned color to a
    +    higher-contrast equivalent.  No-op in dark mode."""
    +    if not _detect_light_mode():
    +        return hex_color
    +    if not hex_color or not hex_color.startswith("#"):
    +        return hex_color
    +    # Case-insensitive lookup
    +    upper = hex_color.upper()
    +    if upper in _LIGHT_MODE_REMAP_UPPER:
    +        return _LIGHT_MODE_REMAP_UPPER[upper]
    +    return hex_color
    +
    +
    +# Pre-uppercased lookup table for case-insensitive remapping
    +_LIGHT_MODE_REMAP_UPPER = {k.upper(): v for k, v in _LIGHT_MODE_REMAP.items()}
    +
    +
    +def _install_skin_light_mode_hook() -> None:
    +    """Wrap SkinConfig.get_color at import time so EVERY skin color read goes
    +    through the light-mode remap.  Idempotent."""
    +    try:
    +        from hermes_cli.skin_engine import SkinConfig  # type: ignore[import]
    +    except Exception:
    +        return
    +    if getattr(SkinConfig, "_hermes_light_mode_hook_installed", False):
    +        return
    +    _orig_get_color = SkinConfig.get_color
    +
    +    def _wrapped_get_color(self, key, fallback=""):
    +        value = _orig_get_color(self, key, fallback)
    +        try:
    +            return _maybe_remap_for_light_mode(value)
    +        except Exception:
    +            return value
    +
    +    SkinConfig.get_color = _wrapped_get_color  # type: ignore[method-assign]
    +    SkinConfig._hermes_light_mode_hook_installed = True  # type: ignore[attr-defined]
    +
    +
    +_install_skin_light_mode_hook()
    +
    +
    +# Prime the light-mode detection cache early (at module load) when
    +# we're running interactively so OSC 11 happens before pt grabs the
    +# tty.  Skip for non-tty contexts (subagents, gateway, tests).
    +try:
    +    if sys.stdin.isatty() and sys.stdout.isatty():
    +        _detect_light_mode()
    +except Exception:
    +    pass
    +
    +
    +
     class _SkinAwareAnsi:
         """Lazy ANSI escape that resolves from the skin engine on first use.
     
    ```

    ```diff
    @@ -1290,7 +1540,12 @@ def reset(self) -> None:
     
     
     _ACCENT = _SkinAwareAnsi("response_border", "#FFD700", bold=True)
    -_DIM = _SkinAwareAnsi("banner_dim", "#B8860B")
    +# Use ANSI dim+italic attributes (\x1b[2;3m) instead of a hardcoded
    +# hex color so dim/thinking text inherits the terminal's default
    +# foreground color and stays readable in both light and dark
    +# Terminal.app modes.  Hardcoded skin colors like #B8860B
    +# (dark goldenrod) become invisible against light cream backgrounds.
    +_DIM = "\x1b[2;3m"
     
     
     def _accent_hex() -> str:
    ```

    ```diff
    @@ -7947,8 +8202,8 @@ def _bg_thinking(text: str) -> None:
                             from hermes_cli.skin_engine import get_active_skin
                             _skin = get_active_skin()
                             label = _skin.get_branding("response_label", "⚕ Hermes")
    -                        _resp_color = _skin.get_color("response_border", "#CD7F32")
    -                        _resp_text = _skin.get_color("banner_text", "#FFF8DC")
    +                        _resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32"))
    +                        _resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC"))
                         except Exception:
                             label = "⚕ Hermes"
                             _resp_color = "#CD7F32"
    ```

    ```diff
    @@ -8549,7 +8804,8 @@ def _handle_skin_command(self, cmd: str):
     
             set_active_skin(new_skin)
             _ACCENT.reset()  # Re-resolve ANSI color for the new skin
    -        _DIM.reset()     # Re-resolve dim/secondary ANSI color for the new skin
    +        # _DIM is now a fixed dim+italic ANSI escape (terminal-default fg)
    +        # so it doesn't need re-resolving on skin switch.
             if save_config_value("display.skin", new_skin):
                 print(f"  Skin set to: {new_skin} (saved)")
             else:
    ```

    ```diff
    @@ -10928,12 +11184,12 @@ def run_agent():
                         from hermes_cli.skin_engine import get_active_skin
                         _skin = get_active_skin()
                         label = _skin.get_branding("response_label", "⚕ Hermes")
    -                    _resp_color = _skin.get_color("response_border", "#CD7F32")
    -                    _resp_text = _skin.get_color("banner_text", "#FFF8DC")
    +                    _resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32"))
    +                    _resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC"))
                     except Exception:
                         label = "⚕ Hermes"
    -                    _resp_color = "#CD7F32"
    -                    _resp_text = "#FFF8DC"
    +                    _resp_color = _maybe_remap_for_light_mode("#CD7F32")
    +                    _resp_text = _maybe_remap_for_light_mode("#FFF8DC")
     
                     is_error_response = result and (result.get("failed") or result.get("partial"))
                     already_streamed = self._stream_started and self._stream_box_opened and not is_error_response
    ```

    ```diff
    @@ -11172,13 +11428,48 @@ def _get_tui_prompt_text(self) -> str:
             return "".join(text for _, text in self._get_tui_prompt_fragments())
     
         def _build_tui_style_dict(self) -> dict[str, str]:
    -        """Layer the active skin's prompt_toolkit colors over the base TUI style."""
    +        """Layer the active skin's prompt_toolkit colors over the base TUI style.
    +
    +        Also rewrites any hex-color tokens in the resulting style strings
    +        to their light-mode equivalents (via _LIGHT_MODE_REMAP) when the
    +        terminal is detected as light.  This makes the chrome readable
    +        on cream Terminal.app backgrounds without per-skin overrides.
    +        """
             style_dict = dict(getattr(self, "_tui_style_base", {}) or {})
             try:
                 from hermes_cli.skin_engine import get_prompt_toolkit_style_overrides
                 style_dict.update(get_prompt_toolkit_style_overrides())
             except Exception:
                 pass
    +        # Light-mode remap on the style strings.  Each value is a pt
    +        # style string like "bg:#1a1a2e #C0C0C0 bold" — split on space,
    +        # rewrite any "#XXX" tokens (including "bg:#XXX") through the
    +        # light-mode remap, rejoin.
    +        #
    +        # CRITICAL: skip the remap entirely when a style string already
    +        # specifies its own bg (e.g. status-bar / completion-menu styles
    +        # with `bg:#1a1a2e ...`).  Those colors were tuned for that
    +        # specific dark bg and remapping the FG to a dark equivalent
    +        # would produce dark-on-dark (invisible).  The terminal's BG
    +        # mode is irrelevant — what matters is the bg the style itself
    +        # paints.
    +        try:
    +            if _detect_light_mode():
    +                def _remap_value(v: str) -> str:
    +                    if not v:
    +                        return v
    +                    tokens = [redacted:secret-assignment]
    +                    has_explicit_bg = any(t.startswith("bg:") for t in tokens)
    +                    if has_explicit_bg:
    +                        # The style paints its own bg — leave its fg alone.
    +                        return v
    +                    return " ".join(
    +                        _maybe_remap_for_light_mode(t) if t.startswith("#") else t
    +                        for t in tokens
    +                    )
    +                style_dict = {k: _remap_value(v or "") for k, v in style_dict.items()}
    +        except Exception:
    +            pass
             return style_dict
     
         def _apply_tui_skin_style(self) -> bool:
    ```

    ```diff
    @@ -11264,6 +11555,13 @@ def _build_tui_layout_children(
     
         def run(self):
             """Run the interactive CLI loop with persistent input at bottom."""
    +        # Detect light/dark terminal mode now (before pt grabs the tty).
    +        # Caches the result so subsequent _hex_to_ansi / style calls
    +        # don't risk re-querying mid-render.
    +        try:
    +            _detect_light_mode()
    +        except Exception:
    +            pass
             # Push the entire TUI to the bottom of the terminal so the banner,
             # responses, and prompt all appear pinned to the bottom — empty
             # space stays above, not below.  This prints enough blank lines to
    ```

    ```diff
    @@ -13027,11 +13325,16 @@ def _get_voice_status():
             
             # Style for the application
             self._tui_style_base = {
    -            'input-area': '#FFF8DC',
    -            'placeholder': '#555555 italic',
    -            'prompt': '#FFF8DC',
    +            # Input area / prompt: empty style strings inherit the
    +            # terminal's default foreground/background, so the typed
    +            # text is readable in both light and dark Terminal.app
    +            # color schemes.  (Hardcoding a near-white #FFF8DC made
    +            # input invisible on light backgrounds.)
    +            'input-area': '',
    +            'placeholder': '#888888 italic',
    +            'prompt': '',
                 'prompt-working': '#888888 italic',
    -            'hint': '#555555 italic',
    +            'hint': '#888888 italic',
                 'status-bar': 'bg:#1a1a2e #C0C0C0',
                 'status-bar-strong': 'bg:#1a1a2e #FFD700 bold',
                 'status-bar-dim': 'bg:#1a1a2e #8B8682',
    ```

    ```diff
    @@ -13090,19 +13393,70 @@ def _get_voice_status():
             self._app = app  # Store reference for clarify_callback
     
             # ── Fix ghost status-bar lines on terminal resize ──────────────
    -        # When the terminal shrinks (e.g. un-maximize), the emulator reflows
    -        # the previously-rendered full-width rows (status bar, input rules)
    -        # into multiple narrower rows.  prompt_toolkit's _on_resize handler
    -        # only cursor_up()s by the stored layout height, missing the extra
    -        # rows created by reflow — leaving ghost duplicates visible.
    +        # Resize handling: monkey-patch prompt_toolkit's _output_screen_diff
    +        # to suppress the deliberate "reserve vertical space" scroll-up.
             #
    -        # It's not just column-shrink: widening, row-shrinking, and
    -        # multiplexer-driven SIGWINCH-less redraws (cmux / tmux tab switch)
    -        # all produce the same class of drift, where the renderer's tracked
    -        # _cursor_pos.y no longer matches terminal reality. The only reliable
    -        # recovery is a full screen-clear (\x1b[2J\x1b[H) before the next
    -        # redraw, so we force one on every resize rather than trying to
    -        # compute the exact drift.
    +        # Background: prompt_toolkit's renderer (renderer.py L232-242)
    +        # explicitly moves the cursor to the bottom of the canvas after
    +        # painting "to make sure the terminal scrolls up, even when the
    +        # lower lines of the canvas just contain whitespace".  In
    +        # non-fullscreen mode this scrolls chrome content (status bar,
    +        # input rules) into terminal scrollback on every render.  When
    +        # the terminal column-shrinks, the emulator reflows the previously
    +        # rendered full-width rows into multiple narrower rows that get
    +        # pushed up — leaving ghost duplicates AND polluting scrollback.
    +        # Same issue as pt #29 (open since 2014), #1675, #1933.
    +        #
    +        # Surgical fix: wrap _output_screen_diff so that when its internal
    +        # `if current_height > previous_screen.height` branch fires (the
    +        # one that does the bottom-cursor-move), we make it fall through
    +        # by inflating previous_screen.height first.
    +        try:
    +            import prompt_toolkit.renderer as _pt_renderer
    +            from prompt_toolkit.renderer import _output_screen_diff as _orig_osd
    +
    +            if not getattr(_pt_renderer, "_hermes_osd_patched", False):
    +                def _patched_output_screen_diff(
    +                    app, output, screen, current_pos, color_depth,
    +                    previous_screen, last_style, is_done, full_screen,
    +                    attrs_for_style_string, style_string_has_style,
    +                    size, previous_width,
    +                ):
    +                    """Wraps pt's _output_screen_diff to suppress the
    +                    reserve-vertical-space scroll (renderer.py L232-242).
    +
    +                    Strategy: ONLY when previous_screen is non-None and
    +                    its current height is genuinely smaller than the new
    +                    screen's height, inflate it to match.  This prevents
    +                    the bottom-cursor-move at L242 without changing any
    +                    other code path's behavior.
    +
    +                    Critical: do NOT replace a None previous_screen with
    +                    a fresh Screen() — that would skip the proper
    +                    reset_attributes()+erase_down() at L178-185 which
    +                    fires when previous_screen is None (first-paint /
    +                    width-change).  Without that reset, ANSI styles
    +                    leak between renders.
    +                    """
    +                    try:
    +                        if previous_screen is not None and hasattr(previous_screen, "height"):
    +                            if previous_screen.height < screen.height:
    +                                previous_screen.height = screen.height
    +                    except Exception:
    +                        pass
    +
    +                    return _orig_osd(
    +                        app, output, screen, current_pos, color_depth,
    +                        previous_screen, last_style, is_done, full_screen,
    +                        attrs_for_style_string, style_string_has_style,
    +                        size, previous_width,
    +                    )
    +
    +                _pt_renderer._output_screen_diff = _patched_output_screen_diff
    +                _pt_renderer._hermes_osd_patched = True
    +        except Exception:
    +            pass
    +
             _original_on_resize = app._on_resize
     
             def _resize_clear_ghosts():
    ```
  - `hermes_cli/skin_engine.py`

    ```diff
    @@ -849,10 +849,14 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
         except Exception:
             return {}
     
    -    prompt = skin.get_color("prompt", "#FFF8DC")
    +    # Input/prompt: leave unset by default so the typed text inherits
    +    # the terminal's foreground color (readable in both light and dark
    +    # color schemes).  Skins can opt into a colored prompt by setting
    +    # `prompt` explicitly in their YAML.
    +    prompt = skin.get_color("prompt", "")
         input_rule = skin.get_color("input_rule", "#CD7F32")
         title = skin.get_color("banner_title", "#FFD700")
    -    text = skin.get_color("banner_text", prompt)
    +    text = skin.get_color("banner_text", "#FFF8DC")
         dim = skin.get_color("banner_dim", "#555555")
         label = skin.get_color("ui_label", title)
         warn = skin.get_color("ui_warn", "#FF8C00")
    ```

    ```diff
    @@ -872,7 +876,11 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
         menu_meta_current_bg = skin.get_color("completion_menu_meta_current_bg", menu_current_bg)
     
         return {
    -        "input-area": prompt,
    +        # Typed input always uses terminal default fg/bg so it's
    +        # readable in both light and dark Terminal.app modes.  The
    +        # skin's `prompt` color (if any) only styles the prompt symbol,
    +        # NOT the user's typed text.
    +        "input-area": "",
             "placeholder": f"{dim} italic",
             "prompt": prompt,
             "prompt-working": f"{dim} italic",
    ```
  - `tests/cli/test_cli_light_mode.py`

    ```diff
    @@ -0,0 +1,154 @@
    +"""Tests for the light-mode terminal detection + color remap in cli.py.
    +
    +Covers the env-override path and the SkinConfig.get_color() wrapper that
    +the resize / light-mode salvage installs at module import time.  We don't
    +try to fake an OSC 11 reply — the env-override branch short-circuits
    +before the terminal query, which is the path most users hit.
    +"""
    +
    +from __future__ import annotations
    +
    +import importlib
    +
    +import pytest
    +
    +
    [redacted:email]
    +def cli_mod(monkeypatch):
    +    """Import cli with the light-mode cache cleared each test."""
    +    import cli as _cli
    +
    +    # The module-level _install_skin_light_mode_hook() and import-time
    +    # _detect_light_mode() prime ran once at first import.  We just reset
    +    # the detection cache so the per-test env override takes effect.
    +    monkeypatch.setattr(_cli, "_LIGHT_MODE_CACHE", None)
    +    return _cli
    +
    +
    +class TestLightModeDetection:
    +    def test_hermes_light_env_true_forces_light(self, cli_mod, monkeypatch):
    +        monkeypatch.setenv("HERMES_LIGHT", "1")
    +        assert cli_mod._detect_light_mode() is True
    +
    +    def test_hermes_light_env_false_forces_dark(self, cli_mod, monkeypatch):
    +        monkeypatch.setenv("HERMES_LIGHT", "0")
    +        # Also blank out other signals so nothing else flips it light.
    +        monkeypatch.delenv("HERMES_TUI_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_THEME", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_BACKGROUND", raising=False)
    +        monkeypatch.delenv("COLORFGBG", raising=False)
    +        assert cli_mod._detect_light_mode() is False
    +
    +    def test_theme_hint_light(self, cli_mod, monkeypatch):
    +        monkeypatch.delenv("HERMES_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_LIGHT", raising=False)
    +        monkeypatch.setenv("HERMES_TUI_THEME", "light")
    +        assert cli_mod._detect_light_mode() is True
    +
    +    def test_background_hex_hint_light(self, cli_mod, monkeypatch):
    +        monkeypatch.delenv("HERMES_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_THEME", raising=False)
    +        monkeypatch.setenv("HERMES_TUI_BACKGROUND", "#FFFFFF")
    +        assert cli_mod._detect_light_mode() is True
    +
    +    def test_background_hex_hint_dark(self, cli_mod, monkeypatch):
    +        monkeypatch.delenv("HERMES_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_THEME", raising=False)
    +        monkeypatch.setenv("HERMES_TUI_BACKGROUND", "#1a1a2e")
    +        monkeypatch.delenv("COLORFGBG", raising=False)
    +        assert cli_mod._detect_light_mode() is False
    +
    +    def test_colorfgbg_light_bg_slot(self, cli_mod, monkeypatch):
    +        monkeypatch.delenv("HERMES_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_LIGHT", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_THEME", raising=False)
    +        monkeypatch.delenv("HERMES_TUI_BACKGROUND", raising=False)
    +        monkeypatch.setenv("COLORFGBG", "0;15")  # bg slot 15 = light
    +        assert cli_mod._detect_light_mode() is True
    +
    +    def test_cache_is_sticky(self, cli_mod, monkeypatch):
    +        monkeypatch.setenv("HERMES_LIGHT", "1")
    +        assert cli_mod._detect_light_mode() is True
    +        # Even if the env flips, the cached result wins until reset.
    +        monkeypatch.setenv("HERMES_LIGHT", "0")
    +        assert cli_mod._detect_light_mode() is True
    +
    +
    +class TestLightModeRemap:
    +    def test_remap_no_op_in_dark_mode(self, cli_mod, monkeypatch):
    +        monkeypatch.setenv("HERMES_LIGHT", "0")
    +        # Cache is None from the fixture; first call sticks at False.
    +        assert cli_mod._maybe_remap_for_light_mode("#FFF8DC") == "#FFF8DC"
    +
    +    def test_remap_known_dark_color(self, cli_mod, monkeypatch):
    +        monkeypatch.setenv("HERMES_LIGHT", "1")
    +        # Force the detect cache to True for this test.
    +        cli_mod._LIGHT_MODE_CACHE = True
    +        assert cli_mod._maybe_remap_for_light_mode("#FFF8DC") == "#1A1A1A"
    +        assert cli_mod._maybe_remap_for_light_mode("#FFD700") == "#9A6B00"
    +
    +    def test_remap_case_insensitive(self, cli_mod, monkeypatch):
    +        cli_mod._LIGHT_MODE_CACHE = True
    +        # Lowercase input should still remap.
    +        assert cli_mod._maybe_remap_for_light_mode("#fff8dc") == "#1A1A1A"
    +
    +    def test_remap_unknown_color_passthrough(self, cli_mod, monkeypatch):
    +        cli_mod._LIGHT_MODE_CACHE = True
    +        # A color not in the remap table is returned unchanged.
    +        assert cli_mod._maybe_remap_for_light_mode("#ABCDEF") == "#ABCDEF"
    +
    +    def test_remap_skips_statusbar_paired_colors(self, cli_mod, monkeypatch):
    +        """Colors that live on a dark bg (status bar fg) MUST NOT be
    +        remapped — otherwise they go dark-on-dark and disappear.
    +
    +        Regression guard for the patch-11 fix (intentional table omission).
    +        """
    +        cli_mod._LIGHT_MODE_CACHE = True
    +        for fg in ("#C0C0C0", "#888888", "#555555", "#8B8682"):
    +            assert cli_mod._maybe_remap_for_light_mode(fg) == fg, (
    +                f"{fg} is a status-bar fg paired with dark bg; remapping it "
    +                "would produce dark-on-dark"
    +            )
    +
    +
    +class TestSkinConfigHook:
    +    """The salvage wraps SkinConfig.get_color at module import time so
    +    every skin color read goes through the light-mode remap.  Verify
    +    the hook installed and functions correctly.
    +    """
    +
    +    def test_hook_installed(self, cli_mod):
    +        from hermes_cli.skin_engine import SkinConfig
    +
    +        assert getattr(SkinConfig, "_hermes_light_mode_hook_installed", False) is True
    +
    +    def test_hook_is_idempotent(self, cli_mod):
    +        # Calling the installer twice must not double-wrap (the marker
    +        # attribute is the guard).
    +        from hermes_cli.skin_engine import SkinConfig
    +
    +        before = SkinConfig.get_color
    +        cli_mod._install_skin_light_mode_hook()
    +        after = SkinConfig.get_color
    +        assert before is after
    +
    +    def test_skin_color_remaps_through_wrapper_in_light_mode(self, cli_mod, monkeypatch):
    +        from hermes_cli.skin_engine import SkinConfig
    +
    +        cli_mod._LIGHT_MODE_CACHE = True
    +        skin = SkinConfig(
    +            name="test",
    +            colors={"banner_text": "#FFF8DC", "response_border": "#FFD700"},
    +        )
    +        # The wrapper kicks in at get_color, not at construction time.
    +        assert skin.get_color("banner_text") == "#1A1A1A"
    +        assert skin.get_color("response_border") == "#9A6B00"
    +
    +    def test_skin_color_passthrough_in_dark_mode(self, cli_mod, monkeypatch):
    +        from hermes_cli.skin_engine import SkinConfig
    +
    +        cli_mod._LIGHT_MODE_CACHE = False
    +        skin = SkinConfig(name="test", colors={"banner_text": "#FFF8DC"})
    +        assert skin.get_color("banner_text") == "#FFF8DC"
    ```
- **#4811: feat(skin): add built-in light skin for light/cream terminal backgrounds**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#9461`
- **#8567: fix(cli): improve default skin contrast on light terminals**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#9461`
- **#9808: fix(colors): readable defaults on light terminals via HERMES_THEME + COLORFGBG**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17113`, `#13103`, `#17113`, `#17695`, `#26137`, `#17113`
- **#11311: fix(tui): replace hardcoded yellow colors unreadable on light backgrounds**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#11335`
- **#11335: fix(tui): replace bright yellow with dark_goldenrod for light bg contrast**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#11536: fix(cli): auto-select daylight skin on light terminal backgrounds**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#11553`, `#12337`, `#11300`, `#4807`
- **#11553: fix: add light terminal background detection**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#11300`
- **#12337: fix: adaptive color scheme for light terminal backgrounds (#12330)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17113`, `#17113`, `#26137`
- **#12487: fix(tui): replace unreadable yellow colors with dark goldenrod (#11300)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#58545: fix(qr): use invert=False for QR code terminal rendering (preserve quiet zone)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#58038`, `#58038`
  - files: `hermes_cli/telegram_managed_bot.py`
- **#60171: fix(tui): improve light theme text contrast**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `4cc6da84a155b3c1d4dd81ec6d24285c8d5b7b5e`, `#17695`, `#60141`
- **#60274: fix(desktop): use default font-smoothing on Retina to fix faint text (#59751)**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#60452: fix(desktop): use default font-smoothing on Retina to fix faint text (#59751)**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#60972: fix(cli): improve light terminal body contrast**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#60141`
  - files: `cli.py`, `hermes_cli/banner.py`, `tests/cli/test_stream_delta_think_tag.py`, `tests/hermes_cli/test_banner.py`
- **#61553: fix(cli,tui): skip unreliable COLORFGBG in Zed terminal (#61536)**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#61600: fix(theme): ignore inherited COLORFGBG in Zed**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#61553`, `#61536`, `#61553`
  - files: `cli.py`, `tests/cli/test_cli_light_mode.py`, `ui-tui/src/__tests__/theme.test.ts`, `ui-tui/src/theme.ts`

## Fix status

- Merged to main in PR #9461 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-15`.
- Subsystem: `apps/desktop`
