fix(cli): implement spec-required structured panels in agents plan explain rich output

Replace the single flat 'Decision Details' table in explain_decision_cmd()
with six structured Rich panels matching the spec exactly:

- Decision panel: ID, Type, Question, Chosen, Confidence, Plan, Sequence, Created
- Alternatives Considered panel: numbered list with '(chosen)' marker
- Impact panel: Downstream Decisions, Downstream Child Plans, Artifacts Produced,
  Correction Impact (derived from downstream_decision_ids count)
- Context Snapshot panel (--show-context): resource paths + hot context hash
- Rationale panel (--show-reasoning): rationale text
- Correction panel: 'agents plan correct <ID> --mode revert --guidance "..."' hint

Also adds the missing success message '✓ OK Decision explained'.

Updates _build_explain_dict() to:
- Accept total_decisions parameter for 'X of Y' sequence format
- Return structured alternatives list [{index, description, chosen}] instead
  of flat string list
- Include impact dict with downstream_decisions, downstream_child_plans,
  artifacts_produced, correction_impact
- Include correction_hint field

Updates feature files and step definitions to test the new output structure.

ISSUES CLOSED: #2815
This commit is contained in:
2026-04-05 09:02:44 +00:00
committed by drew
parent 0fe78e7eff
commit 56164b7937
5 changed files with 441 additions and 557 deletions
+34 -2
View File
@@ -14,6 +14,8 @@ Feature: Plan explain and decision tree CLI commands
And the explain dict should contain key "chosen"
And the explain dict should contain key "type"
And the explain dict should contain key "alternatives"
And the explain dict should contain key "impact"
And the explain dict should contain key "correction_hint"
And the explain dict should not contain key "rationale"
And the explain dict should not contain key "context_snapshot"
@@ -47,8 +49,38 @@ Feature: Plan explain and decision tree CLI commands
When I build the explain dict with default options
Then the explain dict should contain key "alternatives"
And the alternatives list should have 2 items
And each alternative should have keys "index", "description", and "chosen"
And exactly one alternative should have chosen set to true
# ------------------------------------------------------------------
# plan explain - impact dict always included
# ------------------------------------------------------------------
Scenario: Explain includes impact dict by default
Given a test decision for explain
When I build the explain dict with default options
Then the explain dict should contain key "impact"
And the impact dict should contain key "downstream_decisions"
And the impact dict should contain key "downstream_child_plans"
And the impact dict should contain key "artifacts_produced"
And the impact dict should contain key "correction_impact"
# ------------------------------------------------------------------
# plan explain - correction_hint always included
# ------------------------------------------------------------------
Scenario: Explain includes correction_hint by default
Given a test decision for explain
When I build the explain dict with default options
Then the explain dict should contain key "correction_hint"
And the correction hint should reference the decision id
# ------------------------------------------------------------------
# plan explain - sequence format
# ------------------------------------------------------------------
Scenario: Explain sequence shows X of Y when total provided
Given a test decision for explain
When I build the explain dict with total decisions 5
Then the sequence should be formatted as "X of Y"
# ------------------------------------------------------------------
# plan explain - json format
+2 -2
View File
@@ -12,7 +12,7 @@ Feature: Plan explain and tree CLI command coverage
Given pec a mock DecisionService returning a valid decision
When pec I invoke "explain" with the decision id
Then pec the exit code should be 0
And pec the output should contain "Decision Details"
And pec the output should contain "Decision"
And pec the output should contain "decision_id"
Scenario: Explain CLI renders json format
@@ -49,7 +49,7 @@ Feature: Plan explain and tree CLI command coverage
Given pec a mock DecisionService returning a decision with alternatives
When pec I invoke "explain" with the decision id
Then pec the exit code should be 0
And pec the output should contain "alternatives"
And pec the output should contain "Alternatives Considered"
# ------------------------------------------------------------------
# tree_decisions_cmd - rich tree format
@@ -106,6 +106,8 @@ def _make_decision(
def _mock_container_with_decision_svc(svc_mock: MagicMock) -> MagicMock:
# Ensure count_decisions returns an int for sequence formatting.
svc_mock.count_decisions.return_value = 3
container = MagicMock()
container.decision_service.return_value = svc_mock
return container
+31
View File
@@ -238,6 +238,13 @@ def step_build_explain_reasoning(context: Context) -> None:
)
@when("I build the explain dict with total decisions {total:d}")
def step_build_explain_with_total(context: Context, total: int) -> None:
context.pe_explain_dict = _build_explain_dict(
context.pe_decision, total_decisions=total
)
def _capture_format_output(data, fmt):
"""Call format_output capturing stdout (machine-readable formats write there)."""
from contextlib import redirect_stdout
@@ -321,6 +328,15 @@ def step_snapshot_has_key(context: Context, key: str) -> None:
assert key in snap, f"Expected key '{key}' in snapshot: {list(snap.keys())}"
@then('the impact dict should contain key "{key}"')
def step_impact_has_key(context: Context, key: str) -> None:
impact = context.pe_explain_dict["impact"]
assert isinstance(impact, dict), (
f"Expected 'impact' to be a dict, got {type(impact)}"
)
assert key in impact, f"Expected key '{key}' in impact: {list(impact.keys())}"
@then("the alternatives list should have {count:d} items")
def step_alternatives_count(context: Context, count: int) -> None:
alts = context.pe_explain_dict["alternatives"]
@@ -352,6 +368,21 @@ def step_exactly_one_chosen(context: Context) -> None:
)
@then("the correction hint should reference the decision id")
def step_correction_hint_has_decision_id(context: Context) -> None:
hint = context.pe_explain_dict["correction_hint"]
decision_id = context.pe_explain_dict["decision_id"]
assert decision_id in str(hint), (
f"Expected decision_id '{decision_id}' in correction_hint '{hint}'"
)
@then('the sequence should be formatted as "X of Y"')
def step_sequence_x_of_y(context: Context) -> None:
seq = context.pe_explain_dict["sequence"]
assert " of " in str(seq), f"Expected sequence in 'X of Y' format, got '{seq}'"
@then('the json output should contain "{text}"')
def step_json_contains(context: Context, text: str) -> None:
assert text in context.pe_json_output, f"Expected '{text}' in JSON output"
File diff suppressed because it is too large Load Diff