@hustle-together/api-dev-tools 3.12.16 → 4.5.3
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 +10 -0
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/settings.local.json +1 -7
- 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 +34 -20
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +365 -38
- 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 +488 -0
- package/README.md +346 -53
- package/bin/cli.js +359 -123
- 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 +97 -20
- package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
- package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
- package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
- package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
- package/{.claude/hooks → hooks}/hook_utils.py +0 -0
- package/hooks/ntfy-on-question.py +15 -2
- package/hooks/orchestrator-handoff.py +81 -3
- package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
- package/hooks/periodic-reground.py +40 -0
- package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
- package/hooks/run-code-review.py +176 -29
- package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
- package/package.json +1 -1
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- 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 +53 -1
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +74 -7
- package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/.claude/commands/hustle-combine.md +0 -1089
- package/.claude/commands/hustle-ui-create-page.md +0 -1078
- package/.claude/commands/hustle-ui-create.md +0 -1058
- package/.claude/hooks/auto-answer.py +0 -305
- package/.claude/hooks/cache-research.py +0 -337
- package/.claude/hooks/check-api-routes.py +0 -168
- package/.claude/hooks/check-playwright-setup.py +0 -103
- package/.claude/hooks/check-storybook-setup.py +0 -81
- package/.claude/hooks/check-update.py +0 -132
- package/.claude/hooks/detect-interruption.py +0 -165
- package/.claude/hooks/enforce-a11y-audit.py +0 -202
- package/.claude/hooks/enforce-brand-guide.py +0 -241
- package/.claude/hooks/enforce-component-type-confirm.py +0 -97
- package/.claude/hooks/enforce-freshness.py +0 -184
- package/.claude/hooks/enforce-page-components.py +0 -186
- package/.claude/hooks/enforce-page-data-schema.py +0 -155
- package/.claude/hooks/enforce-questions-sourced.py +0 -146
- package/.claude/hooks/enforce-schema-from-interview.py +0 -248
- package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
- package/.claude/hooks/enforce-ui-interview.py +0 -130
- package/.claude/hooks/generate-manifest-entry.py +0 -1161
- package/.claude/hooks/lib/__init__.py +0 -1
- package/.claude/hooks/lib/greptile.py +0 -355
- package/.claude/hooks/lib/ntfy.py +0 -209
- package/.claude/hooks/notify-input-needed.py +0 -73
- package/.claude/hooks/notify-phase-complete.py +0 -90
- package/.claude/hooks/ntfy-on-question.py +0 -240
- package/.claude/hooks/orchestrator-completion.py +0 -313
- package/.claude/hooks/orchestrator-handoff.py +0 -267
- package/.claude/hooks/orchestrator-session-startup.py +0 -146
- package/.claude/hooks/run-code-review.py +0 -393
- package/.claude/hooks/session-logger.py +0 -323
- package/.claude/hooks/test-orchestrator-reground.py +0 -248
- package/.claude/hooks/track-scope-coverage.py +0 -220
- package/.claude/hooks/track-token-usage.py +0 -121
- package/.claude/hooks/update-api-showcase.py +0 -161
- package/.claude/hooks/update-registry.py +0 -352
- package/.claude/hooks/update-ui-showcase.py +0 -224
- package/.claude/test-auto-answer-bot.py +0 -183
- package/.claude/test-completion-detector.py +0 -263
- package/.claude/test-orchestrator-state.json +0 -20
- package/.claude/test-orchestrator.sh +0 -271
- /package/{.claude/commands → commands}/hustle-build.md +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
- /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
- /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: PreToolUse (Write|Edit)
|
|
4
|
-
Purpose: Enforce research freshness for the active endpoint
|
|
5
|
-
|
|
6
|
-
This hook blocks Write/Edit operations if:
|
|
7
|
-
1. There is an active endpoint in api-dev-state.json
|
|
8
|
-
2. Research exists for that endpoint
|
|
9
|
-
3. Research is older than 7 days (configurable)
|
|
10
|
-
|
|
11
|
-
The user can:
|
|
12
|
-
- Run /hustle-api-research to refresh the research
|
|
13
|
-
- Set "enforce_freshness": false in the endpoint config to disable
|
|
14
|
-
- Research is only enforced for the ACTIVE endpoint
|
|
15
|
-
|
|
16
|
-
Exit Codes:
|
|
17
|
-
- 0: Continue (no active endpoint, research is fresh, or enforcement disabled)
|
|
18
|
-
- 2: Block with message (research is stale, requires re-research)
|
|
19
|
-
|
|
20
|
-
Added in v3.7.0:
|
|
21
|
-
- User requested enforcement (not just warning) for stale research
|
|
22
|
-
- Only enforces for the active endpoint being worked on
|
|
23
|
-
"""
|
|
24
|
-
import json
|
|
25
|
-
import sys
|
|
26
|
-
import os
|
|
27
|
-
from datetime import datetime
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
|
|
30
|
-
# State file is in .claude/ directory (sibling to hooks/)
|
|
31
|
-
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
32
|
-
RESEARCH_INDEX = Path(__file__).parent.parent / "research" / "index.json"
|
|
33
|
-
|
|
34
|
-
# Default freshness threshold (days)
|
|
35
|
-
FRESHNESS_THRESHOLD_DAYS = 7
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def get_active_endpoint(state):
|
|
39
|
-
"""Get active endpoint - supports both old and new state formats."""
|
|
40
|
-
if "endpoints" in state and "active_endpoint" in state:
|
|
41
|
-
active = state.get("active_endpoint")
|
|
42
|
-
if active and active in state["endpoints"]:
|
|
43
|
-
return active, state["endpoints"][active]
|
|
44
|
-
return None, None
|
|
45
|
-
|
|
46
|
-
# Old format
|
|
47
|
-
endpoint = state.get("endpoint")
|
|
48
|
-
if endpoint:
|
|
49
|
-
return endpoint, state
|
|
50
|
-
|
|
51
|
-
return None, None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def load_research_index():
|
|
55
|
-
"""Load research index from .claude/research/index.json file."""
|
|
56
|
-
if not RESEARCH_INDEX.exists():
|
|
57
|
-
return {}
|
|
58
|
-
try:
|
|
59
|
-
index = json.loads(RESEARCH_INDEX.read_text())
|
|
60
|
-
return index.get("apis", {})
|
|
61
|
-
except (json.JSONDecodeError, IOError):
|
|
62
|
-
return {}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def calculate_days_old(timestamp_str):
|
|
66
|
-
"""Calculate how many days old a timestamp is."""
|
|
67
|
-
if not timestamp_str:
|
|
68
|
-
return 0
|
|
69
|
-
try:
|
|
70
|
-
last_updated = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
71
|
-
now = datetime.now(last_updated.tzinfo) if last_updated.tzinfo else datetime.now()
|
|
72
|
-
return (now - last_updated).days
|
|
73
|
-
except (ValueError, TypeError):
|
|
74
|
-
return 0
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def is_api_related_file(file_path):
|
|
78
|
-
"""Check if the file being written is API-related."""
|
|
79
|
-
if not file_path:
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
file_path = file_path.lower()
|
|
83
|
-
|
|
84
|
-
# Files that indicate API development
|
|
85
|
-
api_indicators = [
|
|
86
|
-
'/api/',
|
|
87
|
-
'/route.ts',
|
|
88
|
-
'/route.js',
|
|
89
|
-
'.api.test.',
|
|
90
|
-
'/schemas/',
|
|
91
|
-
'api-tests-manifest',
|
|
92
|
-
'/v2/'
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
return any(indicator in file_path for indicator in api_indicators)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def main():
|
|
99
|
-
# Read hook input from stdin
|
|
100
|
-
try:
|
|
101
|
-
input_data = json.load(sys.stdin)
|
|
102
|
-
except json.JSONDecodeError:
|
|
103
|
-
input_data = {}
|
|
104
|
-
|
|
105
|
-
# Get the file being written (if applicable)
|
|
106
|
-
tool_input = input_data.get("toolInput", {})
|
|
107
|
-
file_path = tool_input.get("file_path", "")
|
|
108
|
-
|
|
109
|
-
# Only enforce for API-related files
|
|
110
|
-
if not is_api_related_file(file_path):
|
|
111
|
-
print(json.dumps({"continue": True}))
|
|
112
|
-
sys.exit(0)
|
|
113
|
-
|
|
114
|
-
# Check if state file exists
|
|
115
|
-
if not STATE_FILE.exists():
|
|
116
|
-
print(json.dumps({"continue": True}))
|
|
117
|
-
sys.exit(0)
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
state = json.loads(STATE_FILE.read_text())
|
|
121
|
-
except json.JSONDecodeError:
|
|
122
|
-
print(json.dumps({"continue": True}))
|
|
123
|
-
sys.exit(0)
|
|
124
|
-
|
|
125
|
-
# Get active endpoint
|
|
126
|
-
endpoint, endpoint_data = get_active_endpoint(state)
|
|
127
|
-
if not endpoint or not endpoint_data:
|
|
128
|
-
# No active endpoint - allow
|
|
129
|
-
print(json.dumps({"continue": True}))
|
|
130
|
-
sys.exit(0)
|
|
131
|
-
|
|
132
|
-
# Check if freshness enforcement is disabled for this endpoint
|
|
133
|
-
if endpoint_data.get("enforce_freshness") is False:
|
|
134
|
-
print(json.dumps({"continue": True}))
|
|
135
|
-
sys.exit(0)
|
|
136
|
-
|
|
137
|
-
# Check research freshness
|
|
138
|
-
research_index = load_research_index()
|
|
139
|
-
|
|
140
|
-
if endpoint not in research_index:
|
|
141
|
-
# No research indexed yet - allow but note this is caught by enforce-research.py
|
|
142
|
-
print(json.dumps({"continue": True}))
|
|
143
|
-
sys.exit(0)
|
|
144
|
-
|
|
145
|
-
entry = research_index[endpoint]
|
|
146
|
-
last_updated = entry.get("last_updated", "")
|
|
147
|
-
days_old = calculate_days_old(last_updated)
|
|
148
|
-
|
|
149
|
-
# Get custom threshold if set
|
|
150
|
-
threshold = endpoint_data.get("freshness_threshold_days", FRESHNESS_THRESHOLD_DAYS)
|
|
151
|
-
|
|
152
|
-
if days_old > threshold:
|
|
153
|
-
# Research is stale - block and require re-research
|
|
154
|
-
output = {
|
|
155
|
-
"decision": "block",
|
|
156
|
-
"reason": f"""🔄 STALE RESEARCH DETECTED
|
|
157
|
-
|
|
158
|
-
Research for '{endpoint}' is {days_old} days old (threshold: {threshold} days).
|
|
159
|
-
|
|
160
|
-
**Action Required:**
|
|
161
|
-
Run `/hustle-api-research {endpoint}` to refresh the research before continuing.
|
|
162
|
-
|
|
163
|
-
**Why This Matters:**
|
|
164
|
-
- API documentation may have changed
|
|
165
|
-
- New parameters or features may be available
|
|
166
|
-
- Breaking changes may have been introduced
|
|
167
|
-
- Your implementation may not match current docs
|
|
168
|
-
|
|
169
|
-
**To Skip (Not Recommended):**
|
|
170
|
-
Set `"enforce_freshness": false` in api-dev-state.json for this endpoint.
|
|
171
|
-
|
|
172
|
-
Last researched: {last_updated or 'Unknown'}
|
|
173
|
-
Research location: .claude/research/{endpoint}/CURRENT.md"""
|
|
174
|
-
}
|
|
175
|
-
print(json.dumps(output))
|
|
176
|
-
sys.exit(2) # Exit code 2 = block with message
|
|
177
|
-
|
|
178
|
-
# Research is fresh - continue
|
|
179
|
-
print(json.dumps({"continue": True}))
|
|
180
|
-
sys.exit(0)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if __name__ == "__main__":
|
|
184
|
-
main()
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: enforce-page-components.py
|
|
4
|
-
Trigger: PreToolUse (Write|Edit)
|
|
5
|
-
Purpose: Check that components from registry are considered before creating new ones
|
|
6
|
-
|
|
7
|
-
For ui-create-page workflow, ensures Phase 5 (PAGE ANALYSIS) is complete and
|
|
8
|
-
encourages reuse of existing components from the registry.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
import os
|
|
14
|
-
import re
|
|
15
|
-
|
|
16
|
-
def load_state():
|
|
17
|
-
"""Load the api-dev-state.json file"""
|
|
18
|
-
state_paths = [
|
|
19
|
-
".claude/api-dev-state.json",
|
|
20
|
-
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
for path in state_paths:
|
|
24
|
-
if os.path.exists(path):
|
|
25
|
-
with open(path, 'r') as f:
|
|
26
|
-
return json.load(f)
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
def load_registry():
|
|
30
|
-
"""Load the registry.json file"""
|
|
31
|
-
registry_paths = [
|
|
32
|
-
".claude/registry.json",
|
|
33
|
-
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/registry.json")
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
for path in registry_paths:
|
|
37
|
-
if os.path.exists(path):
|
|
38
|
-
with open(path, 'r') as f:
|
|
39
|
-
return json.load(f)
|
|
40
|
-
return {}
|
|
41
|
-
|
|
42
|
-
def is_page_workflow(state):
|
|
43
|
-
"""Check if current workflow is ui-create-page"""
|
|
44
|
-
workflow = state.get("workflow", "")
|
|
45
|
-
return workflow == "ui-create-page"
|
|
46
|
-
|
|
47
|
-
def get_active_element(state):
|
|
48
|
-
"""Get the active element being worked on"""
|
|
49
|
-
active = state.get("active_element", "")
|
|
50
|
-
if not active:
|
|
51
|
-
active = state.get("endpoint", "")
|
|
52
|
-
return active
|
|
53
|
-
|
|
54
|
-
def is_creating_new_component(file_path):
|
|
55
|
-
"""Check if the file path suggests creating a new standalone component"""
|
|
56
|
-
if not file_path:
|
|
57
|
-
return False
|
|
58
|
-
|
|
59
|
-
# Patterns that suggest a new standalone component (not page-specific)
|
|
60
|
-
standalone_patterns = [
|
|
61
|
-
r"src/components/[A-Z]",
|
|
62
|
-
r"components/ui/",
|
|
63
|
-
r"components/shared/",
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
return any(re.search(pattern, file_path) for pattern in standalone_patterns)
|
|
67
|
-
|
|
68
|
-
def is_page_specific_component(file_path, element_name):
|
|
69
|
-
"""Check if the file is a page-specific component (allowed)"""
|
|
70
|
-
if not file_path or not element_name:
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
# Page-specific components in _components folder are allowed
|
|
74
|
-
patterns = [
|
|
75
|
-
f"src/app/{element_name}/_components/",
|
|
76
|
-
f"app/{element_name}/_components/",
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
return any(pattern in file_path for pattern in patterns)
|
|
80
|
-
|
|
81
|
-
def check_page_analysis_phase(state, element_name):
|
|
82
|
-
"""Check if page analysis phase is complete"""
|
|
83
|
-
elements = state.get("elements", {})
|
|
84
|
-
element = elements.get(element_name, {})
|
|
85
|
-
phases = element.get("phases", {})
|
|
86
|
-
|
|
87
|
-
page_analysis = phases.get("page_analysis", {})
|
|
88
|
-
return page_analysis.get("status") == "complete"
|
|
89
|
-
|
|
90
|
-
def get_available_components(registry):
|
|
91
|
-
"""Get list of available components from registry"""
|
|
92
|
-
components = registry.get("components", {})
|
|
93
|
-
return list(components.keys())
|
|
94
|
-
|
|
95
|
-
def main():
|
|
96
|
-
try:
|
|
97
|
-
# Read tool input from stdin
|
|
98
|
-
input_data = json.loads(sys.stdin.read())
|
|
99
|
-
tool_name = input_data.get("tool_name", "")
|
|
100
|
-
tool_input = input_data.get("tool_input", {})
|
|
101
|
-
|
|
102
|
-
# Only check Write tool
|
|
103
|
-
if tool_name != "Write":
|
|
104
|
-
print(json.dumps({"decision": "allow"}))
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
file_path = tool_input.get("file_path", "")
|
|
108
|
-
|
|
109
|
-
# Load state
|
|
110
|
-
state = load_state()
|
|
111
|
-
if not state:
|
|
112
|
-
print(json.dumps({"decision": "allow"}))
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
# Only apply to ui-create-page workflow
|
|
116
|
-
if not is_page_workflow(state):
|
|
117
|
-
print(json.dumps({"decision": "allow"}))
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
element_name = get_active_element(state)
|
|
121
|
-
if not element_name:
|
|
122
|
-
print(json.dumps({"decision": "allow"}))
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
# Allow page-specific components (in _components folder)
|
|
126
|
-
if is_page_specific_component(file_path, element_name):
|
|
127
|
-
print(json.dumps({"decision": "allow"}))
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
# Check if creating a new standalone component
|
|
131
|
-
if is_creating_new_component(file_path):
|
|
132
|
-
# Check if page analysis phase is complete
|
|
133
|
-
if not check_page_analysis_phase(state, element_name):
|
|
134
|
-
# Load registry to show available components
|
|
135
|
-
registry = load_registry()
|
|
136
|
-
available = get_available_components(registry)
|
|
137
|
-
|
|
138
|
-
component_list = "\n".join([f" - {c}" for c in available[:10]])
|
|
139
|
-
if len(available) > 10:
|
|
140
|
-
component_list += f"\n ... and {len(available) - 10} more"
|
|
141
|
-
|
|
142
|
-
print(json.dumps({
|
|
143
|
-
"decision": "block",
|
|
144
|
-
"reason": f"""
|
|
145
|
-
PAGE ANALYSIS REQUIRED (Phase 5)
|
|
146
|
-
|
|
147
|
-
You are creating a new standalone component, but Page Analysis phase is not complete.
|
|
148
|
-
|
|
149
|
-
Before creating new components:
|
|
150
|
-
1. Check the registry for existing components
|
|
151
|
-
2. Decide which existing components to reuse
|
|
152
|
-
3. Update state: phases.page_analysis.status = "complete"
|
|
153
|
-
|
|
154
|
-
Available Components in Registry:
|
|
155
|
-
{component_list if available else " (No components registered yet)"}
|
|
156
|
-
|
|
157
|
-
If you need a NEW component, consider:
|
|
158
|
-
- Using /ui-create to properly create and document it
|
|
159
|
-
- Or create a page-specific component in src/app/{element_name}/_components/
|
|
160
|
-
"""
|
|
161
|
-
}))
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
# Even if phase is complete, notify about registry
|
|
165
|
-
registry = load_registry()
|
|
166
|
-
available = get_available_components(registry)
|
|
167
|
-
|
|
168
|
-
if available:
|
|
169
|
-
print(json.dumps({
|
|
170
|
-
"decision": "allow",
|
|
171
|
-
"message": f"Note: {len(available)} components available in registry. Consider reusing existing components."
|
|
172
|
-
}))
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
# Allow everything else
|
|
176
|
-
print(json.dumps({"decision": "allow"}))
|
|
177
|
-
|
|
178
|
-
except Exception as e:
|
|
179
|
-
# On error, allow to avoid blocking workflow
|
|
180
|
-
print(json.dumps({
|
|
181
|
-
"decision": "allow",
|
|
182
|
-
"error": str(e)
|
|
183
|
-
}))
|
|
184
|
-
|
|
185
|
-
if __name__ == "__main__":
|
|
186
|
-
main()
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: enforce-page-data-schema.py
|
|
4
|
-
Trigger: PreToolUse (Write|Edit)
|
|
5
|
-
Purpose: Validate that API response types are defined before page implementation
|
|
6
|
-
|
|
7
|
-
For ui-create-page workflow, ensures Phase 6 (DATA SCHEMA) is complete before
|
|
8
|
-
allowing page implementation in Phase 9.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
import os
|
|
14
|
-
import re
|
|
15
|
-
|
|
16
|
-
def load_state():
|
|
17
|
-
"""Load the api-dev-state.json file"""
|
|
18
|
-
state_paths = [
|
|
19
|
-
".claude/api-dev-state.json",
|
|
20
|
-
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
for path in state_paths:
|
|
24
|
-
if os.path.exists(path):
|
|
25
|
-
with open(path, 'r') as f:
|
|
26
|
-
return json.load(f)
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
def is_page_workflow(state):
|
|
30
|
-
"""Check if current workflow is ui-create-page"""
|
|
31
|
-
workflow = state.get("workflow", "")
|
|
32
|
-
return workflow == "ui-create-page"
|
|
33
|
-
|
|
34
|
-
def get_active_element(state):
|
|
35
|
-
"""Get the active element being worked on"""
|
|
36
|
-
active = state.get("active_element", "")
|
|
37
|
-
if not active:
|
|
38
|
-
# Fall back to endpoint for older state files
|
|
39
|
-
active = state.get("endpoint", "")
|
|
40
|
-
return active
|
|
41
|
-
|
|
42
|
-
def is_page_file(file_path, element_name):
|
|
43
|
-
"""Check if the file being written is a page implementation file"""
|
|
44
|
-
if not file_path or not element_name:
|
|
45
|
-
return False
|
|
46
|
-
|
|
47
|
-
patterns = [
|
|
48
|
-
f"src/app/{element_name}/page.tsx",
|
|
49
|
-
f"src/app/{element_name}/layout.tsx",
|
|
50
|
-
f"src/app/{element_name}/_components/",
|
|
51
|
-
f"app/{element_name}/page.tsx",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
return any(pattern in file_path for pattern in patterns)
|
|
55
|
-
|
|
56
|
-
def is_types_file(file_path, element_name):
|
|
57
|
-
"""Check if the file being written is the types/schema file"""
|
|
58
|
-
if not file_path or not element_name:
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
patterns = [
|
|
62
|
-
f"src/app/{element_name}/_types/",
|
|
63
|
-
f"src/app/{element_name}/types.ts",
|
|
64
|
-
f"src/lib/schemas/{element_name}",
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
return any(pattern in file_path for pattern in patterns)
|
|
68
|
-
|
|
69
|
-
def is_test_file(file_path):
|
|
70
|
-
"""Check if file is a test file"""
|
|
71
|
-
return "__tests__" in file_path or ".test." in file_path or ".spec." in file_path
|
|
72
|
-
|
|
73
|
-
def check_data_schema_phase(state, element_name):
|
|
74
|
-
"""Check if data schema phase is complete"""
|
|
75
|
-
elements = state.get("elements", {})
|
|
76
|
-
element = elements.get(element_name, {})
|
|
77
|
-
phases = element.get("phases", {})
|
|
78
|
-
|
|
79
|
-
# Check data_schema phase
|
|
80
|
-
data_schema = phases.get("data_schema", {})
|
|
81
|
-
return data_schema.get("status") == "complete"
|
|
82
|
-
|
|
83
|
-
def main():
|
|
84
|
-
try:
|
|
85
|
-
# Read tool input from stdin
|
|
86
|
-
input_data = json.loads(sys.stdin.read())
|
|
87
|
-
tool_name = input_data.get("tool_name", "")
|
|
88
|
-
tool_input = input_data.get("tool_input", {})
|
|
89
|
-
|
|
90
|
-
# Only check Write and Edit tools
|
|
91
|
-
if tool_name not in ["Write", "Edit"]:
|
|
92
|
-
print(json.dumps({"decision": "allow"}))
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
file_path = tool_input.get("file_path", "")
|
|
96
|
-
|
|
97
|
-
# Load state
|
|
98
|
-
state = load_state()
|
|
99
|
-
if not state:
|
|
100
|
-
print(json.dumps({"decision": "allow"}))
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
# Only apply to ui-create-page workflow
|
|
104
|
-
if not is_page_workflow(state):
|
|
105
|
-
print(json.dumps({"decision": "allow"}))
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
element_name = get_active_element(state)
|
|
109
|
-
if not element_name:
|
|
110
|
-
print(json.dumps({"decision": "allow"}))
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
# Allow writing types/schema files (Phase 6)
|
|
114
|
-
if is_types_file(file_path, element_name):
|
|
115
|
-
print(json.dumps({"decision": "allow"}))
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
# Allow writing test files (Phase 8)
|
|
119
|
-
if is_test_file(file_path):
|
|
120
|
-
print(json.dumps({"decision": "allow"}))
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
# Check if writing page implementation file
|
|
124
|
-
if is_page_file(file_path, element_name):
|
|
125
|
-
# Verify data schema phase is complete
|
|
126
|
-
if not check_data_schema_phase(state, element_name):
|
|
127
|
-
print(json.dumps({
|
|
128
|
-
"decision": "block",
|
|
129
|
-
"reason": f"""
|
|
130
|
-
DATA SCHEMA REQUIRED (Phase 6)
|
|
131
|
-
|
|
132
|
-
You are trying to implement page code, but the data schema phase is not complete.
|
|
133
|
-
|
|
134
|
-
Before writing page implementation:
|
|
135
|
-
1. Define TypeScript interfaces for API responses
|
|
136
|
-
2. Create types in src/app/{element_name}/_types/index.ts
|
|
137
|
-
3. Update state: phases.data_schema.status = "complete"
|
|
138
|
-
|
|
139
|
-
Page implementation requires knowing the data structure first.
|
|
140
|
-
"""
|
|
141
|
-
}))
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
# Allow everything else
|
|
145
|
-
print(json.dumps({"decision": "allow"}))
|
|
146
|
-
|
|
147
|
-
except Exception as e:
|
|
148
|
-
# On error, allow to avoid blocking workflow
|
|
149
|
-
print(json.dumps({
|
|
150
|
-
"decision": "allow",
|
|
151
|
-
"error": str(e)
|
|
152
|
-
}))
|
|
153
|
-
|
|
154
|
-
if __name__ == "__main__":
|
|
155
|
-
main()
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: PreToolUse for AskUserQuestion
|
|
4
|
-
Purpose: Validate interview questions come from research, not templates
|
|
5
|
-
|
|
6
|
-
This hook ensures that questions asked during the interview phase are
|
|
7
|
-
generated from actual research findings, not generic template questions.
|
|
8
|
-
|
|
9
|
-
Added in v3.6.7 for question quality enforcement.
|
|
10
|
-
|
|
11
|
-
Returns:
|
|
12
|
-
- {"permissionDecision": "allow"} - Question is properly sourced
|
|
13
|
-
- {"permissionDecision": "allow", "message": "..."} - Allow with reminder
|
|
14
|
-
"""
|
|
15
|
-
import json
|
|
16
|
-
import sys
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_active_endpoint(state):
|
|
23
|
-
"""Get active endpoint - supports both old and new state formats."""
|
|
24
|
-
if "endpoints" in state and "active_endpoint" in state:
|
|
25
|
-
active = state.get("active_endpoint")
|
|
26
|
-
if active and active in state["endpoints"]:
|
|
27
|
-
return active, state["endpoints"][active]
|
|
28
|
-
return None, None
|
|
29
|
-
|
|
30
|
-
endpoint = state.get("endpoint")
|
|
31
|
-
if endpoint:
|
|
32
|
-
return endpoint, state
|
|
33
|
-
|
|
34
|
-
return None, None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_research_keywords(state, endpoint_data):
|
|
38
|
-
"""Extract keywords from research that should appear in questions."""
|
|
39
|
-
keywords = set()
|
|
40
|
-
|
|
41
|
-
# From research queries
|
|
42
|
-
for query in state.get("research_queries", []):
|
|
43
|
-
q = query.get("query", "")
|
|
44
|
-
# Extract meaningful words (length > 3)
|
|
45
|
-
words = [w.lower() for w in q.split() if len(w) > 3]
|
|
46
|
-
keywords.update(words)
|
|
47
|
-
|
|
48
|
-
# From initial research sources
|
|
49
|
-
initial = endpoint_data.get("phases", {}).get("research_initial", {})
|
|
50
|
-
for src in initial.get("sources", []):
|
|
51
|
-
if isinstance(src, dict):
|
|
52
|
-
summary = src.get("summary", "")
|
|
53
|
-
words = [w.lower() for w in summary.split() if len(w) > 3]
|
|
54
|
-
keywords.update(words)
|
|
55
|
-
|
|
56
|
-
# From deep research sources
|
|
57
|
-
deep = endpoint_data.get("phases", {}).get("research_deep", {})
|
|
58
|
-
for src in deep.get("sources", []):
|
|
59
|
-
if isinstance(src, dict):
|
|
60
|
-
summary = src.get("summary", "")
|
|
61
|
-
words = [w.lower() for w in summary.split() if len(w) > 3]
|
|
62
|
-
keywords.update(words)
|
|
63
|
-
|
|
64
|
-
return keywords
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def main():
|
|
68
|
-
try:
|
|
69
|
-
input_data = json.load(sys.stdin)
|
|
70
|
-
except json.JSONDecodeError:
|
|
71
|
-
print(json.dumps({"permissionDecision": "allow"}))
|
|
72
|
-
sys.exit(0)
|
|
73
|
-
|
|
74
|
-
tool_name = input_data.get("tool_name", "")
|
|
75
|
-
tool_input = input_data.get("tool_input", {})
|
|
76
|
-
|
|
77
|
-
if tool_name != "AskUserQuestion":
|
|
78
|
-
print(json.dumps({"permissionDecision": "allow"}))
|
|
79
|
-
sys.exit(0)
|
|
80
|
-
|
|
81
|
-
if not STATE_FILE.exists():
|
|
82
|
-
print(json.dumps({"permissionDecision": "allow"}))
|
|
83
|
-
sys.exit(0)
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
state = json.loads(STATE_FILE.read_text())
|
|
87
|
-
except json.JSONDecodeError:
|
|
88
|
-
print(json.dumps({"permissionDecision": "allow"}))
|
|
89
|
-
sys.exit(0)
|
|
90
|
-
|
|
91
|
-
endpoint, endpoint_data = get_active_endpoint(state)
|
|
92
|
-
if not endpoint or not endpoint_data:
|
|
93
|
-
print(json.dumps({"permissionDecision": "allow"}))
|
|
94
|
-
sys.exit(0)
|
|
95
|
-
|
|
96
|
-
# Only enforce during interview phase
|
|
97
|
-
interview = endpoint_data.get("phases", {}).get("interview", {})
|
|
98
|
-
if interview.get("status") != "in_progress":
|
|
99
|
-
print(json.dumps({"permissionDecision": "allow"}))
|
|
100
|
-
sys.exit(0)
|
|
101
|
-
|
|
102
|
-
# Check if research has been done
|
|
103
|
-
initial = endpoint_data.get("phases", {}).get("research_initial", {})
|
|
104
|
-
if initial.get("status") != "complete":
|
|
105
|
-
# Allow question but remind to do research first
|
|
106
|
-
print(json.dumps({
|
|
107
|
-
"permissionDecision": "allow",
|
|
108
|
-
"message": "REMINDER: Initial research (Phase 3) should be complete before interview. Questions should be generated FROM research findings."
|
|
109
|
-
}))
|
|
110
|
-
sys.exit(0)
|
|
111
|
-
|
|
112
|
-
# Get the question being asked
|
|
113
|
-
question = tool_input.get("question", "")
|
|
114
|
-
|
|
115
|
-
# Get research keywords
|
|
116
|
-
keywords = get_research_keywords(state, endpoint_data)
|
|
117
|
-
|
|
118
|
-
# Check if question contains any research-derived terms
|
|
119
|
-
question_lower = question.lower()
|
|
120
|
-
found_keywords = [k for k in keywords if k in question_lower]
|
|
121
|
-
|
|
122
|
-
if not found_keywords and len(keywords) > 5:
|
|
123
|
-
# No research keywords found - this might be a generic question
|
|
124
|
-
print(json.dumps({
|
|
125
|
-
"permissionDecision": "allow",
|
|
126
|
-
"message": f"""NOTE: This question doesn't appear to reference terms discovered in research.
|
|
127
|
-
|
|
128
|
-
Research-derived terms include: {', '.join(list(keywords)[:10])}...
|
|
129
|
-
|
|
130
|
-
BEST PRACTICE: Interview questions should be generated FROM research findings.
|
|
131
|
-
Example: "I discovered the API supports [feature]. Do you want to implement this?"
|
|
132
|
-
|
|
133
|
-
Proceeding anyway, but consider revising the question."""
|
|
134
|
-
}))
|
|
135
|
-
sys.exit(0)
|
|
136
|
-
|
|
137
|
-
# Question looks good
|
|
138
|
-
print(json.dumps({
|
|
139
|
-
"permissionDecision": "allow",
|
|
140
|
-
"message": f"Question references research terms: {', '.join(found_keywords[:5])}"
|
|
141
|
-
}))
|
|
142
|
-
sys.exit(0)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if __name__ == "__main__":
|
|
146
|
-
main()
|