@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.
Files changed (159) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +7 -463
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. 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()