From eb60d03f4b472e528c2e0cffb495da324720708f Mon Sep 17 00:00:00 2001 From: Himanshu Dongre Date: Mon, 18 May 2026 00:02:07 +0530 Subject: [PATCH] Render list-valued blocked_by consistently in state --compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `smriti current` already normalizes a list-valued blocked_by to a clean comma-separated string via the backend CurrentTask validator, but `smriti state` rendered it through an f-string and leaked the raw Python list repr (['a', 'b']) into the state brief — the surface agents read at session start. Add a `_coerce_blocked_by` helper in the CLI formatters mirroring that validator, and apply it in `_normalize_task_item` so every `smriti state` task line renders blocked_by the same way `smriti current` does. Adds regression tests. --- cli/smriti_cli/formatters.py | 21 +++++++++++++++++++++ cli/tests/test_state_multi_branch.py | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cli/smriti_cli/formatters.py b/cli/smriti_cli/formatters.py index cb59585..03a89c7 100644 --- a/cli/smriti_cli/formatters.py +++ b/cli/smriti_cli/formatters.py @@ -120,6 +120,25 @@ def _list_section(heading: str, items: list[str]) -> str: return "\n".join(lines) + "\n" +def _coerce_blocked_by(value: object) -> str | None: + """Normalize blocked_by — string, list of dependency labels, or null — + to a single display string. + + Real task payloads carry blocked_by as a plain string or as a list (a + task blocked by several others). Mirrors the backend CurrentTask + validator so `smriti state` and `smriti current` render it identically + instead of leaking a raw Python list repr into the state brief. + """ + if value is None: + return None + if isinstance(value, str): + return value.strip() or None + if isinstance(value, (list, tuple)): + labels = [str(item).strip() for item in value if str(item).strip()] + return ", ".join(labels) or None + return str(value).strip() or None + + def _normalize_task_item(item) -> dict: """Normalize a task to a dict with at least a 'text' key. @@ -131,6 +150,8 @@ def _normalize_task_item(item) -> dict: task = dict(item) if not task.get("intent_hint") and task.get("intent_type"): task["intent_hint"] = task["intent_type"] + if "blocked_by" in task: + task["blocked_by"] = _coerce_blocked_by(task["blocked_by"]) return task # Fallback: coerce to string return {"text": str(item)} diff --git a/cli/tests/test_state_multi_branch.py b/cli/tests/test_state_multi_branch.py index 303daf6..6cfb23a 100644 --- a/cli/tests/test_state_multi_branch.py +++ b/cli/tests/test_state_multi_branch.py @@ -504,6 +504,13 @@ def test_normalize_task_item_dict(): assert result == task +def test_normalize_task_item_coerces_list_blocked_by(): + """A list-valued blocked_by is normalized to a comma-separated string.""" + task = {"text": "Wire the limiter", "blocked_by": ["middleware", "load-test"]} + result = _normalize_task_item(task) + assert result["blocked_by"] == "middleware, load-test" + + def test_task_section_empty(): """Empty task list produces empty string.""" assert _task_section([]) == "" @@ -547,6 +554,17 @@ def test_task_section_structured_with_blocked_by(): assert "→ blocked by: freshness-impl" in out +def test_task_section_renders_list_valued_blocked_by(): + """A list-valued blocked_by renders as a clean comma-separated string, + not a raw Python list repr — consistent with `smriti current`.""" + tasks = [ + {"text": "Wire the limiter", "blocked_by": ["middleware", "load-test"]}, + ] + out = _task_section(tasks) + assert "→ blocked by: middleware, load-test" in out + assert "['" not in out # not the raw Python list repr + + def test_task_section_structured_with_done_status(): """Task with status=done renders inline marker.""" tasks = [