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
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:
@@ -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,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user