@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
|
@@ -3,14 +3,22 @@
|
|
|
3
3
|
Hook: PostToolUse (for periodic re-grounding)
|
|
4
4
|
Purpose: Inject context reminders every N turns to prevent context dilution
|
|
5
5
|
|
|
6
|
-
This hook tracks turn count and periodically injects a summary of:
|
|
6
|
+
This hook tracks turn count and periodically injects a comprehensive summary of:
|
|
7
7
|
- Current endpoint and phase
|
|
8
8
|
- Key decisions from interview
|
|
9
|
+
- Existing registry elements (APIs, components, pages)
|
|
10
|
+
- Deferred features (don't re-suggest)
|
|
11
|
+
- Last test status
|
|
12
|
+
- Brand guide status
|
|
9
13
|
- Research cache status
|
|
10
|
-
-
|
|
14
|
+
- Orchestrator context (if in /hustle-build)
|
|
11
15
|
|
|
12
16
|
The goal is to keep Claude grounded during long sessions where
|
|
13
|
-
the original CLAUDE.md context may get diluted.
|
|
17
|
+
the original CLAUDE.md context may get diluted ("lost in the middle").
|
|
18
|
+
|
|
19
|
+
Based on best practices from:
|
|
20
|
+
- Manus: "Manipulate Attention Through Recitation"
|
|
21
|
+
- Sankalp: "Context as limited attention budget"
|
|
14
22
|
|
|
15
23
|
Configuration:
|
|
16
24
|
- REGROUND_INTERVAL: Number of turns between re-grounding (default: 7)
|
|
@@ -27,8 +35,261 @@ from pathlib import Path
|
|
|
27
35
|
# Configuration
|
|
28
36
|
REGROUND_INTERVAL = 7 # Re-ground every N turns
|
|
29
37
|
|
|
30
|
-
# State
|
|
31
|
-
|
|
38
|
+
# State files (in .claude/ directory)
|
|
39
|
+
PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
|
|
40
|
+
STATE_FILE = PROJECT_DIR / ".claude" / "api-dev-state.json"
|
|
41
|
+
REGISTRY_FILE = PROJECT_DIR / ".claude" / "registry.json"
|
|
42
|
+
BUILD_STATE_FILE = PROJECT_DIR / ".claude" / "hustle-build-state.json"
|
|
43
|
+
BRAND_GUIDE_FILE = PROJECT_DIR / ".claude" / "BRAND_GUIDE.md"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_json_file(filepath):
|
|
47
|
+
"""Safely load a JSON file"""
|
|
48
|
+
if filepath.exists():
|
|
49
|
+
try:
|
|
50
|
+
return json.loads(filepath.read_text())
|
|
51
|
+
except (json.JSONDecodeError, Exception):
|
|
52
|
+
pass
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_list(items, max_items=5, max_chars=80):
|
|
57
|
+
"""Format a list of items with truncation"""
|
|
58
|
+
if not items:
|
|
59
|
+
return "None"
|
|
60
|
+
truncated = list(items)[:max_items]
|
|
61
|
+
result = ", ".join(str(item)[:20] for item in truncated)
|
|
62
|
+
if len(items) > max_items:
|
|
63
|
+
result += f" (+{len(items) - max_items} more)"
|
|
64
|
+
return result[:max_chars]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_registry_summary(registry):
|
|
68
|
+
"""Get summary of existing registry elements"""
|
|
69
|
+
if not registry:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
summary = {}
|
|
73
|
+
# Core elements
|
|
74
|
+
for category in ["apis", "components", "pages", "combined"]:
|
|
75
|
+
items = registry.get(category, {})
|
|
76
|
+
if items:
|
|
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
|
+
|
|
105
|
+
return summary if summary else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_test_status(state):
|
|
109
|
+
"""Get last test run status"""
|
|
110
|
+
test_run = state.get("last_test_run", {})
|
|
111
|
+
if not test_run:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
passed = test_run.get("passed", 0)
|
|
115
|
+
failed = test_run.get("failed", 0)
|
|
116
|
+
timestamp = test_run.get("timestamp", "")
|
|
117
|
+
|
|
118
|
+
if passed or failed:
|
|
119
|
+
return {
|
|
120
|
+
"passed": passed,
|
|
121
|
+
"failed": failed,
|
|
122
|
+
"total": passed + failed,
|
|
123
|
+
"status": "GREEN" if failed == 0 else "RED",
|
|
124
|
+
"timestamp": timestamp
|
|
125
|
+
}
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_brand_guide_status():
|
|
130
|
+
"""Check if brand guide exists and get key info"""
|
|
131
|
+
if not BRAND_GUIDE_FILE.exists():
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
content = BRAND_GUIDE_FILE.read_text()
|
|
136
|
+
# Extract key colors if present
|
|
137
|
+
colors = []
|
|
138
|
+
for line in content.split("\n"):
|
|
139
|
+
if "primary" in line.lower() and "#" in line:
|
|
140
|
+
colors.append("primary found")
|
|
141
|
+
break
|
|
142
|
+
return {"exists": True, "has_colors": len(colors) > 0}
|
|
143
|
+
except Exception:
|
|
144
|
+
return {"exists": True}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_orchestrator_status(build_state):
|
|
148
|
+
"""Get orchestrator build status if active"""
|
|
149
|
+
if not build_state:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
status = build_state.get("status")
|
|
153
|
+
if status not in ["in_progress", "paused"]:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
build_id = build_state.get("build_id", "unknown")
|
|
157
|
+
decomposition = build_state.get("decomposition", {})
|
|
158
|
+
|
|
159
|
+
total = 0
|
|
160
|
+
completed = 0
|
|
161
|
+
for wf_type in ["apis", "components", "combined_apis", "pages"]:
|
|
162
|
+
workflows = decomposition.get(wf_type, [])
|
|
163
|
+
total += len(workflows)
|
|
164
|
+
completed += len([w for w in workflows if w.get("status") == "complete"])
|
|
165
|
+
|
|
166
|
+
active = build_state.get("active_sub_workflow", {})
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"build_id": build_id,
|
|
170
|
+
"progress": f"{completed}/{total}",
|
|
171
|
+
"active_type": active.get("type", "none"),
|
|
172
|
+
"active_name": active.get("name", "none")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_reground_context(state, turn_count):
|
|
177
|
+
"""Build comprehensive re-grounding context"""
|
|
178
|
+
parts = []
|
|
179
|
+
parts.append(f"## Re-Grounding Reminder (Turn {turn_count})")
|
|
180
|
+
parts.append("")
|
|
181
|
+
|
|
182
|
+
# === Current Workflow ===
|
|
183
|
+
endpoint = state.get("endpoint", "unknown")
|
|
184
|
+
parts.append(f"**Active Endpoint:** `{endpoint}`")
|
|
185
|
+
|
|
186
|
+
# Get current phase
|
|
187
|
+
phases = state.get("phases", {})
|
|
188
|
+
phase_order = [
|
|
189
|
+
"disambiguation", "scope", "research_initial", "interview",
|
|
190
|
+
"research_deep", "schema_creation", "environment_check",
|
|
191
|
+
"tdd_red", "tdd_green", "verify", "code_review", "tdd_refactor",
|
|
192
|
+
"documentation", "completion"
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
current_phase = None
|
|
196
|
+
completed_phases = []
|
|
197
|
+
for phase_name in phase_order:
|
|
198
|
+
phase = phases.get(phase_name, {})
|
|
199
|
+
status = phase.get("status", "not_started")
|
|
200
|
+
if status == "complete":
|
|
201
|
+
completed_phases.append(phase_name)
|
|
202
|
+
elif status == "in_progress" and not current_phase:
|
|
203
|
+
current_phase = phase_name
|
|
204
|
+
|
|
205
|
+
if not current_phase:
|
|
206
|
+
for phase_name in phase_order:
|
|
207
|
+
phase = phases.get(phase_name, {})
|
|
208
|
+
if phase.get("status", "not_started") == "not_started":
|
|
209
|
+
current_phase = phase_name
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
parts.append(f"**Current Phase:** {current_phase or 'completion'}")
|
|
213
|
+
parts.append(f"**Completed:** {len(completed_phases)}/{len(phase_order)} phases")
|
|
214
|
+
|
|
215
|
+
# === Key Decisions ===
|
|
216
|
+
interview = phases.get("interview", {})
|
|
217
|
+
decisions = interview.get("decisions", {})
|
|
218
|
+
if decisions:
|
|
219
|
+
parts.append("")
|
|
220
|
+
parts.append("**Key Decisions:**")
|
|
221
|
+
for key, value in list(decisions.items())[:5]:
|
|
222
|
+
response = value.get("value", value.get("response", "N/A"))
|
|
223
|
+
if response:
|
|
224
|
+
parts.append(f" - {key}: {str(response)[:40]}")
|
|
225
|
+
|
|
226
|
+
# === Registry Summary ===
|
|
227
|
+
registry = load_json_file(REGISTRY_FILE)
|
|
228
|
+
registry_summary = get_registry_summary(registry)
|
|
229
|
+
if registry_summary:
|
|
230
|
+
parts.append("")
|
|
231
|
+
parts.append("**Existing Elements (don't recreate):**")
|
|
232
|
+
if registry_summary.get("apis"):
|
|
233
|
+
parts.append(f" - APIs: {format_list(registry_summary['apis'])}")
|
|
234
|
+
if registry_summary.get("components"):
|
|
235
|
+
parts.append(f" - Components: {format_list(registry_summary['components'])}")
|
|
236
|
+
if registry_summary.get("pages"):
|
|
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")
|
|
250
|
+
|
|
251
|
+
# === Deferred Features ===
|
|
252
|
+
deferred = state.get("deferred_features", [])
|
|
253
|
+
if deferred:
|
|
254
|
+
parts.append("")
|
|
255
|
+
parts.append(f"**Deferred (don't re-suggest):** {format_list(deferred, max_items=3)}")
|
|
256
|
+
|
|
257
|
+
# === Test Status ===
|
|
258
|
+
test_status = get_test_status(state)
|
|
259
|
+
if test_status:
|
|
260
|
+
parts.append("")
|
|
261
|
+
status_emoji = "GREEN" if test_status["status"] == "GREEN" else "RED"
|
|
262
|
+
parts.append(f"**Last Tests:** {status_emoji} ({test_status['passed']} passed, {test_status['failed']} failed)")
|
|
263
|
+
|
|
264
|
+
# === Brand Guide ===
|
|
265
|
+
brand_status = get_brand_guide_status()
|
|
266
|
+
if brand_status and brand_status.get("exists"):
|
|
267
|
+
parts.append("")
|
|
268
|
+
parts.append("**Brand Guide:** Active - use `.claude/BRAND_GUIDE.md` for styling")
|
|
269
|
+
|
|
270
|
+
# === Research Freshness ===
|
|
271
|
+
research_index = state.get("research_index", {})
|
|
272
|
+
if endpoint in research_index:
|
|
273
|
+
entry = research_index[endpoint]
|
|
274
|
+
days_old = entry.get("days_old", 0)
|
|
275
|
+
if days_old > 7:
|
|
276
|
+
parts.append("")
|
|
277
|
+
parts.append(f"**WARNING:** Research is {days_old} days old. Consider `/api-research`.")
|
|
278
|
+
|
|
279
|
+
# === Orchestrator Context ===
|
|
280
|
+
build_state = load_json_file(BUILD_STATE_FILE)
|
|
281
|
+
orchestrator = get_orchestrator_status(build_state)
|
|
282
|
+
if orchestrator:
|
|
283
|
+
parts.append("")
|
|
284
|
+
parts.append(f"**Orchestrated Build:** {orchestrator['build_id']}")
|
|
285
|
+
parts.append(f" - Progress: {orchestrator['progress']} workflows")
|
|
286
|
+
parts.append(f" - Active: [{orchestrator['active_type']}] {orchestrator['active_name']}")
|
|
287
|
+
|
|
288
|
+
# === Quick Reminders ===
|
|
289
|
+
parts.append("")
|
|
290
|
+
parts.append("**Remember:** Research-first | Questions FROM findings | Verify after green")
|
|
291
|
+
|
|
292
|
+
return "\n".join(parts)
|
|
32
293
|
|
|
33
294
|
|
|
34
295
|
def main():
|
|
@@ -59,73 +320,15 @@ def main():
|
|
|
59
320
|
should_reground = turn_count % REGROUND_INTERVAL == 0
|
|
60
321
|
|
|
61
322
|
if should_reground and state.get("endpoint"):
|
|
62
|
-
# Build re-grounding context
|
|
63
|
-
|
|
64
|
-
context_parts.append(f"## Re-Grounding Reminder (Turn {turn_count})")
|
|
65
|
-
context_parts.append("")
|
|
66
|
-
|
|
67
|
-
endpoint = state.get("endpoint", "unknown")
|
|
68
|
-
context_parts.append(f"**Active Endpoint:** {endpoint}")
|
|
69
|
-
|
|
70
|
-
# Get current phase
|
|
71
|
-
phases = state.get("phases", {})
|
|
72
|
-
phase_order = [
|
|
73
|
-
"disambiguation", "scope", "research_initial", "interview",
|
|
74
|
-
"research_deep", "schema_creation", "environment_check",
|
|
75
|
-
"tdd_red", "tdd_green", "verify", "tdd_refactor", "documentation"
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
current_phase = None
|
|
79
|
-
completed_phases = []
|
|
80
|
-
for phase_name in phase_order:
|
|
81
|
-
phase = phases.get(phase_name, {})
|
|
82
|
-
status = phase.get("status", "not_started")
|
|
83
|
-
if status == "complete":
|
|
84
|
-
completed_phases.append(phase_name)
|
|
85
|
-
elif status == "in_progress" and not current_phase:
|
|
86
|
-
current_phase = phase_name
|
|
87
|
-
|
|
88
|
-
if not current_phase:
|
|
89
|
-
# Find first not_started phase
|
|
90
|
-
for phase_name in phase_order:
|
|
91
|
-
phase = phases.get(phase_name, {})
|
|
92
|
-
if phase.get("status", "not_started") == "not_started":
|
|
93
|
-
current_phase = phase_name
|
|
94
|
-
break
|
|
95
|
-
|
|
96
|
-
context_parts.append(f"**Current Phase:** {current_phase or 'documentation'}")
|
|
97
|
-
context_parts.append(f"**Completed:** {', '.join(completed_phases) if completed_phases else 'None'}")
|
|
98
|
-
|
|
99
|
-
# Key decisions summary
|
|
100
|
-
interview = phases.get("interview", {})
|
|
101
|
-
decisions = interview.get("decisions", {})
|
|
102
|
-
if decisions:
|
|
103
|
-
context_parts.append("")
|
|
104
|
-
context_parts.append("**Key Decisions:**")
|
|
105
|
-
for key, value in list(decisions.items())[:5]: # Limit to 5 key decisions
|
|
106
|
-
response = value.get("value", value.get("response", "N/A"))
|
|
107
|
-
if response:
|
|
108
|
-
context_parts.append(f" - {key}: {str(response)[:50]}")
|
|
109
|
-
|
|
110
|
-
# Research freshness warning
|
|
111
|
-
research_index = state.get("research_index", {})
|
|
112
|
-
if endpoint in research_index:
|
|
113
|
-
entry = research_index[endpoint]
|
|
114
|
-
days_old = entry.get("days_old", 0)
|
|
115
|
-
if days_old > 7:
|
|
116
|
-
context_parts.append("")
|
|
117
|
-
context_parts.append(f"**WARNING:** Research is {days_old} days old. Consider re-researching.")
|
|
118
|
-
|
|
119
|
-
# File reminders
|
|
120
|
-
context_parts.append("")
|
|
121
|
-
context_parts.append("**Key Files:** .claude/api-dev-state.json, .claude/research/")
|
|
323
|
+
# Build comprehensive re-grounding context
|
|
324
|
+
context = build_reground_context(state, turn_count)
|
|
122
325
|
|
|
123
326
|
# Add to reground history
|
|
124
327
|
reground_history = state.setdefault("reground_history", [])
|
|
125
328
|
reground_history.append({
|
|
126
329
|
"turn": turn_count,
|
|
127
330
|
"timestamp": datetime.now().isoformat(),
|
|
128
|
-
"phase": current_phase
|
|
331
|
+
"phase": state.get("phases", {}).get("current_phase", "unknown")
|
|
129
332
|
})
|
|
130
333
|
# Keep only last 10 reground events
|
|
131
334
|
state["reground_history"] = reground_history[-10:]
|
|
@@ -138,7 +341,7 @@ def main():
|
|
|
138
341
|
"continue": True,
|
|
139
342
|
"hookSpecificOutput": {
|
|
140
343
|
"hookEventName": "PostToolUse",
|
|
141
|
-
"additionalContext":
|
|
344
|
+
"additionalContext": context
|
|
142
345
|
}
|
|
143
346
|
}
|
|
144
347
|
print(json.dumps(output))
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Project Document Prompt Hook
|
|
4
|
+
|
|
5
|
+
Prompts users for a project document (PRD, spec, deep research output) at the
|
|
6
|
+
start of /hustle-build. Stores the document in state for AI-powered decomposition.
|
|
7
|
+
|
|
8
|
+
Hook Type: PreToolUse (matcher: Skill)
|
|
9
|
+
Trigger: When /hustle-build is invoked
|
|
10
|
+
Version: 4.6.0
|
|
11
|
+
|
|
12
|
+
Flags:
|
|
13
|
+
--skip-document Skip the project document prompt
|
|
14
|
+
--from-document PATH Use specified file as project document
|
|
15
|
+
--no-document Alias for --skip-document
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from hook_utils import load_state, save_state, get_project_dir, log_workflow_event
|
|
26
|
+
UTILS_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
UTILS_AVAILABLE = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_project_dir_fallback():
|
|
32
|
+
"""Get project directory from environment or current directory."""
|
|
33
|
+
return Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_hustle_build_state():
|
|
37
|
+
"""Load hustle-build orchestration state."""
|
|
38
|
+
project_dir = get_project_dir_fallback()
|
|
39
|
+
state_file = project_dir / ".claude" / "hustle-build-state.json"
|
|
40
|
+
if state_file.exists():
|
|
41
|
+
try:
|
|
42
|
+
return json.loads(state_file.read_text())
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_hustle_build_state(state):
|
|
49
|
+
"""Save hustle-build orchestration state."""
|
|
50
|
+
project_dir = get_project_dir_fallback()
|
|
51
|
+
state_file = project_dir / ".claude" / "hustle-build-state.json"
|
|
52
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_flags(args):
|
|
57
|
+
"""Parse command-line style flags from arguments string."""
|
|
58
|
+
flags = {
|
|
59
|
+
"skip_document": False,
|
|
60
|
+
"from_document": None,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if not args:
|
|
64
|
+
return flags
|
|
65
|
+
|
|
66
|
+
# Check for skip flags
|
|
67
|
+
if "--skip-document" in args or "--no-document" in args:
|
|
68
|
+
flags["skip_document"] = True
|
|
69
|
+
|
|
70
|
+
# Check for --from-document PATH
|
|
71
|
+
if "--from-document" in args:
|
|
72
|
+
# Extract path after --from-document
|
|
73
|
+
parts = args.split("--from-document")
|
|
74
|
+
if len(parts) > 1:
|
|
75
|
+
path_part = parts[1].strip().split()[0] if parts[1].strip() else None
|
|
76
|
+
if path_part and not path_part.startswith("--"):
|
|
77
|
+
flags["from_document"] = path_part
|
|
78
|
+
|
|
79
|
+
return flags
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def read_document_file(file_path):
|
|
83
|
+
"""Read a document file and detect its format."""
|
|
84
|
+
path = Path(file_path)
|
|
85
|
+
|
|
86
|
+
if not path.exists():
|
|
87
|
+
# Try relative to project dir
|
|
88
|
+
project_dir = get_project_dir_fallback()
|
|
89
|
+
path = project_dir / file_path
|
|
90
|
+
|
|
91
|
+
if not path.exists():
|
|
92
|
+
return None, None, f"File not found: {file_path}"
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
content = path.read_text()
|
|
96
|
+
|
|
97
|
+
# Detect format
|
|
98
|
+
suffix = path.suffix.lower()
|
|
99
|
+
if suffix in [".md", ".markdown"]:
|
|
100
|
+
fmt = "markdown"
|
|
101
|
+
elif suffix == ".json":
|
|
102
|
+
fmt = "json"
|
|
103
|
+
elif suffix in [".txt", ".text"]:
|
|
104
|
+
fmt = "text"
|
|
105
|
+
else:
|
|
106
|
+
# Guess based on content
|
|
107
|
+
if content.strip().startswith("{") or content.strip().startswith("["):
|
|
108
|
+
fmt = "json"
|
|
109
|
+
elif content.startswith("#") or "##" in content[:500]:
|
|
110
|
+
fmt = "markdown"
|
|
111
|
+
else:
|
|
112
|
+
fmt = "text"
|
|
113
|
+
|
|
114
|
+
return content, fmt, None
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return None, None, f"Error reading file: {e}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main():
|
|
120
|
+
# Read tool input from stdin or environment
|
|
121
|
+
tool_input_raw = os.environ.get("CLAUDE_TOOL_INPUT", "")
|
|
122
|
+
|
|
123
|
+
# Also check stdin for hook input
|
|
124
|
+
try:
|
|
125
|
+
if not sys.stdin.isatty():
|
|
126
|
+
stdin_data = sys.stdin.read()
|
|
127
|
+
if stdin_data:
|
|
128
|
+
try:
|
|
129
|
+
hook_input = json.loads(stdin_data)
|
|
130
|
+
tool_input_raw = json.dumps(hook_input.get("tool_input", {}))
|
|
131
|
+
except json.JSONDecodeError:
|
|
132
|
+
pass
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(tool_input_raw) if tool_input_raw else {}
|
|
138
|
+
skill_name = data.get("skill", "")
|
|
139
|
+
args = data.get("args", "")
|
|
140
|
+
except Exception:
|
|
141
|
+
# Not a skill invocation or invalid JSON
|
|
142
|
+
print(json.dumps({"continue": True}))
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Only trigger for hustle-build skill
|
|
146
|
+
if skill_name != "hustle-build":
|
|
147
|
+
print(json.dumps({"continue": True}))
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Parse flags from arguments
|
|
151
|
+
flags = parse_flags(args)
|
|
152
|
+
|
|
153
|
+
# Check for skip flag
|
|
154
|
+
if flags["skip_document"]:
|
|
155
|
+
print(json.dumps({"continue": True}))
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Check if project_spec already exists with content
|
|
159
|
+
state = load_hustle_build_state()
|
|
160
|
+
if state and state.get("project_spec", {}).get("raw_content"):
|
|
161
|
+
print(json.dumps({"continue": True}))
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Handle --from-document flag
|
|
165
|
+
if flags["from_document"]:
|
|
166
|
+
content, fmt, error = read_document_file(flags["from_document"])
|
|
167
|
+
|
|
168
|
+
if error:
|
|
169
|
+
# Inject error message
|
|
170
|
+
result = {
|
|
171
|
+
"continue": True,
|
|
172
|
+
"additionalContext": f"""
|
|
173
|
+
## Project Document Error
|
|
174
|
+
|
|
175
|
+
Could not load document: {error}
|
|
176
|
+
|
|
177
|
+
Please provide the document path again or use `--skip-document` to proceed without a document.
|
|
178
|
+
"""
|
|
179
|
+
}
|
|
180
|
+
print(json.dumps(result))
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Initialize or update state with document
|
|
184
|
+
if not state:
|
|
185
|
+
state = {
|
|
186
|
+
"version": "4.6.0",
|
|
187
|
+
"build_id": f"build-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
|
|
188
|
+
"status": "initializing"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
state["project_spec"] = {
|
|
192
|
+
"source": "file",
|
|
193
|
+
"file_path": flags["from_document"],
|
|
194
|
+
"raw_content": content,
|
|
195
|
+
"format": fmt,
|
|
196
|
+
"loaded_at": datetime.now().isoformat(),
|
|
197
|
+
"word_count": len(content.split()),
|
|
198
|
+
"extracted": None, # Will be filled by Phase 0.5
|
|
199
|
+
"user_modifications": {
|
|
200
|
+
"added": [],
|
|
201
|
+
"removed": [],
|
|
202
|
+
"modified": []
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
save_hustle_build_state(state)
|
|
207
|
+
|
|
208
|
+
# Log the event
|
|
209
|
+
if UTILS_AVAILABLE:
|
|
210
|
+
log_workflow_event("project_document_loaded", {
|
|
211
|
+
"source": "file",
|
|
212
|
+
"file_path": flags["from_document"],
|
|
213
|
+
"format": fmt,
|
|
214
|
+
"word_count": len(content.split())
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
# Inject confirmation
|
|
218
|
+
result = {
|
|
219
|
+
"continue": True,
|
|
220
|
+
"additionalContext": f"""
|
|
221
|
+
## Project Document Loaded
|
|
222
|
+
|
|
223
|
+
Successfully loaded project document:
|
|
224
|
+
- **Source:** `{flags["from_document"]}`
|
|
225
|
+
- **Format:** {fmt}
|
|
226
|
+
- **Size:** {len(content.split())} words
|
|
227
|
+
|
|
228
|
+
The document will be analyzed in Phase 0.5 to extract:
|
|
229
|
+
- Pages/routes
|
|
230
|
+
- Components
|
|
231
|
+
- APIs
|
|
232
|
+
- Data models
|
|
233
|
+
- External integrations
|
|
234
|
+
|
|
235
|
+
Proceeding to parse your build request...
|
|
236
|
+
"""
|
|
237
|
+
}
|
|
238
|
+
print(json.dumps(result))
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# No document provided - inject prompt asking for one
|
|
242
|
+
context = """
|
|
243
|
+
## Project Document Intake
|
|
244
|
+
|
|
245
|
+
Before decomposing this build request, I need to check if you have a comprehensive project document.
|
|
246
|
+
|
|
247
|
+
**Do you have a project document (PRD, spec, deep research output)?**
|
|
248
|
+
|
|
249
|
+
A project document helps me:
|
|
250
|
+
- Identify ALL pages, components, and APIs upfront
|
|
251
|
+
- Build accurate dependency graphs
|
|
252
|
+
- Reference the spec throughout each sub-workflow
|
|
253
|
+
- Ensure nothing is missed
|
|
254
|
+
|
|
255
|
+
### How to Provide a Document
|
|
256
|
+
|
|
257
|
+
**Option 1: File Path**
|
|
258
|
+
```
|
|
259
|
+
I have a document at ./docs/my-prd.md
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Option 2: Paste Content**
|
|
263
|
+
Just paste the document content directly in your next message.
|
|
264
|
+
|
|
265
|
+
**Option 3: URL**
|
|
266
|
+
```
|
|
267
|
+
Fetch the document from https://example.com/my-spec.md
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Option 4: No Document**
|
|
271
|
+
```
|
|
272
|
+
No document, proceed with parsing my description
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Supported Formats
|
|
276
|
+
- Markdown (`.md`) - PRDs, specs, research outputs
|
|
277
|
+
- Plain text (`.txt`) - Notes, outlines
|
|
278
|
+
- JSON (`.json`) - Structured specs, API definitions
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
_To skip this prompt in the future, use:_
|
|
283
|
+
```
|
|
284
|
+
/hustle-build --skip-document [description]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
_Or provide a document directly:_
|
|
288
|
+
```
|
|
289
|
+
/hustle-build --from-document ./docs/spec.md [description]
|
|
290
|
+
```
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
result = {
|
|
294
|
+
"continue": True,
|
|
295
|
+
"additionalContext": context
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
print(json.dumps(result))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
main()
|