@hustle-together/api-dev-tools 3.6.5 → 3.9.2
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/README.md +5307 -258
- package/bin/cli.js +348 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +22 -2
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +385 -19
- package/hooks/cache-research.py +337 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-brand-guide.py +131 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +981 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +65 -10
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +148 -0
- package/package.json +8 -2
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +179 -161
- package/templates/api-showcase/APICard.tsx +153 -0
- package/templates/api-showcase/APIModal.tsx +375 -0
- package/templates/api-showcase/APIShowcase.tsx +231 -0
- package/templates/api-showcase/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +74 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PreToolUse for Write/Edit on schema files
|
|
4
|
+
Purpose: Validate schema fields match interview decisions
|
|
5
|
+
|
|
6
|
+
This hook ensures that when writing Zod schema files, the fields
|
|
7
|
+
match what the user selected during the interview phase.
|
|
8
|
+
|
|
9
|
+
Added in v3.6.7 for schema-interview consistency enforcement.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
- {"permissionDecision": "allow"} - Schema matches interview
|
|
13
|
+
- {"permissionDecision": "allow", "message": "..."} - Allow with warning
|
|
14
|
+
- {"permissionDecision": "deny", "reason": "..."} - Block with explanation
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_active_endpoint(state):
|
|
25
|
+
"""Get active endpoint - supports both old and new state formats."""
|
|
26
|
+
if "endpoints" in state and "active_endpoint" in state:
|
|
27
|
+
active = state.get("active_endpoint")
|
|
28
|
+
if active and active in state["endpoints"]:
|
|
29
|
+
return active, state["endpoints"][active]
|
|
30
|
+
return None, None
|
|
31
|
+
|
|
32
|
+
endpoint = state.get("endpoint")
|
|
33
|
+
if endpoint:
|
|
34
|
+
return endpoint, state
|
|
35
|
+
|
|
36
|
+
return None, None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_schema_fields_from_content(content):
|
|
40
|
+
"""Extract field names from Zod schema content."""
|
|
41
|
+
fields = set()
|
|
42
|
+
|
|
43
|
+
# Match Zod object field definitions
|
|
44
|
+
# Patterns like: fieldName: z.string(), fieldName: z.number().optional()
|
|
45
|
+
field_pattern = r'(\w+):\s*z\.\w+\('
|
|
46
|
+
|
|
47
|
+
for match in re.finditer(field_pattern, content):
|
|
48
|
+
field_name = match.group(1)
|
|
49
|
+
# Skip common non-field names
|
|
50
|
+
if field_name not in {'z', 'const', 'export', 'type', 'interface'}:
|
|
51
|
+
fields.add(field_name.lower())
|
|
52
|
+
|
|
53
|
+
return fields
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def extract_interview_approved_fields(endpoint_data):
|
|
57
|
+
"""Extract field names that were approved during interview."""
|
|
58
|
+
approved_fields = set()
|
|
59
|
+
|
|
60
|
+
interview = endpoint_data.get("phases", {}).get("interview", {})
|
|
61
|
+
decisions = interview.get("decisions", {})
|
|
62
|
+
questions = interview.get("questions", [])
|
|
63
|
+
|
|
64
|
+
# Extract from decisions
|
|
65
|
+
for key, value in decisions.items():
|
|
66
|
+
# Decision keys often match field names
|
|
67
|
+
approved_fields.add(key.lower())
|
|
68
|
+
|
|
69
|
+
# Values might be lists of approved options
|
|
70
|
+
if isinstance(value, list):
|
|
71
|
+
for v in value:
|
|
72
|
+
if isinstance(v, str):
|
|
73
|
+
approved_fields.add(v.lower())
|
|
74
|
+
elif isinstance(value, str):
|
|
75
|
+
approved_fields.add(value.lower())
|
|
76
|
+
|
|
77
|
+
# Extract from question text (look for parameters mentioned)
|
|
78
|
+
for q in questions:
|
|
79
|
+
if isinstance(q, dict):
|
|
80
|
+
q_text = q.get("question", "") + " " + q.get("answer", "")
|
|
81
|
+
else:
|
|
82
|
+
q_text = str(q)
|
|
83
|
+
|
|
84
|
+
# Look for parameter-like words
|
|
85
|
+
param_pattern = r'\b(param|field|property|attribute)[:=\s]+["\']?(\w+)["\']?'
|
|
86
|
+
for match in re.finditer(param_pattern, q_text, re.IGNORECASE):
|
|
87
|
+
approved_fields.add(match.group(2).lower())
|
|
88
|
+
|
|
89
|
+
# Also extract snake_case and camelCase words that look like fields
|
|
90
|
+
field_like = re.findall(r'\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b', q_text, re.IGNORECASE)
|
|
91
|
+
for f in field_like:
|
|
92
|
+
approved_fields.add(f.lower())
|
|
93
|
+
|
|
94
|
+
field_like = re.findall(r'\b([a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)+)\b', q_text)
|
|
95
|
+
for f in field_like:
|
|
96
|
+
# Convert camelCase to lowercase for comparison
|
|
97
|
+
approved_fields.add(f.lower())
|
|
98
|
+
|
|
99
|
+
# Extract from scope if available
|
|
100
|
+
scope = endpoint_data.get("scope", {})
|
|
101
|
+
for feature in scope.get("implemented_features", []):
|
|
102
|
+
if isinstance(feature, dict):
|
|
103
|
+
approved_fields.add(feature.get("name", "").lower())
|
|
104
|
+
else:
|
|
105
|
+
approved_fields.add(str(feature).lower())
|
|
106
|
+
|
|
107
|
+
return approved_fields
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
try:
|
|
112
|
+
input_data = json.load(sys.stdin)
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
115
|
+
sys.exit(0)
|
|
116
|
+
|
|
117
|
+
tool_name = input_data.get("tool_name", "")
|
|
118
|
+
tool_input = input_data.get("tool_input", {})
|
|
119
|
+
|
|
120
|
+
# Only check Write and Edit tools
|
|
121
|
+
if tool_name not in ["Write", "Edit"]:
|
|
122
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
# Check if writing to a schema file
|
|
126
|
+
file_path = tool_input.get("file_path", "")
|
|
127
|
+
|
|
128
|
+
# Detect schema files by path patterns
|
|
129
|
+
is_schema_file = any([
|
|
130
|
+
"/schemas/" in file_path,
|
|
131
|
+
"schema.ts" in file_path.lower(),
|
|
132
|
+
"schemas.ts" in file_path.lower(),
|
|
133
|
+
".schema.ts" in file_path.lower(),
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
if not is_schema_file:
|
|
137
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
# Load state
|
|
141
|
+
if not STATE_FILE.exists():
|
|
142
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
state = json.loads(STATE_FILE.read_text())
|
|
147
|
+
except json.JSONDecodeError:
|
|
148
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
149
|
+
sys.exit(0)
|
|
150
|
+
|
|
151
|
+
endpoint, endpoint_data = get_active_endpoint(state)
|
|
152
|
+
if not endpoint or not endpoint_data:
|
|
153
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
154
|
+
sys.exit(0)
|
|
155
|
+
|
|
156
|
+
# Check if interview phase is complete
|
|
157
|
+
interview = endpoint_data.get("phases", {}).get("interview", {})
|
|
158
|
+
if interview.get("status") != "complete":
|
|
159
|
+
# Interview not done yet - allow but warn
|
|
160
|
+
print(json.dumps({
|
|
161
|
+
"permissionDecision": "allow",
|
|
162
|
+
"message": "WARNING: Writing schema before interview is complete. Schema fields should be derived from interview decisions."
|
|
163
|
+
}))
|
|
164
|
+
sys.exit(0)
|
|
165
|
+
|
|
166
|
+
# Get schema content being written
|
|
167
|
+
if tool_name == "Write":
|
|
168
|
+
schema_content = tool_input.get("content", "")
|
|
169
|
+
else: # Edit
|
|
170
|
+
new_string = tool_input.get("new_string", "")
|
|
171
|
+
schema_content = new_string
|
|
172
|
+
|
|
173
|
+
if not schema_content:
|
|
174
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
175
|
+
sys.exit(0)
|
|
176
|
+
|
|
177
|
+
# Extract fields from schema
|
|
178
|
+
schema_fields = extract_schema_fields_from_content(schema_content)
|
|
179
|
+
|
|
180
|
+
if not schema_fields:
|
|
181
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
# Extract approved fields from interview
|
|
185
|
+
approved_fields = extract_interview_approved_fields(endpoint_data)
|
|
186
|
+
|
|
187
|
+
if not approved_fields:
|
|
188
|
+
# No interview data to compare against
|
|
189
|
+
print(json.dumps({
|
|
190
|
+
"permissionDecision": "allow",
|
|
191
|
+
"message": "NOTE: No interview decisions found to validate schema against."
|
|
192
|
+
}))
|
|
193
|
+
sys.exit(0)
|
|
194
|
+
|
|
195
|
+
# Find fields in schema that weren't discussed in interview
|
|
196
|
+
# Use fuzzy matching - if any part of field name matches approved
|
|
197
|
+
unmatched_fields = []
|
|
198
|
+
for field in schema_fields:
|
|
199
|
+
matched = False
|
|
200
|
+
for approved in approved_fields:
|
|
201
|
+
# Check if field contains or is contained by approved field
|
|
202
|
+
if field in approved or approved in field:
|
|
203
|
+
matched = True
|
|
204
|
+
break
|
|
205
|
+
# Check word overlap
|
|
206
|
+
field_words = set(re.split(r'[_\s]', field))
|
|
207
|
+
approved_words = set(re.split(r'[_\s]', approved))
|
|
208
|
+
if field_words & approved_words:
|
|
209
|
+
matched = True
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if not matched:
|
|
213
|
+
unmatched_fields.append(field)
|
|
214
|
+
|
|
215
|
+
# Common fields that are always okay
|
|
216
|
+
common_fields = {'id', 'createdat', 'updatedat', 'error', 'message', 'success', 'data', 'status', 'result'}
|
|
217
|
+
unmatched_fields = [f for f in unmatched_fields if f not in common_fields]
|
|
218
|
+
|
|
219
|
+
if unmatched_fields:
|
|
220
|
+
# Found fields not in interview - warn but allow
|
|
221
|
+
print(json.dumps({
|
|
222
|
+
"permissionDecision": "allow",
|
|
223
|
+
"message": f"""SCHEMA VALIDATION NOTE:
|
|
224
|
+
|
|
225
|
+
The following schema fields were not explicitly discussed in the interview:
|
|
226
|
+
{', '.join(unmatched_fields[:5])}{'...' if len(unmatched_fields) > 5 else ''}
|
|
227
|
+
|
|
228
|
+
Interview-approved terms: {', '.join(list(approved_fields)[:10])}{'...' if len(approved_fields) > 10 else ''}
|
|
229
|
+
|
|
230
|
+
This is allowed, but consider:
|
|
231
|
+
1. Did research discover these fields?
|
|
232
|
+
2. Should the user be asked about these?
|
|
233
|
+
3. Are these derived from approved fields?
|
|
234
|
+
|
|
235
|
+
Proceeding with schema write."""
|
|
236
|
+
}))
|
|
237
|
+
sys.exit(0)
|
|
238
|
+
|
|
239
|
+
# All fields match interview
|
|
240
|
+
print(json.dumps({
|
|
241
|
+
"permissionDecision": "allow",
|
|
242
|
+
"message": f"Schema fields validated against interview: {', '.join(list(schema_fields)[:5])}"
|
|
243
|
+
}))
|
|
244
|
+
sys.exit(0)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PreToolUse for Write/Edit
|
|
4
|
+
Purpose: Block UI implementation until component/page type is clarified (Phase 1)
|
|
5
|
+
|
|
6
|
+
This hook ensures that Phase 1 (Disambiguation) is complete before any
|
|
7
|
+
component or page files are written. It checks that:
|
|
8
|
+
- Component type (atom/molecule/organism) is specified for components
|
|
9
|
+
- Page type (landing/dashboard/form/list) is specified for pages
|
|
10
|
+
|
|
11
|
+
Version: 3.9.0
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
- {"continue": true} - If disambiguation is complete or not a UI workflow
|
|
15
|
+
- {"continue": false, "reason": "..."} - If disambiguation is incomplete
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# State file is in .claude/ directory (sibling to hooks/)
|
|
22
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
# Read hook input from stdin
|
|
27
|
+
try:
|
|
28
|
+
input_data = json.load(sys.stdin)
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
print(json.dumps({"continue": True}))
|
|
31
|
+
sys.exit(0)
|
|
32
|
+
|
|
33
|
+
tool_name = input_data.get("tool_name", "")
|
|
34
|
+
tool_input = input_data.get("tool_input", {})
|
|
35
|
+
|
|
36
|
+
# Only check Write/Edit operations
|
|
37
|
+
if tool_name not in ["Write", "Edit"]:
|
|
38
|
+
print(json.dumps({"continue": True}))
|
|
39
|
+
sys.exit(0)
|
|
40
|
+
|
|
41
|
+
# Check if targeting component or page files
|
|
42
|
+
file_path = tool_input.get("file_path", "")
|
|
43
|
+
is_component = "/components/" in file_path and file_path.endswith(".tsx")
|
|
44
|
+
is_page = "/app/" in file_path and "page.tsx" in file_path
|
|
45
|
+
|
|
46
|
+
if not is_component and not is_page:
|
|
47
|
+
print(json.dumps({"continue": True}))
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
# Check if state file exists
|
|
51
|
+
if not STATE_FILE.exists():
|
|
52
|
+
print(json.dumps({"continue": True}))
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
|
|
55
|
+
# Load state
|
|
56
|
+
try:
|
|
57
|
+
state = json.loads(STATE_FILE.read_text())
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
print(json.dumps({"continue": True}))
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
workflow = state.get("workflow", "")
|
|
63
|
+
|
|
64
|
+
# Only enforce for UI workflows
|
|
65
|
+
if workflow not in ["ui-create-component", "ui-create-page"]:
|
|
66
|
+
print(json.dumps({"continue": True}))
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
# Get UI config
|
|
70
|
+
ui_config = state.get("ui_config", {})
|
|
71
|
+
|
|
72
|
+
# Check disambiguation for components
|
|
73
|
+
if workflow == "ui-create-component":
|
|
74
|
+
component_type = ui_config.get("component_type", "")
|
|
75
|
+
if not component_type:
|
|
76
|
+
print(json.dumps({
|
|
77
|
+
"continue": False,
|
|
78
|
+
"reason": (
|
|
79
|
+
"Phase 1 (Disambiguation) incomplete.\n\n"
|
|
80
|
+
"Before creating this component, you must clarify:\n"
|
|
81
|
+
"- Is this an Atom, Molecule, or Organism?\n\n"
|
|
82
|
+
"Please complete the disambiguation phase first."
|
|
83
|
+
)
|
|
84
|
+
}))
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
|
|
87
|
+
# Check disambiguation for pages
|
|
88
|
+
if workflow == "ui-create-page":
|
|
89
|
+
page_type = ui_config.get("page_type", "")
|
|
90
|
+
if not page_type:
|
|
91
|
+
print(json.dumps({
|
|
92
|
+
"continue": False,
|
|
93
|
+
"reason": (
|
|
94
|
+
"Phase 1 (Disambiguation) incomplete.\n\n"
|
|
95
|
+
"Before creating this page, you must clarify:\n"
|
|
96
|
+
"- Is this a Landing, Dashboard, Form, or List page?\n\n"
|
|
97
|
+
"Please complete the disambiguation phase first."
|
|
98
|
+
)
|
|
99
|
+
}))
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
# Disambiguation complete
|
|
103
|
+
print(json.dumps({"continue": True}))
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PreToolUse for Write/Edit
|
|
4
|
+
Purpose: Inject UI interview decisions during component/page implementation
|
|
5
|
+
|
|
6
|
+
This hook injects the user's interview answers (variants, accessibility level,
|
|
7
|
+
component dependencies, etc.) when Claude writes implementation code.
|
|
8
|
+
This ensures the implementation matches what the user specified.
|
|
9
|
+
|
|
10
|
+
Version: 3.9.0
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
- {"continue": true} - Always continues
|
|
14
|
+
- May include "notify" with key decisions summary
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# State file is in .claude/ directory (sibling to hooks/)
|
|
21
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_decisions(decisions):
|
|
25
|
+
"""Format interview decisions for display."""
|
|
26
|
+
formatted = []
|
|
27
|
+
|
|
28
|
+
for key, value in decisions.items():
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
# Extract value from nested structure
|
|
31
|
+
display_value = value.get("value", value.get("response", str(value)))
|
|
32
|
+
else:
|
|
33
|
+
display_value = value
|
|
34
|
+
|
|
35
|
+
# Format for display
|
|
36
|
+
if isinstance(display_value, list):
|
|
37
|
+
display_value = ", ".join(str(v) for v in display_value)
|
|
38
|
+
elif isinstance(display_value, bool):
|
|
39
|
+
display_value = "Yes" if display_value else "No"
|
|
40
|
+
|
|
41
|
+
formatted.append(f"{key}: {display_value}")
|
|
42
|
+
|
|
43
|
+
return formatted
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main():
|
|
47
|
+
# Read hook input from stdin
|
|
48
|
+
try:
|
|
49
|
+
input_data = json.load(sys.stdin)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
print(json.dumps({"continue": True}))
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
tool_name = input_data.get("tool_name", "")
|
|
55
|
+
tool_input = input_data.get("tool_input", {})
|
|
56
|
+
|
|
57
|
+
# Only check Write/Edit operations
|
|
58
|
+
if tool_name not in ["Write", "Edit"]:
|
|
59
|
+
print(json.dumps({"continue": True}))
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
# Check if targeting component or page files
|
|
63
|
+
file_path = tool_input.get("file_path", "")
|
|
64
|
+
is_component = "/components/" in file_path and file_path.endswith(".tsx")
|
|
65
|
+
is_page = "/app/" in file_path and "page.tsx" in file_path
|
|
66
|
+
|
|
67
|
+
if not is_component and not is_page:
|
|
68
|
+
print(json.dumps({"continue": True}))
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
# Check if state file exists
|
|
72
|
+
if not STATE_FILE.exists():
|
|
73
|
+
print(json.dumps({"continue": True}))
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
# Load state
|
|
77
|
+
try:
|
|
78
|
+
state = json.loads(STATE_FILE.read_text())
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
print(json.dumps({"continue": True}))
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
workflow = state.get("workflow", "")
|
|
84
|
+
|
|
85
|
+
# Only apply for UI workflows
|
|
86
|
+
if workflow not in ["ui-create-component", "ui-create-page"]:
|
|
87
|
+
print(json.dumps({"continue": True}))
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
# Get interview decisions
|
|
91
|
+
# Try elements format first, then fall back to phases format
|
|
92
|
+
active_element = state.get("active_element", "")
|
|
93
|
+
elements = state.get("elements", {})
|
|
94
|
+
|
|
95
|
+
if active_element and active_element in elements:
|
|
96
|
+
phases = elements[active_element].get("phases", {})
|
|
97
|
+
else:
|
|
98
|
+
phases = state.get("phases", {})
|
|
99
|
+
|
|
100
|
+
interview = phases.get("interview", {})
|
|
101
|
+
decisions = interview.get("decisions", {})
|
|
102
|
+
|
|
103
|
+
if not decisions:
|
|
104
|
+
print(json.dumps({"continue": True}))
|
|
105
|
+
sys.exit(0)
|
|
106
|
+
|
|
107
|
+
# Format key decisions for notification
|
|
108
|
+
formatted = format_decisions(decisions)
|
|
109
|
+
|
|
110
|
+
# Limit to most important decisions
|
|
111
|
+
key_decisions = []
|
|
112
|
+
priority_keys = ["component_type", "page_type", "variants", "accessibility", "design_system", "data_fetching"]
|
|
113
|
+
|
|
114
|
+
for key in priority_keys:
|
|
115
|
+
for f in formatted:
|
|
116
|
+
if f.lower().startswith(key.replace("_", " ")):
|
|
117
|
+
key_decisions.append(f)
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
if key_decisions:
|
|
121
|
+
notify_msg = "Interview decisions: " + " | ".join(key_decisions[:4])
|
|
122
|
+
print(json.dumps({"continue": True, "notify": notify_msg}))
|
|
123
|
+
else:
|
|
124
|
+
print(json.dumps({"continue": True}))
|
|
125
|
+
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
main()
|