Add PlanApplyService with diff(), artifacts(), persist_apply_summary(), handle_merge_failure(), and guard_empty_changeset() methods. - plan diff: renders changeset in rich/plain/json/yaml formats - plan artifacts: shows plan metadata, changeset summary, sandbox refs - persist_apply_summary: stores files_changed/validations_run in plan metadata - handle_merge_failure: transitions plan to error state with conflict details - guard_empty_changeset: blocks apply on empty changeset (--allow-empty override) CLI: plan diff and plan artifacts commands with --format flag. Tests: 20 Behave scenarios, 8 Robot integration tests, 4 ASV benchmarks. Docs: docs/reference/plan_apply.md with CLI reference. Implements D0b.apply from the implementation plan.
4.2 KiB
Plan Diff & Apply Reference
This document describes the diff review and apply integration features added in D0b.apply.
CLI Commands
agents plan diff <plan_id>
Show the ChangeSet produced during Execute as a unified diff, grouped by resource path.
Options:
| Flag | Description |
|---|---|
--format, -f |
Output format: rich (default), plain, json, yaml |
Examples:
# Rich output with coloured operation labels
agents plan diff 01HXYZ...
# Plain unified-diff output
agents plan diff 01HXYZ... --format plain
# Machine-readable JSON
agents plan diff 01HXYZ... --format json
Output fields (JSON/YAML):
| Field | Type | Description |
|---|---|---|
changeset_id |
string | ULID of the ChangeSet |
plan_id |
string | ULID of the plan |
total_changes |
int | Number of change entries |
summary |
object | {creates, modifies, deletes, renames, paths_changed, resources_involved} |
entries |
list | Per-file entries with path, operation, hashes, timestamps |
agents plan artifacts <plan_id>
Show plan artifacts: ChangeSet metadata, sandbox references, file change list, and validation results.
Options:
| Flag | Description |
|---|---|
--format, -f |
Output format: rich (default), plain, json, yaml |
Output fields (JSON/YAML):
| Field | Type | Description |
|---|---|---|
plan_id |
string | Plan ULID |
phase |
string | Current lifecycle phase |
processing_state |
string | Current processing state |
changeset_id |
string | ChangeSet ULID (null if Execute not complete) |
sandbox_refs |
list | Active sandbox reference IDs |
changeset_summary |
object | Summary counts from SpecChangeSet |
files_changed |
list | {path, operation} per changed file |
validation_summary |
object | Validation results (if available) |
apply_summary |
object | Files changed count, validations run (after apply) |
Apply Integration
Empty ChangeSet Guard
The apply pipeline checks whether the ChangeSet has any entries before proceeding. If the ChangeSet is empty, apply is blocked with a clear message:
Plan <id> has an empty ChangeSet. No changes to apply. Use --allow-empty to override.
Set --allow-empty to bypass this check for plans that intentionally
produce no file changes (e.g. validation-only plans).
Apply Summary Persistence
When apply completes, the following metadata is stored in the plan:
apply_files_changed: Number of files written/modified/deletedapply_validations_run: Number of validation checks executedapply_completed_at: ISO-8601 timestamp of apply completion
This metadata is visible via agents plan status and agents plan artifacts.
Merge Failure Handling
When a sandbox merge fails during apply:
- The plan transitions to
ERROREDprocessing state error_messageis set to"Merge failed: <details>"error_detailsincludes:merge_conflict: Description of the conflictsandbox_rollback: Set to"pending"for cleanup
Recovery steps:
- Review conflict details via
agents plan status <id> - Re-run execute phase after resolving conflicts:
agents plan execute <id> - Or fix validation issues and retry apply
Processing State Flow
Execute/COMPLETE
|
v
Apply/QUEUED --> Apply/PROCESSING --> Apply/APPLIED (terminal success)
|
+--> Apply/ERRORED (merge failure)
+--> Apply/CONSTRAINED (invariant violation)
Service API
PlanApplyService
from cleveragents.application.services.plan_apply_service import PlanApplyService
service = PlanApplyService(
lifecycle_service=lifecycle,
changeset_store=store, # optional InMemoryChangeSetStore
)
# Generate diff
diff_text = service.diff(plan_id, fmt="json")
# Get artifacts
artifacts = service.artifacts(plan_id, fmt="json")
# Guard empty changeset
service.guard_empty_changeset(plan_id, allow_empty=False)
# Persist apply summary
service.persist_apply_summary(plan_id, files_changed=5, validations_run=3)
# Handle merge failure
service.handle_merge_failure(plan_id, conflict_details="...")