@hustle-together/api-dev-tools 3.12.3 → 3.12.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/hustle-build.md +259 -0
- package/.claude/commands/hustle-combine.md +1089 -0
- package/.claude/commands/hustle-ui-create-page.md +1078 -0
- package/.claude/commands/hustle-ui-create.md +1058 -0
- package/.claude/hooks/auto-answer.py +305 -0
- package/.claude/hooks/cache-research.py +337 -0
- package/.claude/hooks/check-api-routes.py +168 -0
- package/.claude/hooks/check-playwright-setup.py +103 -0
- package/.claude/hooks/check-storybook-setup.py +81 -0
- package/.claude/hooks/check-update.py +132 -0
- package/.claude/hooks/completion-promise-detector.py +293 -0
- package/.claude/hooks/context-capacity-warning.py +171 -0
- package/.claude/hooks/detect-interruption.py +165 -0
- package/.claude/hooks/docs-update-check.py +120 -0
- package/.claude/hooks/enforce-a11y-audit.py +202 -0
- package/.claude/hooks/enforce-brand-guide.py +241 -0
- package/.claude/hooks/enforce-component-type-confirm.py +97 -0
- package/.claude/hooks/enforce-dry-run.py +134 -0
- package/.claude/hooks/enforce-freshness.py +184 -0
- package/.claude/hooks/enforce-page-components.py +186 -0
- package/.claude/hooks/enforce-page-data-schema.py +155 -0
- package/.claude/hooks/enforce-questions-sourced.py +146 -0
- package/.claude/hooks/enforce-schema-from-interview.py +248 -0
- package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
- package/.claude/hooks/enforce-ui-interview.py +130 -0
- package/.claude/hooks/generate-adr-options.py +282 -0
- package/.claude/hooks/generate-manifest-entry.py +1161 -0
- package/.claude/hooks/hook_utils.py +609 -0
- package/.claude/hooks/lib/__init__.py +1 -0
- package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/.claude/hooks/lib/greptile.py +355 -0
- package/.claude/hooks/lib/ntfy.py +209 -0
- package/.claude/hooks/notify-input-needed.py +73 -0
- package/.claude/hooks/notify-phase-complete.py +90 -0
- package/.claude/hooks/ntfy-on-question.py +240 -0
- package/.claude/hooks/orchestrator-completion.py +313 -0
- package/.claude/hooks/orchestrator-handoff.py +267 -0
- package/.claude/hooks/orchestrator-session-startup.py +146 -0
- package/.claude/hooks/parallel-orchestrator.py +451 -0
- package/.claude/hooks/project-document-prompt.py +302 -0
- package/.claude/hooks/remote-question-proxy.py +284 -0
- package/.claude/hooks/remote-question-server.py +1224 -0
- package/.claude/hooks/run-code-review.py +393 -0
- package/.claude/hooks/run-visual-qa.py +338 -0
- package/.claude/hooks/session-logger.py +323 -0
- package/.claude/hooks/test-orchestrator-reground.py +248 -0
- package/.claude/hooks/track-scope-coverage.py +220 -0
- package/.claude/hooks/track-token-usage.py +121 -0
- package/.claude/hooks/update-adr-decision.py +236 -0
- package/.claude/hooks/update-api-showcase.py +161 -0
- package/.claude/hooks/update-registry.py +352 -0
- package/.claude/hooks/update-testing-checklist.py +195 -0
- package/.claude/hooks/update-ui-showcase.py +224 -0
- package/.claude/settings.local.json +7 -1
- package/.claude/test-auto-answer-bot.py +183 -0
- package/.claude/test-completion-detector.py +263 -0
- package/.claude/test-orchestrator-state.json +20 -0
- package/.claude/test-orchestrator.sh +271 -0
- package/.skills/api-create/SKILL.md +88 -3
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/hustle-build/SKILL.md +459 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/CHANGELOG.md +87 -0
- package/README.md +86 -9
- package/bin/cli.js +1302 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/auto-answer.py +228 -0
- package/hooks/check-update.py +132 -0
- package/hooks/ntfy-on-question.py +227 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +189 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/periodic-reground.py +230 -67
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/docs/page.tsx +230 -0
- package/templates/hustle-build-defaults.json +84 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/settings.json +88 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
- package/templates/ui-showcase/page.tsx +1 -1
- package/.claude/api-dev-state.json +0 -466
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Completion Promise Detector Hook (Ralph Wiggum Pattern)
|
|
4
|
+
|
|
5
|
+
Detects when the agent outputs a completion promise signal like:
|
|
6
|
+
<promise>DONE</promise>
|
|
7
|
+
<promise>FIXED</promise>
|
|
8
|
+
<promise>REFACTORED</promise>
|
|
9
|
+
<promise>COMPLETE</promise>
|
|
10
|
+
|
|
11
|
+
This enables autonomous loops to self-terminate gracefully when work is done,
|
|
12
|
+
rather than relying solely on max-iterations safety nets.
|
|
13
|
+
|
|
14
|
+
Hook Type: PostToolUse (monitors Bash, Write, Edit outputs)
|
|
15
|
+
Stop (allows graceful termination)
|
|
16
|
+
|
|
17
|
+
References:
|
|
18
|
+
- Geoffrey Huntley's Ralph Wiggum Technique: https://ghuntley.com/ralph/
|
|
19
|
+
- docs/CLAUDE_CODE_BEST_PRACTICES.md - Section on autonomous loops
|
|
20
|
+
|
|
21
|
+
Updated in v4.5.0:
|
|
22
|
+
- Add workflow logging for promise_emitted events
|
|
23
|
+
- Add iteration counting and max-iterations enforcement
|
|
24
|
+
- Log phase_transition events
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
# Import shared utilities for logging and iteration tracking (v4.5.0)
|
|
35
|
+
try:
|
|
36
|
+
from hook_utils import (
|
|
37
|
+
log_workflow_event,
|
|
38
|
+
increment_phase_iteration,
|
|
39
|
+
get_phase_iterations
|
|
40
|
+
)
|
|
41
|
+
UTILS_AVAILABLE = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
UTILS_AVAILABLE = False
|
|
44
|
+
|
|
45
|
+
# Completion promise patterns
|
|
46
|
+
PROMISE_PATTERNS = [
|
|
47
|
+
r'<promise>(DONE|COMPLETE|FINISHED|SUCCESS)</promise>',
|
|
48
|
+
r'<promise>(FIXED|RESOLVED|SOLVED)</promise>',
|
|
49
|
+
r'<promise>(REFACTORED|CLEANED|IMPROVED)</promise>',
|
|
50
|
+
r'<promise>(TESTED|VERIFIED|VALIDATED)</promise>',
|
|
51
|
+
r'<promise>(DEPLOYED|SHIPPED|RELEASED)</promise>',
|
|
52
|
+
# Custom promises defined in state
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# State file for tracking promises
|
|
56
|
+
STATE_FILE = '.claude/completion-promises.json'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_state():
|
|
60
|
+
"""Load completion promise state."""
|
|
61
|
+
state_path = Path(STATE_FILE)
|
|
62
|
+
if state_path.exists():
|
|
63
|
+
try:
|
|
64
|
+
return json.loads(state_path.read_text())
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
pass
|
|
67
|
+
return {
|
|
68
|
+
'active_promise': None,
|
|
69
|
+
'custom_patterns': [],
|
|
70
|
+
'history': []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_state(state):
|
|
75
|
+
"""Save completion promise state."""
|
|
76
|
+
state_path = Path(STATE_FILE)
|
|
77
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
state_path.write_text(json.dumps(state, indent=2))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_all_patterns(state):
|
|
82
|
+
"""Get all promise patterns including custom ones."""
|
|
83
|
+
patterns = PROMISE_PATTERNS.copy()
|
|
84
|
+
for custom in state.get('custom_patterns', []):
|
|
85
|
+
patterns.append(rf'<promise>({custom})</promise>')
|
|
86
|
+
return patterns
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def detect_promise(text, state):
|
|
90
|
+
"""Detect if text contains a completion promise."""
|
|
91
|
+
if not text:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
patterns = get_all_patterns(state)
|
|
95
|
+
for pattern in patterns:
|
|
96
|
+
match = re.search(pattern, text, re.IGNORECASE)
|
|
97
|
+
if match:
|
|
98
|
+
return match.group(1).upper()
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def handle_post_tool_use():
|
|
103
|
+
"""Handle PostToolUse event - detect promises in tool output."""
|
|
104
|
+
try:
|
|
105
|
+
hook_input = json.loads(sys.stdin.read())
|
|
106
|
+
except json.JSONDecodeError:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
tool_name = hook_input.get('tool_name', '')
|
|
110
|
+
tool_result = hook_input.get('tool_result', '')
|
|
111
|
+
|
|
112
|
+
# Only check tools that produce output
|
|
113
|
+
if tool_name not in ['Bash', 'Write', 'Edit', 'Read']:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
state = load_state()
|
|
117
|
+
|
|
118
|
+
# v4.5.0: Check iteration limits for tools that indicate phase progress
|
|
119
|
+
if tool_name in ['Write', 'Edit'] and UTILS_AVAILABLE:
|
|
120
|
+
try:
|
|
121
|
+
# Detect current phase from context or state
|
|
122
|
+
current_phase = state.get('current_phase', 'unknown')
|
|
123
|
+
current_iter, max_iter, exceeded = increment_phase_iteration(current_phase)
|
|
124
|
+
|
|
125
|
+
if exceeded:
|
|
126
|
+
# Log the limit exceeded event
|
|
127
|
+
log_workflow_event("iteration_limit_exceeded", {
|
|
128
|
+
"phase": current_phase,
|
|
129
|
+
"current": current_iter,
|
|
130
|
+
"limit": max_iter
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
print(json.dumps({
|
|
134
|
+
'result': 'block',
|
|
135
|
+
'message': f"""
|
|
136
|
+
{'='*60}
|
|
137
|
+
MAX ITERATIONS EXCEEDED: {current_phase}
|
|
138
|
+
{'='*60}
|
|
139
|
+
|
|
140
|
+
Current iteration: {current_iter}
|
|
141
|
+
Limit: {max_iter}
|
|
142
|
+
|
|
143
|
+
The autonomous loop has exceeded the maximum iterations for this phase.
|
|
144
|
+
This is a safety mechanism to prevent infinite loops.
|
|
145
|
+
|
|
146
|
+
To continue:
|
|
147
|
+
- Run /ralph-continue to reset and proceed
|
|
148
|
+
- Or increase max_iterations in hustle-build-defaults.json
|
|
149
|
+
"""
|
|
150
|
+
}))
|
|
151
|
+
return
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Check for promise in output
|
|
156
|
+
promise = detect_promise(str(tool_result), state)
|
|
157
|
+
|
|
158
|
+
if promise:
|
|
159
|
+
# Record the promise detection
|
|
160
|
+
state['active_promise'] = promise
|
|
161
|
+
state['history'].append({
|
|
162
|
+
'promise': promise,
|
|
163
|
+
'tool': tool_name,
|
|
164
|
+
'detected_at': datetime.now().isoformat(),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
# Keep only last 50 history entries
|
|
168
|
+
state['history'] = state['history'][-50:]
|
|
169
|
+
save_state(state)
|
|
170
|
+
|
|
171
|
+
# v4.5.0: Log the promise detection
|
|
172
|
+
if UTILS_AVAILABLE:
|
|
173
|
+
try:
|
|
174
|
+
log_workflow_event("promise_emitted", {
|
|
175
|
+
"promise": promise,
|
|
176
|
+
"tool": tool_name,
|
|
177
|
+
"phase": state.get('current_phase', 'unknown')
|
|
178
|
+
})
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# Output notification
|
|
183
|
+
print(json.dumps({
|
|
184
|
+
'result': 'continue',
|
|
185
|
+
'message': f"\n{'='*60}\n COMPLETION PROMISE DETECTED: {promise}\n{'='*60}\n\nThe autonomous loop can now terminate gracefully.\nTo continue anyway, use: /ralph-continue\n"
|
|
186
|
+
}))
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# No promise detected, continue normally
|
|
190
|
+
print(json.dumps({'result': 'continue'}))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def handle_stop():
|
|
194
|
+
"""Handle Stop event - check if we should allow graceful termination."""
|
|
195
|
+
try:
|
|
196
|
+
hook_input = json.loads(sys.stdin.read())
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
print(json.dumps({'result': 'continue'}))
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
state = load_state()
|
|
202
|
+
active_promise = state.get('active_promise')
|
|
203
|
+
|
|
204
|
+
if active_promise:
|
|
205
|
+
# Clear the active promise
|
|
206
|
+
state['active_promise'] = None
|
|
207
|
+
save_state(state)
|
|
208
|
+
|
|
209
|
+
# Allow graceful termination with summary
|
|
210
|
+
print(json.dumps({
|
|
211
|
+
'result': 'continue',
|
|
212
|
+
'message': f"\n AUTONOMOUS LOOP COMPLETE \n\nCompletion promise '{active_promise}' was fulfilled.\nThe agent has signaled that the task is done.\n"
|
|
213
|
+
}))
|
|
214
|
+
else:
|
|
215
|
+
# No active promise, continue with normal stop behavior
|
|
216
|
+
print(json.dumps({'result': 'continue'}))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def handle_user_prompt():
|
|
220
|
+
"""Handle UserPromptSubmit - detect promise configuration."""
|
|
221
|
+
try:
|
|
222
|
+
hook_input = json.loads(sys.stdin.read())
|
|
223
|
+
except json.JSONDecodeError:
|
|
224
|
+
print(json.dumps({'result': 'continue'}))
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
prompt = hook_input.get('prompt', '').lower()
|
|
228
|
+
state = load_state()
|
|
229
|
+
|
|
230
|
+
# Check for Ralph Wiggum loop start
|
|
231
|
+
if '/ralph-loop' in prompt or '--completion-promise' in prompt:
|
|
232
|
+
# Extract custom promise if specified
|
|
233
|
+
match = re.search(r'--completion-promise\s+["\']?(\w+)["\']?', prompt, re.IGNORECASE)
|
|
234
|
+
if match:
|
|
235
|
+
custom_promise = match.group(1).upper()
|
|
236
|
+
if custom_promise not in state.get('custom_patterns', []):
|
|
237
|
+
state.setdefault('custom_patterns', []).append(custom_promise)
|
|
238
|
+
save_state(state)
|
|
239
|
+
|
|
240
|
+
print(json.dumps({
|
|
241
|
+
'result': 'continue',
|
|
242
|
+
'message': f"\n RALPH WIGGUM LOOP INITIALIZED \n\nListening for completion promises:\n- DONE, COMPLETE, FINISHED, SUCCESS\n- FIXED, RESOLVED, SOLVED\n- REFACTORED, CLEANED, IMPROVED\n- TESTED, VERIFIED, VALIDATED\n- DEPLOYED, SHIPPED, RELEASED\n{f'- {custom_promise} (custom)' if match else ''}\n\nOutput <promise>DONE</promise> when the task is complete.\n"
|
|
243
|
+
}))
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# Check for continue command
|
|
247
|
+
if '/ralph-continue' in prompt:
|
|
248
|
+
state['active_promise'] = None
|
|
249
|
+
save_state(state)
|
|
250
|
+
print(json.dumps({
|
|
251
|
+
'result': 'continue',
|
|
252
|
+
'message': "Cleared active promise. Autonomous loop will continue."
|
|
253
|
+
}))
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# Check for status command
|
|
257
|
+
if '/ralph-status' in prompt:
|
|
258
|
+
active = state.get('active_promise', 'None')
|
|
259
|
+
history = state.get('history', [])[-5:]
|
|
260
|
+
|
|
261
|
+
status_msg = f"\n RALPH WIGGUM STATUS \n\nActive Promise: {active}\n\nRecent History:\n"
|
|
262
|
+
for h in history:
|
|
263
|
+
status_msg += f" - {h.get('promise')} via {h.get('tool')} at {h.get('detected_at', 'unknown')[:19]}\n"
|
|
264
|
+
|
|
265
|
+
if not history:
|
|
266
|
+
status_msg += " (no promises detected yet)\n"
|
|
267
|
+
|
|
268
|
+
print(json.dumps({
|
|
269
|
+
'result': 'continue',
|
|
270
|
+
'message': status_msg
|
|
271
|
+
}))
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
print(json.dumps({'result': 'continue'}))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main():
|
|
278
|
+
"""Main entry point - determine hook type from environment."""
|
|
279
|
+
hook_type = os.environ.get('CLAUDE_HOOK_TYPE', 'PostToolUse')
|
|
280
|
+
|
|
281
|
+
if hook_type == 'PostToolUse':
|
|
282
|
+
handle_post_tool_use()
|
|
283
|
+
elif hook_type == 'Stop':
|
|
284
|
+
handle_stop()
|
|
285
|
+
elif hook_type == 'UserPromptSubmit':
|
|
286
|
+
handle_user_prompt()
|
|
287
|
+
else:
|
|
288
|
+
# Unknown hook type, pass through
|
|
289
|
+
print(json.dumps({'result': 'continue'}))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == '__main__':
|
|
293
|
+
main()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PostToolUse
|
|
4
|
+
Purpose: Warn when context capacity reaches thresholds
|
|
5
|
+
|
|
6
|
+
Monitors estimated context usage and warns at:
|
|
7
|
+
- 50% capacity: Suggest summarizing or compacting
|
|
8
|
+
- 75% capacity: Urgent warning, recommend /summarize
|
|
9
|
+
- 90% capacity: Critical, workflow may be interrupted
|
|
10
|
+
|
|
11
|
+
Uses token tracking from api-dev-state.json to estimate context size.
|
|
12
|
+
|
|
13
|
+
Version: 4.0.0
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
# Context window sizes for Claude models (approximate)
|
|
21
|
+
MODEL_CONTEXT_LIMITS = {
|
|
22
|
+
"claude-3-opus": 200000,
|
|
23
|
+
"claude-opus-4": 200000,
|
|
24
|
+
"claude-opus-4-5": 200000,
|
|
25
|
+
"claude-3-sonnet": 200000,
|
|
26
|
+
"claude-sonnet-4": 200000,
|
|
27
|
+
"claude-3-haiku": 200000,
|
|
28
|
+
"claude-3-5-sonnet": 200000,
|
|
29
|
+
"claude-3-5-haiku": 200000,
|
|
30
|
+
"default": 200000,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Warning thresholds
|
|
34
|
+
THRESHOLDS = {
|
|
35
|
+
"info": 0.50, # 50% - informational
|
|
36
|
+
"warning": 0.75, # 75% - warning
|
|
37
|
+
"critical": 0.90, # 90% - critical
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def estimate_context_usage(state: dict) -> int:
|
|
42
|
+
"""
|
|
43
|
+
Estimate current context usage from state file.
|
|
44
|
+
|
|
45
|
+
Uses token tracking data to estimate how much of the
|
|
46
|
+
context window has been consumed.
|
|
47
|
+
"""
|
|
48
|
+
token_usage = state.get("token_usage", {})
|
|
49
|
+
by_phase = token_usage.get("by_phase", {})
|
|
50
|
+
|
|
51
|
+
if not by_phase:
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
# Get latest phase token count
|
|
55
|
+
phase_keys = list(by_phase.keys())
|
|
56
|
+
if phase_keys:
|
|
57
|
+
latest = by_phase[phase_keys[-1]]
|
|
58
|
+
return latest.get("total_tokens", 0)
|
|
59
|
+
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_context_limit() -> int:
|
|
64
|
+
"""Get the context limit for the current model."""
|
|
65
|
+
# Default to 200k for Opus/Sonnet
|
|
66
|
+
return MODEL_CONTEXT_LIMITS.get("default", 200000)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_warning(level: str, usage_pct: float, tokens: int, limit: int) -> str:
|
|
70
|
+
"""Format the warning message based on level."""
|
|
71
|
+
remaining = limit - tokens
|
|
72
|
+
|
|
73
|
+
if level == "info":
|
|
74
|
+
return f"""
|
|
75
|
+
📊 Context Usage: {usage_pct:.0%}
|
|
76
|
+
Tokens used: ~{tokens:,} / {limit:,}
|
|
77
|
+
Remaining: ~{remaining:,}
|
|
78
|
+
|
|
79
|
+
💡 Consider running /summarize to reduce context usage.
|
|
80
|
+
"""
|
|
81
|
+
elif level == "warning":
|
|
82
|
+
return f"""
|
|
83
|
+
⚠️ CONTEXT WARNING: {usage_pct:.0%} CAPACITY
|
|
84
|
+
Tokens used: ~{tokens:,} / {limit:,}
|
|
85
|
+
Remaining: ~{remaining:,}
|
|
86
|
+
|
|
87
|
+
🔧 Recommended actions:
|
|
88
|
+
1. Run /summarize to compact conversation
|
|
89
|
+
2. Complete current phase before starting new work
|
|
90
|
+
3. Consider splitting remaining work into new session
|
|
91
|
+
"""
|
|
92
|
+
else: # critical
|
|
93
|
+
return f"""
|
|
94
|
+
🚨 CRITICAL: CONTEXT AT {usage_pct:.0%} CAPACITY
|
|
95
|
+
Tokens used: ~{tokens:,} / {limit:,}
|
|
96
|
+
Remaining: ~{remaining:,}
|
|
97
|
+
|
|
98
|
+
⚡ IMMEDIATE ACTION REQUIRED:
|
|
99
|
+
1. Run /summarize NOW
|
|
100
|
+
2. Complete or save current work
|
|
101
|
+
3. Context may auto-compact soon
|
|
102
|
+
4. Risk of work interruption if limit reached
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
# Read hook input from stdin
|
|
108
|
+
try:
|
|
109
|
+
input_data = json.load(sys.stdin)
|
|
110
|
+
except json.JSONDecodeError:
|
|
111
|
+
sys.exit(0)
|
|
112
|
+
|
|
113
|
+
# Read state file
|
|
114
|
+
cwd = Path.cwd()
|
|
115
|
+
state_file = cwd / ".claude" / "api-dev-state.json"
|
|
116
|
+
|
|
117
|
+
if not state_file.exists():
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
state = json.loads(state_file.read_text())
|
|
122
|
+
except (json.JSONDecodeError, IOError):
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
# Estimate context usage
|
|
126
|
+
tokens_used = estimate_context_usage(state)
|
|
127
|
+
context_limit = get_context_limit()
|
|
128
|
+
usage_pct = tokens_used / context_limit if context_limit > 0 else 0
|
|
129
|
+
|
|
130
|
+
# Check if we've already warned at this level
|
|
131
|
+
capacity_state = state.get("capacity_warnings", {})
|
|
132
|
+
last_warning_level = capacity_state.get("last_level", "")
|
|
133
|
+
|
|
134
|
+
# Determine warning level
|
|
135
|
+
warning_level = None
|
|
136
|
+
if usage_pct >= THRESHOLDS["critical"]:
|
|
137
|
+
warning_level = "critical"
|
|
138
|
+
elif usage_pct >= THRESHOLDS["warning"]:
|
|
139
|
+
warning_level = "warning"
|
|
140
|
+
elif usage_pct >= THRESHOLDS["info"]:
|
|
141
|
+
warning_level = "info"
|
|
142
|
+
|
|
143
|
+
# Only warn if level has increased
|
|
144
|
+
level_order = {"": 0, "info": 1, "warning": 2, "critical": 3}
|
|
145
|
+
should_warn = (
|
|
146
|
+
warning_level and
|
|
147
|
+
level_order.get(warning_level, 0) > level_order.get(last_warning_level, 0)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if should_warn:
|
|
151
|
+
# Output warning
|
|
152
|
+
warning_msg = format_warning(warning_level, usage_pct, tokens_used, context_limit)
|
|
153
|
+
print(warning_msg, file=sys.stderr)
|
|
154
|
+
|
|
155
|
+
# Update state to track warning
|
|
156
|
+
state["capacity_warnings"] = {
|
|
157
|
+
"last_level": warning_level,
|
|
158
|
+
"last_warning_at": datetime.now().isoformat(),
|
|
159
|
+
"tokens_at_warning": tokens_used,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
164
|
+
except IOError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: SessionStart
|
|
4
|
+
Purpose: Detect and prompt for interrupted workflows
|
|
5
|
+
|
|
6
|
+
This hook runs at session start and checks if there are any
|
|
7
|
+
in-progress workflows that were interrupted. If found, it injects
|
|
8
|
+
a prompt asking the user if they want to resume.
|
|
9
|
+
|
|
10
|
+
Added in v3.6.7 for session continuation support.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
- JSON with additionalContext about interrupted workflows
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import os
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_interrupted_workflows(state):
|
|
25
|
+
"""Find all workflows that are in_progress but not active."""
|
|
26
|
+
interrupted = []
|
|
27
|
+
|
|
28
|
+
# New format (v3.6.7+): check endpoints object
|
|
29
|
+
if "endpoints" in state:
|
|
30
|
+
active = state.get("active_endpoint")
|
|
31
|
+
for endpoint_name, endpoint_data in state["endpoints"].items():
|
|
32
|
+
status = endpoint_data.get("status", "not_started")
|
|
33
|
+
if status == "in_progress" and endpoint_name != active:
|
|
34
|
+
# Find the current phase
|
|
35
|
+
phases = endpoint_data.get("phases", {})
|
|
36
|
+
current_phase = None
|
|
37
|
+
for phase_name, phase_data in phases.items():
|
|
38
|
+
if phase_data.get("status") == "in_progress":
|
|
39
|
+
current_phase = phase_name
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
interrupted.append({
|
|
43
|
+
"endpoint": endpoint_name,
|
|
44
|
+
"status": status,
|
|
45
|
+
"current_phase": current_phase,
|
|
46
|
+
"started_at": endpoint_data.get("started_at"),
|
|
47
|
+
"interrupted_at": endpoint_data.get("session", {}).get("interrupted_at"),
|
|
48
|
+
"interrupted_phase": endpoint_data.get("session", {}).get("interrupted_phase")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
# Also check if active endpoint is not fully started
|
|
52
|
+
if active and active in state["endpoints"]:
|
|
53
|
+
active_data = state["endpoints"][active]
|
|
54
|
+
session = active_data.get("session", {})
|
|
55
|
+
if session.get("interrupted_at"):
|
|
56
|
+
# Active endpoint was previously interrupted
|
|
57
|
+
interrupted.insert(0, {
|
|
58
|
+
"endpoint": active,
|
|
59
|
+
"status": active_data.get("status"),
|
|
60
|
+
"current_phase": session.get("interrupted_phase"),
|
|
61
|
+
"started_at": active_data.get("started_at"),
|
|
62
|
+
"interrupted_at": session.get("interrupted_at"),
|
|
63
|
+
"is_active": True
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
# Old format: single endpoint
|
|
67
|
+
elif state.get("endpoint"):
|
|
68
|
+
endpoint = state.get("endpoint")
|
|
69
|
+
phases = state.get("phases", {})
|
|
70
|
+
|
|
71
|
+
# Check if any phase is in_progress
|
|
72
|
+
for phase_name, phase_data in phases.items():
|
|
73
|
+
if phase_data.get("status") == "in_progress":
|
|
74
|
+
interrupted.append({
|
|
75
|
+
"endpoint": endpoint,
|
|
76
|
+
"status": "in_progress",
|
|
77
|
+
"current_phase": phase_name,
|
|
78
|
+
"started_at": state.get("created_at"),
|
|
79
|
+
"is_legacy": True
|
|
80
|
+
})
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return interrupted
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_interrupted_message(interrupted):
|
|
87
|
+
"""Format a user-friendly message about interrupted workflows."""
|
|
88
|
+
if not interrupted:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
lines = [
|
|
92
|
+
"",
|
|
93
|
+
"=" * 60,
|
|
94
|
+
" INTERRUPTED WORKFLOW DETECTED",
|
|
95
|
+
"=" * 60,
|
|
96
|
+
""
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for i, workflow in enumerate(interrupted, 1):
|
|
100
|
+
endpoint = workflow["endpoint"]
|
|
101
|
+
phase = workflow.get("current_phase", "unknown")
|
|
102
|
+
started = workflow.get("started_at", "unknown")
|
|
103
|
+
interrupted_at = workflow.get("interrupted_at", "")
|
|
104
|
+
|
|
105
|
+
lines.append(f"{i}. **{endpoint}**")
|
|
106
|
+
lines.append(f" - Phase: {phase}")
|
|
107
|
+
lines.append(f" - Started: {started}")
|
|
108
|
+
if interrupted_at:
|
|
109
|
+
lines.append(f" - Interrupted: {interrupted_at}")
|
|
110
|
+
lines.append("")
|
|
111
|
+
|
|
112
|
+
lines.extend([
|
|
113
|
+
"To resume an interrupted workflow, use:",
|
|
114
|
+
" /api-continue [endpoint-name]",
|
|
115
|
+
"",
|
|
116
|
+
"Or start a new workflow with:",
|
|
117
|
+
" /api-create [new-endpoint-name]",
|
|
118
|
+
"",
|
|
119
|
+
"=" * 60
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main():
|
|
126
|
+
try:
|
|
127
|
+
input_data = json.load(sys.stdin)
|
|
128
|
+
except json.JSONDecodeError:
|
|
129
|
+
input_data = {}
|
|
130
|
+
|
|
131
|
+
# Check if state file exists
|
|
132
|
+
if not STATE_FILE.exists():
|
|
133
|
+
print(json.dumps({"continue": True}))
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
state = json.loads(STATE_FILE.read_text())
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
print(json.dumps({"continue": True}))
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
# Find interrupted workflows
|
|
143
|
+
interrupted = get_interrupted_workflows(state)
|
|
144
|
+
|
|
145
|
+
if not interrupted:
|
|
146
|
+
print(json.dumps({"continue": True}))
|
|
147
|
+
sys.exit(0)
|
|
148
|
+
|
|
149
|
+
# Format message
|
|
150
|
+
message = format_interrupted_message(interrupted)
|
|
151
|
+
|
|
152
|
+
output = {
|
|
153
|
+
"hookSpecificOutput": {
|
|
154
|
+
"hookEventName": "SessionStart",
|
|
155
|
+
"additionalContext": message,
|
|
156
|
+
"interruptedWorkflows": interrupted
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print(json.dumps(output))
|
|
161
|
+
sys.exit(0)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|