|
5 | 5 | Fires before context compaction to: |
6 | 6 | 1. Capture current state (active plan, current task, recent decisions) |
7 | 7 | so post-compact-restore.py can surface it afterwards. |
8 | | -2. Block compaction once when an active plan is still DRAFT, to avoid |
9 | | - losing mid-plan context before the user has approved it. ON by |
10 | | - default (v2.0); opt out via CLAUDE_PRECOMPACT_BLOCK_ON_DRAFT=0. |
11 | | - Blocks at most once per DRAFT plan, fail-open — a user can always |
12 | | - re-run compaction to proceed. |
| 8 | +2. OPTIONALLY block compaction once when an active plan is still DRAFT, |
| 9 | + to avoid losing mid-plan context before approval. **Opt-in** via |
| 10 | + CLAUDE_PRECOMPACT_BLOCK_ON_DRAFT=1 (default OFF — the v2.0 default-ON |
| 11 | + flip was reverted: blocking the harness's *automatic* compaction can |
| 12 | + strand a user at the context ceiling, and post-compact-restore.py |
| 13 | + re-injects the plan afterward anyway). Blocks at most once per DRAFT |
| 14 | + plan, fail-open. |
13 | 15 |
|
14 | 16 | The blocking protocol follows modern Claude Code semantics: |
15 | 17 | exit 0 + JSON {"decision": "block", "reason": "..."} on stdout. |
@@ -61,16 +63,18 @@ def find_active_plan(project_dir: str) -> dict | None: |
61 | 63 | for plan_file in plan_files[:3]: # Check last 3 plans |
62 | 64 | content = plan_file.read_text() |
63 | 65 |
|
64 | | - # Skip completed plans |
65 | | - if "COMPLETED" in content.upper(): |
66 | | - continue |
67 | | - |
68 | | - # Extract status |
69 | | - status = "in_progress" |
70 | | - if "APPROVED" in content.upper(): |
71 | | - status = "approved" |
72 | | - elif "DRAFT" in content.upper(): |
73 | | - status = "draft" |
| 66 | + # Parse the plan's Status FIELD (e.g. "**Status:** DRAFT"), not a |
| 67 | + # whole-file substring — a DRAFT plan whose body merely mentions |
| 68 | + # "APPROVED" or "COMPLETED" (a checklist item, or the legend |
| 69 | + # "Status (DRAFT/APPROVED/COMPLETED)") must not be mis-classified. |
| 70 | + m = re.search(r"^\s*\**\s*status\s*\**\s*:\s*\**\s*" |
| 71 | + r"(draft|approved|completed|implemented|in[ -]?progress)", |
| 72 | + content, re.IGNORECASE | re.MULTILINE) |
| 73 | + v = m.group(1).lower() if m else "in_progress" |
| 74 | + if v.startswith(("completed", "implemented")): |
| 75 | + continue # skip finished plans |
| 76 | + status = "approved" if v.startswith("approved") else ( |
| 77 | + "draft" if v.startswith("draft") else "in_progress") |
74 | 78 |
|
75 | 79 | # Find current task (first unchecked item) |
76 | 80 | current_task = None |
@@ -151,7 +155,7 @@ def should_block_draft(plan_info: dict | None) -> tuple[bool, str]: |
151 | 155 | which would make this guard fire again on every subsequent |
152 | 156 | compaction of the same DRAFT plan. |
153 | 157 | """ |
154 | | - if os.environ.get("CLAUDE_PRECOMPACT_BLOCK_ON_DRAFT", "1") != "1": |
| 158 | + if os.environ.get("CLAUDE_PRECOMPACT_BLOCK_ON_DRAFT", "0") != "1": |
155 | 159 | return False, "" |
156 | 160 | if not plan_info or plan_info.get("status") != "draft": |
157 | 161 | return False, "" |
|
0 commit comments