fix(plan): wrap plan tree JSON/YAML output in spec-required command envelope.
CI / lint (pull_request) Failing after 26s
CI / typecheck (pull_request) Successful in 1m3s
CI / quality (pull_request) Successful in 37s
CI / security (pull_request) Successful in 58s
CI / coverage (pull_request) Has been skipped
CI / build (pull_request) Successful in 25s
CI / push-validation (pull_request) Successful in 24s
CI / helm (pull_request) Successful in 34s
CI / e2e_tests (pull_request) Failing after 4m14s
CI / integration_tests (pull_request) Successful in 4m21s
CI / unit_tests (pull_request) Failing after 5m59s
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 1s

This commit is contained in:
2026-04-14 14:40:27 +00:00
parent acc5f01155
commit 94305a67f1
+140 -1
View File
@@ -4374,6 +4374,144 @@ def _get_decision_label(decision_type: str, per_type_ordinal: int = 0) -> str:
return base_label
def _build_tree_envelope(
plan_id: str,
tree_data: list[dict[str, object]],
decisions: list[Decision],
show_superseded: bool = False,
) -> dict[str, object]:
"""Build the spec-required envelope for ``agents plan tree --format json/yaml``."""
from datetime import timezone
filtered = (
decisions if show_superseded else [d for d in decisions if not d.is_superseded]
)
def count_nodes(nodes: list[dict[str, object]]) -> int:
count = 0
for node in nodes:
count += 1
children = node.get("children", [])
if isinstance(children, list):
count += count_nodes(children)
return count
def compute_depth(nodes: list[dict[str, object]]) -> int:
if not nodes:
return 0
max_depth = 0
for node in nodes:
children = node.get("children", [])
if isinstance(children, list) and children:
max_depth = max(max_depth, 1 + compute_depth(children))
return max_depth
nodes_count = count_nodes(tree_data)
tree_depth = compute_depth(tree_data)
child_plan_ids: set[str] = set()
for d in filtered:
if d.decision_type in ("subplan_spawn", "subplan_parallel_spawn"):
if hasattr(d, "plan_id") and d.plan_id:
child_plan_ids.add(d.plan_id)
child_plans_count = len(child_plan_ids)
child_plans_str = f"{child_plans_count}+" if child_plans_count > 0 else "0"
invariants_count = sum(
1 for d in filtered if d.decision_type == "invariant_enforced"
)
superseded_count = sum(1 for d in decisions if d.is_superseded)
summary = {
"nodes": nodes_count,
"depth": tree_depth,
"child_plans": child_plans_str,
"invariants": invariants_count,
"superseded": superseded_count,
}
type_counts: dict[str, int] = {}
decision_ids: dict[str, str] = {}
for d in filtered:
type_counts[d.decision_type] = type_counts.get(d.decision_type, 0) + 1
ordinal = type_counts[d.decision_type]
if d.decision_type == "prompt_definition":
key = "root"
elif d.decision_type == "invariant_enforced":
key = f"invariant_{ordinal}"
elif d.decision_type == "strategy_choice":
key = "strategy"
elif d.decision_type == "implementation_choice":
key = f"implementation_{ordinal}"
elif d.decision_type == "subplan_spawn":
key = f"spawn_{ordinal}"
elif d.decision_type == "subplan_parallel_spawn":
key = f"parallel_{ordinal}"
else:
key = f"{d.decision_type}_{ordinal}"
decision_ids[key] = d.decision_id
child_plans_list: list[dict[str, object]] = []
for d in filtered:
if d.decision_type in ("subplan_spawn", "subplan_parallel_spawn"):
if hasattr(d, "plan_id") and d.plan_id:
child_plans_list.append(
{
"id": d.plan_id,
"phase": "execute",
"state": "queued",
}
)
timing: dict[str, object] = {
"started": datetime.now(timezone.utc).isoformat(),
"duration_ms": 0,
}
def convert_tree_node(node: dict[str, object]) -> dict[str, object]:
"""Convert internal tree node format to spec format."""
spec_node: dict[str, object] = {
"type": node.get("type"),
"description": node.get("question"),
}
if node.get("confidence") is not None:
spec_node["confidence"] = node.get("confidence")
if node.get("type") in ("subplan_spawn", "subplan_parallel_spawn"):
spec_node["plan_id"] = node.get("plan_id", "")
children = node.get("children", [])
if isinstance(children, list) and children:
spec_node["children"] = [convert_tree_node(child) for child in children]
return spec_node
spec_tree = convert_tree_node(tree_data[0]) if tree_data else {}
data: dict[str, object] = {
"plan_id": plan_id,
"tree": spec_tree,
"summary": summary,
"child_plans": child_plans_list,
"decision_ids": decision_ids,
}
return {
"command": "plan tree",
"status": "ok",
"exit_code": 0,
"data": data,
"timing": timing,
"messages": ["Decision tree rendered"],
}
@app.command("tree")
def tree_decisions_cmd(
plan_id: Annotated[
@@ -4410,7 +4548,8 @@ def tree_decisions_cmd(
)
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
console.print(format_output(tree_data, fmt))
envelope = _build_tree_envelope(plan_id, tree_data, decisions, show_superseded)
console.print(format_output(envelope, fmt))
elif fmt == OutputFormat.TABLE:
# Flatten for table view
filtered = (