@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.
- 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 +10 -0
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/settings.local.json +1 -7
- 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 +34 -20
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +365 -38
- 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 +488 -0
- package/README.md +346 -53
- package/bin/cli.js +359 -123
- 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 +97 -20
- package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
- package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
- package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
- package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
- package/{.claude/hooks → hooks}/hook_utils.py +0 -0
- package/hooks/ntfy-on-question.py +15 -2
- package/hooks/orchestrator-handoff.py +81 -3
- package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
- package/hooks/periodic-reground.py +40 -0
- package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
- package/hooks/run-code-review.py +176 -29
- package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
- package/package.json +1 -1
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- 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 +53 -1
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +74 -7
- package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/.claude/commands/hustle-combine.md +0 -1089
- package/.claude/commands/hustle-ui-create-page.md +0 -1078
- package/.claude/commands/hustle-ui-create.md +0 -1058
- package/.claude/hooks/auto-answer.py +0 -305
- package/.claude/hooks/cache-research.py +0 -337
- package/.claude/hooks/check-api-routes.py +0 -168
- package/.claude/hooks/check-playwright-setup.py +0 -103
- package/.claude/hooks/check-storybook-setup.py +0 -81
- package/.claude/hooks/check-update.py +0 -132
- package/.claude/hooks/detect-interruption.py +0 -165
- package/.claude/hooks/enforce-a11y-audit.py +0 -202
- package/.claude/hooks/enforce-brand-guide.py +0 -241
- package/.claude/hooks/enforce-component-type-confirm.py +0 -97
- package/.claude/hooks/enforce-freshness.py +0 -184
- package/.claude/hooks/enforce-page-components.py +0 -186
- package/.claude/hooks/enforce-page-data-schema.py +0 -155
- package/.claude/hooks/enforce-questions-sourced.py +0 -146
- package/.claude/hooks/enforce-schema-from-interview.py +0 -248
- package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
- package/.claude/hooks/enforce-ui-interview.py +0 -130
- package/.claude/hooks/generate-manifest-entry.py +0 -1161
- package/.claude/hooks/lib/__init__.py +0 -1
- package/.claude/hooks/lib/greptile.py +0 -355
- package/.claude/hooks/lib/ntfy.py +0 -209
- package/.claude/hooks/notify-input-needed.py +0 -73
- package/.claude/hooks/notify-phase-complete.py +0 -90
- package/.claude/hooks/ntfy-on-question.py +0 -240
- package/.claude/hooks/orchestrator-completion.py +0 -313
- package/.claude/hooks/orchestrator-handoff.py +0 -267
- package/.claude/hooks/orchestrator-session-startup.py +0 -146
- package/.claude/hooks/run-code-review.py +0 -393
- package/.claude/hooks/session-logger.py +0 -323
- package/.claude/hooks/test-orchestrator-reground.py +0 -248
- package/.claude/hooks/track-scope-coverage.py +0 -220
- package/.claude/hooks/track-token-usage.py +0 -121
- package/.claude/hooks/update-api-showcase.py +0 -161
- package/.claude/hooks/update-registry.py +0 -352
- package/.claude/hooks/update-ui-showcase.py +0 -224
- package/.claude/test-auto-answer-bot.py +0 -183
- package/.claude/test-completion-detector.py +0 -263
- package/.claude/test-orchestrator-state.json +0 -20
- package/.claude/test-orchestrator.sh +0 -271
- /package/{.claude/commands → commands}/hustle-build.md +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
- /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
- /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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"
|
package/hooks/auto-answer.py
CHANGED
|
@@ -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
|
-
#
|
|
176
|
-
|
|
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":
|
|
179
|
-
"
|
|
180
|
-
|
|
241
|
+
"continue": False,
|
|
242
|
+
"reason": f"""## 🤖 Auto-Selected
|
|
243
|
+
|
|
244
|
+
**{header}:** {answer}
|
|
181
245
|
|
|
182
|
-
|
|
183
|
-
{json.dumps(answers, indent=2)}
|
|
246
|
+
_Question: {question_text}_
|
|
184
247
|
|
|
185
|
-
|
|
186
|
-
- Maximum feature coverage
|
|
187
|
-
- Full testing
|
|
188
|
-
- Comprehensive documentation
|
|
189
|
-
- Best practices
|
|
248
|
+
---
|
|
190
249
|
|
|
191
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
166
|
-
|
|
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,
|
|
File without changes
|
|
@@ -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", [])
|
|
File without changes
|