@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.
Files changed (159) 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 +7 -463
  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/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -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 +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. package/templates/ui-showcase/page.tsx +1 -1
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NTFY notification hook for AskUserQuestion.
4
+
5
+ Sends a push notification via NTFY when a question is asked,
6
+ allowing the user to be notified on their phone/desktop.
7
+
8
+ Hook Type: PostToolUse (matcher: AskUserQuestion)
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ try:
17
+ import requests
18
+ HAS_REQUESTS = True
19
+ except ImportError:
20
+ HAS_REQUESTS = False
21
+
22
+
23
+ def load_state():
24
+ """Load workflow state for context"""
25
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
26
+
27
+ # Check hustle-build state first
28
+ build_state = Path(project_dir) / ".claude" / "hustle-build-state.json"
29
+ if build_state.exists():
30
+ try:
31
+ return json.loads(build_state.read_text())
32
+ except Exception:
33
+ pass
34
+
35
+ # Check api-dev state
36
+ api_state = Path(project_dir) / ".claude" / "api-dev-state.json"
37
+ if api_state.exists():
38
+ try:
39
+ return json.loads(api_state.read_text())
40
+ except Exception:
41
+ pass
42
+
43
+ return {}
44
+
45
+
46
+ def get_ntfy_config():
47
+ """Get NTFY configuration from environment, .env file, or hustle-build-defaults.json"""
48
+ topic = os.environ.get("NTFY_TOPIC")
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
64
+
65
+ if not topic:
66
+ # Try loading from .env
67
+ env_file = Path(project_dir) / ".env"
68
+
69
+ if env_file.exists():
70
+ try:
71
+ for line in env_file.read_text().splitlines():
72
+ if line.startswith("NTFY_TOPIC="):
73
+ topic = line.split("=", 1)[1].strip().strip('"\'')
74
+ elif line.startswith("NTFY_SERVER="):
75
+ server = line.split("=", 1)[1].strip().strip('"\'')
76
+ except Exception:
77
+ pass
78
+
79
+ return topic, server
80
+
81
+
82
+ def send_notification(topic, server, title, message, priority="default", tags=None):
83
+ """Send notification via NTFY"""
84
+ if not HAS_REQUESTS:
85
+ # Fallback to curl
86
+ import subprocess
87
+ try:
88
+ headers = ["-H", f"Title: {title}", "-H", f"Priority: {priority}"]
89
+ if tags:
90
+ headers.extend(["-H", f"Tags: {','.join(tags)}"])
91
+
92
+ subprocess.run(
93
+ ["curl", "-s", "-d", message, *headers, f"{server}/{topic}"],
94
+ capture_output=True,
95
+ timeout=5
96
+ )
97
+ return True
98
+ except Exception:
99
+ return False
100
+
101
+ try:
102
+ headers = {
103
+ "Title": title,
104
+ "Priority": priority,
105
+ }
106
+ if tags:
107
+ headers["Tags"] = ",".join(tags)
108
+
109
+ response = requests.post(
110
+ f"{server}/{topic}",
111
+ data=message.encode("utf-8"),
112
+ headers=headers,
113
+ timeout=5
114
+ )
115
+ return response.status_code == 200
116
+ except Exception:
117
+ return False
118
+
119
+
120
+ def extract_question_summary(tool_input):
121
+ """Extract question summary from tool input"""
122
+ try:
123
+ data = json.loads(tool_input)
124
+ questions = data.get("questions", [])
125
+
126
+ if not questions:
127
+ return None, None
128
+
129
+ # Get first question
130
+ q = questions[0]
131
+ header = q.get("header", "Question")
132
+ question = q.get("question", "")
133
+ options = q.get("options", [])
134
+
135
+ # Truncate question for notification
136
+ if len(question) > 100:
137
+ question = question[:97] + "..."
138
+
139
+ # Add option count
140
+ option_text = f" ({len(options)} options)"
141
+
142
+ return header, question + option_text
143
+ except Exception:
144
+ return None, None
145
+
146
+
147
+ def main():
148
+ tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
149
+
150
+ # Check if in auto mode (skip notification in auto mode)
151
+ state = load_state()
152
+ if state.get("mode") == "auto":
153
+ # In auto mode, questions are auto-answered
154
+ # Only notify on errors, not questions
155
+ print(json.dumps({"continue": True}))
156
+ return
157
+
158
+ # Get NTFY config
159
+ topic, server = get_ntfy_config()
160
+
161
+ if not topic:
162
+ # NTFY not configured, skip
163
+ print(json.dumps({"continue": True}))
164
+ return
165
+
166
+ # Extract question info
167
+ header, message = extract_question_summary(tool_input)
168
+
169
+ if not header or not message:
170
+ print(json.dumps({"continue": True}))
171
+ return
172
+
173
+ # Build context for notification
174
+ workflow = state.get("workflow", "")
175
+ endpoint = state.get("active_endpoint") or state.get("active_element") or ""
176
+ phase = ""
177
+
178
+ # Find current phase
179
+ phases = state.get("phases", {})
180
+ for phase_name, phase_data in phases.items():
181
+ if phase_data.get("status") == "in_progress":
182
+ phase = phase_name.replace("_", " ").title()
183
+ break
184
+
185
+ # Build title
186
+ title_parts = ["Hustle Dev"]
187
+ if workflow:
188
+ title_parts.append(workflow)
189
+ if endpoint:
190
+ title_parts.append(endpoint)
191
+ title = " - ".join(title_parts)
192
+
193
+ # Build message
194
+ full_message = f"{header}: {message}"
195
+ if phase:
196
+ full_message = f"[{phase}] {full_message}"
197
+
198
+ # Determine priority
199
+ priority = "default"
200
+ tags = ["question", "hustle"]
201
+
202
+ # Higher priority for blocking questions
203
+ if "required" in message.lower() or "must" in message.lower():
204
+ priority = "high"
205
+ tags.append("warning")
206
+
207
+ # Send notification
208
+ success = send_notification(topic, server, title, full_message, priority, tags)
209
+
210
+ # Log notification
211
+ if success:
212
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
213
+ logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
214
+ logs_dir.mkdir(parents=True, exist_ok=True)
215
+
216
+ log_file = logs_dir / "ntfy-log.json"
217
+ try:
218
+ if log_file.exists():
219
+ log = json.loads(log_file.read_text())
220
+ else:
221
+ log = {"notifications": []}
222
+
223
+ from datetime import datetime
224
+ log["notifications"].append({
225
+ "timestamp": datetime.now().isoformat(),
226
+ "title": title,
227
+ "message": full_message,
228
+ "workflow": workflow,
229
+ "endpoint": endpoint
230
+ })
231
+
232
+ log_file.write_text(json.dumps(log, indent=2))
233
+ except Exception:
234
+ pass
235
+
236
+ print(json.dumps({"continue": True}))
237
+
238
+
239
+ if __name__ == "__main__":
240
+ main()
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Orchestrator completion hook.
4
+
5
+ After a Skill completes, this hook updates the orchestration state
6
+ and determines the next workflow to execute.
7
+
8
+ Hook Type: PostToolUse (matcher: Skill)
9
+ """
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+
16
+ try:
17
+ import requests
18
+ HAS_REQUESTS = True
19
+ except ImportError:
20
+ HAS_REQUESTS = False
21
+
22
+
23
+ def load_build_state():
24
+ """Load hustle-build orchestration state"""
25
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
26
+ state_file = Path(project_dir) / ".claude" / "hustle-build-state.json"
27
+
28
+ if state_file.exists():
29
+ try:
30
+ return json.loads(state_file.read_text())
31
+ except Exception:
32
+ pass
33
+ return None
34
+
35
+
36
+ def save_build_state(state):
37
+ """Save hustle-build orchestration state"""
38
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
39
+ state_file = Path(project_dir) / ".claude" / "hustle-build-state.json"
40
+
41
+ try:
42
+ state_file.write_text(json.dumps(state, indent=2))
43
+ return True
44
+ except Exception:
45
+ return False
46
+
47
+
48
+ def load_api_state():
49
+ """Load api-dev state to check completion"""
50
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
51
+ state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
52
+
53
+ if state_file.exists():
54
+ try:
55
+ return json.loads(state_file.read_text())
56
+ except Exception:
57
+ pass
58
+ return {}
59
+
60
+
61
+ def get_skill_name(tool_input):
62
+ """Extract skill name from tool input"""
63
+ try:
64
+ data = json.loads(tool_input)
65
+ return data.get("skill", "")
66
+ except Exception:
67
+ return ""
68
+
69
+
70
+ def check_workflow_complete(api_state):
71
+ """Check if the current workflow completed successfully"""
72
+ phases = api_state.get("phases", {})
73
+
74
+ # Check if documentation phase is complete (last phase)
75
+ doc_phase = phases.get("documentation", {})
76
+ if doc_phase.get("status") == "complete":
77
+ return True
78
+
79
+ # Alternative: check verification
80
+ verify_phase = phases.get("verify", {})
81
+ if verify_phase.get("status") == "complete":
82
+ return True
83
+
84
+ return False
85
+
86
+
87
+ def find_next_workflow(build_state):
88
+ """Find the next pending workflow based on dependencies"""
89
+ decomposition = build_state.get("decomposition", {})
90
+ completed_names = set()
91
+
92
+ # Collect completed workflow names
93
+ for wf_type in ["apis", "components", "combined_apis", "pages"]:
94
+ workflows = decomposition.get(wf_type, [])
95
+ for wf in workflows:
96
+ if wf.get("status") == "complete":
97
+ completed_names.add(wf.get("name"))
98
+
99
+ # Find first pending workflow with satisfied dependencies
100
+ for wf_type in ["apis", "components", "combined_apis", "pages"]:
101
+ workflows = decomposition.get(wf_type, [])
102
+ for wf in workflows:
103
+ if wf.get("status") != "pending":
104
+ continue
105
+
106
+ # Check dependencies
107
+ deps = wf.get("depends_on", [])
108
+ if all(dep in completed_names for dep in deps):
109
+ return wf, wf_type
110
+
111
+ return None, None
112
+
113
+
114
+ def send_ntfy(title, message, priority="default"):
115
+ """Send NTFY notification"""
116
+ topic = os.environ.get("NTFY_TOPIC")
117
+ server = os.environ.get("NTFY_SERVER", "https://ntfy.sh")
118
+
119
+ if not topic:
120
+ return
121
+
122
+ try:
123
+ if HAS_REQUESTS:
124
+ requests.post(
125
+ f"{server}/{topic}",
126
+ data=message.encode("utf-8"),
127
+ headers={"Title": title, "Priority": priority, "Tags": "hustle,check"},
128
+ timeout=5
129
+ )
130
+ else:
131
+ import subprocess
132
+ subprocess.run(
133
+ ["curl", "-s", "-d", message, "-H", f"Title: {title}", f"{server}/{topic}"],
134
+ capture_output=True,
135
+ timeout=5
136
+ )
137
+ except Exception:
138
+ pass
139
+
140
+
141
+ def main():
142
+ tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
143
+ tool_result = os.environ.get("CLAUDE_TOOL_RESULT", "")
144
+
145
+ # Get skill that completed
146
+ skill_name = get_skill_name(tool_input)
147
+
148
+ # Check if this is a workflow skill
149
+ workflow_skills = [
150
+ "api-create", "hustle-ui-create", "hustle-ui-create-page",
151
+ "hustle-combine", "cycle"
152
+ ]
153
+
154
+ if skill_name not in workflow_skills:
155
+ print(json.dumps({"continue": True}))
156
+ return
157
+
158
+ # Check if we're in an orchestrated build
159
+ build_state = load_build_state()
160
+
161
+ if not build_state or build_state.get("status") != "in_progress":
162
+ print(json.dumps({"continue": True}))
163
+ return
164
+
165
+ # Check if the workflow completed successfully
166
+ api_state = load_api_state()
167
+ workflow_complete = check_workflow_complete(api_state)
168
+
169
+ # Get active workflow info
170
+ active = build_state.get("active_sub_workflow", {})
171
+ active_name = active.get("name")
172
+ active_type = active.get("type")
173
+
174
+ # Update workflow status
175
+ decomposition = build_state.get("decomposition", {})
176
+
177
+ if workflow_complete and active_name:
178
+ # Mark workflow as complete
179
+ for wf_type in ["apis", "components", "combined_apis", "pages"]:
180
+ workflows = decomposition.get(wf_type, [])
181
+ for wf in workflows:
182
+ if wf.get("name") == active_name:
183
+ wf["status"] = "complete"
184
+ wf["completed_at"] = datetime.now().isoformat()
185
+
186
+ # Add to completed list
187
+ if "completed_sub_workflows" not in build_state:
188
+ build_state["completed_sub_workflows"] = []
189
+
190
+ build_state["completed_sub_workflows"].append({
191
+ "type": active_type,
192
+ "name": active_name,
193
+ "completed_at": datetime.now().isoformat()
194
+ })
195
+
196
+ # Find next workflow
197
+ next_wf, next_type = find_next_workflow(build_state)
198
+
199
+ if next_wf:
200
+ # Set next as active
201
+ build_state["active_sub_workflow"] = {
202
+ "type": next_type.rstrip("s"),
203
+ "name": next_wf.get("name"),
204
+ "workflow_id": f"wf-{len(build_state.get('completed_sub_workflows', []))+1:03d}"
205
+ }
206
+
207
+ # Mark as in progress
208
+ next_wf["status"] = "in_progress"
209
+ next_wf["started_at"] = datetime.now().isoformat()
210
+
211
+ save_build_state(build_state)
212
+
213
+ # Determine which skill to run
214
+ skill_mapping = {
215
+ "api": "/api-create",
216
+ "component": "/hustle-ui-create",
217
+ "combined_api": "/hustle-combine api",
218
+ "page": "/hustle-ui-create-page"
219
+ }
220
+
221
+ next_skill = skill_mapping.get(next_type.rstrip("s"), "/api-create")
222
+ next_name = next_wf.get("name")
223
+
224
+ context = f"""
225
+ ## Workflow Complete: {active_name}
226
+
227
+ The [{active_type}] **{active_name}** workflow has completed.
228
+
229
+ ### Next Workflow: [{next_type}] {next_name}
230
+
231
+ Run: `{next_skill} {next_name}`
232
+
233
+ Progress: {len(build_state.get('completed_sub_workflows', []))}/{sum(len(decomposition.get(t, [])) for t in decomposition)} complete
234
+ """
235
+
236
+ result = {
237
+ "continue": True,
238
+ "additionalContext": context
239
+ }
240
+
241
+ else:
242
+ # All workflows complete!
243
+ build_state["status"] = "complete"
244
+ build_state["completed_at"] = datetime.now().isoformat()
245
+ build_state["active_sub_workflow"] = None
246
+
247
+ save_build_state(build_state)
248
+
249
+ # Send completion notification
250
+ completed_count = len(build_state.get("completed_sub_workflows", []))
251
+ build_id = build_state.get("build_id", "unknown")
252
+
253
+ send_ntfy(
254
+ "Hustle Build Complete!",
255
+ f"All {completed_count} workflows finished.\nReview: /hustle-build-review {build_id}",
256
+ "high"
257
+ )
258
+
259
+ context = f"""
260
+ ## BUILD COMPLETE
261
+
262
+ All workflows have finished successfully!
263
+
264
+ **Build ID:** {build_id}
265
+ **Total Workflows:** {completed_count}
266
+ **Duration:** {calculate_duration(build_state)}
267
+
268
+ ### Created Elements:
269
+ {format_created_elements(build_state)}
270
+
271
+ ### Next Steps:
272
+ - `/hustle-build-review {build_id}` - Review all decisions and results
273
+ - `/commit` - Commit all changes
274
+ - `/pr` - Create pull request
275
+
276
+ Visit `/hustle-dev-dashboard` to see all created elements.
277
+ """
278
+
279
+ result = {
280
+ "continue": True,
281
+ "additionalContext": context
282
+ }
283
+
284
+ print(json.dumps(result))
285
+
286
+
287
+ def calculate_duration(build_state):
288
+ """Calculate build duration"""
289
+ try:
290
+ started = datetime.fromisoformat(build_state.get("created_at", ""))
291
+ ended = datetime.fromisoformat(build_state.get("completed_at", datetime.now().isoformat()))
292
+ duration = ended - started
293
+ minutes = int(duration.total_seconds() / 60)
294
+ return f"{minutes} minutes"
295
+ except Exception:
296
+ return "Unknown"
297
+
298
+
299
+ def format_created_elements(build_state):
300
+ """Format list of created elements"""
301
+ completed = build_state.get("completed_sub_workflows", [])
302
+ lines = []
303
+
304
+ for wf in completed:
305
+ wf_type = wf.get("type", "unknown")
306
+ name = wf.get("name", "unnamed")
307
+ lines.append(f" - [{wf_type}] {name}")
308
+
309
+ return "\n".join(lines) if lines else " None"
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()