@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
package/hooks/session-startup.py
CHANGED
|
@@ -18,6 +18,12 @@ Updated in v3.6.7:
|
|
|
18
18
|
- Support multi-API state structure (endpoints object)
|
|
19
19
|
- Read research index from .claude/research/index.json file
|
|
20
20
|
- Calculate freshness from timestamps
|
|
21
|
+
|
|
22
|
+
Updated in v4.5.0:
|
|
23
|
+
- Ensure .claude/ directories exist on session start
|
|
24
|
+
- Create registry.json from template if not exists
|
|
25
|
+
- Log session_start event to workflow logs
|
|
26
|
+
- Detect --dry-run and --resume flags
|
|
21
27
|
"""
|
|
22
28
|
import json
|
|
23
29
|
import sys
|
|
@@ -25,6 +31,20 @@ import os
|
|
|
25
31
|
from datetime import datetime
|
|
26
32
|
from pathlib import Path
|
|
27
33
|
|
|
34
|
+
# Import shared utilities
|
|
35
|
+
try:
|
|
36
|
+
from hook_utils import (
|
|
37
|
+
ensure_directories,
|
|
38
|
+
ensure_registry,
|
|
39
|
+
log_workflow_event,
|
|
40
|
+
load_state as utils_load_state,
|
|
41
|
+
save_state as utils_save_state,
|
|
42
|
+
get_project_dir
|
|
43
|
+
)
|
|
44
|
+
UTILS_AVAILABLE = True
|
|
45
|
+
except ImportError:
|
|
46
|
+
UTILS_AVAILABLE = False
|
|
47
|
+
|
|
28
48
|
# State file is in .claude/ directory (sibling to hooks/)
|
|
29
49
|
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
30
50
|
RESEARCH_INDEX = Path(__file__).parent.parent / "research" / "index.json"
|
|
@@ -99,6 +119,93 @@ def calculate_days_old(timestamp_str):
|
|
|
99
119
|
return 0
|
|
100
120
|
|
|
101
121
|
|
|
122
|
+
def setup_session():
|
|
123
|
+
"""
|
|
124
|
+
Initialize session: create directories, registry, log session start.
|
|
125
|
+
Called at the beginning of every session (v4.5.0).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dict: Setup results with directories created, registry status
|
|
129
|
+
"""
|
|
130
|
+
results = {
|
|
131
|
+
"directories_created": [],
|
|
132
|
+
"registry_created": False,
|
|
133
|
+
"session_logged": False
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if not UTILS_AVAILABLE:
|
|
137
|
+
return results
|
|
138
|
+
|
|
139
|
+
# Ensure directories exist
|
|
140
|
+
try:
|
|
141
|
+
results["directories_created"] = ensure_directories()
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# Ensure registry exists
|
|
146
|
+
try:
|
|
147
|
+
success, created = ensure_registry()
|
|
148
|
+
results["registry_created"] = created
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
# Log session start event
|
|
153
|
+
try:
|
|
154
|
+
log_workflow_event("session_start", {
|
|
155
|
+
"directories_created": results["directories_created"],
|
|
156
|
+
"registry_created": results["registry_created"]
|
|
157
|
+
})
|
|
158
|
+
results["session_logged"] = True
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
return results
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect_flags(input_data):
|
|
166
|
+
"""
|
|
167
|
+
Detect --dry-run, --resume, and other flags from conversation context.
|
|
168
|
+
Sets flags in state for other hooks to use (v4.5.0).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
input_data: Hook input from stdin
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
dict: Detected flags
|
|
175
|
+
"""
|
|
176
|
+
flags = {
|
|
177
|
+
"dry_run": False,
|
|
178
|
+
"resume": None,
|
|
179
|
+
"parallel": False
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if not UTILS_AVAILABLE:
|
|
183
|
+
return flags
|
|
184
|
+
|
|
185
|
+
# The conversation context might contain flags
|
|
186
|
+
# Note: In SessionStart, we have limited context - flags are typically
|
|
187
|
+
# detected by skills reading the user's initial message
|
|
188
|
+
# This function prepares the state structure for flag detection
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
state = utils_load_state()
|
|
192
|
+
|
|
193
|
+
# Initialize flags structure if not present
|
|
194
|
+
if "flags" not in state:
|
|
195
|
+
state["flags"] = {
|
|
196
|
+
"dry_run": False,
|
|
197
|
+
"resume": None,
|
|
198
|
+
"parallel": False
|
|
199
|
+
}
|
|
200
|
+
utils_save_state(state)
|
|
201
|
+
|
|
202
|
+
flags = state.get("flags", flags)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
return flags
|
|
207
|
+
|
|
208
|
+
|
|
102
209
|
def main():
|
|
103
210
|
# Read hook input from stdin
|
|
104
211
|
try:
|
|
@@ -108,6 +215,12 @@ def main():
|
|
|
108
215
|
|
|
109
216
|
cwd = input_data.get("cwd", os.getcwd())
|
|
110
217
|
|
|
218
|
+
# v4.5.0: Initialize session (directories, registry, logging)
|
|
219
|
+
setup_results = setup_session()
|
|
220
|
+
|
|
221
|
+
# v4.5.0: Detect and store flags
|
|
222
|
+
flags = detect_flags(input_data)
|
|
223
|
+
|
|
111
224
|
# Check if state file exists
|
|
112
225
|
if not STATE_FILE.exists():
|
|
113
226
|
# No active workflow - just continue without injection
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ADR Decision Updater Hook
|
|
4
|
+
|
|
5
|
+
Updates Architecture Decision Records when user makes a decision during interview.
|
|
6
|
+
Changes status from PROPOSED to ACCEPTED and records the decision with reasoning.
|
|
7
|
+
|
|
8
|
+
Hook Type: PostToolUse (matcher: AskUserQuestion)
|
|
9
|
+
|
|
10
|
+
Flow:
|
|
11
|
+
1. Interview phase presents options to user (referencing ADR)
|
|
12
|
+
2. User selects an option
|
|
13
|
+
3. Hook detects the answer relates to a PROPOSED ADR
|
|
14
|
+
4. Updates ADR with decision, reasoning, and consequences
|
|
15
|
+
5. Updates registry with decision
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_config():
|
|
26
|
+
"""Load ADR configuration"""
|
|
27
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
28
|
+
|
|
29
|
+
config_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
|
|
30
|
+
if config_file.exists():
|
|
31
|
+
try:
|
|
32
|
+
config = json.loads(config_file.read_text())
|
|
33
|
+
return config.get("adr", {})
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
return {"enabled": True}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_registry():
|
|
41
|
+
"""Load current registry"""
|
|
42
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
43
|
+
registry_file = Path(project_dir) / ".claude" / "registry.json"
|
|
44
|
+
|
|
45
|
+
if registry_file.exists():
|
|
46
|
+
try:
|
|
47
|
+
return json.loads(registry_file.read_text())
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_registry(registry):
|
|
54
|
+
"""Save registry"""
|
|
55
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
56
|
+
registry_file = Path(project_dir) / ".claude" / "registry.json"
|
|
57
|
+
registry_file.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
registry_file.write_text(json.dumps(registry, indent=2))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_matching_adr(question_text, answer_text, registry):
|
|
62
|
+
"""
|
|
63
|
+
Find a PROPOSED ADR that matches the question/answer.
|
|
64
|
+
Matches based on category keywords in question and answer options.
|
|
65
|
+
"""
|
|
66
|
+
adrs = registry.get("adrs", {})
|
|
67
|
+
|
|
68
|
+
for adr_key, adr in adrs.items():
|
|
69
|
+
if adr.get("status") != "proposed":
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
category = adr.get("category", "")
|
|
73
|
+
options = adr.get("options_considered", [])
|
|
74
|
+
|
|
75
|
+
# Check if question mentions the category
|
|
76
|
+
if category.lower() in question_text.lower():
|
|
77
|
+
# Check if answer matches one of the options
|
|
78
|
+
for opt in options:
|
|
79
|
+
if opt.lower() in answer_text.lower():
|
|
80
|
+
return adr_key, adr, opt
|
|
81
|
+
|
|
82
|
+
# Check if answer directly matches an option
|
|
83
|
+
for opt in options:
|
|
84
|
+
if opt.lower() in answer_text.lower():
|
|
85
|
+
# Verify category is relevant to question
|
|
86
|
+
category_keywords = {
|
|
87
|
+
"database": ["database", "storage", "data", "db"],
|
|
88
|
+
"auth": ["auth", "authentication", "login", "security"],
|
|
89
|
+
"cache": ["cache", "caching", "performance"],
|
|
90
|
+
"hosting": ["host", "deploy", "platform"],
|
|
91
|
+
"state": ["state", "store", "management"],
|
|
92
|
+
"styling": ["style", "css", "design", "ui"],
|
|
93
|
+
}
|
|
94
|
+
keywords = category_keywords.get(category, [category])
|
|
95
|
+
if any(kw in question_text.lower() for kw in keywords):
|
|
96
|
+
return adr_key, adr, opt
|
|
97
|
+
|
|
98
|
+
return None, None, None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def update_adr_file(adr, decision):
|
|
102
|
+
"""Update the ADR markdown file with the decision"""
|
|
103
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
104
|
+
adr_file = Path(project_dir) / adr.get("file", "")
|
|
105
|
+
|
|
106
|
+
if not adr_file.exists():
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
content = adr_file.read_text()
|
|
110
|
+
|
|
111
|
+
# Update status
|
|
112
|
+
content = re.sub(
|
|
113
|
+
r"\*\*Status:\*\* PROPOSED",
|
|
114
|
+
"**Status:** ACCEPTED",
|
|
115
|
+
content
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Update decision section
|
|
119
|
+
decision_section = f"""## Decision
|
|
120
|
+
|
|
121
|
+
We will use **{decision.title()}** based on user selection during interview.
|
|
122
|
+
|
|
123
|
+
**Reasoning:** User prioritized this option based on project requirements.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
content = re.sub(
|
|
127
|
+
r"## Decision\n\n_Pending user selection during interview phase\._",
|
|
128
|
+
decision_section,
|
|
129
|
+
content
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Update consequences section
|
|
133
|
+
consequences_section = f"""## Consequences
|
|
134
|
+
|
|
135
|
+
### Positive
|
|
136
|
+
- Decision has been made, enabling implementation to proceed
|
|
137
|
+
- Choice aligns with user's stated requirements
|
|
138
|
+
|
|
139
|
+
### Negative
|
|
140
|
+
- Alternative options were not selected (may revisit if requirements change)
|
|
141
|
+
|
|
142
|
+
### Implementation Notes
|
|
143
|
+
- Proceed with {decision.title()} integration
|
|
144
|
+
- Update environment variables as needed
|
|
145
|
+
- Follow {decision.title()} best practices
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
content = re.sub(
|
|
149
|
+
r"## Consequences\n\n_To be documented after decision is made\._",
|
|
150
|
+
consequences_section,
|
|
151
|
+
content
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Add decision timestamp
|
|
155
|
+
content = content.replace(
|
|
156
|
+
"_This ADR was auto-generated during research.",
|
|
157
|
+
f"_Decision recorded: {datetime.now().strftime('%Y-%m-%d %H:%M')}_\n\n_This ADR was auto-generated during research."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
adr_file.write_text(content)
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
# Get tool info
|
|
166
|
+
tool_name = os.environ.get("CLAUDE_TOOL_NAME", "")
|
|
167
|
+
tool_output = os.environ.get("CLAUDE_TOOL_OUTPUT", "")
|
|
168
|
+
tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
|
|
169
|
+
|
|
170
|
+
# Only process AskUserQuestion results
|
|
171
|
+
if tool_name != "AskUserQuestion":
|
|
172
|
+
print(json.dumps({"continue": True}))
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Load config
|
|
176
|
+
config = load_config()
|
|
177
|
+
if not config.get("enabled", True):
|
|
178
|
+
print(json.dumps({"continue": True}))
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Parse question and answer
|
|
182
|
+
try:
|
|
183
|
+
input_data = json.loads(tool_input)
|
|
184
|
+
questions = input_data.get("questions", [])
|
|
185
|
+
if not questions:
|
|
186
|
+
print(json.dumps({"continue": True}))
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
question_text = questions[0].get("question", "")
|
|
190
|
+
except Exception:
|
|
191
|
+
question_text = ""
|
|
192
|
+
|
|
193
|
+
# The answer is in tool_output
|
|
194
|
+
answer_text = tool_output
|
|
195
|
+
|
|
196
|
+
if not question_text or not answer_text:
|
|
197
|
+
print(json.dumps({"continue": True}))
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Load registry and find matching ADR
|
|
201
|
+
registry = load_registry()
|
|
202
|
+
adr_key, adr, decision = find_matching_adr(question_text, answer_text, registry)
|
|
203
|
+
|
|
204
|
+
if not adr_key:
|
|
205
|
+
print(json.dumps({"continue": True}))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Update ADR file
|
|
209
|
+
update_adr_file(adr, decision)
|
|
210
|
+
|
|
211
|
+
# Update registry
|
|
212
|
+
registry["adrs"][adr_key]["status"] = "accepted"
|
|
213
|
+
registry["adrs"][adr_key]["decision"] = decision
|
|
214
|
+
registry["adrs"][adr_key]["phase"] = "interview"
|
|
215
|
+
registry["adrs"][adr_key]["decided_at"] = datetime.now().isoformat()
|
|
216
|
+
save_registry(registry)
|
|
217
|
+
|
|
218
|
+
# Notify about ADR update
|
|
219
|
+
result = {
|
|
220
|
+
"continue": True,
|
|
221
|
+
"additionalContext": f"""## ADR Updated
|
|
222
|
+
|
|
223
|
+
**ADR-{adr.get('number', 0):04d}: {adr.get('title', '')}** has been updated.
|
|
224
|
+
|
|
225
|
+
- **Status:** PROPOSED → ACCEPTED
|
|
226
|
+
- **Decision:** {decision.title()}
|
|
227
|
+
- **File:** {adr.get('file', '')}
|
|
228
|
+
|
|
229
|
+
This decision is now recorded for future reference.
|
|
230
|
+
"""
|
|
231
|
+
}
|
|
232
|
+
print(json.dumps(result))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
@@ -27,12 +27,15 @@ def copy_showcase_templates(cwd):
|
|
|
27
27
|
"""Copy API showcase templates to src/app/api-showcase/."""
|
|
28
28
|
# Source templates (installed by CLI)
|
|
29
29
|
templates_dir = Path(__file__).parent.parent / "templates" / "api-showcase"
|
|
30
|
+
shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
|
|
30
31
|
|
|
31
32
|
# Destination
|
|
32
33
|
showcase_dir = cwd / "src" / "app" / "api-showcase"
|
|
34
|
+
shared_dir = cwd / "src" / "app" / "shared"
|
|
33
35
|
|
|
34
|
-
# Create
|
|
36
|
+
# Create directories if needed
|
|
35
37
|
showcase_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
shared_dir.mkdir(parents=True, exist_ok=True)
|
|
36
39
|
|
|
37
40
|
# Copy template files
|
|
38
41
|
templates_to_copy = [
|
|
@@ -55,6 +58,15 @@ def copy_showcase_templates(cwd):
|
|
|
55
58
|
shutil.copy2(src_path, dest_path)
|
|
56
59
|
created_files.append(str(dest_path.relative_to(cwd)))
|
|
57
60
|
|
|
61
|
+
# Also copy shared components (HeroHeader, etc.)
|
|
62
|
+
if shared_templates_dir.exists():
|
|
63
|
+
for src_file in shared_templates_dir.iterdir():
|
|
64
|
+
if src_file.is_file():
|
|
65
|
+
dest_path = shared_dir / src_file.name
|
|
66
|
+
if not dest_path.exists():
|
|
67
|
+
shutil.copy2(src_file, dest_path)
|
|
68
|
+
created_files.append(str(dest_path.relative_to(cwd)))
|
|
69
|
+
|
|
58
70
|
return created_files
|
|
59
71
|
|
|
60
72
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Auto-update TESTING_CHECKLIST.md when tests pass.
|
|
4
|
+
|
|
5
|
+
Hook Type: PostToolUse (matcher: Bash)
|
|
6
|
+
|
|
7
|
+
Detects test pass patterns and updates the checklist file with:
|
|
8
|
+
- Test results (PASS/FAIL)
|
|
9
|
+
- Timestamp
|
|
10
|
+
- Comments
|
|
11
|
+
|
|
12
|
+
Works by:
|
|
13
|
+
1. Detecting test-related Bash commands
|
|
14
|
+
2. Parsing output for pass/fail patterns
|
|
15
|
+
3. Updating the corresponding checklist rows
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_tool_result():
|
|
27
|
+
"""Get the tool result from environment"""
|
|
28
|
+
result = os.environ.get("CLAUDE_TOOL_RESULT", "")
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_tool_input():
|
|
33
|
+
"""Get the tool input from environment"""
|
|
34
|
+
try:
|
|
35
|
+
input_json = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
|
|
36
|
+
return json.loads(input_json)
|
|
37
|
+
except Exception:
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def detect_test_type(command: str, output: str) -> dict:
|
|
42
|
+
"""Detect what type of test was run and if it passed"""
|
|
43
|
+
result = {
|
|
44
|
+
"is_test": False,
|
|
45
|
+
"test_type": None,
|
|
46
|
+
"passed": None,
|
|
47
|
+
"hook_name": None,
|
|
48
|
+
"details": None
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
command_lower = command.lower()
|
|
52
|
+
|
|
53
|
+
# Hook compilation test
|
|
54
|
+
if "python3" in command_lower and ".py" in command_lower:
|
|
55
|
+
if "hooks/" in command or ".claude/hooks/" in command:
|
|
56
|
+
result["is_test"] = True
|
|
57
|
+
result["test_type"] = "hook_compile"
|
|
58
|
+
# Extract hook name
|
|
59
|
+
match = re.search(r'(?:hooks/|\.claude/hooks/)([^/\s]+\.py)', command)
|
|
60
|
+
if match:
|
|
61
|
+
result["hook_name"] = match.group(1)
|
|
62
|
+
# Check for pass/fail
|
|
63
|
+
if "Traceback" in output or "Error" in output or "SyntaxError" in output:
|
|
64
|
+
result["passed"] = False
|
|
65
|
+
result["details"] = "Syntax/import error"
|
|
66
|
+
elif "exit code" in output.lower():
|
|
67
|
+
exit_match = re.search(r'exit code[:\s]+(\d+)', output.lower())
|
|
68
|
+
if exit_match:
|
|
69
|
+
result["passed"] = exit_match.group(1) == "0"
|
|
70
|
+
else:
|
|
71
|
+
result["passed"] = True
|
|
72
|
+
result["details"] = "Compiles"
|
|
73
|
+
|
|
74
|
+
# Hook enforcement test
|
|
75
|
+
if "python3" in command_lower and ("enforce" in command_lower or "verify" in command_lower):
|
|
76
|
+
result["is_test"] = True
|
|
77
|
+
result["test_type"] = "hook_enforcement"
|
|
78
|
+
match = re.search(r'(?:hooks/|\.claude/hooks/)([^/\s]+\.py)', command)
|
|
79
|
+
if match:
|
|
80
|
+
result["hook_name"] = match.group(1)
|
|
81
|
+
|
|
82
|
+
# Check for blocking behavior
|
|
83
|
+
if '"permissionDecision": "deny"' in output or "BLOCKED" in output:
|
|
84
|
+
result["passed"] = True
|
|
85
|
+
result["details"] = "BLOCKS correctly"
|
|
86
|
+
elif '"permissionDecision": "allow"' in output:
|
|
87
|
+
result["passed"] = True
|
|
88
|
+
result["details"] = "ALLOWS correctly"
|
|
89
|
+
elif '"continue": true' in output:
|
|
90
|
+
result["passed"] = True
|
|
91
|
+
result["details"] = "Continues"
|
|
92
|
+
|
|
93
|
+
# pnpm test
|
|
94
|
+
if "pnpm test" in command_lower or "npm test" in command_lower:
|
|
95
|
+
result["is_test"] = True
|
|
96
|
+
result["test_type"] = "unit_test"
|
|
97
|
+
if "PASS" in output or "passed" in output.lower():
|
|
98
|
+
result["passed"] = True
|
|
99
|
+
elif "FAIL" in output or "failed" in output.lower():
|
|
100
|
+
result["passed"] = False
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def update_checklist(hook_name: str, status: str, comment: str):
|
|
106
|
+
"""Update the TESTING_CHECKLIST.md file with test results"""
|
|
107
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
108
|
+
checklist_path = Path(project_dir) / "TESTING_CHECKLIST.md"
|
|
109
|
+
|
|
110
|
+
if not checklist_path.exists():
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
content = checklist_path.read_text()
|
|
115
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
116
|
+
|
|
117
|
+
# Pattern to find hook row in table (with empty Status column)
|
|
118
|
+
# Format: | `hook_name` | Type | Phase/Trigger | | |
|
|
119
|
+
pattern = rf'(\| `{re.escape(hook_name)}` \|[^|]+\|[^|]+\|)\s*\|\s*\|'
|
|
120
|
+
replacement = rf'\1 {status} | {comment} ({today}) |'
|
|
121
|
+
|
|
122
|
+
new_content = re.sub(pattern, replacement, content)
|
|
123
|
+
|
|
124
|
+
if new_content != content:
|
|
125
|
+
checklist_path.write_text(new_content)
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
# Try alternate pattern for already-filled rows (update existing)
|
|
129
|
+
pattern2 = rf'(\| `{re.escape(hook_name)}` \|[^|]+\|[^|]+\|)[^|]+\|[^|]+\|'
|
|
130
|
+
replacement2 = rf'\1 {status} | {comment} ({today}) |'
|
|
131
|
+
|
|
132
|
+
new_content = re.sub(pattern2, replacement2, content)
|
|
133
|
+
if new_content != content:
|
|
134
|
+
checklist_path.write_text(new_content)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
# Log error but don't fail
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main():
|
|
145
|
+
tool_input = get_tool_input()
|
|
146
|
+
command = tool_input.get("command", "")
|
|
147
|
+
output = get_tool_result()
|
|
148
|
+
|
|
149
|
+
# Detect what test was run
|
|
150
|
+
test_info = detect_test_type(command, output)
|
|
151
|
+
|
|
152
|
+
if not test_info["is_test"]:
|
|
153
|
+
print(json.dumps({"continue": True}))
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Update checklist if we have a hook name
|
|
157
|
+
if test_info["hook_name"] and test_info["passed"] is not None:
|
|
158
|
+
status = "PASS" if test_info["passed"] else "FAIL"
|
|
159
|
+
comment = test_info["details"] or ("Tested" if test_info["passed"] else "Failed")
|
|
160
|
+
|
|
161
|
+
updated = update_checklist(
|
|
162
|
+
test_info["hook_name"],
|
|
163
|
+
status,
|
|
164
|
+
comment
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if updated:
|
|
168
|
+
# Log the update
|
|
169
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
170
|
+
logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
|
|
171
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
log_file = logs_dir / "checklist-updates.json"
|
|
174
|
+
try:
|
|
175
|
+
if log_file.exists():
|
|
176
|
+
log = json.loads(log_file.read_text())
|
|
177
|
+
else:
|
|
178
|
+
log = {"updates": []}
|
|
179
|
+
|
|
180
|
+
log["updates"].append({
|
|
181
|
+
"timestamp": datetime.now().isoformat(),
|
|
182
|
+
"hook": test_info["hook_name"],
|
|
183
|
+
"status": status,
|
|
184
|
+
"comment": comment
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
log_file.write_text(json.dumps(log, indent=2))
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
print(json.dumps({"continue": True}))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
main()
|
|
@@ -84,12 +84,15 @@ def copy_showcase_templates(cwd):
|
|
|
84
84
|
"""Copy UI showcase templates to src/app/ui-showcase/."""
|
|
85
85
|
# Source templates (installed by CLI)
|
|
86
86
|
templates_dir = Path(__file__).parent.parent / "templates" / "ui-showcase"
|
|
87
|
+
shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
|
|
87
88
|
|
|
88
89
|
# Destination
|
|
89
90
|
showcase_dir = cwd / "src" / "app" / "ui-showcase"
|
|
91
|
+
shared_dir = cwd / "src" / "app" / "shared"
|
|
90
92
|
|
|
91
|
-
# Create
|
|
93
|
+
# Create directories if needed
|
|
92
94
|
showcase_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
shared_dir.mkdir(parents=True, exist_ok=True)
|
|
93
96
|
|
|
94
97
|
# Copy template files
|
|
95
98
|
templates_to_copy = [
|
|
@@ -111,6 +114,15 @@ def copy_showcase_templates(cwd):
|
|
|
111
114
|
shutil.copy2(src_path, dest_path)
|
|
112
115
|
created_files.append(str(dest_path.relative_to(cwd)))
|
|
113
116
|
|
|
117
|
+
# Also copy shared components (HeroHeader, etc.)
|
|
118
|
+
if shared_templates_dir.exists():
|
|
119
|
+
for src_file in shared_templates_dir.iterdir():
|
|
120
|
+
if src_file.is_file():
|
|
121
|
+
dest_path = shared_dir / src_file.name
|
|
122
|
+
if not dest_path.exists():
|
|
123
|
+
shutil.copy2(src_file, dest_path)
|
|
124
|
+
created_files.append(str(dest_path.relative_to(cwd)))
|
|
125
|
+
|
|
114
126
|
return created_files
|
|
115
127
|
|
|
116
128
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hustle-together/api-dev-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"description": "Interview-driven, research-first API development toolkit with 14-phase TDD workflow, enforcement hooks, and 23 Agent Skills for cross-platform AI agents",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,11 +23,15 @@
|
|
|
23
23
|
"test": "node bin/cli.js --scope=project",
|
|
24
24
|
"usage": "ccusage",
|
|
25
25
|
"format": "prettier --write .",
|
|
26
|
-
"lint": "eslint . --fix"
|
|
26
|
+
"lint": "eslint . --fix",
|
|
27
|
+
"typedoc": "typedoc",
|
|
28
|
+
"typedoc:watch": "typedoc --watch"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"prettier": "^3.0.0",
|
|
30
|
-
"eslint": "^8.0.0"
|
|
32
|
+
"eslint": "^8.0.0",
|
|
33
|
+
"typedoc": "^0.27.0",
|
|
34
|
+
"typedoc-plugin-markdown": "^4.4.0"
|
|
31
35
|
},
|
|
32
36
|
"optionalDependencies": {
|
|
33
37
|
"ccusage": "^1.0.0"
|