@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,284 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Remote Question Proxy Hook
4
+
5
+ When REMOTE_QUESTIONS_ENABLED=true, this hook:
6
+ 1. Writes the current question to .claude/current-question.json
7
+ 2. Sends NTFY notification with link to the web UI
8
+ 3. Optionally waits for remote answer
9
+
10
+ Hook Type: PreToolUse (matcher: AskUserQuestion)
11
+ Version: 4.6.0
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import sys
17
+ import time
18
+ import subprocess
19
+ from pathlib import Path
20
+ from datetime import datetime
21
+
22
+ # Configuration
23
+ DEFAULT_PORT = 8765
24
+ POLL_INTERVAL = 2 # seconds
25
+ MAX_WAIT_TIME = 300 # 5 minutes
26
+
27
+
28
+ def get_project_dir():
29
+ """Get project directory from environment."""
30
+ return Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
31
+
32
+
33
+ def is_remote_questions_enabled():
34
+ """Check if remote questions feature is enabled."""
35
+ return os.environ.get("REMOTE_QUESTIONS_ENABLED", "").lower() == "true"
36
+
37
+
38
+ def get_remote_url():
39
+ """Get the remote URL (Cloudflare tunnel or localhost)."""
40
+ url = os.environ.get("REMOTE_QUESTIONS_URL", "")
41
+ if url:
42
+ return url.rstrip("/")
43
+
44
+ port = os.environ.get("REMOTE_QUESTIONS_PORT", DEFAULT_PORT)
45
+ return f"http://localhost:{port}"
46
+
47
+
48
+ def get_ntfy_topic():
49
+ """Get NTFY topic from environment."""
50
+ return os.environ.get("NTFY_TOPIC", "layers-mf-08ebf1d1")
51
+
52
+
53
+ def parse_question_input(tool_input_raw):
54
+ """Parse the AskUserQuestion tool input."""
55
+ try:
56
+ data = json.loads(tool_input_raw)
57
+ questions = data.get("questions", [])
58
+ return questions
59
+ except Exception:
60
+ return []
61
+
62
+
63
+ def write_question_file(questions, phase="unknown"):
64
+ """Write question to .claude/current-question.json for the server."""
65
+ project_dir = get_project_dir()
66
+ question_file = project_dir / ".claude" / "current-question.json"
67
+ question_file.parent.mkdir(parents=True, exist_ok=True)
68
+
69
+ # Format questions for the web UI
70
+ formatted_questions = []
71
+ for q in questions:
72
+ formatted_q = {
73
+ "id": q.get("header", "question").lower().replace(" ", "-"),
74
+ "question": q.get("question", ""),
75
+ "header": q.get("header", "Question"),
76
+ "options": [],
77
+ "multiSelect": q.get("multiSelect", False),
78
+ "timestamp": datetime.now().isoformat()
79
+ }
80
+
81
+ for opt in q.get("options", []):
82
+ formatted_q["options"].append({
83
+ "label": opt.get("label", ""),
84
+ "description": opt.get("description", "")
85
+ })
86
+
87
+ formatted_questions.append(formatted_q)
88
+
89
+ question_data = {
90
+ "questions": formatted_questions,
91
+ "phase": phase,
92
+ "created_at": datetime.now().isoformat(),
93
+ "status": "pending"
94
+ }
95
+
96
+ question_file.write_text(json.dumps(question_data, indent=2))
97
+ return question_file
98
+
99
+
100
+ def clear_answer_file():
101
+ """Clear any existing answer file."""
102
+ project_dir = get_project_dir()
103
+ answer_file = project_dir / ".claude" / "pending-answer.json"
104
+ if answer_file.exists():
105
+ answer_file.unlink()
106
+
107
+
108
+ def send_ntfy_notification(url):
109
+ """Send NTFY notification with link to question UI."""
110
+ topic = get_ntfy_topic()
111
+ message = f"[INPUT NEEDED] Answer question at: {url}"
112
+
113
+ try:
114
+ subprocess.run(
115
+ ["curl", "-s", "-d", message, f"ntfy.sh/{topic}"],
116
+ capture_output=True,
117
+ timeout=10
118
+ )
119
+ except Exception:
120
+ pass # Don't fail if notification fails
121
+
122
+
123
+ def wait_for_answer(timeout=MAX_WAIT_TIME):
124
+ """Wait for answer to appear in pending-answer.json."""
125
+ project_dir = get_project_dir()
126
+ answer_file = project_dir / ".claude" / "pending-answer.json"
127
+
128
+ start_time = time.time()
129
+
130
+ while time.time() - start_time < timeout:
131
+ if answer_file.exists():
132
+ try:
133
+ answer_data = json.loads(answer_file.read_text())
134
+ if answer_data.get("status") == "submitted":
135
+ return answer_data
136
+ except Exception:
137
+ pass
138
+
139
+ time.sleep(POLL_INTERVAL)
140
+
141
+ return None
142
+
143
+
144
+ def get_current_phase():
145
+ """Try to determine current workflow phase from state."""
146
+ project_dir = get_project_dir()
147
+
148
+ # Check hustle-build state
149
+ build_state_file = project_dir / ".claude" / "hustle-build-state.json"
150
+ if build_state_file.exists():
151
+ try:
152
+ state = json.loads(build_state_file.read_text())
153
+ phase = state.get("current_phase", "")
154
+ if phase:
155
+ return phase
156
+ except Exception:
157
+ pass
158
+
159
+ # Check api-dev state
160
+ api_state_file = project_dir / ".claude" / "api-dev-state.json"
161
+ if api_state_file.exists():
162
+ try:
163
+ state = json.loads(api_state_file.read_text())
164
+ phases = state.get("phases", {})
165
+ for phase_name, phase_data in phases.items():
166
+ if phase_data.get("status") == "in_progress":
167
+ return phase_name
168
+ except Exception:
169
+ pass
170
+
171
+ return "workflow"
172
+
173
+
174
+ def main():
175
+ # Check if remote questions is enabled
176
+ if not is_remote_questions_enabled():
177
+ # Not enabled, let the question proceed normally
178
+ print(json.dumps({"continue": True}))
179
+ return
180
+
181
+ # Read tool input
182
+ tool_input_raw = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
183
+
184
+ # Also check stdin for hook input
185
+ try:
186
+ if not sys.stdin.isatty():
187
+ stdin_data = sys.stdin.read()
188
+ if stdin_data:
189
+ try:
190
+ hook_input = json.loads(stdin_data)
191
+ tool_input_raw = json.dumps(hook_input.get("tool_input", {}))
192
+ except json.JSONDecodeError:
193
+ pass
194
+ except Exception:
195
+ pass
196
+
197
+ # Parse questions
198
+ questions = parse_question_input(tool_input_raw)
199
+
200
+ if not questions:
201
+ print(json.dumps({"continue": True}))
202
+ return
203
+
204
+ # Get current phase for context
205
+ phase = get_current_phase()
206
+
207
+ # Clear any existing answer
208
+ clear_answer_file()
209
+
210
+ # Write question to file for server
211
+ write_question_file(questions, phase)
212
+
213
+ # Get remote URL
214
+ remote_url = get_remote_url()
215
+
216
+ # Send NTFY notification
217
+ send_ntfy_notification(remote_url)
218
+
219
+ # Check if we should wait for remote answer
220
+ wait_mode = os.environ.get("REMOTE_QUESTIONS_WAIT", "false").lower() == "true"
221
+
222
+ if wait_mode:
223
+ # Wait for remote answer
224
+ answer = wait_for_answer()
225
+
226
+ if answer:
227
+ # Inject the answer as context
228
+ answers = answer.get("answers", {})
229
+
230
+ context = f"""
231
+ ## Remote Answer Received
232
+
233
+ The user answered remotely via the question interface:
234
+
235
+ ```json
236
+ {json.dumps(answers, indent=2)}
237
+ ```
238
+
239
+ Use these answers to proceed with the workflow.
240
+ """
241
+
242
+ # Clear the question file
243
+ project_dir = get_project_dir()
244
+ question_file = project_dir / ".claude" / "current-question.json"
245
+ if question_file.exists():
246
+ question_file.unlink()
247
+
248
+ print(json.dumps({
249
+ "continue": True,
250
+ "additionalContext": context
251
+ }))
252
+ return
253
+ else:
254
+ # Timeout - let the local question proceed
255
+ context = """
256
+ ## Remote Question Timeout
257
+
258
+ The remote question interface timed out waiting for an answer.
259
+ The question will be displayed locally instead.
260
+ """
261
+ print(json.dumps({
262
+ "continue": True,
263
+ "additionalContext": context
264
+ }))
265
+ return
266
+
267
+ # Non-blocking mode - just notify and continue with local question
268
+ context = f"""
269
+ ## Remote Question Notification Sent
270
+
271
+ A notification was sent to answer this question remotely at:
272
+ {remote_url}
273
+
274
+ The question will also be displayed here. Answer either locally or remotely.
275
+ """
276
+
277
+ print(json.dumps({
278
+ "continue": True,
279
+ "additionalContext": context
280
+ }))
281
+
282
+
283
+ if __name__ == "__main__":
284
+ main()