diff --git a/CHANGELOG.md b/CHANGELOG.md index 905c8b89c..062d90df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). from the TDD test so both scenarios run as normal regression guards. (#988) ### Fixed +- **Wrapped plan tree JSON/YAML output in spec-required command envelope (#9313)**: The + ``agents plan tree`` CLI command's JSON and YAML output paths now include the + standardised envelope fields (``command``, ``status``, ``exit_code``, ``data``, + ``timing``, ``messages``) as required by the specification §Output Rendering + Framework. Previously the envelope was created with an empty ``command`` field, + which broke programmatic consumers that parse the ``command`` key to identify + the source command. Updated step definitions to validate against the envelope + structure (checking ``data`` for the tree array). Removed ``@tdd_expected_fail`` + tag from the JSON format BDD scenario so it runs as a normal regression guard. + - **Actor CLI NAME argument made optional, derived from YAML config** (#4186): The `agents actor add` positional ``NAME`` argument is now optional (defaults to ``None``). When omitted, the actor name is derived from the ``name`` field in diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b7d1bc7a0..cf80688e3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -35,3 +35,6 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files. * HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation. * HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase. + + +* HAL 9000 has contributed the plan tree JSON/YAML command envelope fix (PR #9313): updated ``tree_decisions_cmd`` to pass ``command="plan tree"`` to ``format_output()`` so that machine-readable output conforms to the spec-required command envelope structure. diff --git a/features/plan_explain.feature b/features/plan_explain.feature index 7051cfc33..63dd8ba9f 100644 --- a/features/plan_explain.feature +++ b/features/plan_explain.feature @@ -107,12 +107,12 @@ Feature: Plan explain and decision tree CLI commands # plan tree - json format # ------------------------------------------------------------------ - @tdd_issue @tdd_issue_4254 @tdd_expected_fail Scenario: Tree with json format Given a set of test decisions forming a tree When I format the tree as json Then the json tree output should be valid json And the json tree output should contain "decision_id" + And the json tree envelope should contain "command" field equal to "plan tree" # ------------------------------------------------------------------ # plan tree - yaml format diff --git a/features/steps/plan_explain_steps.py b/features/steps/plan_explain_steps.py index 52f66681a..7ea686fd1 100644 --- a/features/steps/plan_explain_steps.py +++ b/features/steps/plan_explain_steps.py @@ -402,14 +402,31 @@ def step_roots_no_children(context: Context) -> None: @then("the json tree output should be valid json") def step_tree_json_valid(context: Context) -> None: parsed = json.loads(context.pe_tree_json) - assert isinstance(parsed, list), "Expected a JSON array" + assert isinstance(parsed, dict), f"Expected a JSON envelope object, got {type(parsed).__name__}" + assert "data" in parsed, f"Expected 'data' in envelope, keys: {list(parsed.keys())}" + assert isinstance(parsed["data"], list), "Expected 'data' field to be a JSON array (tree)" @then('the json tree output should contain "{text}"') def step_tree_json_contains(context: Context, text: str) -> None: + parsed = json.loads(context.pe_tree_json) assert text in context.pe_tree_json, f"Expected '{text}' in tree JSON" + # Verify the text is within the data (envelope payload). + if isinstance(parsed.get("data"), list): + data_str = json.dumps(parsed["data"]) + assert text in data_str, f"Expected '{text}' in parsed 'data' field" + +@then('the json tree envelope should contain "{field}" field equal to "{value}"') +def step_tree_envelope_has_field(context: Context, field: str, value: str) -> None: + """Validate that the JSON command envelope has a specific top-level field with a given value.""" + parsed = json.loads(context.pe_tree_json) + assert field in parsed, f"Expected '{field}' in envelope at top level. Keys: {list(parsed.keys())}" + assert parsed[field] == value, ( + f"Envelope field '{field}' expected '{value}', got '{parsed[field]}'" + ) + @then('the yaml tree output should contain "{text}"') def step_tree_yaml_contains(context: Context, text: str) -> None: assert text in context.pe_tree_yaml, f"Expected '{text}' in tree YAML" diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index b2a7f64d0..3b6e4478b 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -4153,7 +4153,15 @@ def tree_decisions_cmd( ) if fmt in (OutputFormat.JSON, OutputFormat.YAML): - console.print(format_output(tree_data, fmt)) + console.print( + format_output( + tree_data, + fmt, + command="plan tree", + status="ok", + exit_code=0, + ) + ) elif fmt == OutputFormat.TABLE: # Flatten for table view filtered = (