@hustle-together/api-dev-tools 3.12.3 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Orchestrator handoff hook.
|
|
4
|
+
|
|
5
|
+
When a Skill is invoked, this hook checks if we're in an orchestrated build
|
|
6
|
+
and injects shared_decisions into the sub-workflow's state.
|
|
7
|
+
|
|
8
|
+
Hook Type: PreToolUse (matcher: Skill)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_build_state():
|
|
18
|
+
"""Load hustle-build orchestration state"""
|
|
19
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
20
|
+
state_file = Path(project_dir) / ".claude" / "hustle-build-state.json"
|
|
21
|
+
|
|
22
|
+
if state_file.exists():
|
|
23
|
+
try:
|
|
24
|
+
return json.loads(state_file.read_text())
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_api_state():
|
|
31
|
+
"""Load api-dev state"""
|
|
32
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
33
|
+
state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
|
|
34
|
+
|
|
35
|
+
if state_file.exists():
|
|
36
|
+
try:
|
|
37
|
+
return json.loads(state_file.read_text())
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_api_state(state):
|
|
44
|
+
"""Save api-dev state with shared decisions"""
|
|
45
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
46
|
+
state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
50
|
+
return True
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_skill_name(tool_input):
|
|
56
|
+
"""Extract skill name from tool input"""
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(tool_input)
|
|
59
|
+
return data.get("skill", "")
|
|
60
|
+
except Exception:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
|
|
66
|
+
|
|
67
|
+
# Get skill being invoked
|
|
68
|
+
skill_name = get_skill_name(tool_input)
|
|
69
|
+
|
|
70
|
+
# Check if this is a workflow skill
|
|
71
|
+
workflow_skills = [
|
|
72
|
+
"api-create", "hustle-ui-create", "hustle-ui-create-page",
|
|
73
|
+
"hustle-combine", "red", "green", "refactor", "cycle"
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if skill_name not in workflow_skills:
|
|
77
|
+
print(json.dumps({"continue": True}))
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Check if we're in an orchestrated build
|
|
81
|
+
build_state = load_build_state()
|
|
82
|
+
|
|
83
|
+
if not build_state or build_state.get("status") != "in_progress":
|
|
84
|
+
print(json.dumps({"continue": True}))
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Get shared decisions
|
|
88
|
+
shared_decisions = build_state.get("shared_decisions", {})
|
|
89
|
+
mode = build_state.get("mode", "interactive")
|
|
90
|
+
|
|
91
|
+
if not shared_decisions and mode != "auto":
|
|
92
|
+
print(json.dumps({"continue": True}))
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Load current api-dev state
|
|
96
|
+
api_state = load_api_state()
|
|
97
|
+
|
|
98
|
+
# Inject shared decisions
|
|
99
|
+
api_state["orchestrated"] = True
|
|
100
|
+
api_state["build_id"] = build_state.get("build_id")
|
|
101
|
+
api_state["mode"] = mode
|
|
102
|
+
|
|
103
|
+
# Pre-fill interview decisions from shared decisions
|
|
104
|
+
if "phases" not in api_state:
|
|
105
|
+
api_state["phases"] = {}
|
|
106
|
+
|
|
107
|
+
if "interview" not in api_state["phases"]:
|
|
108
|
+
api_state["phases"]["interview"] = {"status": "not_started", "decisions": {}}
|
|
109
|
+
|
|
110
|
+
# Map shared decisions to interview decisions
|
|
111
|
+
decision_mappings = {
|
|
112
|
+
"auth_required": "authentication",
|
|
113
|
+
"error_handling": "error_strategy",
|
|
114
|
+
"brand_guide": "use_brand_guide",
|
|
115
|
+
"testing_level": "testing_thoroughness",
|
|
116
|
+
"caching_strategy": "caching"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for shared_key, interview_key in decision_mappings.items():
|
|
120
|
+
if shared_key in shared_decisions:
|
|
121
|
+
api_state["phases"]["interview"]["decisions"][interview_key] = shared_decisions[shared_key]
|
|
122
|
+
|
|
123
|
+
# Mark which decisions are from orchestrator (so sub-workflow knows not to re-ask)
|
|
124
|
+
api_state["shared_decisions_applied"] = list(shared_decisions.keys())
|
|
125
|
+
|
|
126
|
+
# Save updated state
|
|
127
|
+
save_api_state(api_state)
|
|
128
|
+
|
|
129
|
+
# Update build state with current active workflow
|
|
130
|
+
decomposition = build_state.get("decomposition", {})
|
|
131
|
+
|
|
132
|
+
# Find the workflow being started
|
|
133
|
+
for wf_type in ["apis", "components", "combined_apis", "pages"]:
|
|
134
|
+
workflows = decomposition.get(wf_type, [])
|
|
135
|
+
for wf in workflows:
|
|
136
|
+
if wf.get("status") == "pending":
|
|
137
|
+
# This might be the one being started
|
|
138
|
+
# We'll rely on the skill to update status
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
# Log the handoff
|
|
142
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
143
|
+
logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
|
|
144
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
log_file = logs_dir / f"{build_state.get('build_id', 'unknown')}.json"
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
if log_file.exists():
|
|
150
|
+
log = json.loads(log_file.read_text())
|
|
151
|
+
else:
|
|
152
|
+
log = {"handoffs": []}
|
|
153
|
+
|
|
154
|
+
log["handoffs"].append({
|
|
155
|
+
"timestamp": datetime.now().isoformat(),
|
|
156
|
+
"skill": skill_name,
|
|
157
|
+
"shared_decisions_applied": list(shared_decisions.keys()),
|
|
158
|
+
"mode": mode
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
log_file.write_text(json.dumps(log, indent=2))
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# Build context about orchestration
|
|
166
|
+
context_parts = [f"""
|
|
167
|
+
## Orchestrated Workflow
|
|
168
|
+
|
|
169
|
+
This workflow is part of a larger build: **{build_state.get('build_id')}**
|
|
170
|
+
|
|
171
|
+
### Pre-Filled Decisions (from orchestrator):
|
|
172
|
+
{json.dumps(shared_decisions, indent=2)}
|
|
173
|
+
|
|
174
|
+
These decisions are already applied. **Do not re-ask** questions about:
|
|
175
|
+
{', '.join(shared_decisions.keys())}
|
|
176
|
+
|
|
177
|
+
Only ask workflow-specific questions not covered above.
|
|
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)
|
|
257
|
+
|
|
258
|
+
result = {
|
|
259
|
+
"continue": True,
|
|
260
|
+
"additionalContext": context
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
print(json.dumps(result))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
main()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Orchestrator session startup hook.
|
|
4
|
+
|
|
5
|
+
Injects hustle-build orchestration state into context at session start.
|
|
6
|
+
This ensures Claude has awareness of multi-workflow builds in progress.
|
|
7
|
+
|
|
8
|
+
Hook Type: SessionStart
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_build_state():
|
|
18
|
+
"""Load hustle-build orchestration state if exists"""
|
|
19
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
20
|
+
state_file = Path(project_dir) / ".claude" / "hustle-build-state.json"
|
|
21
|
+
|
|
22
|
+
if state_file.exists():
|
|
23
|
+
try:
|
|
24
|
+
return json.loads(state_file.read_text())
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_workflow_status(workflows):
|
|
31
|
+
"""Format workflow list for context injection"""
|
|
32
|
+
if not workflows:
|
|
33
|
+
return "No sub-workflows defined yet."
|
|
34
|
+
|
|
35
|
+
lines = []
|
|
36
|
+
for wf in workflows:
|
|
37
|
+
status_emoji = {
|
|
38
|
+
"complete": "✅",
|
|
39
|
+
"in_progress": "🔄",
|
|
40
|
+
"pending": "⏳",
|
|
41
|
+
"failed": "❌"
|
|
42
|
+
}.get(wf.get("status", "pending"), "⏳")
|
|
43
|
+
|
|
44
|
+
wf_type = wf.get("type", "unknown")
|
|
45
|
+
name = wf.get("name", "unnamed")
|
|
46
|
+
deps = wf.get("depends_on", [])
|
|
47
|
+
|
|
48
|
+
line = f" {status_emoji} [{wf_type}] {name}"
|
|
49
|
+
if deps:
|
|
50
|
+
line += f" (depends on: {', '.join(deps)})"
|
|
51
|
+
lines.append(line)
|
|
52
|
+
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_shared_decisions(decisions):
|
|
57
|
+
"""Format shared decisions for context injection"""
|
|
58
|
+
if not decisions:
|
|
59
|
+
return "No shared decisions configured."
|
|
60
|
+
|
|
61
|
+
lines = []
|
|
62
|
+
for key, value in decisions.items():
|
|
63
|
+
lines.append(f" - {key}: {value}")
|
|
64
|
+
|
|
65
|
+
return "\n".join(lines)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
state = load_build_state()
|
|
70
|
+
|
|
71
|
+
if not state:
|
|
72
|
+
# No active build, continue normally
|
|
73
|
+
print(json.dumps({"continue": True}))
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Check if build is in progress
|
|
77
|
+
status = state.get("status", "unknown")
|
|
78
|
+
|
|
79
|
+
if status not in ["in_progress", "paused"]:
|
|
80
|
+
print(json.dumps({"continue": True}))
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Build context for injection
|
|
84
|
+
build_id = state.get("build_id", "unknown")
|
|
85
|
+
mode = state.get("mode", "interactive")
|
|
86
|
+
request = state.get("request", {}).get("original", "Unknown request")
|
|
87
|
+
|
|
88
|
+
# Get workflow statuses
|
|
89
|
+
decomposition = state.get("decomposition", {})
|
|
90
|
+
all_workflows = []
|
|
91
|
+
|
|
92
|
+
for wf_type in ["apis", "components", "combined_apis", "pages"]:
|
|
93
|
+
workflows = decomposition.get(wf_type, [])
|
|
94
|
+
for wf in workflows:
|
|
95
|
+
wf["type"] = wf_type.rstrip("s")
|
|
96
|
+
all_workflows.append(wf)
|
|
97
|
+
|
|
98
|
+
# Count progress
|
|
99
|
+
completed = len([w for w in all_workflows if w.get("status") == "complete"])
|
|
100
|
+
total = len(all_workflows)
|
|
101
|
+
in_progress = [w for w in all_workflows if w.get("status") == "in_progress"]
|
|
102
|
+
|
|
103
|
+
# Get active sub-workflow
|
|
104
|
+
active = state.get("active_sub_workflow", {})
|
|
105
|
+
active_name = active.get("name", "None")
|
|
106
|
+
active_type = active.get("type", "unknown")
|
|
107
|
+
|
|
108
|
+
# Format shared decisions
|
|
109
|
+
shared_decisions = state.get("shared_decisions", {})
|
|
110
|
+
|
|
111
|
+
context = f"""
|
|
112
|
+
## Hustle Build In Progress
|
|
113
|
+
|
|
114
|
+
**Build ID:** {build_id}
|
|
115
|
+
**Mode:** {mode}
|
|
116
|
+
**Original Request:** "{request}"
|
|
117
|
+
|
|
118
|
+
### Progress: {completed}/{total} workflows complete
|
|
119
|
+
|
|
120
|
+
**Currently Active:** [{active_type}] {active_name}
|
|
121
|
+
|
|
122
|
+
### Sub-Workflows:
|
|
123
|
+
{format_workflow_status(all_workflows)}
|
|
124
|
+
|
|
125
|
+
### Shared Decisions (applied to all):
|
|
126
|
+
{format_shared_decisions(shared_decisions)}
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
**Commands:**
|
|
131
|
+
- Continue current workflow
|
|
132
|
+
- `/hustle-build-review {build_id}` - View build log
|
|
133
|
+
- Set `mode: "paused"` in state to pause
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
result = {
|
|
138
|
+
"continue": True,
|
|
139
|
+
"additionalContext": context
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
print(json.dumps(result))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|