-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpending_questions.py
More file actions
166 lines (141 loc) · 5.33 KB
/
pending_questions.py
File metadata and controls
166 lines (141 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
"""UserPromptSubmit hook: remind about pending unanswered questions from Claude."""
import json
import re
import sys
from pathlib import Path
PENDING_FILE = Path("/tmp/claude_pending_questions.json")
STATUSLINE_FILE = Path("/tmp/claude_statusline.json")
MAX_PENDING = 5
REMIND_AFTER = 3 # turns before reminding
# Patterns that indicate a decision/action question directed at user
QUESTION_PATTERNS = re.compile(
r"(want me to|which|shall I|ready to|sound right\?|should I|"
r"do you want|would you like|do you prefer|A or B|"
r"proceed with|go ahead|confirm|ok to)",
re.IGNORECASE,
)
# Patterns that are likely rhetorical (exclude)
RHETORICAL_PATTERNS = re.compile(
r"(why would|how would|what if|isn't it|doesn't that|isn't that|"
r"who would|what could|how can I help)",
re.IGNORECASE,
)
def load_pending() -> list:
if not PENDING_FILE.exists():
return []
try:
return json.loads(PENDING_FILE.read_text())
except (json.JSONDecodeError, OSError):
return []
def save_pending(pending: list) -> None:
PENDING_FILE.write_text(json.dumps(pending, indent=2))
def get_last_assistant_message(transcript_path: str) -> str:
"""Read transcript JSONL, return the last assistant message text."""
p = Path(transcript_path)
if not p.exists():
return ""
lines = p.read_text().splitlines()
last_text = ""
for line in reversed(lines):
if not line.strip():
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
msg = entry.get("message", {})
if msg.get("role") == "assistant":
content = msg.get("content", "")
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
last_text = "\n".join(parts)
elif isinstance(content, str):
last_text = content
break
return last_text
def extract_questions(text: str) -> list:
"""Extract candidate questions from assistant text."""
questions = []
for line in text.splitlines():
line = line.strip()
if not line.endswith("?"):
continue
if len(line) < 10:
continue
if RHETORICAL_PATTERNS.search(line):
continue
if QUESTION_PATTERNS.search(line):
questions.append(line)
return questions
def user_answers_question(prompt: str, question: str) -> bool:
"""Check if user's prompt seems to address a pending question."""
# Extract significant words from question (4+ chars)
keywords = [w.lower() for w in re.findall(r"\b\w{4,}\b", question)
if w.lower() not in {"want", "shall", "would", "should", "which", "that",
"this", "will", "with", "have", "your", "from",
"them", "they", "what", "when", "where", "need"}]
if not keywords:
return False
prompt_lower = prompt.lower()
matches = sum(1 for kw in keywords if kw in prompt_lower)
return matches >= max(1, len(keywords) // 3)
def main():
try:
hook_input = json.load(sys.stdin)
prompt = hook_input.get("prompt", "")
except (json.JSONDecodeError, EOFError):
print("{}")
return
# Get transcript path
transcript_path = None
if STATUSLINE_FILE.exists():
try:
data = json.loads(STATUSLINE_FILE.read_text())
transcript_path = data.get("transcript_path")
except (json.JSONDecodeError, OSError):
pass
pending = load_pending()
# Remove questions that user seems to be answering
if prompt:
pending = [q for q in pending if not user_answers_question(prompt, q["text"])]
# Increment turn counters
for q in pending:
q["turns"] = q.get("turns", 0) + 1
# Scan last assistant message for new questions
if transcript_path:
last_msg = get_last_assistant_message(transcript_path)
if last_msg:
new_questions = extract_questions(last_msg)
existing_texts = {q["text"] for q in pending}
for q_text in new_questions:
if q_text not in existing_texts:
pending.append({"text": q_text, "turns": 0})
existing_texts.add(q_text)
# Cap at MAX_PENDING, drop oldest
if len(pending) > MAX_PENDING:
pending = pending[-MAX_PENDING:]
save_pending(pending)
# Remind about questions that have been open >= REMIND_AFTER turns
old_questions = [q for q in pending if q.get("turns", 0) >= REMIND_AFTER]
if old_questions:
remind = old_questions[:3]
bullets = "\n".join(f" - {q['text']}" for q in remind)
output = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": (
f"⏳ Still open ({len(remind)} unanswered question(s) from earlier):\n"
f"{bullets}\n"
"Address these if relevant, or ignore if superseded."
)
}
}
print(json.dumps(output))
else:
print("{}")
if __name__ == "__main__":
main()