@hustle-together/api-dev-tools 3.12.3 → 3.12.16

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.
Files changed (96) hide show
  1. package/.claude/commands/hustle-build.md +259 -0
  2. package/.claude/commands/hustle-combine.md +1089 -0
  3. package/.claude/commands/hustle-ui-create-page.md +1078 -0
  4. package/.claude/commands/hustle-ui-create.md +1058 -0
  5. package/.claude/hooks/auto-answer.py +305 -0
  6. package/.claude/hooks/cache-research.py +337 -0
  7. package/.claude/hooks/check-api-routes.py +168 -0
  8. package/.claude/hooks/check-playwright-setup.py +103 -0
  9. package/.claude/hooks/check-storybook-setup.py +81 -0
  10. package/.claude/hooks/check-update.py +132 -0
  11. package/.claude/hooks/completion-promise-detector.py +293 -0
  12. package/.claude/hooks/context-capacity-warning.py +171 -0
  13. package/.claude/hooks/detect-interruption.py +165 -0
  14. package/.claude/hooks/docs-update-check.py +120 -0
  15. package/.claude/hooks/enforce-a11y-audit.py +202 -0
  16. package/.claude/hooks/enforce-brand-guide.py +241 -0
  17. package/.claude/hooks/enforce-component-type-confirm.py +97 -0
  18. package/.claude/hooks/enforce-dry-run.py +134 -0
  19. package/.claude/hooks/enforce-freshness.py +184 -0
  20. package/.claude/hooks/enforce-page-components.py +186 -0
  21. package/.claude/hooks/enforce-page-data-schema.py +155 -0
  22. package/.claude/hooks/enforce-questions-sourced.py +146 -0
  23. package/.claude/hooks/enforce-schema-from-interview.py +248 -0
  24. package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/.claude/hooks/enforce-ui-interview.py +130 -0
  26. package/.claude/hooks/generate-adr-options.py +282 -0
  27. package/.claude/hooks/generate-manifest-entry.py +1161 -0
  28. package/.claude/hooks/hook_utils.py +609 -0
  29. package/.claude/hooks/lib/__init__.py +1 -0
  30. package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  32. package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  33. package/.claude/hooks/lib/greptile.py +355 -0
  34. package/.claude/hooks/lib/ntfy.py +209 -0
  35. package/.claude/hooks/notify-input-needed.py +73 -0
  36. package/.claude/hooks/notify-phase-complete.py +90 -0
  37. package/.claude/hooks/ntfy-on-question.py +240 -0
  38. package/.claude/hooks/orchestrator-completion.py +313 -0
  39. package/.claude/hooks/orchestrator-handoff.py +267 -0
  40. package/.claude/hooks/orchestrator-session-startup.py +146 -0
  41. package/.claude/hooks/parallel-orchestrator.py +451 -0
  42. package/.claude/hooks/project-document-prompt.py +302 -0
  43. package/.claude/hooks/remote-question-proxy.py +284 -0
  44. package/.claude/hooks/remote-question-server.py +1224 -0
  45. package/.claude/hooks/run-code-review.py +393 -0
  46. package/.claude/hooks/run-visual-qa.py +338 -0
  47. package/.claude/hooks/session-logger.py +323 -0
  48. package/.claude/hooks/test-orchestrator-reground.py +248 -0
  49. package/.claude/hooks/track-scope-coverage.py +220 -0
  50. package/.claude/hooks/track-token-usage.py +121 -0
  51. package/.claude/hooks/update-adr-decision.py +236 -0
  52. package/.claude/hooks/update-api-showcase.py +161 -0
  53. package/.claude/hooks/update-registry.py +352 -0
  54. package/.claude/hooks/update-testing-checklist.py +195 -0
  55. package/.claude/hooks/update-ui-showcase.py +224 -0
  56. package/.claude/settings.local.json +7 -1
  57. package/.claude/test-auto-answer-bot.py +183 -0
  58. package/.claude/test-completion-detector.py +263 -0
  59. package/.claude/test-orchestrator-state.json +20 -0
  60. package/.claude/test-orchestrator.sh +271 -0
  61. package/.skills/api-create/SKILL.md +88 -3
  62. package/.skills/docs-sync/SKILL.md +260 -0
  63. package/.skills/hustle-build/SKILL.md +459 -0
  64. package/.skills/hustle-build-review/SKILL.md +518 -0
  65. package/CHANGELOG.md +87 -0
  66. package/README.md +86 -9
  67. package/bin/cli.js +1302 -88
  68. package/commands/hustle-api-create.md +22 -0
  69. package/commands/hustle-combine.md +81 -2
  70. package/commands/hustle-ui-create-page.md +84 -2
  71. package/commands/hustle-ui-create.md +82 -2
  72. package/hooks/auto-answer.py +228 -0
  73. package/hooks/check-update.py +132 -0
  74. package/hooks/ntfy-on-question.py +227 -0
  75. package/hooks/orchestrator-completion.py +313 -0
  76. package/hooks/orchestrator-handoff.py +189 -0
  77. package/hooks/orchestrator-session-startup.py +146 -0
  78. package/hooks/periodic-reground.py +230 -67
  79. package/hooks/update-api-showcase.py +13 -1
  80. package/hooks/update-ui-showcase.py +13 -1
  81. package/package.json +7 -3
  82. package/scripts/extract-schema-docs.cjs +322 -0
  83. package/templates/CLAUDE-SECTION.md +89 -64
  84. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  85. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  86. package/templates/api-showcase/_components/APITester.tsx +367 -58
  87. package/templates/docs/page.tsx +230 -0
  88. package/templates/hustle-build-defaults.json +84 -0
  89. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  90. package/templates/playwright-report/page.tsx +258 -0
  91. package/templates/settings.json +88 -7
  92. package/templates/test-results/page.tsx +237 -0
  93. package/templates/typedoc.json +19 -0
  94. package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
  95. package/templates/ui-showcase/page.tsx +1 -1
  96. package/.claude/api-dev-state.json +0 -466
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for Write/Edit
4
+ Purpose: Auto-create UI Showcase page when first component/page is created
5
+ and auto-populate showcase data from registry.
6
+
7
+ This hook monitors for new component or page registrations. When the first
8
+ UI element is added to registry.json, it creates the UI Showcase page
9
+ at src/app/ui-showcase/ if it doesn't exist.
10
+
11
+ Also generates src/app/ui-showcase/data.json from registry for auto-population.
12
+
13
+ Version: 3.10.0
14
+
15
+ Returns:
16
+ - {"continue": true} - Always continues
17
+ - May include "notify" about showcase creation
18
+ """
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+ import shutil
23
+ from datetime import datetime
24
+
25
+ # State and registry files in .claude/ directory
26
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
27
+ REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
28
+
29
+
30
+ def generate_showcase_data(registry, cwd):
31
+ """Generate showcase data file from registry for auto-population.
32
+
33
+ Creates src/app/ui-showcase/data.json with component/page listings.
34
+ """
35
+ components = registry.get("components", {})
36
+ pages = registry.get("pages", {})
37
+
38
+ showcase_data = {
39
+ "version": "3.10.0",
40
+ "generated_at": datetime.now().isoformat(),
41
+ "components": [],
42
+ "pages": []
43
+ }
44
+
45
+ # Process components
46
+ for name, comp in components.items():
47
+ showcase_data["components"].append({
48
+ "id": name,
49
+ "name": comp.get("name", name),
50
+ "description": comp.get("description", ""),
51
+ "type": comp.get("type", "atom"),
52
+ "path": comp.get("path", f"src/components/{name}/{name}.tsx"),
53
+ "storybook_url": comp.get("storybook_url", f"/?path=/story/{name.lower()}--default"),
54
+ "variants": comp.get("variants", []),
55
+ "props": list(comp.get("props", {}).keys()) if isinstance(comp.get("props"), dict) else [],
56
+ "created_at": comp.get("created_at", ""),
57
+ "status": comp.get("status", "ready")
58
+ })
59
+
60
+ # Process pages
61
+ for name, page in pages.items():
62
+ showcase_data["pages"].append({
63
+ "id": name,
64
+ "name": page.get("name", name),
65
+ "description": page.get("description", ""),
66
+ "route": page.get("route", f"/{name}"),
67
+ "page_type": page.get("page_type", "landing"),
68
+ "path": page.get("path", f"src/app/{name}/page.tsx"),
69
+ "requires_auth": page.get("requires_auth", False),
70
+ "data_sources": page.get("data_sources", []),
71
+ "created_at": page.get("created_at", ""),
72
+ "status": page.get("status", "ready")
73
+ })
74
+
75
+ # Write data file
76
+ data_file = cwd / "src" / "app" / "ui-showcase" / "data.json"
77
+ data_file.parent.mkdir(parents=True, exist_ok=True)
78
+ data_file.write_text(json.dumps(showcase_data, indent=2))
79
+
80
+ return str(data_file.relative_to(cwd))
81
+
82
+
83
+ def copy_showcase_templates(cwd):
84
+ """Copy UI showcase templates to src/app/ui-showcase/."""
85
+ # Source templates (installed by CLI)
86
+ templates_dir = Path(__file__).parent.parent / "templates" / "ui-showcase"
87
+ shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
88
+
89
+ # Destination
90
+ showcase_dir = cwd / "src" / "app" / "ui-showcase"
91
+ shared_dir = cwd / "src" / "app" / "shared"
92
+
93
+ # Create directories if needed
94
+ showcase_dir.mkdir(parents=True, exist_ok=True)
95
+ shared_dir.mkdir(parents=True, exist_ok=True)
96
+
97
+ # Copy template files
98
+ templates_to_copy = [
99
+ ("page.tsx", "page.tsx"),
100
+ ("UIShowcase.tsx", "_components/UIShowcase.tsx"),
101
+ ("PreviewCard.tsx", "_components/PreviewCard.tsx"),
102
+ ("PreviewModal.tsx", "_components/PreviewModal.tsx"),
103
+ ]
104
+
105
+ created_files = []
106
+ for src_name, dest_name in templates_to_copy:
107
+ src_path = templates_dir / src_name
108
+ dest_path = showcase_dir / dest_name
109
+
110
+ # Create subdirectories if needed
111
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
112
+
113
+ if src_path.exists() and not dest_path.exists():
114
+ shutil.copy2(src_path, dest_path)
115
+ created_files.append(str(dest_path.relative_to(cwd)))
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
+
126
+ return created_files
127
+
128
+
129
+ def main():
130
+ # Read hook input from stdin
131
+ try:
132
+ input_data = json.load(sys.stdin)
133
+ except json.JSONDecodeError:
134
+ print(json.dumps({"continue": True}))
135
+ sys.exit(0)
136
+
137
+ tool_name = input_data.get("tool_name", "")
138
+
139
+ # Only process Write/Edit operations
140
+ if tool_name not in ["Write", "Edit"]:
141
+ print(json.dumps({"continue": True}))
142
+ sys.exit(0)
143
+
144
+ # Check if state file exists
145
+ if not STATE_FILE.exists():
146
+ print(json.dumps({"continue": True}))
147
+ sys.exit(0)
148
+
149
+ # Load state
150
+ try:
151
+ state = json.loads(STATE_FILE.read_text())
152
+ except json.JSONDecodeError:
153
+ print(json.dumps({"continue": True}))
154
+ sys.exit(0)
155
+
156
+ workflow = state.get("workflow", "")
157
+
158
+ # Only apply for UI workflows
159
+ if workflow not in ["ui-create-component", "ui-create-page"]:
160
+ print(json.dumps({"continue": True}))
161
+ sys.exit(0)
162
+
163
+ # Check if completion phase is complete
164
+ active_element = state.get("active_element", "")
165
+ elements = state.get("elements", {})
166
+
167
+ if active_element and active_element in elements:
168
+ phases = elements[active_element].get("phases", {})
169
+ else:
170
+ phases = state.get("phases", {})
171
+
172
+ completion = phases.get("completion", {})
173
+ if completion.get("status") != "complete":
174
+ print(json.dumps({"continue": True}))
175
+ sys.exit(0)
176
+
177
+ # Check if showcase already exists
178
+ cwd = Path.cwd()
179
+ showcase_page = cwd / "src" / "app" / "ui-showcase" / "page.tsx"
180
+
181
+ if showcase_page.exists():
182
+ print(json.dumps({"continue": True}))
183
+ sys.exit(0)
184
+
185
+ # Check if we have components or pages in registry
186
+ if not REGISTRY_FILE.exists():
187
+ print(json.dumps({"continue": True}))
188
+ sys.exit(0)
189
+
190
+ try:
191
+ registry = json.loads(REGISTRY_FILE.read_text())
192
+ except json.JSONDecodeError:
193
+ print(json.dumps({"continue": True}))
194
+ sys.exit(0)
195
+
196
+ components = registry.get("components", {})
197
+ pages = registry.get("pages", {})
198
+
199
+ # Create showcase if we have at least one component or page
200
+ if components or pages:
201
+ created_files = copy_showcase_templates(cwd)
202
+
203
+ # Always update data.json from registry
204
+ data_file = generate_showcase_data(registry, cwd)
205
+
206
+ if created_files:
207
+ print(json.dumps({
208
+ "continue": True,
209
+ "notify": f"Created UI Showcase at /ui-showcase ({len(created_files)} files) + data.json"
210
+ }))
211
+ else:
212
+ # Just updated data.json
213
+ print(json.dumps({
214
+ "continue": True,
215
+ "notify": f"Updated UI Showcase data ({len(components)} components, {len(pages)} pages)"
216
+ }))
217
+ else:
218
+ print(json.dumps({"continue": True}))
219
+
220
+ sys.exit(0)
221
+
222
+
223
+ if __name__ == "__main__":
224
+ main()
@@ -6,7 +6,13 @@
6
6
  "Bash(npm version:*)",
7
7
  "Bash(git add:*)",
8
8
  "Bash(git commit:*)",
9
- "Bash(tree:*)"
9
+ "Bash(tree:*)",
10
+ "Bash(python3:*)",
11
+ "Bash(ls:*)",
12
+ "Bash(git checkout:*)",
13
+ "Bash(git pull:*)",
14
+ "Bash(git stash:*)",
15
+ "Bash(npm view:*)"
10
16
  ]
11
17
  }
12
18
  }
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-Answer Bot for Test Orchestration
4
+
5
+ Monitors .claude/current-question.json in test directory and automatically
6
+ answers questions by writing to .claude/pending-answer.json.
7
+
8
+ This enables autonomous testing by auto-selecting "comprehensive" options
9
+ without manual intervention.
10
+
11
+ Usage:
12
+ python test-auto-answer-bot.py <test_directory>
13
+
14
+ Example:
15
+ python test-auto-answer-bot.py ~/test-api-dev-tools-auto
16
+
17
+ Version: 1.0.0
18
+ """
19
+
20
+ import json
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+ from datetime import datetime
25
+
26
+ # Configuration
27
+ POLL_INTERVAL = 2 # seconds
28
+ MAX_WAIT_TIME = 3600 # 1 hour max
29
+ AFFIRMATIVE_KEYWORDS = [
30
+ "comprehensive", "all", "yes", "proceed", "continue",
31
+ "recommended", "auto", "defaults", "use auto", "use defaults"
32
+ ]
33
+
34
+
35
+ def find_best_answer(question_data):
36
+ """
37
+ Find the best answer for a question.
38
+
39
+ Strategy:
40
+ 1. Look for options with affirmative keywords (comprehensive, all, yes, etc.)
41
+ 2. If multiple match, prefer first one
42
+ 3. If none match, select first option
43
+
44
+ Args:
45
+ question_data: Parsed question data from current-question.json
46
+
47
+ Returns:
48
+ dict: Answer data with selected option
49
+ """
50
+ questions = question_data.get("questions", [])
51
+
52
+ if not questions:
53
+ return None
54
+
55
+ # For now, handle first question (can extend to handle multiple)
56
+ first_q = questions[0]
57
+ options = first_q.get("options", [])
58
+
59
+ if not options:
60
+ return None
61
+
62
+ # Find option with affirmative keyword
63
+ selected_index = 0
64
+ selected_label = None
65
+
66
+ for i, opt in enumerate(options):
67
+ label = opt.get("label", "") if isinstance(opt, dict) else str(opt)
68
+ label_lower = label.lower()
69
+
70
+ # Check for affirmative keywords
71
+ for keyword in AFFIRMATIVE_KEYWORDS:
72
+ if keyword in label_lower:
73
+ selected_index = i
74
+ selected_label = label
75
+ break
76
+
77
+ if selected_label:
78
+ break
79
+
80
+ # If no affirmative found, use first option
81
+ if not selected_label:
82
+ first_opt = options[0]
83
+ selected_label = first_opt.get("label", "") if isinstance(first_opt, dict) else str(first_opt)
84
+
85
+ return {
86
+ "question_id": first_q.get("id", "question"),
87
+ "question": first_q.get("question", ""),
88
+ "header": first_q.get("header", "Question"),
89
+ "answer": selected_label,
90
+ "option_index": selected_index,
91
+ "phase": question_data.get("phase", "unknown"),
92
+ "status": "submitted",
93
+ "submitted_at": datetime.now().isoformat(),
94
+ "auto_answered": True,
95
+ "answers": {
96
+ first_q.get("header", "Question"): selected_label
97
+ }
98
+ }
99
+
100
+
101
+ def watch_and_answer(test_dir):
102
+ """
103
+ Watch for questions and auto-answer them.
104
+
105
+ Args:
106
+ test_dir: Path to test directory
107
+ """
108
+ test_path = Path(test_dir).expanduser()
109
+ question_file = test_path / ".claude" / "current-question.json"
110
+ answer_file = test_path / ".claude" / "pending-answer.json"
111
+
112
+ print(f"Auto-Answer Bot started")
113
+ print(f"Monitoring: {question_file}")
114
+ print(f"Writing answers to: {answer_file}")
115
+ print(f"Poll interval: {POLL_INTERVAL}s")
116
+ print()
117
+
118
+ start_time = time.time()
119
+ answered_count = 0
120
+
121
+ while True:
122
+ # Check timeout
123
+ if time.time() - start_time > MAX_WAIT_TIME:
124
+ print(f"Max wait time ({MAX_WAIT_TIME}s) exceeded. Exiting.")
125
+ break
126
+
127
+ # Check for question
128
+ if question_file.exists():
129
+ try:
130
+ # Read question
131
+ question_data = json.loads(question_file.read_text())
132
+
133
+ # Skip if already answered recently
134
+ if answer_file.exists():
135
+ answer_data = json.loads(answer_file.read_text())
136
+ answer_time = answer_data.get("submitted_at", "")
137
+ question_time = question_data.get("created_at", "")
138
+
139
+ # If answer is newer than question, skip
140
+ if answer_time > question_time:
141
+ time.sleep(POLL_INTERVAL)
142
+ continue
143
+
144
+ # Find best answer
145
+ answer_data = find_best_answer(question_data)
146
+
147
+ if answer_data:
148
+ # Write answer
149
+ answer_file.parent.mkdir(parents=True, exist_ok=True)
150
+ answer_file.write_text(json.dumps(answer_data, indent=2))
151
+
152
+ answered_count += 1
153
+ print(f"[{datetime.now().strftime('%H:%M:%S')}] Answered question #{answered_count}")
154
+ print(f" Question: {answer_data.get('question', '')[:80]}...")
155
+ print(f" Answer: {answer_data.get('answer', '')}")
156
+ print(f" Phase: {answer_data.get('phase', 'unknown')}")
157
+ print()
158
+
159
+ # Clear the question file
160
+ question_file.unlink()
161
+
162
+ except Exception as e:
163
+ print(f"Error processing question: {e}")
164
+
165
+ time.sleep(POLL_INTERVAL)
166
+
167
+ print(f"Bot finished. Answered {answered_count} questions.")
168
+
169
+
170
+ def main():
171
+ if len(sys.argv) < 2:
172
+ print("Usage: python test-auto-answer-bot.py <test_directory>")
173
+ print()
174
+ print("Example:")
175
+ print(" python test-auto-answer-bot.py ~/test-api-dev-tools-auto")
176
+ sys.exit(1)
177
+
178
+ test_dir = sys.argv[1]
179
+ watch_and_answer(test_dir)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test Completion Detector
4
+
5
+ Programmatically verifies that a workflow completed successfully by checking:
6
+ 1. All 14 phases marked as "complete" in api-dev-state.json
7
+ 2. All expected artifacts exist (route.ts, schema.ts, tests, README)
8
+ 3. All hooks logged in workflow-logs/
9
+ 4. Tests pass (if test files exist)
10
+
11
+ Usage:
12
+ python test-completion-detector.py <test_directory> <command_type>
13
+
14
+ Example:
15
+ python test-completion-detector.py ~/test-api-dev-tools-auto api-create
16
+
17
+ Returns:
18
+ Exit code 0 if complete, 1 if incomplete, 2 if error
19
+ Prints JSON with detailed status
20
+
21
+ Version: 1.0.0
22
+ """
23
+
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+ from datetime import datetime
28
+
29
+
30
+ def load_state(test_dir):
31
+ """Load api-dev-state.json."""
32
+ state_file = Path(test_dir) / ".claude" / "api-dev-state.json"
33
+
34
+ if not state_file.exists():
35
+ return None
36
+
37
+ try:
38
+ return json.loads(state_file.read_text())
39
+ except Exception as e:
40
+ print(f"Error loading state: {e}", file=sys.stderr)
41
+ return None
42
+
43
+
44
+ def check_phases_complete(state):
45
+ """Check if all 14 phases are marked complete."""
46
+ required_phases = [
47
+ "disambiguation",
48
+ "scope",
49
+ "research_initial",
50
+ "interview",
51
+ "research_deep",
52
+ "schema_creation",
53
+ "environment_check",
54
+ "tdd_red",
55
+ "tdd_green",
56
+ "verify",
57
+ "code_review",
58
+ "tdd_refactor",
59
+ "documentation",
60
+ "completion"
61
+ ]
62
+
63
+ phases = state.get("phases", {})
64
+ incomplete_phases = []
65
+
66
+ for phase_name in required_phases:
67
+ phase_data = phases.get(phase_name, {})
68
+ status = phase_data.get("status", "not_started")
69
+
70
+ if status != "complete":
71
+ # Special case: research_deep can be skipped if comprehensive docs found
72
+ if phase_name == "research_deep" and status == "skipped":
73
+ continue
74
+
75
+ # Special case: code_review can be partial if no API key
76
+ if phase_name == "code_review" and status == "partial":
77
+ continue
78
+
79
+ incomplete_phases.append({
80
+ "phase": phase_name,
81
+ "status": status,
82
+ "reason": phase_data.get("incomplete_reason", "Unknown")
83
+ })
84
+
85
+ return {
86
+ "complete": len(incomplete_phases) == 0,
87
+ "total_phases": len(required_phases),
88
+ "complete_phases": len(required_phases) - len(incomplete_phases),
89
+ "incomplete": incomplete_phases
90
+ }
91
+
92
+
93
+ def check_artifacts_exist(test_dir, command_type, state):
94
+ """Check if all expected artifacts exist."""
95
+ endpoint = state.get("endpoint", "unknown")
96
+ missing_artifacts = []
97
+ found_artifacts = []
98
+
99
+ # Define expected artifacts by command type
100
+ if command_type == "api-create":
101
+ expected = {
102
+ f"src/app/api/v2/{endpoint}/route.ts": "API route handler",
103
+ f"src/app/api/v2/{endpoint}/schema.ts": "Zod schema definitions",
104
+ f"src/app/api/v2/{endpoint}/__tests__/{endpoint}.api.test.ts": "API tests",
105
+ f"src/app/api/v2/{endpoint}/README.md": "API documentation"
106
+ }
107
+ elif command_type == "hustle-ui-create":
108
+ component_name = state.get("component_name", endpoint)
109
+ expected = {
110
+ f"src/components/{component_name}/{component_name}.tsx": "Component file",
111
+ f"src/components/{component_name}/{component_name}.test.tsx": "Component tests",
112
+ f"src/components/{component_name}/{component_name}.stories.tsx": "Storybook story"
113
+ }
114
+ elif command_type == "hustle-ui-create-page":
115
+ page_route = state.get("page_route", endpoint)
116
+ expected = {
117
+ f"src/app/{page_route}/page.tsx": "Page file",
118
+ f"e2e/{page_route}.spec.ts": "E2E tests"
119
+ }
120
+ elif command_type == "hustle-combine":
121
+ expected = {
122
+ f"src/app/api/v2/{endpoint}/route.ts": "Combined API route",
123
+ f"src/app/api/v2/{endpoint}/schema.ts": "Combined schema",
124
+ f"src/app/api/v2/{endpoint}/__tests__/{endpoint}.api.test.ts": "Integration tests"
125
+ }
126
+ elif command_type == "hustle-build":
127
+ # Build creates multiple artifacts - check decomposition
128
+ expected = {} # Will be checked differently
129
+ else:
130
+ return {
131
+ "complete": False,
132
+ "error": f"Unknown command type: {command_type}"
133
+ }
134
+
135
+ # Check each artifact
136
+ test_path = Path(test_dir)
137
+ for artifact_path, description in expected.items():
138
+ full_path = test_path / artifact_path
139
+ if full_path.exists():
140
+ found_artifacts.append({"path": artifact_path, "description": description})
141
+ else:
142
+ missing_artifacts.append({"path": artifact_path, "description": description})
143
+
144
+ return {
145
+ "complete": len(missing_artifacts) == 0,
146
+ "found": found_artifacts,
147
+ "missing": missing_artifacts
148
+ }
149
+
150
+
151
+ def check_registry_updated(test_dir, command_type):
152
+ """Check if registry.json was updated."""
153
+ registry_file = Path(test_dir) / ".claude" / "registry.json"
154
+
155
+ if not registry_file.exists():
156
+ return {"complete": False, "reason": "Registry file missing"}
157
+
158
+ try:
159
+ registry = json.loads(registry_file.read_text())
160
+
161
+ # Check if there are entries
162
+ if command_type == "api-create":
163
+ apis = registry.get("apis", [])
164
+ if len(apis) == 0:
165
+ return {"complete": False, "reason": "No APIs in registry"}
166
+ elif command_type == "hustle-ui-create":
167
+ components = registry.get("components", [])
168
+ if len(components) == 0:
169
+ return {"complete": False, "reason": "No components in registry"}
170
+ elif command_type == "hustle-ui-create-page":
171
+ pages = registry.get("pages", [])
172
+ if len(pages) == 0:
173
+ return {"complete": False, "reason": "No pages in registry"}
174
+
175
+ return {"complete": True}
176
+
177
+ except Exception as e:
178
+ return {"complete": False, "reason": f"Error reading registry: {e}"}
179
+
180
+
181
+ def check_workflow_logs(test_dir):
182
+ """Check if workflow events were logged."""
183
+ logs_dir = Path(test_dir) / ".claude" / "workflow-logs"
184
+
185
+ if not logs_dir.exists():
186
+ return {"complete": False, "reason": "Workflow logs directory missing"}
187
+
188
+ log_files = list(logs_dir.glob("*.json"))
189
+
190
+ if len(log_files) == 0:
191
+ return {"complete": False, "reason": "No workflow log files found"}
192
+
193
+ return {
194
+ "complete": True,
195
+ "log_files": [str(f.name) for f in log_files]
196
+ }
197
+
198
+
199
+ def main():
200
+ if len(sys.argv) < 3:
201
+ print(json.dumps({
202
+ "error": "Usage: python test-completion-detector.py <test_directory> <command_type>"
203
+ }))
204
+ sys.exit(2)
205
+
206
+ test_dir = Path(sys.argv[1]).expanduser()
207
+ command_type = sys.argv[2]
208
+
209
+ if not test_dir.exists():
210
+ print(json.dumps({
211
+ "error": f"Test directory does not exist: {test_dir}"
212
+ }))
213
+ sys.exit(2)
214
+
215
+ # Load state
216
+ state = load_state(test_dir)
217
+
218
+ if not state:
219
+ print(json.dumps({
220
+ "complete": False,
221
+ "error": "Could not load api-dev-state.json"
222
+ }))
223
+ sys.exit(1)
224
+
225
+ # Run all checks
226
+ phases_check = check_phases_complete(state)
227
+ artifacts_check = check_artifacts_exist(test_dir, command_type, state)
228
+ registry_check = check_registry_updated(test_dir, command_type)
229
+ logs_check = check_workflow_logs(test_dir)
230
+
231
+ # Overall result
232
+ all_complete = (
233
+ phases_check["complete"] and
234
+ artifacts_check["complete"] and
235
+ registry_check["complete"] and
236
+ logs_check["complete"]
237
+ )
238
+
239
+ result = {
240
+ "complete": all_complete,
241
+ "timestamp": datetime.now().isoformat(),
242
+ "test_directory": str(test_dir),
243
+ "command_type": command_type,
244
+ "endpoint": state.get("endpoint", "unknown"),
245
+ "checks": {
246
+ "phases": phases_check,
247
+ "artifacts": artifacts_check,
248
+ "registry": registry_check,
249
+ "logs": logs_check
250
+ }
251
+ }
252
+
253
+ print(json.dumps(result, indent=2))
254
+
255
+ # Exit code based on result
256
+ if all_complete:
257
+ sys.exit(0)
258
+ else:
259
+ sys.exit(1)
260
+
261
+
262
+ if __name__ == "__main__":
263
+ main()