diff --git a/CHANGELOG.md b/CHANGELOG.md index aab6227ab..bce648a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Changed `wf10_batch.robot` to be less likely to create files, and `plan_generation_graph.robot` to give more test answers. ## [Unreleased] +- **Plan Prompt JSON Timing Field** (#9353): `agents plan prompt --format json` now + includes `timing.started` as an ISO 8601 UTC timestamp in the JSON envelope, + matching the spec (§CLI Commands — `agents plan prompt`). Extended + `cleveragents.cli.formatting.format_output` (and `_build_envelope`) with an + optional `started_at: datetime` parameter; when provided, the envelope's + `timing` dict includes a `started` field alongside `duration_ms`. Refactored + `prompt_plan_cmd` to delegate envelope construction to `format_output` so the + envelope keys (`command`, `status`, `data`, `timing.started`, `messages`) are + populated correctly at the JSON root rather than nested under a synthetic + inner `data` field. - **fix(plan): NamespacedName digit-start validation** (#2145, #2147): `NamespacedName` field validators now reject `namespace` and `name` components whose first character is a digit, raising `pydantic.ValidationError` with message `"must start with a letter"`. BDD constructor scenarios updated to use the `"a Pydantic ValidationError should be raised"` step so the assertion correctly matches the exception type raised by Pydantic model construction. - **fix(cli/plan): plan correct JSON output envelope fix and BDD test coverage** (#8584 / PR #8662): Restructured `agents plan correct --format json` output to nest correction fields under `data.correction` (e.g., `data.correction.mode`) and populate the spec-required CLI envelope with `command="plan correct"`, `status`, `exit_code`, `timing`, and `messages` fields. Added three BDD scenarios in `features/tdd_plan_correct_json_output.feature` validating the envelope structure for both revert and append modes. - **fix(cli): add --url flag to resource add for git resource type** (#6322): Added support for the `--url` flag on `agents resource add git` command, allowing users to specify a remote URL for git resources. The flag is validated to only apply to git resource types. Includes Behave BDD tests in `features/resource_cli_git_url_flag.feature` and Robot Framework integration tests verifying correct URL validation and CLI behavior. diff --git a/features/plan_prompt_command.feature b/features/plan_prompt_command.feature index 3bce89e63..3dc97c150 100644 --- a/features/plan_prompt_command.feature +++ b/features/plan_prompt_command.feature @@ -7,7 +7,7 @@ Feature: Plan prompt command When I run plan command help Then help output should include plan prompt command - @tdd_issue @tdd_issue_4255 @tdd_expected_fail + @tdd_issue @tdd_issue_4255 Scenario: Plan prompt delivers guidance and returns spec envelope in json Given a mocked lifecycle service prompt response When I run plan prompt with plan id "01HXM8C2ZK4Q7C2B3F2R4VYV6J" and guidance "Use mocks for database tests" in format "json" diff --git a/features/steps/plan_prompt_command_steps.py b/features/steps/plan_prompt_command_steps.py index 719632c38..8f1c9f82b 100644 --- a/features/steps/plan_prompt_command_steps.py +++ b/features/steps/plan_prompt_command_steps.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +from datetime import datetime from unittest.mock import MagicMock, patch from behave import given, then, when @@ -113,6 +114,15 @@ def step_prompt_envelope_data(context) -> None: queue = data.get("queue") assert isinstance(queue, dict) assert queue.get("pending") == 1 + # Verify timing.started is present and is a valid ISO timestamp + timing = payload.get("timing") + assert isinstance(timing, dict) + assert "started" in timing + assert "duration_ms" in timing + started = timing.get("started") + assert isinstance(started, str) + # Verify it is a valid ISO 8601 timestamp + datetime.fromisoformat(started) @then('prompt output should include guidance text "{guidance}"') diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index 671b0d803..20f88453e 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -3218,6 +3218,7 @@ def prompt_plan_cmd( The guidance is injected as a ``user_intervention`` decision and is queued for the next execution step. """ + started_at = datetime.now(UTC) started = time.monotonic() try: service = _get_lifecycle_service() @@ -3229,16 +3230,35 @@ def prompt_plan_cmd( {"plan_id": plan_id, "guidance": guidance}, ) - envelope: dict[str, object] = { - "command": "plan prompt", - "status": "ok", - "exit_code": 0, - "data": prompt_data, - "timing": {"duration_ms": elapsed_ms}, - "messages": ["Guidance queued"], - } + if fmt in (OutputFormat.JSON.value, OutputFormat.YAML.value): + # Spec-required envelope: command/status/data/timing.started populated + # at the JSON/YAML root via format_output's envelope builder. + console.print( + format_output( + prompt_data, + fmt, + command="plan prompt", + status="ok", + messages=["Guidance queued"], + started_at=started_at, + ) + ) + return if fmt != OutputFormat.RICH.value: + # Legacy envelope-wrapping for table/plain/color formats, which + # render the envelope dict directly (no separate envelope builder). + envelope: dict[str, object] = { + "command": "plan prompt", + "status": "ok", + "exit_code": 0, + "data": prompt_data, + "timing": { + "started": started_at.isoformat(), + "duration_ms": elapsed_ms, + }, + "messages": ["Guidance queued"], + } console.print(format_output(envelope, fmt)) return diff --git a/src/cleveragents/cli/formatting.py b/src/cleveragents/cli/formatting.py index c1d14c86b..608c063bc 100644 --- a/src/cleveragents/cli/formatting.py +++ b/src/cleveragents/cli/formatting.py @@ -263,18 +263,22 @@ def _build_envelope( exit_code: int, duration_ms: int, messages: Sequence[str | dict[str, str]], + started_at: datetime | None = None, ) -> dict[str, Any]: """Build the spec-required JSON/YAML output envelope.""" if status not in _VALID_STATUSES: raise ValueError(f"status must be one of {_VALID_STATUSES!r}, got {status!r}") if exit_code < 0: raise ValueError(f"exit_code must be non-negative, got {exit_code!r}") + timing: dict[str, Any] = {"duration_ms": duration_ms} + if started_at is not None: + timing["started"] = started_at.isoformat() return { "command": command, "status": status, "exit_code": exit_code, "data": data, - "timing": {"duration_ms": duration_ms}, + "timing": timing, "messages": list(messages), } @@ -287,6 +291,7 @@ def format_output( status: str = "ok", exit_code: int = 0, messages: Sequence[str | Mapping[str, Any]] | None = None, + started_at: datetime | None = None, ) -> str: """Format *data* according to *format_type*. @@ -350,6 +355,7 @@ def format_output( exit_code, duration_ms, envelope_messages, + started_at=started_at, ) rendered = _format_json(envelope) elif fmt == OutputFormat.YAML.value: @@ -361,6 +367,7 @@ def format_output( exit_code, duration_ms, envelope_messages, + started_at=started_at, ) rendered = _format_yaml(envelope) else: