From d4728a0d801f1ebbc2384547009df17cbf16bfd1 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 13 May 2026 02:59:58 -0400 Subject: [PATCH] fix: fall back to ASCII instinct status bars Fixes #1855 --- .../scripts/instinct-cli.py | 23 ++++++++++++- .../scripts/test_parse_instinct.py | 34 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/skills/continuous-learning-v2/scripts/instinct-cli.py b/skills/continuous-learning-v2/scripts/instinct-cli.py index a4ce1cdb..7b03bc0e 100755 --- a/skills/continuous-learning-v2/scripts/instinct-cli.py +++ b/skills/continuous-learning-v2/scripts/instinct-cli.py @@ -82,6 +82,27 @@ def _normalize_remote_url(remote_url: str) -> str: return normalized.lower() if is_network else normalized +def _stream_can_encode(text: str, stream=None) -> bool: + stream = stream or sys.stdout + encoding = getattr(stream, "encoding", None) or sys.getdefaultencoding() + try: + text.encode(encoding) + except (LookupError, UnicodeEncodeError): + return False + return True + + +def _confidence_bar(confidence, stream=None) -> str: + try: + filled = int(float(confidence) * 10) + except (TypeError, ValueError): + filled = 5 + filled = max(0, min(10, filled)) + + full, empty = ("\u2588", "\u2591") if _stream_can_encode("\u2588\u2591", stream) else ("#", ".") + return full * filled + empty * (10 - filled) + + def _project_hash(value: str) -> str: return hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] @@ -550,7 +571,7 @@ def _print_instincts_by_domain(instincts: list[dict]) -> None: for inst in sorted(domain_instincts, key=lambda x: -x.get('confidence', 0.5)): conf = inst.get('confidence', 0.5) - conf_bar = '\u2588' * int(conf * 10) + '\u2591' * (10 - int(conf * 10)) + conf_bar = _confidence_bar(conf) trigger = inst.get('trigger', 'unknown trigger') scope_tag = f"[{inst.get('scope', '?')}]" diff --git a/skills/continuous-learning-v2/scripts/test_parse_instinct.py b/skills/continuous-learning-v2/scripts/test_parse_instinct.py index 71734a9a..b9871493 100644 --- a/skills/continuous-learning-v2/scripts/test_parse_instinct.py +++ b/skills/continuous-learning-v2/scripts/test_parse_instinct.py @@ -45,6 +45,7 @@ _find_cross_project_instincts = _mod._find_cross_project_instincts load_registry = _mod.load_registry _validate_instinct_id = _mod._validate_instinct_id _update_registry = _mod._update_registry +_confidence_bar = _mod._confidence_bar # ───────────────────────────────────────────── @@ -642,6 +643,39 @@ def test_cmd_status_with_instincts(patch_globals, monkeypatch, capsys): assert "GLOBAL" in out +def test_confidence_bar_uses_unicode_when_supported(): + """Confidence bars should retain block glyphs on UTF-8 streams.""" + stream = SimpleNamespace(encoding="utf-8") + assert _confidence_bar(0.8, stream=stream) == "\u2588" * 8 + "\u2591" * 2 + + +def test_confidence_bar_uses_ascii_when_stream_rejects_block_glyphs(): + """Windows cp1252 streams cannot encode block glyphs.""" + stream = SimpleNamespace(encoding="cp1252") + assert _confidence_bar(0.8, stream=stream) == "########.." + + +def test_print_instincts_by_domain_is_cp1252_safe(monkeypatch): + """Status rendering should not crash on Windows cp1252 stdout.""" + raw = io.BytesIO() + stream = io.TextIOWrapper(raw, encoding="cp1252") + monkeypatch.setattr(_mod.sys, "stdout", stream) + + _mod._print_instincts_by_domain([{ + "id": "windows-safe", + "trigger": "when stdout uses cp1252", + "confidence": 0.8, + "domain": "platform", + "scope": "project", + }]) + + stream.flush() + out = raw.getvalue().decode("cp1252") + assert "########.." in out + assert "\u2588" not in out + assert "\u2591" not in out + + def test_cmd_status_returns_int(patch_globals, monkeypatch): """cmd_status should always return an int.""" tree = patch_globals