fix(plan): restore _apply_output_dict and fix explain_decision_cmd plan_id fallback
CI / load-versions (pull_request) Successful in 9s
CI / push-validation (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 24s
CI / typecheck (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 3m21s
CI / helm (pull_request) Successful in 3m25s
CI / quality (pull_request) Successful in 3m43s
CI / security (pull_request) Successful in 4m33s
CI / unit_tests (pull_request) Failing after 5m24s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 8m29s
CI / status-check (pull_request) Failing after 1s
CI / load-versions (pull_request) Successful in 9s
CI / push-validation (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 24s
CI / typecheck (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 3m21s
CI / helm (pull_request) Successful in 3m25s
CI / quality (pull_request) Successful in 3m43s
CI / security (pull_request) Successful in 4m33s
CI / unit_tests (pull_request) Failing after 5m24s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 8m29s
CI / status-check (pull_request) Failing after 1s
- Restore deleted _apply_output_dict function (104 lines) that builds the spec-required JSON envelope for plan apply output; used by robot integration tests via direct import in helper_plan_apply_json_envelope.py - Restore lifecycle_apply_plan to use _apply_output_dict with full envelope format_output call for non-RICH output formats - Remove re-introduced plan_id fallback from explain_decision_cmd that violated the explicit guard from issue #6325 (TDD regression test tdd_plan_explain_plan_id.feature); restore strict DecisionNotFoundError handling with typer.Exit(1) ISSUES CLOSED: #6325
This commit is contained in:
@@ -477,6 +477,106 @@ def _execute_output_dict(
|
||||
}
|
||||
|
||||
|
||||
def _apply_output_dict(
|
||||
plan: Any,
|
||||
started_at: datetime | None = None,
|
||||
duration_ms: int | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Build the spec-required JSON envelope for plan apply output."""
|
||||
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
|
||||
|
||||
if not isinstance(plan, LifecyclePlan):
|
||||
return {
|
||||
"command": "plan apply",
|
||||
"status": "ok",
|
||||
"exit_code": 0,
|
||||
"data": {"plan": str(plan)},
|
||||
"timing": {},
|
||||
"messages": ["Plan applied"],
|
||||
}
|
||||
|
||||
plan_id = plan.identity.plan_id
|
||||
|
||||
if plan.is_terminal:
|
||||
sandbox_cleanup: dict[str, object] = {
|
||||
"worktree": "removed",
|
||||
"branch": "merged to main",
|
||||
"checkpoint": "archived",
|
||||
}
|
||||
else:
|
||||
sandbox_cleanup = {
|
||||
"worktree": "active",
|
||||
"branch": "open",
|
||||
"checkpoint": "pending",
|
||||
}
|
||||
|
||||
validation: dict[str, object] = {
|
||||
"tests": {"status": "passed", "passed": 0, "total": 0},
|
||||
"lint": {"status": "passed", "warnings": 0},
|
||||
"type_check": {"status": "passed", "errors": 0},
|
||||
"duration_s": 0.0,
|
||||
}
|
||||
vs = getattr(plan, "validation_summary", None)
|
||||
if vs: # pragma: no cover
|
||||
for key in ("tests", "lint", "type_check"):
|
||||
value = vs.get(key)
|
||||
if value is not None:
|
||||
validation[key] = value
|
||||
|
||||
total_cost = "$0.00"
|
||||
if plan.cost_metadata and plan.cost_metadata.total_cost is not None:
|
||||
total_cost = f"${plan.cost_metadata.total_cost:.2f}"
|
||||
|
||||
child_plan_count: int = len(getattr(plan, "child_plan_ids", None) or [])
|
||||
|
||||
lifecycle: dict[str, object] = {
|
||||
"phase": plan.phase.value,
|
||||
"state": plan.processing_state.value,
|
||||
"total_duration": "0s",
|
||||
"total_cost": total_cost,
|
||||
"decisions_made": len(plan.decisions),
|
||||
"child_plans": child_plan_count,
|
||||
}
|
||||
|
||||
project_name: str | None = (
|
||||
plan.project_links[0].project_name if plan.project_links else None
|
||||
)
|
||||
|
||||
applied_at: str | None = (
|
||||
plan.timestamps.applied_at.isoformat()
|
||||
if plan.timestamps.applied_at is not None
|
||||
else None
|
||||
)
|
||||
|
||||
data: dict[str, object] = {
|
||||
"plan_id": plan_id,
|
||||
"artifacts": 0,
|
||||
"changes": {"insertions": 0, "deletions": 0},
|
||||
"project": project_name,
|
||||
"applied_at": applied_at,
|
||||
"validation": validation,
|
||||
"sandbox_cleanup": sandbox_cleanup,
|
||||
"lifecycle": lifecycle,
|
||||
}
|
||||
|
||||
timing: dict[str, object] = {}
|
||||
if applied_at is not None:
|
||||
timing["started"] = applied_at
|
||||
if started_at is not None: # pragma: no cover
|
||||
timing["started"] = started_at.isoformat()
|
||||
if duration_ms is not None: # pragma: no cover
|
||||
timing["duration_ms"] = duration_ms
|
||||
|
||||
return {
|
||||
"command": "plan apply",
|
||||
"status": "ok",
|
||||
"exit_code": 0,
|
||||
"data": data,
|
||||
"timing": timing,
|
||||
"messages": ["Plan applied"],
|
||||
}
|
||||
|
||||
|
||||
def _get_current_project() -> Project:
|
||||
"""Get the current project or exit with error.
|
||||
|
||||
@@ -2364,8 +2464,22 @@ def lifecycle_apply_plan(
|
||||
plan = service._complete_apply_if_queued(plan_id)
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
envelope = _apply_output_dict(plan)
|
||||
payload = cast("dict[str, Any]", envelope.get("data", {}))
|
||||
messages = cast(
|
||||
"list[str | dict[str, str]]",
|
||||
envelope.get("messages", ["Plan applied"]),
|
||||
)
|
||||
console.print(
|
||||
format_output(
|
||||
payload,
|
||||
fmt,
|
||||
command="plan apply",
|
||||
status="ok",
|
||||
exit_code=0,
|
||||
messages=messages,
|
||||
)
|
||||
)
|
||||
else:
|
||||
title = "Plan Applied" if plan.is_terminal else "Plan Applying"
|
||||
_print_lifecycle_plan(plan, title=title)
|
||||
@@ -4082,7 +4196,7 @@ def _build_explain_dict(
|
||||
def explain_decision_cmd(
|
||||
identifier: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Decision or Plan ULID to explain"),
|
||||
typer.Argument(help="Decision ULID to explain"),
|
||||
],
|
||||
fmt: Annotated[
|
||||
str,
|
||||
@@ -4097,7 +4211,7 @@ def explain_decision_cmd(
|
||||
typer.Option("--show-reasoning", help="Include rationale and actor reasoning"),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Explain a single decision or the root decision of a plan."""
|
||||
"""Explain a single decision in a plan."""
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.application.services.decision_service import (
|
||||
DecisionNotFoundError,
|
||||
@@ -4106,24 +4220,12 @@ def explain_decision_cmd(
|
||||
container = get_container()
|
||||
svc = container.decision_service()
|
||||
|
||||
# First, try treating the identifier as a decision_id (backward compat).
|
||||
decision = None
|
||||
with suppress(DecisionNotFoundError):
|
||||
# Look up the decision by its ULID.
|
||||
try:
|
||||
decision = svc.get_decision(identifier)
|
||||
|
||||
# If not found as a decision, try as a plan_id.
|
||||
if decision is None:
|
||||
decisions = svc.list_decisions(identifier)
|
||||
if decisions:
|
||||
# Find root decision (parent_decision_id is None)
|
||||
root_decisions = [d for d in decisions if d.parent_decision_id is None]
|
||||
decision = root_decisions[0] if root_decisions else decisions[0]
|
||||
|
||||
if decision is None:
|
||||
console.print(
|
||||
f"[red]Error:[/red] '{identifier}' not found as a decision or plan."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
except DecisionNotFoundError:
|
||||
console.print(f"[red]Error:[/red] '{identifier}' not found as a decision.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Fetch total decision count for "X of Y" sequence format.
|
||||
total_decisions: int | None = None
|
||||
|
||||
Reference in New Issue
Block a user