# NousResearch/hermes-agent · CLI ANSI rendering fixes and Rich migration

## Open now

- 1 issue, 11 PRs open on GitHub.
- #59397: \[Bug\]: ANSI Code Broken Render in CLI
- PR #9205: fix(cli): make /tools slash commands ANSI-safe and idempotent
- PR #59648: fix(cli): fix broken ANSI code rendering (#59397)
- PR #60073: feat(cli): migrated cron cli to rich rendering
- PR #61322: feat(cli): Migrated Cron and Doctor CLI to Rich Rendering
- PR #61954: feat(cli): Migrated Status CLI to Rich Rendering
- PR #64142: feat(cli): Migrated GUI Uninstall CLI to Rich Rendering
- PR #64307: feat(cli): Migrated cli_output Tools to Rich Rendering
- PR #67993: feat(cli): Migrated Config CLI to Rich Rendering
- PR #68466: feat(cli): Migrated Curses UI to Rich Rendering
- PR #69158: feat(cli): Migrated Setup CLI to Rich Rendering
- PR #87613: fix(cli): stop garbled ANSI from welcome banner and deprecated-env warning (#2262)

These items address garbled or broken ANSI escape code output across CLI and TUI contexts, alongside a broad effort to migrate various CLI components (status, setup, config, uninstall, cron, curses UI) to Rich rendering.

## TL;DR

- 31 member(s): 1 open issue(s), 4 closed issue(s), 11 open PR(s), 3 merged PR(s), 12 PR(s) closed without merging.
- A fix was merged to main in PR #2448. Verify it addresses your case.
- **Recommended:** read PR #2448 (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-2448,pr-56641`. 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.09`: 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

- #59397: \[Bug\]: ANSI Code Broken Render in CLI

## Closed / duplicate issues

- #2262: \[Bug\]: Garbled ANSI codes from status printouts like \`\\verbose\` and \`approaching compaction\`
- #4128: \[Bug\]: /tools list shows raw terminal escape codes
- #9017: \[Feature\]: Improve the display format of the /tools list command
- #56533: /journey slash command leaks raw ANSI escape codes in TUI and Desktop chat views

## Prior & competing PRs

- **#2448: fix: resolve garbled ANSI escape codes in status printouts**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `run_agent.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -1915,6 +1915,9 @@ def _init_agent(self, *, model_override: str = None, runtime_override: dict = No
                     tool_progress_callback=self._on_tool_progress,
                     stream_delta_callback=self._stream_delta if self.streaming_enabled else None,
                 )
    +            # Route agent status output through prompt_toolkit so ANSI escape
    +            # sequences aren't garbled by patch_stdout's StdoutProxy (#2262).
    +            self.agent._print_fn = _cprint
                 self._active_agent_route_signature = (
                     effective_model,
                     runtime.get("provider"),
    ```

    ```diff
    @@ -4238,13 +4241,18 @@ def _toggle_verbose(self):
                 elif not self.show_reasoning:
                     self.agent.reasoning_callback = None
     
    +        # Use raw ANSI codes via _cprint so the output is routed through
    +        # prompt_toolkit's renderer.  self.console.print() with Rich markup
    +        # writes directly to stdout which patch_stdout's StdoutProxy mangles
    +        # into garbled sequences like '?[33mTool progress: NEW?[0m' (#2262).
    +        from hermes_cli.colors import Colors as _Colors
             labels = {
    -            "off": "[dim]Tool progress: OFF[/] — silent mode, just the final response.",
    -            "new": "[yellow]Tool progress: NEW[/] — show each new tool (skip repeats).",
    -            "all": "[green]Tool progress: ALL[/] — show every tool call.",
    -            "verbose": "[bold green]Tool progress: VERBOSE[/] — full args, results, think blocks, and debug logs.",
    +            "off": f"{_Colors.DIM}Tool progress: OFF{_Colors.RESET} — silent mode, just the final response.",
    +            "new": f"{_Colors.YELLOW}Tool progress: NEW{_Colors.RESET} — show each new tool (skip repeats).",
    +            "all": f"{_Colors.GREEN}Tool progress: ALL{_Colors.RESET} — show every tool call.",
    +            "verbose": f"{_Colors.BOLD}{_Colors.GREEN}Tool progress: VERBOSE{_Colors.RESET} — full args, results, think blocks, and debug logs.",
             }
    -        self.console.print(labels.get(self.tool_progress_mode, ""))
    +        _cprint(labels.get(self.tool_progress_mode, ""))
     
         def _handle_reasoning_command(self, cmd: str):
             """Handle /reasoning — manage effort level and display toggle.
    ```
  - `run_agent.py`

    ```diff
    @@ -473,6 +473,11 @@ def __init__(
             self.quiet_mode = quiet_mode
             self.ephemeral_system_prompt = ephemeral_system_prompt
             self.platform = platform  # "cli", "telegram", "discord", "whatsapp", etc.
    +        # Pluggable print function — CLI replaces this with _cprint so that
    +        # raw ANSI status lines are routed through prompt_toolkit's renderer
    +        # instead of going directly to stdout where patch_stdout's StdoutProxy
    +        # would mangle the escape sequences.  None = use builtins.print.
    +        self._print_fn = None
             self.skip_context_files = skip_context_files
             self.pass_session_id = pass_session_id
             self.log_prefix_chars = log_prefix_chars
    ```

    ```diff
    @@ -1101,16 +1106,21 @@ def reset_session_state(self):
                 self.context_compressor.compression_count = 0
                 self.context_compressor._context_probed = False
         
    -    @staticmethod
    -    def _safe_print(*args, **kwargs):
    +    def _safe_print(self, *args, **kwargs):
             """Print that silently handles broken pipes / closed stdout.
     
             In headless environments (systemd, Docker, nohup) stdout may become
             unavailable mid-session.  A raw ``print()`` raises ``OSError`` which
             can crash cron jobs and lose completed work.
    +
    +        Internally routes through ``self._print_fn`` (default: builtin
    +        ``print``) so callers such as the CLI can inject a renderer that
    +        handles ANSI escape sequences properly (e.g. prompt_toolkit's
    +        ``print_formatted_text(ANSI(...))``) without touching this method.
             """
             try:
    -            print(*args, **kwargs)
    +            fn = self._print_fn or print
    +            fn(*args, **kwargs)
             except OSError:
                 pass
     
    ```
- **#13187: fix: salvage tier-1 bug fixes from PRs #7387, #9091, #6293, #13131**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `gateway/platforms/discord.py`, `gateway/platforms/telegram.py`, `hermes_cli/setup.py`, `scripts/release.py`, `tests/e2e/conftest.py`, `tests/e2e/test_discord_adapter.py`, `tests/hermes_cli/test_setup_openclaw_migration.py`
- **#56641: fix(journey): crash on non-dict skill metadata + ANSI leaks in CLI/desktop**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/learning_graph.py`, `cli.py`, `hermes_cli/cli_commands_mixin.py`, `hermes_cli/journey.py`, `tests/agent/test_learning_graph.py`, `tests/hermes_cli/test_journey_render.py`, `tests/tui_gateway/test_slash_worker_ansi.py`, `tui_gateway/slash_worker.py`
  - fix hunks:
  - `agent/learning_graph.py`

    ```diff
    @@ -48,8 +48,16 @@ def _frontmatter(text: str) -> dict[str, Any]:
             return {}
     
     
    +def _hermes_meta(fm: dict[str, Any]) -> dict[str, Any]:
    +    """``metadata.hermes`` as a dict, tolerant of the string-valued frontmatter
    +    that ``parse_frontmatter``'s malformed-YAML fallback produces."""
    +    meta = fm.get("metadata")
    +    hermes = meta.get("hermes") if isinstance(meta, dict) else None
    +    return hermes if isinstance(hermes, dict) else {}
    +
    +
     def _related(fm: dict[str, Any]) -> list[str]:
    -    raw = fm.get("related_skills") or (fm.get("metadata", {}).get("hermes", {}) or {}).get("related_skills")
    +    raw = fm.get("related_skills") or _hermes_meta(fm).get("related_skills")
         if isinstance(raw, list):
             return [str(r).strip() for r in raw if str(r).strip()]
         if isinstance(raw, str):
    ```

    ```diff
    @@ -58,7 +66,7 @@ def _related(fm: dict[str, Any]) -> list[str]:
     
     
     def _category(fm: dict[str, Any], skill_md: Path) -> str:
    -    cat = fm.get("category") or (fm.get("metadata", {}).get("hermes", {}) or {}).get("category")
    +    cat = fm.get("category") or _hermes_meta(fm).get("category")
         if cat:
             return str(cat)
         # …/skills/<category>/<skill>/SKILL.md
    ```
  - `cli.py`

    ```diff
    @@ -8640,21 +8640,7 @@ def process_command(self, command: str) -> bool:
             elif canonical == "agents":
                 self._handle_agents_command()
             elif canonical == "journey":
    -            try:
    -                import argparse
    -                import shlex
    -
    -                from hermes_cli.journey import register_cli as _register_journey_cli
    -
    -                parser = argparse.ArgumentParser(prog="/journey", add_help=False)
    -                _register_journey_cli(parser)
    -                argv = shlex.split(cmd_original.split(None, 1)[1]) if len(cmd_original.split(None, 1)) > 1 else []
    -                args = parser.parse_args(argv)
    -                args.func(args)
    -            except SystemExit:
    -                pass
    -            except Exception as exc:
    -                _cprint(f"  /journey failed: {exc}")
    +            self._handle_journey_command(cmd_original)
             elif canonical == "background":
                 self._handle_background_command(cmd_original)
             elif canonical == "queue":
    ```
  - `hermes_cli/cli_commands_mixin.py`

    ```diff
    @@ -294,6 +294,43 @@ def _handle_agents_command(self):
             agent_running = getattr(self, "_agent_running", False)
             _cprint(f"  Agent: {'running' if agent_running else 'idle'}")
     
    +    def _handle_journey_command(self, cmd_original: str) -> None:
    +        """Handle /journey — the learning timeline (see `hermes journey`).
    +
    +        The read-only views (default + ``list``) render Rich color, which
    +        patch_stdout would swallow as raw escapes; capture with forced ANSI and
    +        re-emit through ``_cprint``. ``delete``/``edit`` are interactive
    +        (confirm prompt / ``$EDITOR``) so they keep the real stdio.
    +        """
    +        import argparse
    +        import io
    +        import shlex
    +        from contextlib import redirect_stdout
    +
    +        from cli import _cprint
    +        from hermes_cli.journey import register_cli
    +
    +        parser = argparse.ArgumentParser(prog="/journey", add_help=False)
    +        register_cli(parser)
    +        rest = cmd_original.split(None, 1)
    +        try:
    +            args = parser.parse_args(shlex.split(rest[1]) if len(rest) > 1 else [])
    +        except SystemExit:
    +            return
    +
    +        interactive = getattr(args, "journey_action", None) in ("delete", "edit")
    +        try:
    +            if interactive:
    +                args.func(args)
    +                return
    +            args.force_color = True
    +            buf = io.StringIO()
    +            with redirect_stdout(buf):
    +                args.func(args)
    +            _cprint(buf.getvalue().rstrip("\n"))
    +        except Exception as exc:
    +            _cprint(f"  /journey failed: {exc}")
    +
         def _handle_paste_command(self):
             """Handle /paste — explicitly check clipboard for an image.
     
    ```
  - `hermes_cli/journey.py`

    ```diff
    @@ -171,6 +171,17 @@ def label_row(item) -> Text:
         return Group(*parts)
     
     
    +def _console(*, color: bool, width: Optional[int] = None, force: bool = False):
    +    """A Rich console. ``force`` emits truecolor ANSI even into a captured
    +    stream — the interactive CLI grabs that output and re-renders it through
    +    prompt_toolkit (raw escapes to a real terminal would otherwise be
    +    swallowed). Mirrors the ``ChatConsole`` idiom in ``cli.py``."""
    +    from rich.console import Console
    +
    +    extra = {"force_terminal": True, "color_system": "truecolor"} if force else {}
    +    return Console(no_color=not color, width=width, **extra)
    +
    +
     def _cmd_show(args: argparse.Namespace) -> int:
         from rich.console import Console
     
    ```

    ```diff
    @@ -183,7 +194,7 @@ def _cmd_show(args: argparse.Namespace) -> int:
         payload = _build_payload()
         color = not bool(getattr(args, "no_color", False))
         cols, rows = _term_size(getattr(args, "width", None), getattr(args, "height", None))
    -    console = Console(no_color=not color, width=cols)
    +    console = _console(color=color, width=cols, force=bool(getattr(args, "force_color", False)))
     
         if not payload.get("nodes"):
             console.print(
    ```

    ```diff
    @@ -226,11 +237,9 @@ def _clamp(v: float, lo: float, hi: float) -> float:
     
     
     def _cmd_list(args: argparse.Namespace) -> int:
    -    from rich.console import Console
    -
         from agent.learning_graph_render import format_date
     
    -    console = Console(no_color=bool(getattr(args, "no_color", False)))
    +    console = _console(color=not bool(getattr(args, "no_color", False)), force=bool(getattr(args, "force_color", False)))
         nodes = sorted(_build_payload().get("nodes", []), key=lambda n: n.get("timestamp") or 0)
         if not nodes:
             console.print("[grey62]No learning yet.[/grey62]")
    ```

    ```diff
    @@ -315,6 +324,8 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
         parent.add_argument("--width", type=int, default=None, help="Override render width in columns.")
         parent.add_argument("--height", type=int, default=None, help="Override render height in rows.")
         parent.add_argument("--no-color", action="store_true", help="Disable color output.")
    +    # Force ANSI even when stdout is captured — the interactive CLI re-renders it.
    +    parent.add_argument("--force-color", action="store_true", help=argparse.SUPPRESS)
         parent.add_argument("--json", action="store_true", help="Print the raw graph payload as JSON and exit.")
         parent.set_defaults(func=_cmd_show)
     
    ```

    ```diff
    @@ -322,6 +333,7 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
     
         p_list = sub.add_parser("list", help="List node ids (for delete/edit).")
         p_list.add_argument("--no-color", action="store_true")
    +    p_list.add_argument("--force-color", action="store_true", help=argparse.SUPPRESS)
         p_list.set_defaults(func=_cmd_list)
     
         p_del = sub.add_parser("delete", help="Delete a learned skill (archived) or memory by node id.")
    ```
  - `tests/agent/test_learning_graph.py`

    ```diff
    @@ -88,6 +88,30 @@ def test_memory_is_cards_split_on_separator(tmp_path):
         assert any(n["kind"] == "memory" for n in graph["nodes"])
     
     
    +def test_malformed_frontmatter_metadata_does_not_crash(tmp_path):
    +    """``parse_frontmatter``'s malformed-YAML fallback stores every value as a
    +    string, so ``metadata`` can be a str. The graph must tolerate that instead
    +    of crashing on chained ``.get()`` (the /journey base-CLI crash)."""
    +    skill_dir = tmp_path / "skills" / "misc" / "bad-skill"
    +    skill_dir.mkdir(parents=True)
    +    # The unterminated quote makes yaml_load raise → fallback → metadata is a str.
    +    skill_dir.joinpath("SKILL.md").write_text(
    +        '---\nname: bad-skill\nmetadata: not-a-dict\ndescription: "oops\n---\n# Bad\n',
    +        encoding="utf-8",
    +    )
    +
    +    node = learning_graph.build_skill_nodes([("profile", tmp_path / "skills")])["bad-skill"]
    +
    +    assert node.category == "misc"  # directory fallback, not a crash
    +    assert node.related == []
    +
    +
    +def test_hermes_meta_tolerates_non_dict():
    +    assert learning_graph._hermes_meta({"metadata": "junk"}) == {}
    +    assert learning_graph._hermes_meta({"metadata": {"hermes": "junk"}}) == {}
    +    assert learning_graph._hermes_meta({"metadata": {"hermes": {"category": "x"}}}) == {"category": "x"}
    +
    +
     def test_full_payload_shape_and_edge_integrity(tmp_path):
         home = tmp_path / ".hermes"
         home.mkdir()
    ```
  - `tests/hermes_cli/test_journey_render.py`

    ```diff
    @@ -0,0 +1,38 @@
    +"""Behavior contracts for /journey output routing.
    +
    +The interactive CLI captures Rich output and re-renders it through
    +prompt_toolkit, so it needs forced ANSI (``--force-color``); chat surfaces
    +render plain text, so the default captured path must stay escape-free.
    +"""
    +
    +from __future__ import annotations
    +
    +import argparse
    +import contextlib
    +import io
    +
    +
    +def _capture(argv: list[str], *, force: bool) -> str:
    +    from hermes_cli.journey import register_cli
    +
    +    parser = argparse.ArgumentParser(add_help=False)
    +    register_cli(parser)
    +    args = parser.parse_args(argv)
    +    if force:
    +        args.force_color = True
    +
    +    buf = io.StringIO()
    +    with contextlib.redirect_stdout(buf):
    +        args.func(args)
    +    return buf.getvalue()
    +
    +
    +def test_force_color_emits_ansi_for_reemission():
    +    assert "\x1b[" in _capture([], force=True)
    +    assert "\x1b[" in _capture(["list"], force=True)
    +
    +
    +def test_default_capture_is_plain_for_chat_bubbles():
    +    # Rich auto-detects the StringIO as non-tty → no color, no raw escapes.
    +    assert "\x1b[" not in _capture([], force=False)
    +    assert "\x1b[" not in _capture(["list"], force=False)
    ```
  - `tests/tui_gateway/test_slash_worker_ansi.py`

    ```diff
    @@ -0,0 +1,23 @@
    +"""The slash worker feeds desktop chat bubbles, which render plain text — so
    +any ANSI a worker-routed command emits (e.g. /journey's own Rich Console) must
    +be stripped from the worker's return value."""
    +
    +from __future__ import annotations
    +
    +
    +class _FakeCLI:
    +    console = None
    +
    +    def process_command(self, cmd: str) -> None:
    +        import sys
    +
    +        sys.stdout.write("\x1b[38;2;1;2;3mcolored\x1b[0m plain")
    +
    +
    +def test_run_strips_ansi_from_output():
    +    from tui_gateway import slash_worker
    +
    +    out = slash_worker._run(_FakeCLI(), "/anything")
    +
    +    assert "\x1b[" not in out
    +    assert out == "colored plain"
    ```
  - `tui_gateway/slash_worker.py`

    ```diff
    @@ -102,7 +102,14 @@ def _run(cli: HermesCLI, command: str) -> str:
             if old is not None:
                 cli_mod._cprint = old
     
    -    return buf.getvalue().rstrip()
    +    # Desktop chat bubbles render plain text, not ANSI. A worker-routed command
    +    # that emits Rich color (e.g. /journey building its own Console, which picks
    +    # up truecolor from the gateway's inherited COLORTERM) would otherwise leak
    +    # raw escapes; strip them at the single choke point. (The TUI opens /journey
    +    # as an overlay, so it never travels this path.)
    +    from tools.ansi_strip import strip_ansi
    +
    +    return strip_ansi(buf.getvalue().rstrip())
     
     
     def main():
    ```
- **#2265: fix: resolve garbled ANSI escape codes in status printouts**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#2448`
- **#8630: fix(cli): render /tools list through prompt_toolkit**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `9de4a38ce`, `9de4a38ce`
- **#9091: fix(tui): make "/tools list" show real colors instead of "?\[32m" etc.…**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#13187`
- **#9205: fix(cli): make /tools slash commands ANSI-safe and idempotent**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `0904bc7ea`
  - files: `cli.py`, `hermes_cli/tools_config.py`, `tests/cli/test_cli_tools_command.py`, `tests/hermes_cli/test_tools_disable_enable.py`
- **#11335: fix(tui): replace bright yellow with dark_goldenrod for light bg contrast**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#56542: fix(tui): strip ANSI from slash-command output before returning to chat**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#56641`
- **#56546: fix(tui): strip ANSI from slash worker output**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#56641`
- **#59418: fix: enable ANSI rendering on Windows consoles**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `cfeffe851`
- **#59648: fix(cli): fix broken ANSI code rendering (#59397)**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#59418`, `#59397`, `#59418`
  - files: `hermes_cli/stdio.py`, `tests/cli/test_ansi_render.py`
- **#59750: fix(Windows): enable VT processing for ANSI escape sequences**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#59835: fix(bootstrap): enable Windows VT processing for ANSI escape codes**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#59951: fix(cli): enable Windows Virtual Terminal Processing for ANSI colors**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#60011: fix(setup): enable Windows VT processing for ANSI escape codes**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#60073: feat(cli): migrated cron cli to rich rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/colors.py`, `hermes_cli/cron.py`, `tests/hermes_cli/test_cron_rich_render.py`
- **#61322: feat(cli): Migrated Cron and Doctor CLI to Rich Rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/colors.py`, `hermes_cli/cron.py`, `hermes_cli/doctor.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`
- **#61954: feat(cli): Migrated Status CLI to Rich Rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/colors.py`, `hermes_cli/cron.py`, `hermes_cli/doctor.py`, `hermes_cli/gui_uninstall.py`, `hermes_cli/status.py`, `hermes_cli/uninstall.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`, `tests/hermes_cli/test_gui_uninstall_rich_render.py`, `tests/hermes_cli/test_status_rich_render.py`, `tests/hermes_cli/test_uninstall_rich_render.py`
- **#63705: feat(cli): Migrated Uninstall CLI to Rich Rendering**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#64142: feat(cli): Migrated GUI Uninstall CLI to Rich Rendering**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `491689784e6d072d22ee5c2b491798b99ba3eb92`
  - files: `hermes_cli/colors.py`, `hermes_cli/cron.py`, `hermes_cli/doctor.py`, `hermes_cli/gui_uninstall.py`, `hermes_cli/status.py`, `hermes_cli/uninstall.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`, `tests/hermes_cli/test_gui_uninstall_rich_render.py`, `tests/hermes_cli/test_status_rich_render.py`, `tests/hermes_cli/test_uninstall_rich_render.py`
- **#64307: feat(cli): Migrated cli_output Tools to Rich Rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/cli_output.py`, `hermes_cli/colors.py`, `hermes_cli/cron.py`, `hermes_cli/doctor.py`, `hermes_cli/gui_uninstall.py`, `hermes_cli/secret_prompt.py`, `hermes_cli/status.py`, `hermes_cli/uninstall.py`, `tests/hermes_cli/test_cli_output_rich_render.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`, `tests/hermes_cli/test_gui_uninstall_rich_render.py`, `tests/hermes_cli/test_status_rich_render.py`, `tests/hermes_cli/test_uninstall_rich_render.py`
- **#67993: feat(cli): Migrated Config CLI to Rich Rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#59397`
  - files: `hermes_cli/cli_output.py`, `hermes_cli/colors.py`, `hermes_cli/config.py`, `hermes_cli/cron.py`, `hermes_cli/doctor.py`, `hermes_cli/gui_uninstall.py`, `hermes_cli/secret_prompt.py`, `hermes_cli/status.py`, `hermes_cli/uninstall.py`, `tests/hermes_cli/test_cli_output_rich_render.py`, `tests/hermes_cli/test_config_rich_render.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`, `tests/hermes_cli/test_gui_uninstall_rich_render.py`, `tests/hermes_cli/test_status_rich_render.py`, `tests/hermes_cli/test_uninstall_rich_render.py`
- **#68466: feat(cli): Migrated Curses UI to Rich Rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#59397`
  - files: `hermes_cli/cli_output.py`, `hermes_cli/colors.py`, `hermes_cli/config.py`, `hermes_cli/cron.py`, `hermes_cli/curses_ui.py`, `hermes_cli/doctor.py`, `hermes_cli/gui_uninstall.py`, `hermes_cli/secret_prompt.py`, `hermes_cli/status.py`, `hermes_cli/uninstall.py`, `tests/hermes_cli/test_cli_output_rich_render.py`, `tests/hermes_cli/test_config_rich_render.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_curses_ui_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`, `tests/hermes_cli/test_gui_uninstall_rich_render.py`, `tests/hermes_cli/test_status_rich_render.py`, `tests/hermes_cli/test_uninstall_rich_render.py`
- **#69158: feat(cli): Migrated Setup CLI to Rich Rendering**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/cli_output.py`, `hermes_cli/colors.py`, `hermes_cli/config.py`, `hermes_cli/cron.py`, `hermes_cli/curses_ui.py`, `hermes_cli/doctor.py`, `hermes_cli/gui_uninstall.py`, `hermes_cli/secret_prompt.py`, `hermes_cli/setup.py`, `hermes_cli/status.py`, `hermes_cli/uninstall.py`, `tests/hermes_cli/test_cli_output_rich_render.py`, `tests/hermes_cli/test_config_rich_render.py`, `tests/hermes_cli/test_cron_rich_render.py`, `tests/hermes_cli/test_curses_ui_rich_render.py`, `tests/hermes_cli/test_doctor_rich_render.py`, `tests/hermes_cli/test_gui_uninstall_rich_render.py`, `tests/hermes_cli/test_setup_rich_render.py`, `tests/hermes_cli/test_status_rich_render.py`, `tests/hermes_cli/test_uninstall_rich_render.py`
- **#87613: fix(cli): stop garbled ANSI from welcome banner and deprecated-env warning (#2262)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `f310ab8ceccacdb0207354b472c6bba7c605000a`
  - files: `cli.py`, `hermes_cli/colors.py`, `hermes_cli/config.py`, `tests/hermes_cli/test_colors_stream.py`, `tests/hermes_cli/test_config_validation.py`, `tests/hermes_cli/test_deprecated_cwd_warning.py`

## Fix status

- Merged to main in PR #2448 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `dashboard`
