fix(cli): add timing.started ISO timestamp to plan prompt JSON envelope #9447
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user