@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
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: Stop
|
|
4
|
-
Purpose: Save session to .claude/api-sessions/ for later review
|
|
5
|
-
|
|
6
|
-
This hook runs when a Claude Code session ends (Stop event).
|
|
7
|
-
It saves the session data for the completed workflow including:
|
|
8
|
-
- State snapshot at completion
|
|
9
|
-
- Files created during the workflow
|
|
10
|
-
- Summary of phases completed
|
|
11
|
-
- Research sources used
|
|
12
|
-
- Interview decisions made
|
|
13
|
-
|
|
14
|
-
Added in v3.6.7 for session logging support.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
- JSON with session save info
|
|
18
|
-
"""
|
|
19
|
-
import json
|
|
20
|
-
import sys
|
|
21
|
-
import os
|
|
22
|
-
from datetime import datetime
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
import shutil
|
|
25
|
-
|
|
26
|
-
# Import shared utilities for NTFY
|
|
27
|
-
try:
|
|
28
|
-
from hook_utils import send_ntfy_notification
|
|
29
|
-
HAS_NTFY = True
|
|
30
|
-
except ImportError:
|
|
31
|
-
HAS_NTFY = False
|
|
32
|
-
|
|
33
|
-
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
34
|
-
SESSIONS_DIR = Path(__file__).parent.parent / "api-sessions"
|
|
35
|
-
RESEARCH_DIR = Path(__file__).parent.parent / "research"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def get_active_endpoint(state):
|
|
39
|
-
"""Get active endpoint - supports both old and new state formats."""
|
|
40
|
-
# New format (v3.6.7+): endpoints object with active_endpoint pointer
|
|
41
|
-
if "endpoints" in state and "active_endpoint" in state:
|
|
42
|
-
active = state.get("active_endpoint")
|
|
43
|
-
if active and active in state["endpoints"]:
|
|
44
|
-
return active, state["endpoints"][active]
|
|
45
|
-
return None, None
|
|
46
|
-
|
|
47
|
-
# Old format: single endpoint field
|
|
48
|
-
endpoint = state.get("endpoint")
|
|
49
|
-
if endpoint:
|
|
50
|
-
return endpoint, state
|
|
51
|
-
|
|
52
|
-
return None, None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_completed_phases(endpoint_data):
|
|
56
|
-
"""Get list of completed phases."""
|
|
57
|
-
completed = []
|
|
58
|
-
phases = endpoint_data.get("phases", {})
|
|
59
|
-
|
|
60
|
-
phase_order = [
|
|
61
|
-
"disambiguation", "scope", "research_initial", "interview",
|
|
62
|
-
"research_deep", "schema_creation", "environment_check",
|
|
63
|
-
"tdd_red", "tdd_green", "verify", "tdd_refactor", "documentation", "completion"
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
for phase_name in phase_order:
|
|
67
|
-
phase = phases.get(phase_name, {})
|
|
68
|
-
if phase.get("status") == "complete":
|
|
69
|
-
completed.append(phase_name)
|
|
70
|
-
|
|
71
|
-
return completed
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def get_files_created(endpoint_data):
|
|
75
|
-
"""Get list of files created during this workflow."""
|
|
76
|
-
files = []
|
|
77
|
-
|
|
78
|
-
# From completion phase
|
|
79
|
-
completion = endpoint_data.get("phases", {}).get("completion", {})
|
|
80
|
-
files.extend(completion.get("files_created", []))
|
|
81
|
-
|
|
82
|
-
# From schema phase
|
|
83
|
-
schema = endpoint_data.get("phases", {}).get("schema_creation", {})
|
|
84
|
-
if schema.get("schema_file"):
|
|
85
|
-
files.append(schema.get("schema_file"))
|
|
86
|
-
|
|
87
|
-
# From TDD phases
|
|
88
|
-
tdd_red = endpoint_data.get("phases", {}).get("tdd_red", {})
|
|
89
|
-
if tdd_red.get("test_file"):
|
|
90
|
-
files.append(tdd_red.get("test_file"))
|
|
91
|
-
|
|
92
|
-
tdd_green = endpoint_data.get("phases", {}).get("tdd_green", {})
|
|
93
|
-
if tdd_green.get("implementation_file"):
|
|
94
|
-
files.append(tdd_green.get("implementation_file"))
|
|
95
|
-
|
|
96
|
-
return list(set(files)) # Deduplicate
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def generate_summary(endpoint, endpoint_data, state):
|
|
100
|
-
"""Generate a markdown summary of the session."""
|
|
101
|
-
completed = get_completed_phases(endpoint_data)
|
|
102
|
-
files = get_files_created(endpoint_data)
|
|
103
|
-
decisions = endpoint_data.get("phases", {}).get("interview", {}).get("decisions", {})
|
|
104
|
-
|
|
105
|
-
lines = [
|
|
106
|
-
f"# Session Summary: {endpoint}",
|
|
107
|
-
"",
|
|
108
|
-
f"*Generated: {datetime.now().isoformat()}*",
|
|
109
|
-
"",
|
|
110
|
-
"## Overview",
|
|
111
|
-
"",
|
|
112
|
-
f"- **Endpoint:** {endpoint}",
|
|
113
|
-
f"- **Library:** {endpoint_data.get('library', 'N/A')}",
|
|
114
|
-
f"- **Started:** {endpoint_data.get('started_at', 'N/A')}",
|
|
115
|
-
f"- **Completed Phases:** {len(completed)}/13",
|
|
116
|
-
f"- **Status:** {endpoint_data.get('status', 'unknown')}",
|
|
117
|
-
"",
|
|
118
|
-
"## Phases Completed",
|
|
119
|
-
""
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
for i, phase in enumerate(completed, 1):
|
|
123
|
-
lines.append(f"{i}. {phase.replace('_', ' ').title()}")
|
|
124
|
-
|
|
125
|
-
lines.extend([
|
|
126
|
-
"",
|
|
127
|
-
"## Files Created",
|
|
128
|
-
""
|
|
129
|
-
])
|
|
130
|
-
|
|
131
|
-
for f in files:
|
|
132
|
-
lines.append(f"- `{f}`")
|
|
133
|
-
|
|
134
|
-
if decisions:
|
|
135
|
-
lines.extend([
|
|
136
|
-
"",
|
|
137
|
-
"## Interview Decisions",
|
|
138
|
-
""
|
|
139
|
-
])
|
|
140
|
-
for key, value in decisions.items():
|
|
141
|
-
response = value.get("response", value.get("value", "N/A"))
|
|
142
|
-
lines.append(f"- **{key}:** {response}")
|
|
143
|
-
|
|
144
|
-
lines.extend([
|
|
145
|
-
"",
|
|
146
|
-
"## Research Sources",
|
|
147
|
-
""
|
|
148
|
-
])
|
|
149
|
-
|
|
150
|
-
# Check for research cache
|
|
151
|
-
research_path = RESEARCH_DIR / endpoint / "sources.json"
|
|
152
|
-
if research_path.exists():
|
|
153
|
-
try:
|
|
154
|
-
sources = json.loads(research_path.read_text())
|
|
155
|
-
for src in sources.get("sources", [])[:10]: # Limit to 10
|
|
156
|
-
url = src.get("url", src.get("query", ""))
|
|
157
|
-
if url:
|
|
158
|
-
lines.append(f"- {url}")
|
|
159
|
-
except (json.JSONDecodeError, IOError):
|
|
160
|
-
lines.append("- (sources.json not readable)")
|
|
161
|
-
else:
|
|
162
|
-
lines.append("- (no sources.json found)")
|
|
163
|
-
|
|
164
|
-
lines.extend([
|
|
165
|
-
"",
|
|
166
|
-
"---",
|
|
167
|
-
"",
|
|
168
|
-
f"*Session saved to: .claude/api-sessions/{endpoint}_{{timestamp}}/*"
|
|
169
|
-
])
|
|
170
|
-
|
|
171
|
-
return "\n".join(lines)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def save_session(endpoint, endpoint_data, state):
|
|
175
|
-
"""Save session to .claude/api-sessions/."""
|
|
176
|
-
# Create timestamp
|
|
177
|
-
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
178
|
-
session_dir = SESSIONS_DIR / f"{endpoint}_{timestamp}"
|
|
179
|
-
session_dir.mkdir(parents=True, exist_ok=True)
|
|
180
|
-
|
|
181
|
-
# 1. Save state snapshot
|
|
182
|
-
state_snapshot = {
|
|
183
|
-
"saved_at": datetime.now().isoformat(),
|
|
184
|
-
"endpoint": endpoint,
|
|
185
|
-
"endpoint_data": endpoint_data,
|
|
186
|
-
"turn_count": state.get("turn_count", 0),
|
|
187
|
-
"research_queries": state.get("research_queries", [])
|
|
188
|
-
}
|
|
189
|
-
(session_dir / "state-snapshot.json").write_text(json.dumps(state_snapshot, indent=2))
|
|
190
|
-
|
|
191
|
-
# 2. Save files list
|
|
192
|
-
files = get_files_created(endpoint_data)
|
|
193
|
-
(session_dir / "files-created.txt").write_text("\n".join(files))
|
|
194
|
-
|
|
195
|
-
# 3. Generate and save summary
|
|
196
|
-
summary = generate_summary(endpoint, endpoint_data, state)
|
|
197
|
-
(session_dir / "summary.md").write_text(summary)
|
|
198
|
-
|
|
199
|
-
# 4. Copy research cache if exists
|
|
200
|
-
research_src = RESEARCH_DIR / endpoint
|
|
201
|
-
if research_src.exists():
|
|
202
|
-
research_dst = session_dir / "research-cache"
|
|
203
|
-
research_dst.mkdir(exist_ok=True)
|
|
204
|
-
for f in research_src.iterdir():
|
|
205
|
-
if f.is_file():
|
|
206
|
-
shutil.copy2(f, research_dst / f.name)
|
|
207
|
-
|
|
208
|
-
# 5. Update sessions index
|
|
209
|
-
update_sessions_index(endpoint, timestamp, endpoint_data)
|
|
210
|
-
|
|
211
|
-
return session_dir
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def update_sessions_index(endpoint, timestamp, endpoint_data):
|
|
215
|
-
"""Update the sessions index file."""
|
|
216
|
-
index_file = SESSIONS_DIR / "index.json"
|
|
217
|
-
|
|
218
|
-
if index_file.exists():
|
|
219
|
-
try:
|
|
220
|
-
index = json.loads(index_file.read_text())
|
|
221
|
-
except json.JSONDecodeError:
|
|
222
|
-
index = {"version": "3.6.7", "sessions": []}
|
|
223
|
-
else:
|
|
224
|
-
index = {"version": "3.6.7", "sessions": []}
|
|
225
|
-
|
|
226
|
-
# Add this session
|
|
227
|
-
completed = get_completed_phases(endpoint_data)
|
|
228
|
-
index["sessions"].append({
|
|
229
|
-
"endpoint": endpoint,
|
|
230
|
-
"timestamp": timestamp,
|
|
231
|
-
"folder": f"{endpoint}_{timestamp}",
|
|
232
|
-
"status": endpoint_data.get("status", "unknown"),
|
|
233
|
-
"phases_completed": len(completed),
|
|
234
|
-
"created_at": datetime.now().isoformat()
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
index_file.write_text(json.dumps(index, indent=2))
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def main():
|
|
241
|
-
try:
|
|
242
|
-
input_data = json.load(sys.stdin)
|
|
243
|
-
except json.JSONDecodeError:
|
|
244
|
-
print(json.dumps({"continue": True}))
|
|
245
|
-
sys.exit(0)
|
|
246
|
-
|
|
247
|
-
# Check if state file exists
|
|
248
|
-
if not STATE_FILE.exists():
|
|
249
|
-
print(json.dumps({"continue": True}))
|
|
250
|
-
sys.exit(0)
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
state = json.loads(STATE_FILE.read_text())
|
|
254
|
-
except json.JSONDecodeError:
|
|
255
|
-
print(json.dumps({"continue": True}))
|
|
256
|
-
sys.exit(0)
|
|
257
|
-
|
|
258
|
-
# Get active endpoint
|
|
259
|
-
endpoint, endpoint_data = get_active_endpoint(state)
|
|
260
|
-
if not endpoint or not endpoint_data:
|
|
261
|
-
print(json.dumps({"continue": True}))
|
|
262
|
-
sys.exit(0)
|
|
263
|
-
|
|
264
|
-
# Only save if there's meaningful progress
|
|
265
|
-
completed = get_completed_phases(endpoint_data)
|
|
266
|
-
if len(completed) < 2:
|
|
267
|
-
# Not enough progress to save
|
|
268
|
-
print(json.dumps({
|
|
269
|
-
"hookSpecificOutput": {
|
|
270
|
-
"sessionSaved": False,
|
|
271
|
-
"reason": "Not enough progress to save (need at least 2 completed phases)"
|
|
272
|
-
}
|
|
273
|
-
}))
|
|
274
|
-
sys.exit(0)
|
|
275
|
-
|
|
276
|
-
# Save the session
|
|
277
|
-
try:
|
|
278
|
-
session_dir = save_session(endpoint, endpoint_data, state)
|
|
279
|
-
|
|
280
|
-
# Send NTFY notification on session end
|
|
281
|
-
if HAS_NTFY:
|
|
282
|
-
status = endpoint_data.get("status", "unknown")
|
|
283
|
-
if status == "complete":
|
|
284
|
-
send_ntfy_notification(
|
|
285
|
-
title=f"✅ Session Complete: {endpoint}",
|
|
286
|
-
message=f"Completed {len(completed)}/13 phases. Session saved.",
|
|
287
|
-
priority="default",
|
|
288
|
-
tags=["white_check_mark", "robot"]
|
|
289
|
-
)
|
|
290
|
-
else:
|
|
291
|
-
send_ntfy_notification(
|
|
292
|
-
title=f"📋 Session Ended: {endpoint}",
|
|
293
|
-
message=f"Completed {len(completed)}/13 phases. Status: {status}",
|
|
294
|
-
priority="low",
|
|
295
|
-
tags=["clipboard", "robot"]
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
output = {
|
|
299
|
-
"hookSpecificOutput": {
|
|
300
|
-
"sessionSaved": True,
|
|
301
|
-
"endpoint": endpoint,
|
|
302
|
-
"sessionDir": str(session_dir),
|
|
303
|
-
"phasesCompleted": len(completed),
|
|
304
|
-
"notificationSent": HAS_NTFY
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
print(json.dumps(output))
|
|
309
|
-
sys.exit(0)
|
|
310
|
-
|
|
311
|
-
except Exception as e:
|
|
312
|
-
output = {
|
|
313
|
-
"hookSpecificOutput": {
|
|
314
|
-
"sessionSaved": False,
|
|
315
|
-
"error": str(e)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
print(json.dumps(output))
|
|
319
|
-
sys.exit(0)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if __name__ == "__main__":
|
|
323
|
-
main()
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Test Orchestrator Re-grounding Hook
|
|
4
|
-
|
|
5
|
-
Runs every 5 turns during test orchestration to:
|
|
6
|
-
1. Re-inject testing goals and current progress
|
|
7
|
-
2. Send NTFY notification with status update
|
|
8
|
-
3. Prevent context dilution during long test sessions
|
|
9
|
-
|
|
10
|
-
Hook Type: PostToolUse
|
|
11
|
-
Trigger: Every 5 turns
|
|
12
|
-
NTFY Topic: test_api_devtools_alerts
|
|
13
|
-
|
|
14
|
-
Version: 1.0.0
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
|
-
import sys
|
|
20
|
-
import subprocess
|
|
21
|
-
from datetime import datetime
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
# Configuration
|
|
25
|
-
REGROUND_INTERVAL = 5 # Re-ground every 5 turns
|
|
26
|
-
NTFY_TOPIC = "test_api_devtools_alerts"
|
|
27
|
-
STATE_FILE = Path(__file__).parent.parent / "test-orchestrator-state.json"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def send_ntfy(message, title="Test Orchestrator", priority=3, tags=None):
|
|
31
|
-
"""Send NTFY notification."""
|
|
32
|
-
try:
|
|
33
|
-
headers = [
|
|
34
|
-
f"Title: {title}",
|
|
35
|
-
f"Priority: {priority}",
|
|
36
|
-
]
|
|
37
|
-
if tags:
|
|
38
|
-
headers.append(f"Tags: {','.join(tags)}")
|
|
39
|
-
|
|
40
|
-
header_args = []
|
|
41
|
-
for h in headers:
|
|
42
|
-
header_args.extend(["-H", h])
|
|
43
|
-
|
|
44
|
-
subprocess.run(
|
|
45
|
-
["curl", "-s"] + header_args + ["-d", message, f"https://ntfy.sh/{NTFY_TOPIC}"],
|
|
46
|
-
capture_output=True,
|
|
47
|
-
timeout=10
|
|
48
|
-
)
|
|
49
|
-
except Exception as e:
|
|
50
|
-
# Don't fail if notification fails
|
|
51
|
-
print(f"NTFY failed: {e}", file=sys.stderr)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def load_test_state():
|
|
55
|
-
"""Load test orchestrator state."""
|
|
56
|
-
if not STATE_FILE.exists():
|
|
57
|
-
return {
|
|
58
|
-
"turn_count": 0,
|
|
59
|
-
"started_at": datetime.now().isoformat(),
|
|
60
|
-
"commands_tested": {},
|
|
61
|
-
"current_command": None,
|
|
62
|
-
"current_phase": None,
|
|
63
|
-
"total_retries": 0,
|
|
64
|
-
"reground_history": []
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
return json.loads(STATE_FILE.read_text())
|
|
69
|
-
except Exception:
|
|
70
|
-
return {
|
|
71
|
-
"turn_count": 0,
|
|
72
|
-
"started_at": datetime.now().isoformat(),
|
|
73
|
-
"commands_tested": {},
|
|
74
|
-
"current_command": None,
|
|
75
|
-
"current_phase": None,
|
|
76
|
-
"total_retries": 0,
|
|
77
|
-
"reground_history": []
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def save_test_state(state):
|
|
82
|
-
"""Save test orchestrator state."""
|
|
83
|
-
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
-
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def format_progress_summary(state):
|
|
88
|
-
"""Format a progress summary for re-grounding."""
|
|
89
|
-
commands = state.get("commands_tested", {})
|
|
90
|
-
|
|
91
|
-
summary_lines = []
|
|
92
|
-
summary_lines.append("## Test Orchestrator Progress")
|
|
93
|
-
summary_lines.append("")
|
|
94
|
-
|
|
95
|
-
# Overall stats
|
|
96
|
-
turn = state.get("turn_count", 0)
|
|
97
|
-
started = state.get("started_at", "unknown")
|
|
98
|
-
summary_lines.append(f"**Turn:** {turn}")
|
|
99
|
-
summary_lines.append(f"**Started:** {started}")
|
|
100
|
-
summary_lines.append(f"**Total Retries:** {state.get('total_retries', 0)}")
|
|
101
|
-
summary_lines.append("")
|
|
102
|
-
|
|
103
|
-
# Command progress
|
|
104
|
-
summary_lines.append("**Command Progress:**")
|
|
105
|
-
all_commands = [
|
|
106
|
-
"/api-create",
|
|
107
|
-
"/hustle-ui-create",
|
|
108
|
-
"/hustle-ui-create-page",
|
|
109
|
-
"/hustle-combine",
|
|
110
|
-
"/hustle-build"
|
|
111
|
-
]
|
|
112
|
-
|
|
113
|
-
for cmd in all_commands:
|
|
114
|
-
cmd_state = commands.get(cmd, {})
|
|
115
|
-
status = cmd_state.get("status", "NOT STARTED")
|
|
116
|
-
|
|
117
|
-
icon = {
|
|
118
|
-
"PASSED": "✅",
|
|
119
|
-
"FAILED": "❌",
|
|
120
|
-
"IN PROGRESS": "🔄",
|
|
121
|
-
"NOT STARTED": "⏳"
|
|
122
|
-
}.get(status, "❓")
|
|
123
|
-
|
|
124
|
-
phases = cmd_state.get("phases_complete", 0)
|
|
125
|
-
retries = cmd_state.get("retries", 0)
|
|
126
|
-
|
|
127
|
-
if status == "PASSED":
|
|
128
|
-
summary_lines.append(f"- {icon} {cmd}: PASSED ({phases}/14 phases)")
|
|
129
|
-
elif status == "FAILED":
|
|
130
|
-
summary_lines.append(f"- {icon} {cmd}: FAILED at phase {phases}/14 (retry {retries}/∞)")
|
|
131
|
-
elif status == "IN PROGRESS":
|
|
132
|
-
summary_lines.append(f"- {icon} {cmd}: IN PROGRESS (phase {phases}/14)")
|
|
133
|
-
else:
|
|
134
|
-
summary_lines.append(f"- {icon} {cmd}: NOT STARTED")
|
|
135
|
-
|
|
136
|
-
summary_lines.append("")
|
|
137
|
-
|
|
138
|
-
# Current task
|
|
139
|
-
current_cmd = state.get("current_command")
|
|
140
|
-
current_phase = state.get("current_phase")
|
|
141
|
-
if current_cmd:
|
|
142
|
-
summary_lines.append(f"**Current Task:** {current_cmd} - Phase {current_phase}")
|
|
143
|
-
else:
|
|
144
|
-
summary_lines.append("**Current Task:** Initializing test harness")
|
|
145
|
-
|
|
146
|
-
summary_lines.append("")
|
|
147
|
-
summary_lines.append("## Primary Goal")
|
|
148
|
-
summary_lines.append("")
|
|
149
|
-
summary_lines.append("Test ALL 5 commands until they work perfectly:")
|
|
150
|
-
summary_lines.append("1. Run each command in isolated test directory")
|
|
151
|
-
summary_lines.append("2. Auto-answer questions via pending-answer.json")
|
|
152
|
-
summary_lines.append("3. Verify ALL 14 phases complete")
|
|
153
|
-
summary_lines.append("4. Verify ALL hooks fire correctly")
|
|
154
|
-
summary_lines.append("5. If tests fail: research, fix code, rebuild, retry")
|
|
155
|
-
summary_lines.append("6. NEVER STOP until all 5 commands pass")
|
|
156
|
-
summary_lines.append("")
|
|
157
|
-
summary_lines.append("## Key Resources")
|
|
158
|
-
summary_lines.append("")
|
|
159
|
-
summary_lines.append("- Test directory: ~/test-api-dev-tools-auto/")
|
|
160
|
-
summary_lines.append("- .env file: Copy from /Users/alfonso/Documents/GitHub/api-dev-tools/.env.example")
|
|
161
|
-
summary_lines.append("- WORKFLOW_CHECKLIST.md: Track results")
|
|
162
|
-
summary_lines.append("- NTFY topic: test_api_devtools_alerts")
|
|
163
|
-
summary_lines.append("")
|
|
164
|
-
summary_lines.append("## Failure Strategy")
|
|
165
|
-
summary_lines.append("")
|
|
166
|
-
summary_lines.append("If stuck after 5 retries:")
|
|
167
|
-
summary_lines.append("1. Use WebSearch to research the error")
|
|
168
|
-
summary_lines.append("2. Find similar issues and solutions")
|
|
169
|
-
summary_lines.append("3. Try new approaches")
|
|
170
|
-
summary_lines.append("4. Use git commits as savepoints")
|
|
171
|
-
summary_lines.append("5. NEVER give up - keep iterating")
|
|
172
|
-
|
|
173
|
-
return "\n".join(summary_lines)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def main():
|
|
177
|
-
# Load state
|
|
178
|
-
state = load_test_state()
|
|
179
|
-
|
|
180
|
-
# Increment turn count
|
|
181
|
-
turn_count = state.get("turn_count", 0) + 1
|
|
182
|
-
state["turn_count"] = turn_count
|
|
183
|
-
state["last_turn_timestamp"] = datetime.now().isoformat()
|
|
184
|
-
|
|
185
|
-
# Check if we should re-ground
|
|
186
|
-
should_reground = turn_count % REGROUND_INTERVAL == 0
|
|
187
|
-
|
|
188
|
-
if should_reground:
|
|
189
|
-
# Generate progress summary
|
|
190
|
-
summary = format_progress_summary(state)
|
|
191
|
-
|
|
192
|
-
# Send NTFY notification
|
|
193
|
-
commands = state.get("commands_tested", {})
|
|
194
|
-
passed = sum(1 for c in commands.values() if c.get("status") == "PASSED")
|
|
195
|
-
in_progress = sum(1 for c in commands.values() if c.get("status") == "IN PROGRESS")
|
|
196
|
-
failed = sum(1 for c in commands.values() if c.get("status") == "FAILED")
|
|
197
|
-
|
|
198
|
-
ntfy_msg = f"""Turn {turn_count} Update:
|
|
199
|
-
✅ Passed: {passed}/5
|
|
200
|
-
🔄 In Progress: {in_progress}/5
|
|
201
|
-
❌ Failed: {failed}/5
|
|
202
|
-
|
|
203
|
-
Current: {state.get('current_command', 'Initializing')}
|
|
204
|
-
Retries: {state.get('total_retries', 0)}
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
send_ntfy(
|
|
208
|
-
ntfy_msg,
|
|
209
|
-
title=f"🔄 Turn {turn_count} - Test Orchestrator",
|
|
210
|
-
priority=3,
|
|
211
|
-
tags=["robot", "test"]
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
# Add to reground history
|
|
215
|
-
reground_history = state.setdefault("reground_history", [])
|
|
216
|
-
reground_history.append({
|
|
217
|
-
"turn": turn_count,
|
|
218
|
-
"timestamp": datetime.now().isoformat(),
|
|
219
|
-
"current_command": state.get("current_command"),
|
|
220
|
-
"current_phase": state.get("current_phase"),
|
|
221
|
-
"passed": passed,
|
|
222
|
-
"failed": failed
|
|
223
|
-
})
|
|
224
|
-
# Keep only last 20 reground events
|
|
225
|
-
state["reground_history"] = reground_history[-20:]
|
|
226
|
-
|
|
227
|
-
# Save state
|
|
228
|
-
save_test_state(state)
|
|
229
|
-
|
|
230
|
-
# Output with context injection
|
|
231
|
-
output = {
|
|
232
|
-
"continue": True,
|
|
233
|
-
"hookSpecificOutput": {
|
|
234
|
-
"hookEventName": "PostToolUse",
|
|
235
|
-
"additionalContext": summary
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
print(json.dumps(output))
|
|
239
|
-
else:
|
|
240
|
-
# Just update turn count
|
|
241
|
-
save_test_state(state)
|
|
242
|
-
print(json.dumps({"continue": True}))
|
|
243
|
-
|
|
244
|
-
sys.exit(0)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if __name__ == "__main__":
|
|
248
|
-
main()
|