{"repo":"NousResearch/hermes-agent","public_id":"2262","display_label":"CLI ANSI rendering fixes and Rich migration","summary":"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.","subsystem":"dashboard","counts":{"members":31,"open_issues":1,"open_issues_distinct":1,"open_prs":11,"closed_issues":4,"merged_prs":3,"attempt_prs":23},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":2448},"resolution":{"state":"partially_resolved","by":"pr-2448,pr-56641"},"freshness":{"state":"verified","last_verified_at":"2026-07-01T21:34:51+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.09126881720430106},"fix_edges":[{"issue_number":2262,"pr_number":2448,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":5865,"pr_number":13187,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9017,"pr_number":13187,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":13025,"pr_number":13187,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":56533,"pr_number":56641,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":2262,"title":"[Bug]: Garbled ANSI codes from status printouts like `\\verbose` and `approaching compaction`","url":null,"state":"closed"},{"kind":"issue","number":4128,"title":"[Bug]: /tools list shows raw terminal escape codes","url":null,"state":"closed"},{"kind":"issue","number":9017,"title":"[Feature]: Improve the display format of the /tools list command","url":null,"state":"closed"},{"kind":"issue","number":56533,"title":"/journey slash command leaks raw ANSI escape codes in TUI and Desktop chat views","url":null,"state":"closed"},{"kind":"issue","number":59397,"title":"[Bug]: ANSI Code Broken Render in CLI","url":null,"state":"open"},{"kind":"pr","number":2265,"title":"fix: resolve garbled ANSI escape codes in status printouts","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2448","number":2448}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":2448,"title":"fix: resolve garbled ANSI escape codes in status printouts","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","run_agent.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -1915,6 +1915,9 @@ def _init_agent(self, *, model_override: str = None, runtime_override: dict = No\n                 tool_progress_callback=self._on_tool_progress,\n                 stream_delta_callback=self._stream_delta if self.streaming_enabled else None,\n             )\n+            # Route agent status output through prompt_toolkit so ANSI escape\n+            # sequences aren't garbled by patch_stdout's StdoutProxy (#2262).\n+            self.agent._print_fn = _cprint\n             self._active_agent_route_signature = (\n                 effective_model,\n                 runtime.get(\"provider\"),","@@ -4238,13 +4241,18 @@ def _toggle_verbose(self):\n             elif not self.show_reasoning:\n                 self.agent.reasoning_callback = None\n \n+        # Use raw ANSI codes via _cprint so the output is routed through\n+        # prompt_toolkit's renderer.  self.console.print() with Rich markup\n+        # writes directly to stdout which patch_stdout's StdoutProxy mangles\n+        # into garbled sequences like '?[33mTool progress: NEW?[0m' (#2262).\n+        from hermes_cli.colors import Colors as _Colors\n         labels = {\n-            \"off\": \"[dim]Tool progress: OFF[/] — silent mode, just the final response.\",\n-            \"new\": \"[yellow]Tool progress: NEW[/] — show each new tool (skip repeats).\",\n-            \"all\": \"[green]Tool progress: ALL[/] — show every tool call.\",\n-            \"verbose\": \"[bold green]Tool progress: VERBOSE[/] — full args, results, think blocks, and debug logs.\",\n+            \"off\": f\"{_Colors.DIM}Tool progress: OFF{_Colors.RESET} — silent mode, just the final response.\",\n+            \"new\": f\"{_Colors.YELLOW}Tool progress: NEW{_Colors.RESET} — show each new tool (skip repeats).\",\n+            \"all\": f\"{_Colors.GREEN}Tool progress: ALL{_Colors.RESET} — show every tool call.\",\n+            \"verbose\": f\"{_Colors.BOLD}{_Colors.GREEN}Tool progress: VERBOSE{_Colors.RESET} — full args, results, think blocks, and debug logs.\",\n         }\n-        self.console.print(labels.get(self.tool_progress_mode, \"\"))\n+        _cprint(labels.get(self.tool_progress_mode, \"\"))\n \n     def _handle_reasoning_command(self, cmd: str):\n         \"\"\"Handle /reasoning — manage effort level and display toggle."]},{"file":"run_agent.py","hunks":["@@ -473,6 +473,11 @@ def __init__(\n         self.quiet_mode = quiet_mode\n         self.ephemeral_system_prompt = ephemeral_system_prompt\n         self.platform = platform  # \"cli\", \"telegram\", \"discord\", \"whatsapp\", etc.\n+        # Pluggable print function — CLI replaces this with _cprint so that\n+        # raw ANSI status lines are routed through prompt_toolkit's renderer\n+        # instead of going directly to stdout where patch_stdout's StdoutProxy\n+        # would mangle the escape sequences.  None = use builtins.print.\n+        self._print_fn = None\n         self.skip_context_files = skip_context_files\n         self.pass_session_id = pass_session_id\n         self.log_prefix_chars = log_prefix_chars","@@ -1101,16 +1106,21 @@ def reset_session_state(self):\n             self.context_compressor.compression_count = 0\n             self.context_compressor._context_probed = False\n     \n-    @staticmethod\n-    def _safe_print(*args, **kwargs):\n+    def _safe_print(self, *args, **kwargs):\n         \"\"\"Print that silently handles broken pipes / closed stdout.\n \n         In headless environments (systemd, Docker, nohup) stdout may become\n         unavailable mid-session.  A raw ``print()`` raises ``OSError`` which\n         can crash cron jobs and lose completed work.\n+\n+        Internally routes through ``self._print_fn`` (default: builtin\n+        ``print``) so callers such as the CLI can inject a renderer that\n+        handles ANSI escape sequences properly (e.g. prompt_toolkit's\n+        ``print_formatted_text(ANSI(...))``) without touching this method.\n         \"\"\"\n         try:\n-            print(*args, **kwargs)\n+            fn = self._print_fn or print\n+            fn(*args, **kwargs)\n         except OSError:\n             pass\n \n"]}]},{"kind":"pr","number":8630,"title":"fix(cli): render /tools list through prompt_toolkit","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9de4a38ce","number":0},{"kind":"commit","value":"9de4a38ce","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":9091,"title":"fix(tui): make \"/tools list\" show real colors instead of \"?[32m\" etc.…","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#13187","number":13187}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":9205,"title":"fix(cli): make /tools slash commands ANSI-safe and idempotent","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"0904bc7ea","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":11335,"title":"fix(tui): replace bright yellow with dark_goldenrod for light bg contrast","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":13187,"title":"fix: salvage tier-1 bug fixes from PRs #7387, #9091, #6293, #13131","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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"]},{"kind":"pr","number":56542,"title":"fix(tui): strip ANSI from slash-command output before returning to chat","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56641","number":56641}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":56546,"title":"fix(tui): strip ANSI from slash worker output","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56641","number":56641}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":56641,"title":"fix(journey): crash on non-dict skill metadata + ANSI leaks in CLI/desktop","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"agent/learning_graph.py","hunks":["@@ -48,8 +48,16 @@ def _frontmatter(text: str) -> dict[str, Any]:\n         return {}\n \n \n+def _hermes_meta(fm: dict[str, Any]) -> dict[str, Any]:\n+    \"\"\"``metadata.hermes`` as a dict, tolerant of the string-valued frontmatter\n+    that ``parse_frontmatter``'s malformed-YAML fallback produces.\"\"\"\n+    meta = fm.get(\"metadata\")\n+    hermes = meta.get(\"hermes\") if isinstance(meta, dict) else None\n+    return hermes if isinstance(hermes, dict) else {}\n+\n+\n def _related(fm: dict[str, Any]) -> list[str]:\n-    raw = fm.get(\"related_skills\") or (fm.get(\"metadata\", {}).get(\"hermes\", {}) or {}).get(\"related_skills\")\n+    raw = fm.get(\"related_skills\") or _hermes_meta(fm).get(\"related_skills\")\n     if isinstance(raw, list):\n         return [str(r).strip() for r in raw if str(r).strip()]\n     if isinstance(raw, str):","@@ -58,7 +66,7 @@ def _related(fm: dict[str, Any]) -> list[str]:\n \n \n def _category(fm: dict[str, Any], skill_md: Path) -> str:\n-    cat = fm.get(\"category\") or (fm.get(\"metadata\", {}).get(\"hermes\", {}) or {}).get(\"category\")\n+    cat = fm.get(\"category\") or _hermes_meta(fm).get(\"category\")\n     if cat:\n         return str(cat)\n     # …/skills/<category>/<skill>/SKILL.md"]},{"file":"cli.py","hunks":["@@ -8640,21 +8640,7 @@ def process_command(self, command: str) -> bool:\n         elif canonical == \"agents\":\n             self._handle_agents_command()\n         elif canonical == \"journey\":\n-            try:\n-                import argparse\n-                import shlex\n-\n-                from hermes_cli.journey import register_cli as _register_journey_cli\n-\n-                parser = argparse.ArgumentParser(prog=\"/journey\", add_help=False)\n-                _register_journey_cli(parser)\n-                argv = shlex.split(cmd_original.split(None, 1)[1]) if len(cmd_original.split(None, 1)) > 1 else []\n-                args = parser.parse_args(argv)\n-                args.func(args)\n-            except SystemExit:\n-                pass\n-            except Exception as exc:\n-                _cprint(f\"  /journey failed: {exc}\")\n+            self._handle_journey_command(cmd_original)\n         elif canonical == \"background\":\n             self._handle_background_command(cmd_original)\n         elif canonical == \"queue\":"]},{"file":"hermes_cli/cli_commands_mixin.py","hunks":["@@ -294,6 +294,43 @@ def _handle_agents_command(self):\n         agent_running = getattr(self, \"_agent_running\", False)\n         _cprint(f\"  Agent: {'running' if agent_running else 'idle'}\")\n \n+    def _handle_journey_command(self, cmd_original: str) -> None:\n+        \"\"\"Handle /journey — the learning timeline (see `hermes journey`).\n+\n+        The read-only views (default + ``list``) render Rich color, which\n+        patch_stdout would swallow as raw escapes; capture with forced ANSI and\n+        re-emit through ``_cprint``. ``delete``/``edit`` are interactive\n+        (confirm prompt / ``$EDITOR``) so they keep the real stdio.\n+        \"\"\"\n+        import argparse\n+        import io\n+        import shlex\n+        from contextlib import redirect_stdout\n+\n+        from cli import _cprint\n+        from hermes_cli.journey import register_cli\n+\n+        parser = argparse.ArgumentParser(prog=\"/journey\", add_help=False)\n+        register_cli(parser)\n+        rest = cmd_original.split(None, 1)\n+        try:\n+            args = parser.parse_args(shlex.split(rest[1]) if len(rest) > 1 else [])\n+        except SystemExit:\n+            return\n+\n+        interactive = getattr(args, \"journey_action\", None) in (\"delete\", \"edit\")\n+        try:\n+            if interactive:\n+                args.func(args)\n+                return\n+            args.force_color = True\n+            buf = io.StringIO()\n+            with redirect_stdout(buf):\n+                args.func(args)\n+            _cprint(buf.getvalue().rstrip(\"\\n\"))\n+        except Exception as exc:\n+            _cprint(f\"  /journey failed: {exc}\")\n+\n     def _handle_paste_command(self):\n         \"\"\"Handle /paste — explicitly check clipboard for an image.\n "]},{"file":"hermes_cli/journey.py","hunks":["@@ -171,6 +171,17 @@ def label_row(item) -> Text:\n     return Group(*parts)\n \n \n+def _console(*, color: bool, width: Optional[int] = None, force: bool = False):\n+    \"\"\"A Rich console. ``force`` emits truecolor ANSI even into a captured\n+    stream — the interactive CLI grabs that output and re-renders it through\n+    prompt_toolkit (raw escapes to a real terminal would otherwise be\n+    swallowed). Mirrors the ``ChatConsole`` idiom in ``cli.py``.\"\"\"\n+    from rich.console import Console\n+\n+    extra = {\"force_terminal\": True, \"color_system\": \"truecolor\"} if force else {}\n+    return Console(no_color=not color, width=width, **extra)\n+\n+\n def _cmd_show(args: argparse.Namespace) -> int:\n     from rich.console import Console\n ","@@ -183,7 +194,7 @@ def _cmd_show(args: argparse.Namespace) -> int:\n     payload = _build_payload()\n     color = not bool(getattr(args, \"no_color\", False))\n     cols, rows = _term_size(getattr(args, \"width\", None), getattr(args, \"height\", None))\n-    console = Console(no_color=not color, width=cols)\n+    console = _console(color=color, width=cols, force=bool(getattr(args, \"force_color\", False)))\n \n     if not payload.get(\"nodes\"):\n         console.print(","@@ -226,11 +237,9 @@ def _clamp(v: float, lo: float, hi: float) -> float:\n \n \n def _cmd_list(args: argparse.Namespace) -> int:\n-    from rich.console import Console\n-\n     from agent.learning_graph_render import format_date\n \n-    console = Console(no_color=bool(getattr(args, \"no_color\", False)))\n+    console = _console(color=not bool(getattr(args, \"no_color\", False)), force=bool(getattr(args, \"force_color\", False)))\n     nodes = sorted(_build_payload().get(\"nodes\", []), key=lambda n: n.get(\"timestamp\") or 0)\n     if not nodes:\n         console.print(\"[grey62]No learning yet.[/grey62]\")","@@ -315,6 +324,8 @@ def register_cli(parent: argparse.ArgumentParser) -> None:\n     parent.add_argument(\"--width\", type=int, default=None, help=\"Override render width in columns.\")\n     parent.add_argument(\"--height\", type=int, default=None, help=\"Override render height in rows.\")\n     parent.add_argument(\"--no-color\", action=\"store_true\", help=\"Disable color output.\")\n+    # Force ANSI even when stdout is captured — the interactive CLI re-renders it.\n+    parent.add_argument(\"--force-color\", action=\"store_true\", help=argparse.SUPPRESS)\n     parent.add_argument(\"--json\", action=\"store_true\", help=\"Print the raw graph payload as JSON and exit.\")\n     parent.set_defaults(func=_cmd_show)\n ","@@ -322,6 +333,7 @@ def register_cli(parent: argparse.ArgumentParser) -> None:\n \n     p_list = sub.add_parser(\"list\", help=\"List node ids (for delete/edit).\")\n     p_list.add_argument(\"--no-color\", action=\"store_true\")\n+    p_list.add_argument(\"--force-color\", action=\"store_true\", help=argparse.SUPPRESS)\n     p_list.set_defaults(func=_cmd_list)\n \n     p_del = sub.add_parser(\"delete\", help=\"Delete a learned skill (archived) or memory by node id.\")"]},{"file":"tests/agent/test_learning_graph.py","hunks":["@@ -88,6 +88,30 @@ def test_memory_is_cards_split_on_separator(tmp_path):\n     assert any(n[\"kind\"] == \"memory\" for n in graph[\"nodes\"])\n \n \n+def test_malformed_frontmatter_metadata_does_not_crash(tmp_path):\n+    \"\"\"``parse_frontmatter``'s malformed-YAML fallback stores every value as a\n+    string, so ``metadata`` can be a str. The graph must tolerate that instead\n+    of crashing on chained ``.get()`` (the /journey base-CLI crash).\"\"\"\n+    skill_dir = tmp_path / \"skills\" / \"misc\" / \"bad-skill\"\n+    skill_dir.mkdir(parents=True)\n+    # The unterminated quote makes yaml_load raise → fallback → metadata is a str.\n+    skill_dir.joinpath(\"SKILL.md\").write_text(\n+        '---\\nname: bad-skill\\nmetadata: not-a-dict\\ndescription: \"oops\\n---\\n# Bad\\n',\n+        encoding=\"utf-8\",\n+    )\n+\n+    node = learning_graph.build_skill_nodes([(\"profile\", tmp_path / \"skills\")])[\"bad-skill\"]\n+\n+    assert node.category == \"misc\"  # directory fallback, not a crash\n+    assert node.related == []\n+\n+\n+def test_hermes_meta_tolerates_non_dict():\n+    assert learning_graph._hermes_meta({\"metadata\": \"junk\"}) == {}\n+    assert learning_graph._hermes_meta({\"metadata\": {\"hermes\": \"junk\"}}) == {}\n+    assert learning_graph._hermes_meta({\"metadata\": {\"hermes\": {\"category\": \"x\"}}}) == {\"category\": \"x\"}\n+\n+\n def test_full_payload_shape_and_edge_integrity(tmp_path):\n     home = tmp_path / \".hermes\"\n     home.mkdir()"]},{"file":"tests/hermes_cli/test_journey_render.py","hunks":["@@ -0,0 +1,38 @@\n+\"\"\"Behavior contracts for /journey output routing.\n+\n+The interactive CLI captures Rich output and re-renders it through\n+prompt_toolkit, so it needs forced ANSI (``--force-color``); chat surfaces\n+render plain text, so the default captured path must stay escape-free.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import argparse\n+import contextlib\n+import io\n+\n+\n+def _capture(argv: list[str], *, force: bool) -> str:\n+    from hermes_cli.journey import register_cli\n+\n+    parser = argparse.ArgumentParser(add_help=False)\n+    register_cli(parser)\n+    args = parser.parse_args(argv)\n+    if force:\n+        args.force_color = True\n+\n+    buf = io.StringIO()\n+    with contextlib.redirect_stdout(buf):\n+        args.func(args)\n+    return buf.getvalue()\n+\n+\n+def test_force_color_emits_ansi_for_reemission():\n+    assert \"\\x1b[\" in _capture([], force=True)\n+    assert \"\\x1b[\" in _capture([\"list\"], force=True)\n+\n+\n+def test_default_capture_is_plain_for_chat_bubbles():\n+    # Rich auto-detects the StringIO as non-tty → no color, no raw escapes.\n+    assert \"\\x1b[\" not in _capture([], force=False)\n+    assert \"\\x1b[\" not in _capture([\"list\"], force=False)"]},{"file":"tests/tui_gateway/test_slash_worker_ansi.py","hunks":["@@ -0,0 +1,23 @@\n+\"\"\"The slash worker feeds desktop chat bubbles, which render plain text — so\n+any ANSI a worker-routed command emits (e.g. /journey's own Rich Console) must\n+be stripped from the worker's return value.\"\"\"\n+\n+from __future__ import annotations\n+\n+\n+class _FakeCLI:\n+    console = None\n+\n+    def process_command(self, cmd: str) -> None:\n+        import sys\n+\n+        sys.stdout.write(\"\\x1b[38;2;1;2;3mcolored\\x1b[0m plain\")\n+\n+\n+def test_run_strips_ansi_from_output():\n+    from tui_gateway import slash_worker\n+\n+    out = slash_worker._run(_FakeCLI(), \"/anything\")\n+\n+    assert \"\\x1b[\" not in out\n+    assert out == \"colored plain\""]},{"file":"tui_gateway/slash_worker.py","hunks":["@@ -102,7 +102,14 @@ def _run(cli: HermesCLI, command: str) -> str:\n         if old is not None:\n             cli_mod._cprint = old\n \n-    return buf.getvalue().rstrip()\n+    # Desktop chat bubbles render plain text, not ANSI. A worker-routed command\n+    # that emits Rich color (e.g. /journey building its own Console, which picks\n+    # up truecolor from the gateway's inherited COLORTERM) would otherwise leak\n+    # raw escapes; strip them at the single choke point. (The TUI opens /journey\n+    # as an overlay, so it never travels this path.)\n+    from tools.ansi_strip import strip_ansi\n+\n+    return strip_ansi(buf.getvalue().rstrip())\n \n \n def main():\n"]}]},{"kind":"pr","number":59418,"title":"fix: enable ANSI rendering on Windows consoles","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"cfeffe851","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":59648,"title":"fix(cli): fix broken ANSI code rendering (#59397)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#59418","number":59418},{"kind":"pr","value":"#59397","number":59397},{"kind":"pr","value":"#59418","number":59418}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":59750,"title":"fix(Windows): enable VT processing for ANSI escape sequences","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":59835,"title":"fix(bootstrap): enable Windows VT processing for ANSI escape codes","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":59951,"title":"fix(cli): enable Windows Virtual Terminal Processing for ANSI colors","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":60011,"title":"fix(setup): enable Windows VT processing for ANSI escape codes","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":60073,"title":"feat(cli): migrated cron cli to rich rendering","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":61322,"title":"feat(cli): Migrated Cron and Doctor CLI to Rich Rendering","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":61954,"title":"feat(cli): Migrated Status CLI to Rich Rendering","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":63705,"title":"feat(cli): Migrated Uninstall CLI to Rich Rendering","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":64142,"title":"feat(cli): Migrated GUI Uninstall CLI to Rich Rendering","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"491689784e6d072d22ee5c2b491798b99ba3eb92","number":0}],"category":"partial_fix","implication":"needs_llm"},"merged":false},{"kind":"pr","number":64307,"title":"feat(cli): Migrated cli_output Tools to Rich Rendering","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":67993,"title":"feat(cli): Migrated Config CLI to Rich Rendering","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#59397","number":59397}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":68466,"title":"feat(cli): Migrated Curses UI to Rich Rendering","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#59397","number":59397}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":69158,"title":"feat(cli): Migrated Setup CLI to Rich Rendering","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":87613,"title":"fix(cli): stop garbled ANSI from welcome banner and deprecated-env warning (#2262)","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"f310ab8ceccacdb0207354b472c6bba7c605000a","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}