@hustle-together/api-dev-tools 3.12.3 ā 4.5.1
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/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -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/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- 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/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -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,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Documentation Update Check Hook
|
|
4
|
+
|
|
5
|
+
Triggers after significant file changes to remind about documentation updates.
|
|
6
|
+
Runs on PostToolUse for Write/Edit operations.
|
|
7
|
+
|
|
8
|
+
Hook Type: PostToolUse
|
|
9
|
+
Tools: Write, Edit
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Import hook utilities
|
|
18
|
+
try:
|
|
19
|
+
from hook_utils import is_source_repository
|
|
20
|
+
except ImportError:
|
|
21
|
+
def is_source_repository():
|
|
22
|
+
return Path(".git").exists() and Path("package.json").exists() and \
|
|
23
|
+
"api-dev-tools" in Path("package.json").read_text()
|
|
24
|
+
|
|
25
|
+
def get_tool_input():
|
|
26
|
+
"""Parse tool input from environment."""
|
|
27
|
+
tool_input = os.environ.get("TOOL_INPUT", "{}")
|
|
28
|
+
try:
|
|
29
|
+
return json.loads(tool_input)
|
|
30
|
+
except json.JSONDecodeError:
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
def get_file_category(file_path: str) -> str | None:
|
|
34
|
+
"""Categorize file by type for doc update needs."""
|
|
35
|
+
path = Path(file_path)
|
|
36
|
+
|
|
37
|
+
if ".skills/" in file_path and file_path.endswith("SKILL.md"):
|
|
38
|
+
return "skill"
|
|
39
|
+
if "hooks/" in file_path and file_path.endswith(".py"):
|
|
40
|
+
return "hook"
|
|
41
|
+
if ".claude/agents/" in file_path and file_path.endswith(".md"):
|
|
42
|
+
return "agent"
|
|
43
|
+
if "docs/" in file_path and file_path.endswith(".md"):
|
|
44
|
+
return "doc"
|
|
45
|
+
if "templates/" in file_path and file_path.endswith(".tsx"):
|
|
46
|
+
return "template"
|
|
47
|
+
if file_path.endswith("registry.json"):
|
|
48
|
+
return "registry"
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def check_needs_doc_update(file_path: str) -> dict:
|
|
53
|
+
"""Check if file change needs documentation update."""
|
|
54
|
+
category = get_file_category(file_path)
|
|
55
|
+
|
|
56
|
+
if not category:
|
|
57
|
+
return {"needs_update": False}
|
|
58
|
+
|
|
59
|
+
updates_needed = []
|
|
60
|
+
|
|
61
|
+
if category == "skill":
|
|
62
|
+
skill_name = Path(file_path).parent.name
|
|
63
|
+
updates_needed.append(f"docs/SKILLS.md - Add {skill_name} skill")
|
|
64
|
+
updates_needed.append(f"README.md - Update skills count if new")
|
|
65
|
+
updates_needed.append(f"CHANGELOG.md - Add entry for new skill")
|
|
66
|
+
|
|
67
|
+
elif category == "hook":
|
|
68
|
+
hook_name = Path(file_path).stem
|
|
69
|
+
updates_needed.append(f"docs/HOOKS.md - Add {hook_name} hook")
|
|
70
|
+
updates_needed.append(f"README.md - Update hooks count if new")
|
|
71
|
+
|
|
72
|
+
elif category == "agent":
|
|
73
|
+
agent_name = Path(file_path).stem
|
|
74
|
+
updates_needed.append(f"docs/AGENTS.md - Add {agent_name} agent")
|
|
75
|
+
updates_needed.append(f"README.md - Update agents count if new")
|
|
76
|
+
|
|
77
|
+
elif category == "doc":
|
|
78
|
+
doc_name = Path(file_path).name
|
|
79
|
+
updates_needed.append(f"README.md - Link to {doc_name} in Documentation section")
|
|
80
|
+
|
|
81
|
+
elif category == "template":
|
|
82
|
+
template_name = Path(file_path).stem
|
|
83
|
+
updates_needed.append(f"Consider dashboard integration for {template_name}")
|
|
84
|
+
|
|
85
|
+
elif category == "registry":
|
|
86
|
+
updates_needed.append("Check if new registry sections need documentation")
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"needs_update": len(updates_needed) > 0,
|
|
90
|
+
"category": category,
|
|
91
|
+
"file": file_path,
|
|
92
|
+
"updates_needed": updates_needed
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
# Skip if in source repository (we're building the tool itself)
|
|
97
|
+
if is_source_repository():
|
|
98
|
+
# Even in source, we want reminders - just softer ones
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
tool_input = get_tool_input()
|
|
102
|
+
file_path = tool_input.get("file_path", "")
|
|
103
|
+
|
|
104
|
+
if not file_path:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
result = check_needs_doc_update(file_path)
|
|
108
|
+
|
|
109
|
+
if result["needs_update"]:
|
|
110
|
+
# Output reminder (shown to user)
|
|
111
|
+
print(f"\nš Documentation Update Reminder")
|
|
112
|
+
print(f" File: {result['file']}")
|
|
113
|
+
print(f" Category: {result['category']}")
|
|
114
|
+
print(f"\n Consider updating:")
|
|
115
|
+
for update in result["updates_needed"]:
|
|
116
|
+
print(f" ⢠{update}")
|
|
117
|
+
print(f"\n Run /docs-update to auto-check all documentation.\n")
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|