@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,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()
|