Created
February 16, 2026 04:08
-
-
Save arunkumar9t2/ad88a356a4e06c6e73819a094e5d08ee to your computer and use it in GitHub Desktop.
Ensure claude always plans in a certain with hooks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Validate plan files for task management completeness in beads projects. | |
| This validator checks if plans written to ~/.claude/plans/*.md include: | |
| 1. A step to set the task to "in progress" (always required) | |
| 2. A step to close the task with a reason (always required) | |
| 3. A step to check/notify downstream tasks (required when making architectural changes) | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import subprocess | |
| from pathlib import Path | |
| from typing import Any | |
| from ..base import Handler | |
| from ..models import HookInput | |
| from ..outputs import block_stop | |
| from ..utils import is_beads_project | |
| logger = logging.getLogger(__name__) | |
| class BeadsPlanValidator(Handler): | |
| """Ensure plans in beads projects have complete task lifecycle management.""" | |
| events = ["PostToolUse"] | |
| def handle(self, hook_input: HookInput, context: dict[str, Any] | None = None): | |
| """Validate that plans have complete task lifecycle management. | |
| Intercepts Write tool calls to ~/.claude/plans/*.md files and uses | |
| Claude CLI to evaluate whether the plan includes: | |
| - Setting task to in-progress | |
| - Closing task with a reason | |
| - Notifying downstream tasks (when making architectural changes) | |
| """ | |
| # Only process Write tool | |
| if hook_input.tool_name != "Write": | |
| return None | |
| # Get file path from tool_input | |
| tool_input = hook_input.tool_input or {} | |
| file_path = tool_input.get("file_path", "") | |
| # Only plans/*.md files | |
| if "/plans/" not in file_path or not file_path.endswith(".md"): | |
| return None | |
| # Exclude home directory and ~/.claude from beads validation | |
| cwd_path = Path(hook_input.cwd).resolve() | |
| home_path = Path.home().resolve() | |
| claude_config_path = home_path / ".claude" | |
| if cwd_path == home_path or cwd_path == claude_config_path: | |
| logger.debug(f"Skipping beads validation in config directory: {cwd_path}") | |
| return None | |
| # Only beads projects | |
| if not is_beads_project(hook_input.cwd): | |
| logger.debug("Not a beads project - skipping") | |
| return None | |
| # Get plan content directly from tool_input | |
| content = tool_input.get("content", "") | |
| if not content: | |
| logger.debug("No content in Write tool_input") | |
| return None | |
| logger.info(f"Plan file written in beads project: {file_path} - evaluating downstream consideration") | |
| # Call Claude CLI to evaluate the plan | |
| return self._evaluate_plan(content, hook_input.cwd) | |
| def _evaluate_plan(self, plan_content: str, cwd: str): | |
| """Use Claude CLI to evaluate the plan for task management completeness.""" | |
| prompt = f"""Evaluate this implementation plan for beads task management completeness. | |
| PLAN: | |
| {plan_content[:30000]} | |
| EVALUATION CRITERIA (check ALL): | |
| 1. TASK LIFECYCLE (ALWAYS REQUIRED): | |
| a) Does the plan include a step to set the task to "in progress"? | |
| - Look for: "in progress", "in_progress", "bd set --status", "mark as in progress", "start working" | |
| b) Does the plan include a step to close the task with a reason at the end? | |
| - Look for: "close task", "bd close", "complete task with reason", "--reason", "summarizing changes" | |
| 2. DOWNSTREAM NOTIFICATION (CONDITIONAL): | |
| - Only check this if the plan makes KEY ARCHITECTURAL DECISIONS (API changes, schema changes, interface modifications, shared component changes, breaking changes) | |
| - If yes, does it include an EXPLICIT step to check/notify downstream tasks? | |
| - Look for: "downstream", "dependent tasks", "bd list --blocked-by", "notify", "update blocked tasks" | |
| RESPOND WITH: | |
| - APPROVE - if all required criteria are met | |
| - BLOCK: <detailed reason> - if any required criteria is missing | |
| If blocking, your reason MUST be specific and actionable. List each missing item with what needs to be added. Examples: | |
| - "Missing step to set task to in-progress before starting work" | |
| - "Missing final step to close task with --reason flag summarizing changes" | |
| - "Plan modifies shared API but lacks step to check downstream tasks with 'bd list --blocked-by <task-id>' and notify/update those tasks about the breaking changes" | |
| """ | |
| try: | |
| result = subprocess.run( | |
| ["claude", "-p", prompt, "--allowedTools", ""], | |
| capture_output=True, | |
| text=True, | |
| timeout=45, | |
| cwd=cwd, | |
| ) | |
| response = result.stdout.strip() | |
| logger.info(f"Claude evaluation result: {response[:200]}...") | |
| if "BLOCK" in response.upper(): | |
| # Use Claude's response directly - it already contains the descriptive reason | |
| return block_stop(f"REPLAN: {response}") | |
| except subprocess.TimeoutExpired: | |
| logger.warning("Claude CLI evaluation timed out") | |
| except FileNotFoundError: | |
| logger.error("Claude CLI not found in PATH") | |
| except Exception as e: | |
| logger.error(f"Error evaluating plan: {e}") | |
| # On error or approval, don't block | |
| return None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment