Skip to content

Commit 75aa349

Browse files
authored
Merge pull request #13 from desplega-ai/feat/plan-checkbox-hooks
Add plan checkbox hooks for implementing skill
2 parents e31e202 + 4fa0b03 commit 75aa349

7 files changed

Lines changed: 1090 additions & 5 deletions

File tree

cc-plugin/base/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "desplega",
33
"description": "Plugin for effective agentic development",
4-
"version": "1.4.1",
4+
"version": "1.4.2",
55
"author": {
66
"name": "desplega.ai",
77
"email": "contact@desplega.ai"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env python3
2+
"""PostToolUse hook that reminds Claude to update plan checkboxes."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import sys
8+
9+
from plan_utils import count_unchecked_items, find_active_plan, is_plan_file, should_throttle
10+
11+
12+
def _build_reminder(plan_path: str, unchecked_count: int, unchecked_lines: list[str]) -> str:
13+
preview = unchecked_lines[:5]
14+
details = "\n".join(preview)
15+
if unchecked_count > len(preview):
16+
details = f"{details}\n... ({unchecked_count - len(preview)} more)"
17+
18+
return (
19+
"Plan checkbox reminder:\n"
20+
f"Active plan: {plan_path}\n"
21+
f"Unchecked checklist items remaining: {unchecked_count}\n"
22+
"If you completed work, update the plan checkboxes.\n"
23+
f"{details}"
24+
).strip()
25+
26+
27+
def main() -> None:
28+
try:
29+
data = json.load(sys.stdin)
30+
except json.JSONDecodeError:
31+
sys.exit(0)
32+
33+
session_id = data.get("session_id")
34+
cwd = data.get("cwd")
35+
tool_input = data.get("tool_input") or {}
36+
file_path = tool_input.get("file_path", "")
37+
38+
if not session_id or not cwd:
39+
sys.exit(0)
40+
41+
if is_plan_file(file_path):
42+
sys.exit(0)
43+
44+
if should_throttle(session_id):
45+
sys.exit(0)
46+
47+
plan_path = find_active_plan(session_id, cwd)
48+
if not plan_path:
49+
sys.exit(0)
50+
51+
unchecked_count, unchecked_lines = count_unchecked_items(plan_path, automated_only=False)
52+
if unchecked_count <= 0:
53+
sys.exit(0)
54+
55+
print(
56+
json.dumps(
57+
{
58+
"hookSpecificOutput": {
59+
"additionalContext": _build_reminder(
60+
plan_path, unchecked_count, unchecked_lines
61+
)
62+
}
63+
}
64+
)
65+
)
66+
sys.exit(0)
67+
68+
69+
if __name__ == "__main__":
70+
main()
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
"""Stop hook that enforces automated verification checkbox updates."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import re
8+
import sys
9+
10+
from plan_utils import (
11+
count_unchecked_by_phase,
12+
find_active_plan,
13+
marker_exists,
14+
unchecked_items_by_phase,
15+
)
16+
17+
PHASE_NUMBER_RE = re.compile(r"^Phase\s+(\d+)\b")
18+
19+
20+
def _is_started_phase(
21+
phase_name: str, has_any_checked: bool, has_marker: bool, has_active_plan: bool
22+
) -> bool:
23+
if has_any_checked:
24+
return True
25+
26+
match = PHASE_NUMBER_RE.match(phase_name)
27+
if not match:
28+
return False
29+
30+
# Marker files are best-effort (sandboxed runs may not allow ~/.claude writes),
31+
# so keep the Phase 1 cold-start behavior even when marker persistence fails.
32+
return int(match.group(1)) == 1 and (has_marker or has_active_plan)
33+
34+
35+
def main() -> None:
36+
try:
37+
data = json.load(sys.stdin)
38+
except json.JSONDecodeError:
39+
sys.exit(0)
40+
41+
if data.get("stop_hook_active"):
42+
sys.exit(0)
43+
44+
session_id = data.get("session_id")
45+
cwd = data.get("cwd")
46+
if not session_id or not cwd:
47+
sys.exit(0)
48+
49+
plan_path = find_active_plan(session_id, cwd)
50+
if not plan_path:
51+
sys.exit(0)
52+
53+
phase_counts = count_unchecked_by_phase(plan_path, automated_only=True)
54+
if not phase_counts:
55+
sys.exit(0)
56+
57+
phase_items = unchecked_items_by_phase(plan_path, automated_only=True)
58+
has_marker = marker_exists(session_id)
59+
60+
blocking_lines: list[str] = []
61+
for phase_name, (unchecked_count, has_any_checked) in phase_counts.items():
62+
if unchecked_count <= 0:
63+
continue
64+
if not _is_started_phase(
65+
phase_name, has_any_checked, has_marker, has_active_plan=bool(plan_path)
66+
):
67+
continue
68+
69+
unchecked_for_phase = phase_items.get(phase_name, [])
70+
if not unchecked_for_phase:
71+
blocking_lines.append(
72+
f"{phase_name}: {unchecked_count} unchecked automated verification item(s)"
73+
)
74+
continue
75+
76+
for item in unchecked_for_phase:
77+
blocking_lines.append(f"{phase_name}: {item}")
78+
79+
if not blocking_lines:
80+
sys.exit(0)
81+
82+
reason = (
83+
"Cannot stop yet. The active plan has unchecked automated verification items in started phases.\n"
84+
f"Plan: {plan_path}\n"
85+
"Unchecked items:\n"
86+
+ "\n".join(f"- {line}" for line in blocking_lines)
87+
+ "\nUpdate automated verification checkboxes before stopping."
88+
)
89+
90+
print(json.dumps({"decision": "block", "reason": reason}))
91+
sys.exit(0)
92+
93+
94+
if __name__ == "__main__":
95+
main()

0 commit comments

Comments
 (0)