diff --git a/.agentic-workspace/planning/execplans/archive/close-item-blocked-status.plan.json b/.agentic-workspace/planning/execplans/archive/close-item-blocked-status.plan.json new file mode 100644 index 00000000..fff42e05 --- /dev/null +++ b/.agentic-workspace/planning/execplans/archive/close-item-blocked-status.plan.json @@ -0,0 +1,320 @@ +{ + "kind": "planning-execplan/v1", + "title": "Close-item blocked archive closeout status", + "execplan_profile": { + "schema": "execplan-profile/v1", + "task_shape": "bounded", + "required_core": [ + "kind", + "title", + "canonical_core", + "goal", + "non_goals", + "active_milestone", + "validation_commands", + "completion_criteria" + ], + "optional_sections": [ + "intent_continuity", + "intent_interpretation", + "execution_bounds", + "stop_conditions", + "context_budget", + "delegated_judgment", + "post_decomposition_delegation" + ], + "projection_rule": "canonical_core is authoritative for intent, scope, next action, proof, continuation, and closeout; legacy fields remain compatibility projections." + }, + "canonical_core": { + "requested_outcome": "GitHub #1216: close-item JSON must make blocked archive closeout obvious.", + "hard_constraints": "Keep scope to close-item/archive result reporting and focused Planning package tests; do not redesign archive closeout policy or force non-zero CLI exits in this slice.", + "agent_may_decide": "Exact field names for the machine-readable close-item outcome, provided they expose blocked/closed status, mutation state, and next action without hiding existing warnings/actions.", + "escalate_when": "The fix requires changing archive gate semantics, generated command IR, or broad lifecycle result contracts outside close-item reporting.", + "next_action": "Add close-item status fields for blocked archive results, cover them in Planning tests, then run focused and package validation.", + "proof_expectations": [ + "uv run pytest packages/planning/tests/test_close_item.py -q", + "make test-planning", + "make lint-planning", + "make typecheck-planning" + ], + "touched_scope": [ + "packages/planning/src/repo_planning_bootstrap/installer.py", + "packages/planning/tests/test_close_item.py" + ], + "completion_criteria": [ + "close-item JSON includes clear blocked/closed status for archive-flow no-ops.", + "blocked close-item output includes a top-level next_action that names the required repair.", + "successful close-item behavior and non-blocking retention warnings remain compatible.", + "Focused Planning tests cover the blocked archive closeout case." + ], + "continuation_owner": "none", + "closeout_decision": "archive-and-close" + }, + "goal": [ + "GitHub #1216" + ], + "non_goals": [ + "Leave adjacent backlog or follow-on work out of this plan." + ], + "machine_readable_contract": { + "intent": { + "outcome": "GitHub #1216: close-item JSON must make blocked archive closeout obvious.", + "constraints": "Keep scope to close-item/archive result reporting and focused Planning package tests.", + "latitude": "Choose the smallest compatible status payload that preserves existing warnings and actions.", + "escalation": "Escalate if the fix requires changing archive gate semantics, generated command IR, or broad lifecycle result contracts.", + "proof": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py" + }, + "execution": { + "milestone": "close-item-blocked-status", + "status": "active", + "next_step": "Implement close-item blocked status fields and focused tests.", + "proof": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning" + }, + "scope": { + "touched": [ + "packages/planning/src/repo_planning_bootstrap/installer.py", + "packages/planning/tests/test_close_item.py" + ], + "invariants": [ + "Preserve the planning contract and keep the work bounded to this plan." + ] + } + }, + "intent_continuity": { + "larger intended outcome": "GitHub #1216", + "this slice completes the larger intended outcome": "yes", + "continuation surface": "none" + }, + "required_continuation": { + "required follow-on for the larger intended outcome": "no", + "owner surface": "none", + "activation trigger": "none" + }, + "iterative_follow_through": { + "what this slice enabled": "none yet", + "intentionally deferred": "none", + "discovered implications": "none yet", + "proof achieved now": "yes; planning closeout recorded explicit proof input.", + "validation still needed": "current milestone validation remains pending", + "next likely slice": "continue the current milestone until the completion criteria are met" + }, + "intent_interpretation": { + "literal request": "Close-item blocked archive closeout status", + "inferred intended outcome": "GitHub #1216", + "chosen concrete what": "Add a close-item-specific JSON outcome for blocked archive closeout results.", + "interpretation distance": "low", + "review guidance": "Confirm the scaffolded plan still matches the promoted item before broad implementation." + }, + "execution_bounds": { + "allowed paths": "packages/planning/src/repo_planning_bootstrap/installer.py; packages/planning/tests/test_close_item.py; generated/package surfaces only if validation proves they are required.", + "max changed files": "4 implementation/test files plus generated artifacts only if package validation requires regeneration.", + "required validation commands": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning", + "ask-before-refactor threshold": "Ask before broadening beyond the promoted item.", + "stop before touching": "Unrelated backlog, adjacent modules, or canonical contracts not named by this plan." + }, + "stop_conditions": { + "stop when": "The work no longer matches the promoted item or its completion criteria.", + "escalate when boundary reached": "A correct fix requires changing the requested outcome or ownership boundary.", + "escalate on scope drift": "Implementation needs files outside the filled execution bounds.", + "escalate on proof failure": "The selected proof cannot demonstrate the completion criteria." + }, + "context_budget": { + "live working set": "This execplan, the promoted item, and the narrow files needed for the current implementation step.", + "recoverable later": "Repo background, historical reviews, and deferred backlog unless compact outputs point there.", + "externalize before shift": "Update execution_run, proof_report, finished_run_review, and closeout_distillation before pausing.", + "pre-work config pull": "Use compact config/startup/summary outputs before opening raw planning or routing files.", + "pre-work memory pull": "Route to the narrowest relevant memory only when the task needs durable repo knowledge.", + "tiny resumability note": "Continue #1216 by implementing close-item blocked status fields and focused Planning tests.", + "context-shift triggers": "Proof failure, scope drift, interruption, handoff, or closeout." + }, + "delegated_judgment": { + "requested outcome": "GitHub #1216: close-item JSON must make blocked archive closeout obvious.", + "hard constraints": "Keep scope to close-item/archive result reporting and focused Planning package tests.", + "agent may decide locally": "Exact field names for the machine-readable close-item outcome, provided blocked/closed status and next action are explicit.", + "escalate when": "The fix requires changing archive gate semantics, generated command IR, or broad lifecycle result contracts." + }, + "post_decomposition_delegation": { + "status": "pending", + "decision rule": "After this slice is bounded, decide whether direct work, read-only exploration, implementation handoff, validation handoff, or stronger review improves quality or saves tokens safely.", + "route candidates": "keep-local|delegate-exploration|delegate-implementation|delegate-validation|escalate-review|no-safe-route", + "required evidence": "slice id, route, reason, quality risk, token-saving class, read-first refs, write scope, proof burden, stop conditions, and return contract" + }, + "system_intent_alignment": { + "relevant system intent": "Preserve the larger intended outcome separately from this bounded slice.", + "slice shaping bias": "Keep the slice bounded while carrying any larger follow-on through explicit continuation fields.", + "broader-lane validation question": "Did this slice advance the declared larger outcome, or only complete the local task?", + "intent evidence source": ".agentic-workspace/docs/system-intent-contract.md" + }, + "references": [ + { + "kind": "source", + "target": "GitHub #1216", + "label": "GitHub #1216", + "role": "intake", + "locator": "" + } + ], + "active_milestone": { + "id": "close-item-blocked-status", + "status": "completed", + "scope": "Keep this execution thread bounded to the promoted TODO item.", + "ready": "ready", + "blocked": "none", + "optional_deps": "none" + }, + "immediate_next_action": [ + "Implement close-item blocked status fields and focused tests." + ], + "blockers": [ + "None." + ], + "touched_paths": [ + "packages/planning/src/repo_planning_bootstrap/installer.py", + "packages/planning/tests/test_close_item.py" + ], + "invariants": [ + "Preserve the planning contract and keep the work bounded to this plan." + ], + "validation_commands": [ + "uv run pytest packages/planning/tests/test_close_item.py -q", + "make test-planning", + "make lint-planning", + "make typecheck-planning" + ], + "required_tools": [ + "None." + ], + "completion_criteria": [ + "Close-item blocked archive closeout status is implemented, validated, and closed out honestly." + ], + "execution_run": { + "run status": "completed", + "executor": "agentic-planning closeout", + "handoff source": "agentic-planning new-plan", + "what happened": "Implemented close-item JSON outcome fields so blocked archive-flow no-ops report status, closed=false, mutation_applied=false, next_action, and blocked_reason.", + "scope touched": "Planning package close-item lifecycle result formatting and focused close-item tests.", + "changed surfaces": "packages/planning/src/repo_planning_bootstrap/installer.py; packages/planning/tests/test_close_item.py", + "validations run": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py", + "result for continuation": "bounded closeout complete", + "next step": "archive this execplan" + }, + "finished_run_review": { + "review status": "complete", + "scope respected": "Scope stayed inside close-item/archive result reporting; archive closeout gates and existing successful close-item behavior remain unchanged.", + "proof status": "passed", + "intent served": "yes", + "config compliance": "used planning closeout command-owned writer", + "misinterpretation risk": "low", + "follow-on decision": "none" + }, + "delegation_outcome_feedback": { + "route chosen": "not-delegated", + "route skipped reason": "No delegation route was recorded; prepare-closeout normalized archive-only residue.", + "expected savings": "none recorded", + "actual friction": "none recorded", + "proof result": "yes; planning closeout recorded explicit proof input.", + "quality concern": "none recorded", + "decomposition adjustment": "none" + }, + "proof_report": { + "validation proof": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py", + "proof achieved now": "yes; planning closeout recorded explicit proof input.", + "evidence for \"proof achieved\" state": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py" + }, + "intent_satisfaction": { + "original intent": "GitHub #1216", + "was original intent fully satisfied?": "yes", + "evidence of intent satisfaction": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py", + "unsolved intent passed to": "none" + }, + "execution_summary": { + "outcome delivered": "Blocked close-item archive attempts are now machine-readable and harder to miss in automation.", + "validation confirmed": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py", + "follow-on routed to": "none", + "post-work posterity capture": "archive closeout distillation", + "knowledge promoted (memory/docs/config)": "none", + "resume from": "archive", + "knowledge promoted (Memory/Docs/Config)": "none" + }, + "durable_residue": { + "status": "none", + "learned constraint": "No future-relevant learning was identified beyond the closeout evidence.", + "motivation worth preserving": "Closeout reviewed durable residue and found no live follow-up.", + "canonical owner now": "archive", + "promotion trigger": "none", + "retention after promotion": "retain" + }, + "task_intent_promotion": { + "decision": "do-not-promote", + "accepted values": "do-not-promote|memory|subsystem-intent|system-intent|refine-existing-intent|supersede-existing-intent", + "evidence source": "archive-plan --prepare-closeout", + "target scope": "archive", + "proposed durable intent": "Closeout reviewed durable residue and found no live follow-up.", + "confidence": "low", + "needs review": true, + "owner surface": "archive" + }, + "closure_check": { + "closeout scope": "slice", + "slice status": "completed", + "larger-intent status": "closed", + "closure decision": "archive-and-close", + "why this decision is honest": "planning closeout accepted a slice claim with intent-status satisfied.", + "evidence carried forward": "uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py", + "reopen trigger": "None unless new evidence shows the closeout was incomplete." + }, + "improvement_signal_review": { + "status": "not_checked", + "accepted statuses": "not_checked|signals_routed|signals_fixed|signals_dismissed|no_signal_found", + "guidance": "At closeout, report AW smoothness/helpfulness gaps, better-way signals, unused-feature reflections, and places AW could help more. Route each concrete signal to exactly one owner class unless explicitly split, or mark no_signal_found after checking.", + "source": "operating_posture", + "owner classes": [ + "issue", + "Memory", + "Planning", + "docs/checks/contracts", + "direct fix", + "dismissed with reason" + ], + "ordinary output cap": 3, + "signals found": [], + "signals fixed": [], + "signals routed": [], + "signals dismissed": [], + "next owner": "agent closeout reflection" + }, + "closeout_distillation": { + "buckets": { + "discard": [ + { + "summary": "No Memory, docs, or config promotion was needed for local execution detail.", + "owner": "discard", + "source": "execution_summary.knowledge promoted (Memory/Docs/Config)" + } + ], + "continuation": [], + "memory": [], + "config_check": [], + "docs": [], + "issue_follow_up": [] + } + }, + "drift_log": [ + "2026-05-29: Scaffolded by agentic-planning new-plan." + ], + "memory_learning_capture": { + "status": "reviewed", + "memory consult recommended?": "review startup/report memory_consult", + "memory notes read": "not recorded", + "future agents should not rediscover": "no", + "decision": "none", + "target": "none", + "reason": "Derived from durable_residue during closeout preparation." + }, + "generated_closeout": { + "status": "generated", + "source": "archive-plan --prepare-closeout", + "authority": "derived adapter; intent_satisfaction, closure_check, proof_report, durable_residue, execution_run, and execution_summary remain authoritative", + "text": "Generated closeout adapter; structured execplan fields are authoritative.\nIntent: GitHub #1216\nIntent satisfied: yes\nArchive decision: archive-and-close\nProof: uv run pytest packages/planning/tests/test_close_item.py -q; make test-planning; make lint-planning; make typecheck-planning; uv run python scripts/check/check_planning_surfaces.py\nChanged surfaces: packages/planning/src/repo_planning_bootstrap/installer.py; packages/planning/tests/test_close_item.py\nDurable residue: none (archive)\nMemory learning: none\nFollow-up: none" + } +} diff --git a/packages/planning/src/repo_planning_bootstrap/installer.py b/packages/planning/src/repo_planning_bootstrap/installer.py index a4a7b482..81573538 100644 --- a/packages/planning/src/repo_planning_bootstrap/installer.py +++ b/packages/planning/src/repo_planning_bootstrap/installer.py @@ -11973,6 +11973,69 @@ def format_actions(actions: list[Action], target_root: Path) -> list[str]: return lines +_CLOSE_ITEM_NON_BLOCKING_WARNING_CLASSES = {"archive_retention_skipped_by_size_guardrail"} +_CLOSE_ITEM_MUTATION_ACTIONS = {"updated", "archived", "closed"} + + +def _close_item_result_fields(result: InstallResult) -> dict[str, Any]: + if not result.message.startswith("Close planning item "): + return {} + blocking_warnings = [ + warning for warning in result.warnings if str(warning.get("warning_class", "")) not in _CLOSE_ITEM_NON_BLOCKING_WARNING_CLASSES + ] + manual_review_actions = [action for action in result.actions if action.kind == "manual review"] + blocked = bool(blocking_warnings or manual_review_actions) + mutation_applied = any(action.kind in _CLOSE_ITEM_MUTATION_ACTIONS for action in result.actions) + if blocked: + status = "blocked" + closed = False + next_action = _close_item_blocked_next_action( + blocking_warnings=blocking_warnings, + manual_review_actions=manual_review_actions, + ) + elif result.dry_run: + status = "dry-run" + closed = False + next_action = "Review actions and rerun close-item without --dry-run only if the plan matches intent." + elif mutation_applied: + status = "closed" + closed = True + next_action = "Run agentic-workspace summary --target . --format json to verify planning state." + else: + status = "not-changed" + closed = False + next_action = "Inspect close-item actions and rerun summary before assuming the item closed." + fields: dict[str, Any] = { + "status": status, + "closed": closed, + "mutation_applied": mutation_applied, + "next_action": next_action, + } + if blocked: + fields["blocked_reason"] = _close_item_blocked_reason( + blocking_warnings=blocking_warnings, + manual_review_actions=manual_review_actions, + ) + return fields + + +def _close_item_blocked_next_action(*, blocking_warnings: list[dict[str, str]], manual_review_actions: list[Action]) -> str: + if manual_review_actions: + return f"{manual_review_actions[0].detail}; rerun close-item after resolving the blocker." + if blocking_warnings: + return f"{blocking_warnings[0].get('message', 'Resolve close-item warnings')}; rerun close-item after resolving the blocker." + return "Resolve close-item blockers, then rerun close-item." + + +def _close_item_blocked_reason(*, blocking_warnings: list[dict[str, str]], manual_review_actions: list[Action]) -> str: + if blocking_warnings: + warning = blocking_warnings[0] + return f"{warning.get('warning_class', 'warning')}: {warning.get('message', '')}".strip() + if manual_review_actions: + return manual_review_actions[0].detail + return "close-item did not apply its requested mutation" + + def format_result_json(result: InstallResult) -> str: payload: dict[str, Any] = { "target_root": str(result.target_root), @@ -11983,6 +12046,7 @@ def format_result_json(result: InstallResult) -> str: "actions": [{"kind": action.kind, "path": str(action.path), "detail": action.detail} for action in result.actions], "warnings": result.warnings, } + payload.update(_close_item_result_fields(result)) if result.completion_options: payload["completion_options"] = result.completion_options if result.dry_run: diff --git a/packages/planning/tests/test_close_item.py b/packages/planning/tests/test_close_item.py index f13a0614..ecbb5046 100644 --- a/packages/planning/tests/test_close_item.py +++ b/packages/planning/tests/test_close_item.py @@ -106,6 +106,38 @@ def test_close_item_routes_execplan_id_through_archive_flow(tmp_path: Path) -> N assert any(action.kind == "archived" for action in result.actions) +def test_close_item_blocked_archive_flow_reports_machine_readable_status(tmp_path: Path) -> None: + _write( + tmp_path / ".agentic-workspace/planning/state.toml", + """ +kind = "agentic-planning-state" +schema_version = "planning-state/v1" + +[todo] +active_items = [ + { id = "plan-alpha", title = "Plan alpha", status = "completed", path = ".agentic-workspace/planning/execplans/plan-alpha.plan.json" }, +] +""", + ) + plan_path = tmp_path / ".agentic-workspace/planning/execplans/plan-alpha.plan.json" + _write_execplan_record(plan_path, status="completed") + record = json.loads(plan_path.read_text(encoding="utf-8")) + record.pop("closure_check") + installer_mod._write_execplan_record(record_path=plan_path, record=record) + + result = close_planning_item("plan-alpha", target=tmp_path) + payload = json.loads(installer_mod.format_result_json(result)) + state = tomllib.loads((tmp_path / ".agentic-workspace/planning/state.toml").read_text(encoding="utf-8")) + + assert payload["status"] == "blocked" + assert payload["closed"] is False + assert payload["mutation_applied"] is False + assert "Closure Check" in payload["next_action"] + assert payload["blocked_reason"].startswith("archive_missing_closure_check") + assert plan_path.exists() + assert state["todo"]["active_items"][0]["id"] == "plan-alpha" + + def test_close_item_preserves_execplan_when_retained_archive_path_is_stale(tmp_path: Path) -> None: _write( tmp_path / ".agentic-workspace/planning/state.toml", @@ -207,4 +239,34 @@ def test_close_item_runtime_cli_outputs_json(tmp_path: Path, capsys) -> None: payload = json.loads(capsys.readouterr().out) assert payload["message"] == "Close planning item done-item" + assert payload["status"] == "closed" + assert payload["closed"] is True assert any(action["kind"] == "updated" for action in payload["actions"]) + + +def test_close_item_runtime_cli_reports_blocked_json_status(tmp_path: Path, capsys) -> None: + _write( + tmp_path / ".agentic-workspace/planning/state.toml", + """ +kind = "agentic-planning-state" +schema_version = "planning-state/v1" + +[todo] +active_items = [ + { id = "plan-alpha", title = "Plan alpha", status = "completed", path = ".agentic-workspace/planning/execplans/plan-alpha.plan.json" }, +] +""", + ) + plan_path = tmp_path / ".agentic-workspace/planning/execplans/plan-alpha.plan.json" + _write_execplan_record(plan_path, status="completed") + record = json.loads(plan_path.read_text(encoding="utf-8")) + record["closure_check"]["closure decision"] = "keep-active" + installer_mod._write_execplan_record(record_path=plan_path, record=record) + + assert planning_cli.main(["close-item", "plan-alpha", "--target", str(tmp_path), "--format", "json"]) == 0 + payload = json.loads(capsys.readouterr().out) + + assert payload["status"] == "blocked" + assert payload["closed"] is False + assert payload["mutation_applied"] is False + assert "keep the plan active" in payload["next_action"]