@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.
Files changed (180) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +10 -0
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/settings.local.json +1 -7
  9. package/.claude/workflow-logs/None.json +49 -0
  10. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  11. package/.skills/adr-deep-research/SKILL.md +351 -0
  12. package/.skills/api-create/SKILL.md +34 -20
  13. package/.skills/api-research/SKILL.md +130 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +365 -38
  17. package/.skills/parallel-spawn/SKILL.md +212 -0
  18. package/.skills/ralph-continue/SKILL.md +151 -0
  19. package/.skills/ralph-loop/SKILL.md +341 -0
  20. package/.skills/ralph-status/SKILL.md +87 -0
  21. package/.skills/refactor/SKILL.md +59 -0
  22. package/.skills/shadcn/SKILL.md +522 -0
  23. package/.skills/test-all/SKILL.md +210 -0
  24. package/.skills/test-builds/SKILL.md +208 -0
  25. package/.skills/test-debug/SKILL.md +212 -0
  26. package/.skills/test-e2e/SKILL.md +168 -0
  27. package/.skills/test-review/SKILL.md +707 -0
  28. package/.skills/test-unit/SKILL.md +143 -0
  29. package/.skills/test-visual/SKILL.md +301 -0
  30. package/.skills/token-report/SKILL.md +132 -0
  31. package/CHANGELOG.md +488 -0
  32. package/README.md +346 -53
  33. package/bin/cli.js +359 -123
  34. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  35. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  36. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  37. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  38. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  39. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  40. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  95. package/hooks/api-workflow-check.py +34 -0
  96. package/hooks/auto-answer.py +97 -20
  97. package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
  98. package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
  99. package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
  100. package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
  101. package/hooks/enforce-external-research.py +25 -0
  102. package/hooks/enforce-interview.py +20 -0
  103. package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
  104. package/{.claude/hooks → hooks}/hook_utils.py +0 -0
  105. package/hooks/ntfy-on-question.py +15 -2
  106. package/hooks/orchestrator-handoff.py +81 -3
  107. package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
  108. package/hooks/periodic-reground.py +40 -0
  109. package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
  110. package/hooks/run-code-review.py +176 -29
  111. package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
  112. package/hooks/session-logger.py +27 -1
  113. package/hooks/session-startup.py +113 -0
  114. package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
  115. package/package.json +1 -1
  116. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  117. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  118. package/templates/api-dev-state.json +33 -1
  119. package/templates/brand-page/page.tsx +645 -0
  120. package/templates/component/Component.visual.spec.ts +30 -24
  121. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  122. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  123. package/templates/github-workflows/security.yml +274 -0
  124. package/templates/hustle-build-defaults.json +53 -1
  125. package/templates/page/page.e2e.test.ts +30 -26
  126. package/templates/performance-budgets.json +63 -5
  127. package/templates/registry.json +279 -3
  128. package/templates/review-dashboard/page.tsx +510 -0
  129. package/templates/settings.json +74 -7
  130. package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
  131. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  132. package/.claude/commands/hustle-combine.md +0 -1089
  133. package/.claude/commands/hustle-ui-create-page.md +0 -1078
  134. package/.claude/commands/hustle-ui-create.md +0 -1058
  135. package/.claude/hooks/auto-answer.py +0 -305
  136. package/.claude/hooks/cache-research.py +0 -337
  137. package/.claude/hooks/check-api-routes.py +0 -168
  138. package/.claude/hooks/check-playwright-setup.py +0 -103
  139. package/.claude/hooks/check-storybook-setup.py +0 -81
  140. package/.claude/hooks/check-update.py +0 -132
  141. package/.claude/hooks/detect-interruption.py +0 -165
  142. package/.claude/hooks/enforce-a11y-audit.py +0 -202
  143. package/.claude/hooks/enforce-brand-guide.py +0 -241
  144. package/.claude/hooks/enforce-component-type-confirm.py +0 -97
  145. package/.claude/hooks/enforce-freshness.py +0 -184
  146. package/.claude/hooks/enforce-page-components.py +0 -186
  147. package/.claude/hooks/enforce-page-data-schema.py +0 -155
  148. package/.claude/hooks/enforce-questions-sourced.py +0 -146
  149. package/.claude/hooks/enforce-schema-from-interview.py +0 -248
  150. package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
  151. package/.claude/hooks/enforce-ui-interview.py +0 -130
  152. package/.claude/hooks/generate-manifest-entry.py +0 -1161
  153. package/.claude/hooks/lib/__init__.py +0 -1
  154. package/.claude/hooks/lib/greptile.py +0 -355
  155. package/.claude/hooks/lib/ntfy.py +0 -209
  156. package/.claude/hooks/notify-input-needed.py +0 -73
  157. package/.claude/hooks/notify-phase-complete.py +0 -90
  158. package/.claude/hooks/ntfy-on-question.py +0 -240
  159. package/.claude/hooks/orchestrator-completion.py +0 -313
  160. package/.claude/hooks/orchestrator-handoff.py +0 -267
  161. package/.claude/hooks/orchestrator-session-startup.py +0 -146
  162. package/.claude/hooks/run-code-review.py +0 -393
  163. package/.claude/hooks/session-logger.py +0 -323
  164. package/.claude/hooks/test-orchestrator-reground.py +0 -248
  165. package/.claude/hooks/track-scope-coverage.py +0 -220
  166. package/.claude/hooks/track-token-usage.py +0 -121
  167. package/.claude/hooks/update-api-showcase.py +0 -161
  168. package/.claude/hooks/update-registry.py +0 -352
  169. package/.claude/hooks/update-ui-showcase.py +0 -224
  170. package/.claude/test-auto-answer-bot.py +0 -183
  171. package/.claude/test-completion-detector.py +0 -263
  172. package/.claude/test-orchestrator-state.json +0 -20
  173. package/.claude/test-orchestrator.sh +0 -271
  174. /package/{.claude/commands → commands}/hustle-build.md +0 -0
  175. /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  176. /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  177. /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  178. /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
  179. /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
  180. /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
@@ -17,6 +17,10 @@ v3.6.7 Enhancement:
17
17
  - Research cache location
18
18
  - Summary statistics
19
19
 
20
+ v3.12.13 Fix:
21
+ - Skip enforcement when running in source repository (developing the package)
22
+ - Detect via package.json name = @hustle-together/api-dev-tools
23
+
20
24
  Returns:
21
25
  - {"decision": "approve"} - Allow stopping
22
26
  - {"decision": "block", "reason": "..."} - Prevent stopping with explanation
@@ -28,6 +32,36 @@ import re
28
32
  from datetime import datetime
29
33
  from pathlib import Path
30
34
 
35
+
36
+ def is_source_repository() -> bool:
37
+ """
38
+ Check if we're running in the api-dev-tools source repository.
39
+ If so, hooks should NOT enforce workflow - we're developing, not using.
40
+ """
41
+ try:
42
+ # Use parent of hooks dir (project root), not cwd which may be hooks/
43
+ project_root = Path(__file__).parent.parent
44
+ package_json = project_root / "package.json"
45
+ if package_json.exists():
46
+ data = json.loads(package_json.read_text())
47
+ # If this is the source repo, skip enforcement
48
+ if data.get("name") == "@hustle-together/api-dev-tools":
49
+ return True
50
+
51
+ # Also check for templates/ folder (only exists in source repo)
52
+ if (project_root / "templates").is_dir():
53
+ return True
54
+
55
+ except Exception:
56
+ pass
57
+ return False
58
+
59
+
60
+ # Skip enforcement in source repository
61
+ if is_source_repository():
62
+ print(json.dumps({"decision": "approve"}))
63
+ sys.exit(0)
64
+
31
65
  # State file is in .claude/ directory (sibling to hooks/)
32
66
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
33
67
  RESEARCH_DIR = Path(__file__).parent.parent / "research"
@@ -8,6 +8,10 @@ and either:
8
8
  2. Spawns a Haiku sub-agent to pick the most comprehensive option
9
9
 
10
10
  Hook Type: PreToolUse (matcher: AskUserQuestion)
11
+
12
+ Updated in v4.5.0:
13
+ - Use shared hook_utils for logging
14
+ - Log all auto-answered questions to workflow logs
11
15
  """
12
16
 
13
17
  import json
@@ -15,6 +19,13 @@ import os
15
19
  import sys
16
20
  from pathlib import Path
17
21
 
22
+ # Import shared utilities
23
+ try:
24
+ from hook_utils import log_workflow_event
25
+ UTILS_AVAILABLE = True
26
+ except ImportError:
27
+ UTILS_AVAILABLE = False
28
+
18
29
 
19
30
  def load_state():
20
31
  """Load workflow state to check if in auto mode"""
@@ -46,17 +57,33 @@ def load_state():
46
57
  def load_defaults():
47
58
  """Load pre-configured default answers"""
48
59
  project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
49
- defaults_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
50
60
 
61
+ # Check project-specific defaults first
62
+ defaults_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
51
63
  if defaults_file.exists():
52
64
  try:
53
65
  return json.loads(defaults_file.read_text())
54
66
  except Exception:
55
67
  pass
56
68
 
69
+ # Fall back to template defaults
70
+ template_defaults = Path(project_dir) / "templates" / "hustle-build-defaults.json"
71
+ if template_defaults.exists():
72
+ try:
73
+ return json.loads(template_defaults.read_text())
74
+ except Exception:
75
+ pass
76
+
57
77
  return {}
58
78
 
59
79
 
80
+ def is_autonomous_enabled():
81
+ """Check if autonomous mode is enabled by default in settings"""
82
+ defaults = load_defaults()
83
+ autonomous = defaults.get("autonomous", {})
84
+ return autonomous.get("enabled", False) and autonomous.get("skip_interviews", False)
85
+
86
+
60
87
  def find_comprehensive_option(options):
61
88
  """
62
89
  Find the most comprehensive option based on keywords.
@@ -65,6 +92,9 @@ def find_comprehensive_option(options):
65
92
  - "all", "full", "complete", "comprehensive"
66
93
  - Higher numbers (e.g., "100%" vs "50%")
67
94
  - More features listed
95
+
96
+ Also prioritizes affirmative options for phase exits:
97
+ - "yes", "proceed", "continue", "approve", "confirm"
68
98
  """
69
99
  if not options:
70
100
  return None
@@ -74,6 +104,18 @@ def find_comprehensive_option(options):
74
104
  "maximum", "extensive", "detailed", "thorough", "wcag-aa"
75
105
  ]
76
106
 
107
+ # Affirmative keywords for phase exit questions
108
+ affirmative_keywords = [
109
+ "yes", "proceed", "continue", "approve", "confirm",
110
+ "accept", "ready", "go ahead", "move forward",
111
+ "auto", "defaults", "use auto", "use defaults"
112
+ ]
113
+
114
+ # Negative keywords to avoid
115
+ negative_keywords = [
116
+ "no", "skip", "cancel", "stop", "more research", "not ready"
117
+ ]
118
+
77
119
  # Score each option
78
120
  scored = []
79
121
  for i, opt in enumerate(options):
@@ -83,11 +125,21 @@ def find_comprehensive_option(options):
83
125
 
84
126
  score = 0
85
127
 
128
+ # Check for negative keywords first (penalize heavily)
129
+ for keyword in negative_keywords:
130
+ if keyword in text:
131
+ score -= 50
132
+
86
133
  # Check for comprehensive keywords
87
134
  for keyword in comprehensive_keywords:
88
135
  if keyword in text:
89
136
  score += 10
90
137
 
138
+ # Check for affirmative keywords (high priority for phase exits)
139
+ for keyword in affirmative_keywords:
140
+ if keyword in text:
141
+ score += 25
142
+
91
143
  # Check for "(Recommended)" suffix
92
144
  if "recommended" in label.lower():
93
145
  score += 20
@@ -127,14 +179,19 @@ def main():
127
179
  print(json.dumps({"continue": True}))
128
180
  return
129
181
 
130
- # Check if in auto mode
182
+ # Check if in auto mode (explicit flag OR defaults enabled)
131
183
  state, state_type = load_state()
184
+ autonomous_by_default = is_autonomous_enabled()
132
185
 
133
- if not state:
134
- # Not in auto mode, continue normally
186
+ if not state and not autonomous_by_default:
187
+ # Not in auto mode and autonomous not enabled, continue normally
135
188
  print(json.dumps({"continue": True}))
136
189
  return
137
190
 
191
+ # If no state but autonomous is enabled, create a minimal state
192
+ if not state and autonomous_by_default:
193
+ state = {"mode": "auto", "source": "defaults"}
194
+
138
195
  # Load defaults
139
196
  defaults = load_defaults()
140
197
 
@@ -172,23 +229,28 @@ def main():
172
229
  # Log the auto-answer
173
230
  log_auto_answer(state, questions, answers)
174
231
 
175
- # Return the auto-selected answers
176
- # The hook will inject these as if the user selected them
232
+ # Get the first question and answer for display
233
+ first_question = questions[0] if questions else {}
234
+ header = first_question.get("header", "Question")
235
+ question_text = first_question.get("question", "")
236
+ answer = list(answers.values())[0] if answers else "Unknown"
237
+
238
+ # BLOCK the tool and provide the answer in the reason
239
+ # This prevents the question UI from showing and tells the AI to use this answer
177
240
  result = {
178
- "continue": True,
179
- "additionalContext": f"""
180
- ## Auto-Mode Active
241
+ "continue": False,
242
+ "reason": f"""## 🤖 Auto-Selected
243
+
244
+ **{header}:** {answer}
181
245
 
182
- Questions were auto-answered with comprehensive defaults:
183
- {json.dumps(answers, indent=2)}
246
+ _Question: {question_text}_
184
247
 
185
- These selections prioritize:
186
- - Maximum feature coverage
187
- - Full testing
188
- - Comprehensive documentation
189
- - Best practices
248
+ ---
190
249
 
191
- Review in `/hustle-build-review` after completion.
250
+ Autonomous mode is active. The workflow will proceed with this answer.
251
+
252
+ To review auto-selected answers: `.claude/workflow-logs/`
253
+ To disable: Set `autonomous.enabled: false` in `.claude/hustle-build-defaults.json`
192
254
  """
193
255
  }
194
256
  print(json.dumps(result))
@@ -197,19 +259,34 @@ Review in `/hustle-build-review` after completion.
197
259
 
198
260
 
199
261
  def log_auto_answer(state, questions, answers):
200
- """Log auto-answered questions to build log"""
262
+ """Log auto-answered questions to workflow log using shared utility (v4.5.0)"""
263
+ # Use shared utility if available
264
+ if UTILS_AVAILABLE:
265
+ try:
266
+ log_workflow_event("auto_answer", {
267
+ "questions": [q.get("question") for q in questions],
268
+ "headers": [q.get("header") for q in questions],
269
+ "answers": answers,
270
+ "reason": "auto-comprehensive",
271
+ "mode": state.get("mode", "auto") if state else "auto"
272
+ })
273
+ return
274
+ except Exception:
275
+ pass
276
+
277
+ # Fallback to legacy logging
201
278
  project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
202
279
  logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
203
280
  logs_dir.mkdir(parents=True, exist_ok=True)
204
281
 
205
- build_id = state.get("build_id", state.get("workflow_id", "unknown"))
282
+ build_id = state.get("build_id", state.get("workflow_id", "unknown")) if state else "unknown"
206
283
  log_file = logs_dir / f"{build_id}.json"
207
284
 
208
285
  try:
209
286
  if log_file.exists():
210
287
  log = json.loads(log_file.read_text())
211
288
  else:
212
- log = {"auto_answers": []}
289
+ log = {"auto_answers": [], "events": []}
213
290
 
214
291
  from datetime import datetime
215
292
  log["auto_answers"].append({
File without changes
File without changes
@@ -16,6 +16,9 @@ The hook triggers on:
16
16
  - ANY questions about tools, services, or platforms
17
17
  - ANY request for implementation, editing, or changes
18
18
 
19
+ v3.12.13 Fix:
20
+ - Skip enforcement when running in source repository (developing the package)
21
+
19
22
  Returns:
20
23
  - Prints context to stdout (injected into conversation)
21
24
  - Exit 0 to allow the prompt to proceed
@@ -26,6 +29,28 @@ import re
26
29
  from pathlib import Path
27
30
  from datetime import datetime
28
31
 
32
+ # Import shared utilities
33
+ try:
34
+ from hook_utils import is_source_repository
35
+ except ImportError:
36
+ # Fallback if import fails
37
+ def is_source_repository():
38
+ try:
39
+ package_json = Path.cwd() / "package.json"
40
+ if package_json.exists():
41
+ data = json.loads(package_json.read_text())
42
+ if data.get("name") == "@hustle-together/api-dev-tools":
43
+ return True
44
+ if (Path.cwd() / "templates").is_dir():
45
+ return True
46
+ except Exception:
47
+ pass
48
+ return False
49
+
50
+ # Skip enforcement in source repository
51
+ if is_source_repository():
52
+ sys.exit(0)
53
+
29
54
  # State file is in .claude/ directory (sibling to hooks/)
30
55
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
31
56
 
@@ -27,6 +27,13 @@ import json
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
+ # Import shared utilities for logging (v4.5.0)
31
+ try:
32
+ from hook_utils import log_workflow_event
33
+ UTILS_AVAILABLE = True
34
+ except ImportError:
35
+ UTILS_AVAILABLE = False
36
+
30
37
  # State file is in .claude/ directory (sibling to hooks/)
31
38
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
32
39
 
@@ -333,6 +340,19 @@ WHY: User must approve their decisions before they drive implementation."""
333
340
  # Build a reminder of what the user decided
334
341
  decision_summary = _build_decision_summary(decisions)
335
342
 
343
+ # Log the interview decision being applied (v4.5.0)
344
+ if UTILS_AVAILABLE:
345
+ try:
346
+ log_workflow_event("interview_decision", {
347
+ "action": "applying_decisions",
348
+ "file_path": file_path,
349
+ "decision_count": len(decisions),
350
+ "decisions": {k: v.get("value", v.get("response", ""))[:100]
351
+ for k, v in decisions.items()}
352
+ })
353
+ except Exception:
354
+ pass
355
+
336
356
  # Allow but inject context about user decisions
337
357
  print(json.dumps({
338
358
  "permissionDecision": "allow",
File without changes
File without changes
@@ -44,13 +44,26 @@ def load_state():
44
44
 
45
45
 
46
46
  def get_ntfy_config():
47
- """Get NTFY configuration from environment or .env file"""
47
+ """Get NTFY configuration from environment, .env file, or hustle-build-defaults.json"""
48
48
  topic = os.environ.get("NTFY_TOPIC")
49
49
  server = os.environ.get("NTFY_SERVER", "https://ntfy.sh")
50
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
51
+
52
+ if not topic:
53
+ # Try loading from hustle-build-defaults.json first
54
+ defaults_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
55
+ if defaults_file.exists():
56
+ try:
57
+ defaults = json.loads(defaults_file.read_text())
58
+ ntfy_config = defaults.get("ntfy", {})
59
+ if ntfy_config.get("enabled", False):
60
+ topic = ntfy_config.get("topic")
61
+ server = ntfy_config.get("server", server)
62
+ except Exception:
63
+ pass
50
64
 
51
65
  if not topic:
52
66
  # Try loading from .env
53
- project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
54
67
  env_file = Path(project_dir) / ".env"
55
68
 
56
69
  if env_file.exists():
@@ -162,8 +162,8 @@ def main():
162
162
  except Exception:
163
163
  pass
164
164
 
165
- # Inject context about orchestration
166
- context = f"""
165
+ # Build context about orchestration
166
+ context_parts = [f"""
167
167
  ## Orchestrated Workflow
168
168
 
169
169
  This workflow is part of a larger build: **{build_state.get('build_id')}**
@@ -175,7 +175,85 @@ These decisions are already applied. **Do not re-ask** questions about:
175
175
  {', '.join(shared_decisions.keys())}
176
176
 
177
177
  Only ask workflow-specific questions not covered above.
178
- """
178
+ """]
179
+
180
+ # Check for project_spec and inject relevant portion
181
+ project_spec = build_state.get("project_spec", {})
182
+ extracted = project_spec.get("extracted", {})
183
+
184
+ if extracted:
185
+ # Try to find the relevant spec for this workflow
186
+ relevant_spec = None
187
+ spec_type = None
188
+
189
+ # Get the element name from tool input
190
+ try:
191
+ data = json.loads(tool_input)
192
+ args = data.get("args", "")
193
+ element_name = args.split()[0] if args else ""
194
+ except Exception:
195
+ element_name = ""
196
+
197
+ # Search in extracted elements
198
+ for api in extracted.get("apis", []):
199
+ if api.get("name", "").lower() == element_name.lower():
200
+ relevant_spec = api
201
+ spec_type = "API"
202
+ break
203
+
204
+ if not relevant_spec:
205
+ for comp in extracted.get("components", []):
206
+ if comp.get("name", "").lower() == element_name.lower():
207
+ relevant_spec = comp
208
+ spec_type = "Component"
209
+ break
210
+
211
+ if not relevant_spec:
212
+ for page in extracted.get("pages", []):
213
+ if page.get("name", "").lower() == element_name.lower():
214
+ relevant_spec = page
215
+ spec_type = "Page"
216
+ break
217
+
218
+ # Inject relevant spec if found
219
+ if relevant_spec:
220
+ context_parts.append(f"""
221
+ ### Project Spec ({spec_type})
222
+
223
+ This element was extracted from the project document. Use this as the primary source of truth:
224
+
225
+ ```json
226
+ {json.dumps(relevant_spec, indent=2)}
227
+ ```
228
+
229
+ **Important:** Implement according to this specification. If you need to deviate, ask the user first.
230
+ """)
231
+
232
+ # Also inject high-level summary if available
233
+ summary = extracted.get("summary", "")
234
+ if summary:
235
+ context_parts.append(f"""
236
+ ### Project Summary
237
+
238
+ {summary}
239
+ """)
240
+
241
+ # Inject related elements for context
242
+ uses_apis = relevant_spec.get("uses_apis", []) if relevant_spec else []
243
+ uses_components = relevant_spec.get("uses_components", []) if relevant_spec else []
244
+
245
+ if uses_apis or uses_components:
246
+ context_parts.append(f"""
247
+ ### Related Elements
248
+
249
+ This element depends on:
250
+ - APIs: {', '.join(uses_apis) if uses_apis else 'none'}
251
+ - Components: {', '.join(uses_components) if uses_components else 'none'}
252
+
253
+ Ensure types and interfaces align with these dependencies.
254
+ """)
255
+
256
+ context = "\n".join(context_parts)
179
257
 
180
258
  result = {
181
259
  "continue": True,
@@ -70,10 +70,38 @@ def get_registry_summary(registry):
70
70
  return None
71
71
 
72
72
  summary = {}
73
+ # Core elements
73
74
  for category in ["apis", "components", "pages", "combined"]:
74
75
  items = registry.get(category, {})
75
76
  if items:
76
77
  summary[category] = list(items.keys())
78
+
79
+ # Infrastructure tracking (v1.3.0+)
80
+ routes = registry.get("routes", {})
81
+ if routes and not routes.get("_description"):
82
+ # Has actual routes, not just template
83
+ actual_routes = [k for k in routes.keys() if not k.startswith("_")]
84
+ if actual_routes:
85
+ summary["routes"] = actual_routes
86
+
87
+ env_vars = registry.get("env_vars", {})
88
+ if env_vars:
89
+ actual_vars = [k for k in env_vars.keys() if not k.startswith("_")]
90
+ if actual_vars:
91
+ summary["env_vars"] = actual_vars
92
+
93
+ services = registry.get("services", {})
94
+ if services:
95
+ actual_services = [k for k in services.keys() if not k.startswith("_")]
96
+ if actual_services:
97
+ summary["services"] = actual_services
98
+
99
+ webhooks = registry.get("webhooks", {})
100
+ if webhooks:
101
+ actual_webhooks = [k for k in webhooks.keys() if not k.startswith("_")]
102
+ if actual_webhooks:
103
+ summary["webhooks"] = actual_webhooks
104
+
77
105
  return summary if summary else None
78
106
 
79
107
 
@@ -207,6 +235,18 @@ def build_reground_context(state, turn_count):
207
235
  parts.append(f" - Components: {format_list(registry_summary['components'])}")
208
236
  if registry_summary.get("pages"):
209
237
  parts.append(f" - Pages: {format_list(registry_summary['pages'])}")
238
+ if registry_summary.get("routes"):
239
+ parts.append(f" - Routes: {format_list(registry_summary['routes'])}")
240
+
241
+ # === Infrastructure Awareness ===
242
+ if registry_summary:
243
+ if registry_summary.get("services"):
244
+ parts.append("")
245
+ parts.append(f"**External Services:** {format_list(registry_summary['services'])}")
246
+ if registry_summary.get("webhooks"):
247
+ parts.append(f"**Webhooks:** {format_list(registry_summary['webhooks'])}")
248
+ if registry_summary.get("env_vars"):
249
+ parts.append(f"**Env Vars Tracked:** {len(registry_summary['env_vars'])} variables")
210
250
 
211
251
  # === Deferred Features ===
212
252
  deferred = state.get("deferred_features", [])