Merge branch 'master' into tdd/m5-context-tier-runtime
CI / lint (pull_request) Successful in 26s
CI / security (pull_request) Successful in 37s
CI / quality (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 1m15s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 22s
CI / unit_tests (pull_request) Successful in 2m17s
CI / integration_tests (pull_request) Successful in 2m48s
CI / docker (pull_request) Successful in 2m8s
CI / e2e_tests (pull_request) Successful in 6m34s
CI / coverage (pull_request) Successful in 4m47s
CI / benchmark-regression (pull_request) Successful in 38m11s

This commit is contained in:
2026-03-19 22:14:57 +00:00
committed by Forgejo
7 changed files with 67 additions and 49 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ Feature: Plan lifecycle CLI coverage
When I run plan execute without a plan id with 1 complete plans
Then the plan lifecycle command should succeed
And the execute command should run the single ready plan
And the plan lifecycle output should contain "Execute phase"
And the plan lifecycle output should contain "Plan Executed"
Scenario: Plan execute handles invalid phase transition
When I run plan execute for plan id "01ARZ3NDEKTSV4RRFFQ69G5FAV" causing "invalid transition"
@@ -159,6 +159,7 @@ def step_pec_mock_decision_none(context: Context) -> None:
context.pec_decision_id = str(ULID())
svc = MagicMock()
svc.get_decision.return_value = None
svc.list_decisions.return_value = []
context.pec_container = _mock_container_with_decision_svc(svc)
@@ -650,14 +650,12 @@ def step_invoke_execute_plan_queued(context):
executed_plan = _make_mock_lifecycle_plan(phase="execute", state="queued")
completed_plan = _make_mock_lifecycle_plan(phase="execute", state="complete")
# get_plan call sequence:
# 1. read-only check (pre_plan)
# 2. strategize state inspection (current_plan)
# 3. re-fetch after inline strategize
# 4. inline execute state inspection
# 5. re-fetch after inline execute
# get_plan call sequence (consolidated read-only + phase check):
# 1. current_plan (null + read-only + strategize state)
# 2. re-fetch after inline strategize
# 3. inline execute state inspection
# 4. re-fetch after inline execute
mock_service.get_plan.side_effect = [
queued_plan,
queued_plan,
executed_plan,
executed_plan,
@@ -702,14 +700,12 @@ def step_invoke_execute_plan_auto_progressed(context):
executed_plan = _make_mock_lifecycle_plan(phase="execute", state="queued")
completed_plan = _make_mock_lifecycle_plan(phase="execute", state="complete")
# get_plan call sequence:
# 1. read-only check (pre_plan)
# 2. strategize state inspection (current_plan)
# 3. re-fetch after inline strategize → execute/queued
# 4. inline execute state inspection → execute/queued (triggers run_execute)
# 5. re-fetch after inline execute → execute/complete
# get_plan call sequence (consolidated read-only + phase check):
# 1. current_plan (null + read-only + strategize state)
# 2. re-fetch after inline strategize → execute/queued
# 3. inline execute state inspection → execute/queued (triggers run_execute)
# 4. re-fetch after inline execute → execute/complete
mock_service.get_plan.side_effect = [
queued_plan,
queued_plan,
executed_plan,
executed_plan,
@@ -141,13 +141,11 @@ def step_plan_in_strategize_queued(context: Context) -> None:
)
# get_plan call sequence in the CLI execute_plan handler:
# 1. pre_plan (read-only check)
# 2. current_plan (phase detection)
# 3. re-fetch after run_strategize (auto_progress moved to Execute)
# 4. re-fetch for inline execute check
# 5. re-fetch after run_execute
# 1. current_plan (phase detection + read-only check — merged)
# 2. re-fetch after run_strategize (auto_progress moved to Execute)
# 3. re-fetch for inline execute check
# 4. re-fetch after run_execute
context.mock_service_967.get_plan.side_effect = [
queued_plan,
queued_plan,
execute_queued_plan,
execute_queued_plan,
@@ -340,13 +338,11 @@ def step_single_queued_plan_auto_discovery(context: Context) -> None:
]
# get_plan call sequence after auto-discovery selects the plan:
# 1. pre_plan (read-only check)
# 2. current_plan (phase detection)
# 3. re-fetch after run_strategize
# 4. re-fetch for inline execute check
# 5. re-fetch after run_execute
# 1. current_plan (phase detection + read-only check — merged)
# 2. re-fetch after run_strategize
# 3. re-fetch for inline execute check
# 4. re-fetch after run_execute
context.mock_service_967.get_plan.side_effect = [
queued_plan,
queued_plan,
execute_queued_plan,
execute_queued_plan,
+1 -1
View File
@@ -1,4 +1,4 @@
@tdd_expected_fail @tdd_bug @tdd_bug_968 @mock_only
@tdd_bug @tdd_bug_968 @mock_only
Feature: TDD Bug #968 — plan explain expects decision_id but test passes plan_id
As a developer
I want to verify that `plan explain <plan_id>` succeeds when given a plan ID
+2 -2
View File
@@ -17,7 +17,7 @@ TDD Plan Explain Succeeds With Plan ID
[Documentation] Verify that ``plan explain <plan_id>`` exits with rc=0
... when given a plan ID that has associated decisions.
... Bug #968: the command currently exits with rc=1.
[Tags] tdd_expected_fail tdd_bug tdd_bug_968
[Tags] tdd_bug tdd_bug_968
${result}= Run Process ${PYTHON} ${HELPER} explain-with-plan-id cwd=${WORKSPACE} timeout=60s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
@@ -28,7 +28,7 @@ TDD Plan Explain With Plan ID Shows Root Question
[Documentation] Verify that ``plan explain <plan_id>`` output contains
... the root decision question when given a plan ID.
... Bug #968: the command fails before rendering any output.
[Tags] tdd_expected_fail tdd_bug tdd_bug_968
[Tags] tdd_bug tdd_bug_968
${result}= Run Process ${PYTHON} ${HELPER} explain-plan-id-shows-question cwd=${WORKSPACE} timeout=60s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
+44 -19
View File
@@ -10,7 +10,7 @@ plan lifecycle.
| ``agents plan use`` | Create plan from action + project(s) |
| ``agents plan lifecycle-list``| List plans with optional filters |
| ``agents plan status`` | Show plan status / details |
| ``agents plan execute`` | Transition to Execute phase |
| ``agents plan execute`` | Run phase-aware plan execution |
| ``agents plan lifecycle-apply``| Transition to Apply phase |
| ``agents plan cancel`` | Cancel a non-terminal plan |
| ``agents plan diff`` | Show ChangeSet as unified diff |
@@ -1824,20 +1824,19 @@ def execute_plan(
pre.execution_environment = execution_environment.lower()
service._commit_plan(pre)
# Fail-fast: read-only plans must not enter Execute phase
pre_plan = service.get_plan(plan_id)
if pre_plan is not None and pre_plan.read_only is True:
console.print(
f"[red]Cannot execute plan '{plan_id}': plan is read-only.[/red]"
)
raise typer.Abort()
# Determine current phase and run the appropriate processing
current_plan = service.get_plan(plan_id)
if current_plan is None:
console.print(f"[red]Plan '{plan_id}' not found.[/red]")
raise typer.Abort()
# Fail-fast: read-only plans must not enter Execute phase
if current_plan.read_only is True:
console.print(
f"[red]Cannot execute plan '{plan_id}': plan is read-only.[/red]"
)
raise typer.Abort()
# If the plan is still in Strategize/queued, run the strategize
# phase inline before transitioning to Execute. This implements
# the auto_strategize behaviour described in the specification:
@@ -1888,11 +1887,21 @@ def execute_plan(
data = _plan_spec_dict(plan)
console.print(format_output(data, fmt))
else:
_print_lifecycle_plan(plan, title="Plan Executing")
console.print(
"\n[dim]Plan is now in Execute phase. "
"Run 'agents plan apply <id>' when ready.[/dim]"
)
_print_lifecycle_plan(plan, title="Plan Executed")
phase_label = f"{plan.phase.value}/{plan.state.value}"
if plan.phase == PlanPhase.EXECUTE and plan.state in (
ProcessingState.COMPLETE,
ProcessingState.APPLIED,
):
console.print(
f"\n[dim]Plan execution completed ({phase_label}). "
"Run 'agents plan lifecycle-apply <id>' when ready.[/dim]"
)
else:
console.print(
f"\n[dim]Plan is now in {phase_label} state. "
"Run 'agents plan execute <id>' to continue.[/dim]"
)
except PreflightRejection as e:
console.print(f"[red]Pre-flight check failed:[/red] {e}")
@@ -3123,9 +3132,9 @@ def _build_explain_dict(
@app.command("explain")
def explain_decision_cmd(
decision_id: Annotated[
identifier: Annotated[
str,
typer.Argument(help="Decision ULID to explain"),
typer.Argument(help="Decision or Plan ULID to explain"),
],
fmt: Annotated[
str,
@@ -3140,14 +3149,30 @@ def explain_decision_cmd(
typer.Option("--show-reasoning", help="Include rationale and actor reasoning"),
] = False,
) -> None:
"""Explain a single decision in the plan decision tree."""
"""Explain a single decision or the root decision of a plan."""
from cleveragents.application.container import get_container
from cleveragents.application.services.decision_service import (
DecisionNotFoundError,
)
container = get_container()
svc = container.decision_service()
decision = svc.get_decision(decision_id)
# First, try treating the identifier as a decision_id (backward compat).
decision = None
with suppress(DecisionNotFoundError):
decision = svc.get_decision(identifier)
# If not found as a decision, try as a plan_id.
if decision is None:
console.print(f"[red]Error:[/red] Decision '{decision_id}' not found.")
decisions = svc.list_decisions(identifier)
if decisions:
decision = decisions[0]
if decision is None:
console.print(
f"[red]Error:[/red] '{identifier}' not found as a decision or plan."
)
raise typer.Exit(1)
data = _build_explain_dict(