@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
package/hooks/run-code-review.py
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Phase 11: AI Code Review Hook
|
|
3
|
+
Phase 11: AI Code Review Hook (Ralph Wiggum Loop Pattern)
|
|
4
4
|
|
|
5
|
-
Triggers Greptile AI code review
|
|
6
|
-
This ensures
|
|
7
|
-
rather than after PR creation when it's too late.
|
|
5
|
+
Triggers Greptile AI code review and LOOPS until all issues are fixed.
|
|
6
|
+
This ensures code quality before proceeding to refactor phase.
|
|
8
7
|
|
|
9
|
-
Hook Type: PostToolUse (triggers after tests pass
|
|
8
|
+
Hook Type: PostToolUse (triggers after tests pass)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
10
|
+
Ralph Wiggum Pattern:
|
|
11
|
+
1. Run Greptile review
|
|
12
|
+
2. If issues found → inject context for agent to fix
|
|
13
|
+
3. Agent fixes issues
|
|
14
|
+
4. Tests re-run → hook triggers again
|
|
15
|
+
5. Re-review with Greptile
|
|
16
|
+
6. Loop until clean OR max iterations
|
|
17
|
+
7. Emit <promise>REVIEW_CLEAN</promise>
|
|
16
18
|
|
|
17
19
|
Environment Variables:
|
|
18
20
|
GREPTILE_API_KEY: Your Greptile API key (get from https://app.greptile.com)
|
|
19
21
|
GITHUB_TOKEN: GitHub Personal Access Token with repo access
|
|
20
22
|
CODE_REVIEW_ENABLED: Set to 'true' to enable (default: true)
|
|
23
|
+
CODE_REVIEW_MAX_ITERATIONS: Max review cycles (default: 5)
|
|
21
24
|
|
|
22
|
-
Version:
|
|
25
|
+
Version: 2.0.0
|
|
23
26
|
"""
|
|
24
27
|
import os
|
|
25
28
|
import sys
|
|
26
29
|
import json
|
|
27
30
|
import subprocess
|
|
28
31
|
from pathlib import Path
|
|
32
|
+
from datetime import datetime
|
|
29
33
|
|
|
30
34
|
# Add lib directory to path for imports
|
|
31
35
|
HOOK_DIR = Path(__file__).parent
|
|
@@ -44,6 +48,10 @@ try:
|
|
|
44
48
|
except ImportError:
|
|
45
49
|
GREPTILE_AVAILABLE = False
|
|
46
50
|
|
|
51
|
+
# State file for tracking review loops
|
|
52
|
+
REVIEW_STATE_FILE = ".claude/code-review-state.json"
|
|
53
|
+
MAX_ITERATIONS = int(os.environ.get("CODE_REVIEW_MAX_ITERATIONS", "5"))
|
|
54
|
+
|
|
47
55
|
|
|
48
56
|
def get_git_diff() -> tuple:
|
|
49
57
|
"""Get the current git diff and changed files."""
|
|
@@ -98,7 +106,44 @@ def get_repo_info() -> tuple:
|
|
|
98
106
|
return None, None
|
|
99
107
|
|
|
100
108
|
|
|
101
|
-
def
|
|
109
|
+
def load_review_state() -> dict:
|
|
110
|
+
"""Load code review loop state."""
|
|
111
|
+
state_file = Path.cwd() / REVIEW_STATE_FILE
|
|
112
|
+
if state_file.exists():
|
|
113
|
+
try:
|
|
114
|
+
return json.loads(state_file.read_text())
|
|
115
|
+
except (json.JSONDecodeError, IOError):
|
|
116
|
+
pass
|
|
117
|
+
return {
|
|
118
|
+
"iteration": 0,
|
|
119
|
+
"issues_found": [],
|
|
120
|
+
"status": "pending",
|
|
121
|
+
"started_at": None,
|
|
122
|
+
"last_review_at": None
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def save_review_state(state: dict):
|
|
127
|
+
"""Save code review loop state."""
|
|
128
|
+
state_file = Path.cwd() / REVIEW_STATE_FILE
|
|
129
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
try:
|
|
131
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
132
|
+
except IOError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def clear_review_state():
|
|
137
|
+
"""Clear review state after successful completion."""
|
|
138
|
+
state_file = Path.cwd() / REVIEW_STATE_FILE
|
|
139
|
+
if state_file.exists():
|
|
140
|
+
try:
|
|
141
|
+
state_file.unlink()
|
|
142
|
+
except IOError:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_workflow_state() -> dict:
|
|
102
147
|
"""Load current workflow state."""
|
|
103
148
|
state_file = Path.cwd() / ".claude" / "api-dev-state.json"
|
|
104
149
|
if state_file.exists():
|
|
@@ -109,21 +154,22 @@ def load_state() -> dict:
|
|
|
109
154
|
return {}
|
|
110
155
|
|
|
111
156
|
|
|
112
|
-
def
|
|
113
|
-
"""Update state file with code review results."""
|
|
157
|
+
def update_workflow_state_with_review(review_summary: dict, iteration: int):
|
|
158
|
+
"""Update workflow state file with code review results."""
|
|
114
159
|
state_file = Path.cwd() / ".claude" / "api-dev-state.json"
|
|
115
|
-
state =
|
|
160
|
+
state = load_workflow_state()
|
|
116
161
|
|
|
117
162
|
# Add or update code_review phase
|
|
118
163
|
if "phases" not in state:
|
|
119
164
|
state["phases"] = {}
|
|
120
165
|
|
|
121
166
|
state["phases"]["code_review"] = {
|
|
122
|
-
"status": "complete",
|
|
167
|
+
"status": "in_progress" if review_summary.get("issue_count", 0) > 0 else "complete",
|
|
168
|
+
"iteration": iteration,
|
|
123
169
|
"score": review_summary.get("score", 0),
|
|
124
170
|
"issues_found": review_summary.get("issue_count", 0),
|
|
125
171
|
"suggestions": review_summary.get("suggestion_count", 0),
|
|
126
|
-
"reviewed_at":
|
|
172
|
+
"reviewed_at": datetime.now().isoformat()
|
|
127
173
|
}
|
|
128
174
|
|
|
129
175
|
try:
|
|
@@ -140,7 +186,7 @@ def should_run_review(hook_input: dict) -> bool:
|
|
|
140
186
|
|
|
141
187
|
tool_name = hook_input.get("tool_name", "")
|
|
142
188
|
|
|
143
|
-
# Run after tests pass (Phase 9/10)
|
|
189
|
+
# Run after tests pass (Phase 9/10)
|
|
144
190
|
if tool_name == "Bash":
|
|
145
191
|
tool_input = hook_input.get("tool_input", {})
|
|
146
192
|
command = tool_input.get("command", "")
|
|
@@ -160,8 +206,40 @@ def should_run_review(hook_input: dict) -> bool:
|
|
|
160
206
|
return False
|
|
161
207
|
|
|
162
208
|
|
|
209
|
+
def format_issues_for_context(issues: list) -> str:
|
|
210
|
+
"""Format issues as context for the agent to fix."""
|
|
211
|
+
if not issues:
|
|
212
|
+
return ""
|
|
213
|
+
|
|
214
|
+
lines = [
|
|
215
|
+
"",
|
|
216
|
+
"=" * 60,
|
|
217
|
+
"CODE REVIEW ISSUES TO FIX (Ralph Wiggum Loop)",
|
|
218
|
+
"=" * 60,
|
|
219
|
+
"",
|
|
220
|
+
"The following issues were found by Greptile code review.",
|
|
221
|
+
"Please fix ALL issues, then run tests again.",
|
|
222
|
+
"The review will re-run automatically after tests pass.",
|
|
223
|
+
"",
|
|
224
|
+
"ISSUES:",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
for i, issue in enumerate(issues, 1):
|
|
228
|
+
lines.append(f" {i}. {issue}")
|
|
229
|
+
|
|
230
|
+
lines.extend([
|
|
231
|
+
"",
|
|
232
|
+
"After fixing all issues, run: pnpm test",
|
|
233
|
+
"Review will loop until all issues are resolved.",
|
|
234
|
+
"=" * 60,
|
|
235
|
+
""
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
return "\n".join(lines)
|
|
239
|
+
|
|
240
|
+
|
|
163
241
|
def main():
|
|
164
|
-
"""Main hook entry point."""
|
|
242
|
+
"""Main hook entry point with Ralph Wiggum loop pattern."""
|
|
165
243
|
# Read hook input
|
|
166
244
|
try:
|
|
167
245
|
hook_input = json.loads(sys.stdin.read())
|
|
@@ -190,6 +268,28 @@ def main():
|
|
|
190
268
|
}))
|
|
191
269
|
return
|
|
192
270
|
|
|
271
|
+
# Load current review loop state
|
|
272
|
+
review_state = load_review_state()
|
|
273
|
+
|
|
274
|
+
# Increment iteration
|
|
275
|
+
review_state["iteration"] += 1
|
|
276
|
+
iteration = review_state["iteration"]
|
|
277
|
+
|
|
278
|
+
# Check max iterations
|
|
279
|
+
if iteration > MAX_ITERATIONS:
|
|
280
|
+
print(json.dumps({
|
|
281
|
+
"continue": True,
|
|
282
|
+
"message": f"Code review max iterations ({MAX_ITERATIONS}) reached. Proceeding with warnings.\n\n<promise>REVIEW_CLEAN</promise>"
|
|
283
|
+
}))
|
|
284
|
+
clear_review_state()
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
# Track start time on first iteration
|
|
288
|
+
if iteration == 1:
|
|
289
|
+
review_state["started_at"] = datetime.now().isoformat()
|
|
290
|
+
|
|
291
|
+
review_state["last_review_at"] = datetime.now().isoformat()
|
|
292
|
+
|
|
193
293
|
# Get repository info
|
|
194
294
|
repo_owner, repo_name = get_repo_info()
|
|
195
295
|
if not repo_owner or not repo_name:
|
|
@@ -204,11 +304,12 @@ def main():
|
|
|
204
304
|
if not diff_content:
|
|
205
305
|
print(json.dumps({
|
|
206
306
|
"continue": True,
|
|
207
|
-
"message": "No changes detected -
|
|
307
|
+
"message": "No changes detected - code review complete.\n\n<promise>REVIEW_CLEAN</promise>"
|
|
208
308
|
}))
|
|
309
|
+
clear_review_state()
|
|
209
310
|
return
|
|
210
311
|
|
|
211
|
-
# Run the review
|
|
312
|
+
# Run the Greptile review
|
|
212
313
|
result = review_changes(
|
|
213
314
|
repo_owner=repo_owner,
|
|
214
315
|
repo_name=repo_name,
|
|
@@ -223,22 +324,68 @@ def main():
|
|
|
223
324
|
}))
|
|
224
325
|
return
|
|
225
326
|
|
|
226
|
-
# Parse
|
|
327
|
+
# Parse results
|
|
227
328
|
summary = get_review_summary(result)
|
|
228
|
-
|
|
329
|
+
update_workflow_state_with_review(summary, iteration)
|
|
229
330
|
|
|
230
331
|
# Format for display
|
|
231
332
|
display_output = format_review_for_display(summary)
|
|
232
333
|
|
|
233
|
-
#
|
|
234
|
-
|
|
334
|
+
# Check if issues found
|
|
335
|
+
issue_count = summary.get("issue_count", 0)
|
|
336
|
+
issues = summary.get("issues", [])
|
|
337
|
+
|
|
338
|
+
if issue_count == 0:
|
|
339
|
+
# All clean! Emit promise and proceed
|
|
340
|
+
review_state["status"] = "complete"
|
|
341
|
+
save_review_state(review_state)
|
|
342
|
+
|
|
343
|
+
output = f"""
|
|
344
|
+
{display_output}
|
|
345
|
+
|
|
346
|
+
================================================================================
|
|
347
|
+
REVIEW LOOP COMPLETE (Iteration {iteration}/{MAX_ITERATIONS})
|
|
348
|
+
================================================================================
|
|
349
|
+
All code review checks passed!
|
|
350
|
+
Proceeding to next phase.
|
|
351
|
+
|
|
352
|
+
<promise>REVIEW_CLEAN</promise>
|
|
353
|
+
"""
|
|
354
|
+
print(json.dumps({
|
|
355
|
+
"continue": True,
|
|
356
|
+
"message": output
|
|
357
|
+
}))
|
|
358
|
+
clear_review_state()
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Issues found - save state and block for fixes
|
|
362
|
+
review_state["status"] = "needs_fixing"
|
|
363
|
+
review_state["issues_found"] = issues
|
|
364
|
+
save_review_state(review_state)
|
|
365
|
+
|
|
366
|
+
# Format issues as context for agent
|
|
367
|
+
issues_context = format_issues_for_context(issues)
|
|
368
|
+
|
|
369
|
+
output = f"""
|
|
370
|
+
{display_output}
|
|
371
|
+
|
|
372
|
+
================================================================================
|
|
373
|
+
REVIEW LOOP - ITERATION {iteration}/{MAX_ITERATIONS}
|
|
374
|
+
================================================================================
|
|
375
|
+
{issue_count} issue(s) found. Fix them and run tests again.
|
|
376
|
+
Review will re-run automatically after tests pass.
|
|
377
|
+
{issues_context}
|
|
378
|
+
"""
|
|
235
379
|
|
|
380
|
+
# Block workflow - agent needs to fix issues
|
|
236
381
|
print(json.dumps({
|
|
237
|
-
"continue":
|
|
238
|
-
"message":
|
|
382
|
+
"continue": False, # Block until fixed
|
|
383
|
+
"message": output,
|
|
239
384
|
"review_score": summary.get("score", 0),
|
|
240
|
-
"issues_count":
|
|
241
|
-
"
|
|
385
|
+
"issues_count": issue_count,
|
|
386
|
+
"iteration": iteration,
|
|
387
|
+
"action_required": True,
|
|
388
|
+
"next_action": "Fix the issues above, then run tests again"
|
|
242
389
|
}))
|
|
243
390
|
|
|
244
391
|
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Visual QA Hook (Ralph Wiggum Loop Pattern)
|
|
4
|
+
|
|
5
|
+
Runs visual analysis with AI (Haiku) and LOOPS until all issues are fixed.
|
|
6
|
+
This ensures visual quality before proceeding to next phase.
|
|
7
|
+
|
|
8
|
+
Hook Type: PostToolUse (triggers after Storybook/visual tests)
|
|
9
|
+
|
|
10
|
+
Ralph Wiggum Pattern:
|
|
11
|
+
1. Run visual tests / capture screenshots
|
|
12
|
+
2. Analyze with AI (Haiku subagent)
|
|
13
|
+
3. If issues found → inject context for agent to fix
|
|
14
|
+
4. Agent fixes CSS/layout
|
|
15
|
+
5. Re-run visual tests → hook triggers again
|
|
16
|
+
6. Re-analyze with Haiku
|
|
17
|
+
7. Loop until clean OR max iterations
|
|
18
|
+
8. Emit <promise>VISUAL_CLEAN</promise>
|
|
19
|
+
|
|
20
|
+
Environment Variables:
|
|
21
|
+
VISUAL_QA_ENABLED: Set to 'true' to enable (default: true)
|
|
22
|
+
VISUAL_QA_MAX_ITERATIONS: Max QA cycles (default: 5)
|
|
23
|
+
|
|
24
|
+
Version: 1.0.0
|
|
25
|
+
"""
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import json
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
|
|
32
|
+
# State file for tracking visual QA loops
|
|
33
|
+
VISUAL_STATE_FILE = ".claude/visual-qa-state.json"
|
|
34
|
+
MAX_ITERATIONS = int(os.environ.get("VISUAL_QA_MAX_ITERATIONS", "5"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_visual_state() -> dict:
|
|
38
|
+
"""Load visual QA loop state."""
|
|
39
|
+
state_file = Path.cwd() / VISUAL_STATE_FILE
|
|
40
|
+
if state_file.exists():
|
|
41
|
+
try:
|
|
42
|
+
return json.loads(state_file.read_text())
|
|
43
|
+
except (json.JSONDecodeError, IOError):
|
|
44
|
+
pass
|
|
45
|
+
return {
|
|
46
|
+
"iteration": 0,
|
|
47
|
+
"issues_found": [],
|
|
48
|
+
"components_checked": [],
|
|
49
|
+
"viewports_passed": [],
|
|
50
|
+
"status": "pending",
|
|
51
|
+
"started_at": None,
|
|
52
|
+
"last_check_at": None
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_visual_state(state: dict):
|
|
57
|
+
"""Save visual QA loop state."""
|
|
58
|
+
state_file = Path.cwd() / VISUAL_STATE_FILE
|
|
59
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
try:
|
|
61
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
62
|
+
except IOError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def clear_visual_state():
|
|
67
|
+
"""Clear visual state after successful completion."""
|
|
68
|
+
state_file = Path.cwd() / VISUAL_STATE_FILE
|
|
69
|
+
if state_file.exists():
|
|
70
|
+
try:
|
|
71
|
+
state_file.unlink()
|
|
72
|
+
except IOError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_workflow_state() -> dict:
|
|
77
|
+
"""Load current workflow state."""
|
|
78
|
+
state_file = Path.cwd() / ".claude" / "api-dev-state.json"
|
|
79
|
+
if state_file.exists():
|
|
80
|
+
try:
|
|
81
|
+
return json.loads(state_file.read_text())
|
|
82
|
+
except (json.JSONDecodeError, IOError):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Also check hustle-build state
|
|
86
|
+
hustle_state = Path.cwd() / ".claude" / "hustle-build-state.json"
|
|
87
|
+
if hustle_state.exists():
|
|
88
|
+
try:
|
|
89
|
+
return json.loads(hustle_state.read_text())
|
|
90
|
+
except (json.JSONDecodeError, IOError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def update_workflow_state(issues_count: int, iteration: int):
|
|
97
|
+
"""Update workflow state with visual QA results."""
|
|
98
|
+
state_file = Path.cwd() / ".claude" / "api-dev-state.json"
|
|
99
|
+
state = load_workflow_state()
|
|
100
|
+
|
|
101
|
+
if "phases" not in state:
|
|
102
|
+
state["phases"] = {}
|
|
103
|
+
|
|
104
|
+
state["phases"]["visual_qa"] = {
|
|
105
|
+
"status": "in_progress" if issues_count > 0 else "complete",
|
|
106
|
+
"iteration": iteration,
|
|
107
|
+
"issues_found": issues_count,
|
|
108
|
+
"checked_at": datetime.now().isoformat()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
113
|
+
except IOError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def should_run_visual_qa(hook_input: dict) -> bool:
|
|
118
|
+
"""Determine if visual QA should run based on hook context."""
|
|
119
|
+
# Check if visual QA is enabled
|
|
120
|
+
if os.environ.get("VISUAL_QA_ENABLED", "true").lower() == "false":
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
tool_name = hook_input.get("tool_name", "")
|
|
124
|
+
tool_input = hook_input.get("tool_input", {})
|
|
125
|
+
|
|
126
|
+
# Run after Storybook tests
|
|
127
|
+
if tool_name == "Bash":
|
|
128
|
+
command = tool_input.get("command", "")
|
|
129
|
+
tool_result = hook_input.get("tool_result", {})
|
|
130
|
+
stdout = tool_result.get("stdout", "")
|
|
131
|
+
|
|
132
|
+
# Check if visual/storybook tests ran
|
|
133
|
+
visual_triggers = [
|
|
134
|
+
"storybook",
|
|
135
|
+
"test-storybook",
|
|
136
|
+
"chromatic",
|
|
137
|
+
"playwright test --project=visual",
|
|
138
|
+
"visual",
|
|
139
|
+
"screenshot"
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
if any(trigger in command.lower() for trigger in visual_triggers):
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
# Run after Task with visual-analyzer
|
|
146
|
+
if tool_name == "Task":
|
|
147
|
+
subagent_type = tool_input.get("subagent_type", "")
|
|
148
|
+
if subagent_type == "visual-analyzer":
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def parse_visual_issues(hook_input: dict) -> list:
|
|
155
|
+
"""Parse visual issues from tool output."""
|
|
156
|
+
issues = []
|
|
157
|
+
|
|
158
|
+
tool_result = hook_input.get("tool_result", {})
|
|
159
|
+
stdout = tool_result.get("stdout", "")
|
|
160
|
+
message = tool_result.get("message", "")
|
|
161
|
+
|
|
162
|
+
output = stdout + "\n" + message
|
|
163
|
+
|
|
164
|
+
# Look for common issue patterns
|
|
165
|
+
issue_keywords = [
|
|
166
|
+
"touch target",
|
|
167
|
+
"contrast",
|
|
168
|
+
"overflow",
|
|
169
|
+
"clipping",
|
|
170
|
+
"alignment",
|
|
171
|
+
"spacing",
|
|
172
|
+
"typography",
|
|
173
|
+
"safe area",
|
|
174
|
+
"layout issue",
|
|
175
|
+
"responsive",
|
|
176
|
+
"accessibility",
|
|
177
|
+
"wcag"
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
lines = output.split("\n")
|
|
181
|
+
for line in lines:
|
|
182
|
+
line_lower = line.lower()
|
|
183
|
+
if any(keyword in line_lower for keyword in issue_keywords):
|
|
184
|
+
if "issue" in line_lower or "warning" in line_lower or "error" in line_lower or "fail" in line_lower:
|
|
185
|
+
issues.append(line.strip())
|
|
186
|
+
|
|
187
|
+
# Also look for severity markers
|
|
188
|
+
for line in lines:
|
|
189
|
+
if "⚠️" in line or "❌" in line or "warning" in line.lower():
|
|
190
|
+
if line.strip() and line.strip() not in issues:
|
|
191
|
+
issues.append(line.strip())
|
|
192
|
+
|
|
193
|
+
return issues[:10] # Limit to 10 issues
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def format_issues_for_context(issues: list, iteration: int) -> str:
|
|
197
|
+
"""Format issues as context for the agent to fix."""
|
|
198
|
+
if not issues:
|
|
199
|
+
return ""
|
|
200
|
+
|
|
201
|
+
lines = [
|
|
202
|
+
"",
|
|
203
|
+
"=" * 60,
|
|
204
|
+
"VISUAL QA ISSUES TO FIX (Ralph Wiggum Loop)",
|
|
205
|
+
"=" * 60,
|
|
206
|
+
"",
|
|
207
|
+
f"Iteration {iteration}/{MAX_ITERATIONS}",
|
|
208
|
+
"",
|
|
209
|
+
"The following visual issues were found by AI analysis.",
|
|
210
|
+
"Please fix ALL issues, then re-run visual tests.",
|
|
211
|
+
"The QA will re-run automatically.",
|
|
212
|
+
"",
|
|
213
|
+
"ISSUES:",
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
for i, issue in enumerate(issues, 1):
|
|
217
|
+
lines.append(f" {i}. {issue}")
|
|
218
|
+
|
|
219
|
+
lines.extend([
|
|
220
|
+
"",
|
|
221
|
+
"Common fixes:",
|
|
222
|
+
" - Touch targets: Add min-h-[44px] min-w-[44px]",
|
|
223
|
+
" - Contrast: Check text color against background",
|
|
224
|
+
" - Safe areas: Use safe-area-inset-* CSS",
|
|
225
|
+
" - Overflow: Add overflow-hidden or adjust sizing",
|
|
226
|
+
"",
|
|
227
|
+
"After fixing, run: /test-visual",
|
|
228
|
+
"=" * 60,
|
|
229
|
+
""
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
return "\n".join(lines)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main():
|
|
236
|
+
"""Main hook entry point with Ralph Wiggum loop pattern."""
|
|
237
|
+
# Read hook input
|
|
238
|
+
try:
|
|
239
|
+
hook_input = json.loads(sys.stdin.read())
|
|
240
|
+
except json.JSONDecodeError:
|
|
241
|
+
hook_input = {}
|
|
242
|
+
|
|
243
|
+
# Check if we should run
|
|
244
|
+
if not should_run_visual_qa(hook_input):
|
|
245
|
+
print(json.dumps({"continue": True}))
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# Load current visual QA state
|
|
249
|
+
visual_state = load_visual_state()
|
|
250
|
+
|
|
251
|
+
# Increment iteration
|
|
252
|
+
visual_state["iteration"] += 1
|
|
253
|
+
iteration = visual_state["iteration"]
|
|
254
|
+
|
|
255
|
+
# Check max iterations
|
|
256
|
+
if iteration > MAX_ITERATIONS:
|
|
257
|
+
output = f"""
|
|
258
|
+
================================================================================
|
|
259
|
+
VISUAL QA - MAX ITERATIONS REACHED ({MAX_ITERATIONS})
|
|
260
|
+
================================================================================
|
|
261
|
+
Proceeding with remaining warnings. Consider reviewing manually.
|
|
262
|
+
|
|
263
|
+
<promise>VISUAL_CLEAN</promise>
|
|
264
|
+
"""
|
|
265
|
+
print(json.dumps({
|
|
266
|
+
"continue": True,
|
|
267
|
+
"message": output
|
|
268
|
+
}))
|
|
269
|
+
clear_visual_state()
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
# Track timing
|
|
273
|
+
if iteration == 1:
|
|
274
|
+
visual_state["started_at"] = datetime.now().isoformat()
|
|
275
|
+
visual_state["last_check_at"] = datetime.now().isoformat()
|
|
276
|
+
|
|
277
|
+
# Parse issues from the visual test output
|
|
278
|
+
issues = parse_visual_issues(hook_input)
|
|
279
|
+
issue_count = len(issues)
|
|
280
|
+
|
|
281
|
+
update_workflow_state(issue_count, iteration)
|
|
282
|
+
|
|
283
|
+
if issue_count == 0:
|
|
284
|
+
# All clean! Emit promise and proceed
|
|
285
|
+
visual_state["status"] = "complete"
|
|
286
|
+
save_visual_state(visual_state)
|
|
287
|
+
|
|
288
|
+
output = f"""
|
|
289
|
+
================================================================================
|
|
290
|
+
VISUAL QA LOOP COMPLETE (Iteration {iteration}/{MAX_ITERATIONS})
|
|
291
|
+
================================================================================
|
|
292
|
+
All visual checks passed!
|
|
293
|
+
- Layout: ✅
|
|
294
|
+
- Typography: ✅
|
|
295
|
+
- Touch Targets: ✅
|
|
296
|
+
- Safe Areas: ✅
|
|
297
|
+
- Brand Consistency: ✅
|
|
298
|
+
|
|
299
|
+
Proceeding to next phase.
|
|
300
|
+
|
|
301
|
+
<promise>VISUAL_CLEAN</promise>
|
|
302
|
+
"""
|
|
303
|
+
print(json.dumps({
|
|
304
|
+
"continue": True,
|
|
305
|
+
"message": output
|
|
306
|
+
}))
|
|
307
|
+
clear_visual_state()
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# Issues found - save state and provide context for fixes
|
|
311
|
+
visual_state["status"] = "needs_fixing"
|
|
312
|
+
visual_state["issues_found"] = issues
|
|
313
|
+
save_visual_state(visual_state)
|
|
314
|
+
|
|
315
|
+
# Format issues as context
|
|
316
|
+
issues_context = format_issues_for_context(issues, iteration)
|
|
317
|
+
|
|
318
|
+
output = f"""
|
|
319
|
+
================================================================================
|
|
320
|
+
VISUAL QA LOOP - ITERATION {iteration}/{MAX_ITERATIONS}
|
|
321
|
+
================================================================================
|
|
322
|
+
{issue_count} visual issue(s) found. Fix them and re-run visual tests.
|
|
323
|
+
{issues_context}
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
# Block workflow - agent needs to fix issues
|
|
327
|
+
print(json.dumps({
|
|
328
|
+
"continue": False, # Block until fixed
|
|
329
|
+
"message": output,
|
|
330
|
+
"issues_count": issue_count,
|
|
331
|
+
"iteration": iteration,
|
|
332
|
+
"action_required": True,
|
|
333
|
+
"next_action": "Fix the visual issues above, then run /test-visual"
|
|
334
|
+
}))
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
main()
|
package/hooks/session-logger.py
CHANGED
|
@@ -23,6 +23,13 @@ from datetime import datetime
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
import shutil
|
|
25
25
|
|
|
26
|
+
# Import shared utilities for NTFY
|
|
27
|
+
try:
|
|
28
|
+
from hook_utils import send_ntfy_notification
|
|
29
|
+
HAS_NTFY = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
HAS_NTFY = False
|
|
32
|
+
|
|
26
33
|
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
27
34
|
SESSIONS_DIR = Path(__file__).parent.parent / "api-sessions"
|
|
28
35
|
RESEARCH_DIR = Path(__file__).parent.parent / "research"
|
|
@@ -270,12 +277,31 @@ def main():
|
|
|
270
277
|
try:
|
|
271
278
|
session_dir = save_session(endpoint, endpoint_data, state)
|
|
272
279
|
|
|
280
|
+
# Send NTFY notification on session end
|
|
281
|
+
if HAS_NTFY:
|
|
282
|
+
status = endpoint_data.get("status", "unknown")
|
|
283
|
+
if status == "complete":
|
|
284
|
+
send_ntfy_notification(
|
|
285
|
+
title=f"✅ Session Complete: {endpoint}",
|
|
286
|
+
message=f"Completed {len(completed)}/13 phases. Session saved.",
|
|
287
|
+
priority="default",
|
|
288
|
+
tags=["white_check_mark", "robot"]
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
send_ntfy_notification(
|
|
292
|
+
title=f"📋 Session Ended: {endpoint}",
|
|
293
|
+
message=f"Completed {len(completed)}/13 phases. Status: {status}",
|
|
294
|
+
priority="low",
|
|
295
|
+
tags=["clipboard", "robot"]
|
|
296
|
+
)
|
|
297
|
+
|
|
273
298
|
output = {
|
|
274
299
|
"hookSpecificOutput": {
|
|
275
300
|
"sessionSaved": True,
|
|
276
301
|
"endpoint": endpoint,
|
|
277
302
|
"sessionDir": str(session_dir),
|
|
278
|
-
"phasesCompleted": len(completed)
|
|
303
|
+
"phasesCompleted": len(completed),
|
|
304
|
+
"notificationSent": HAS_NTFY
|
|
279
305
|
}
|
|
280
306
|
}
|
|
281
307
|
|