@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.
Files changed (159) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +7 -463
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. package/templates/ui-showcase/page.tsx +1 -1
@@ -0,0 +1,1224 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Remote Question Server with Build Dashboard
4
+
5
+ A comprehensive HTTP server that displays:
6
+ - Build progress and phase status
7
+ - Current question interface
8
+ - Build queue with dependencies
9
+ - Recent activity log
10
+
11
+ Usage:
12
+ python remote-question-server.py [port]
13
+
14
+ Access:
15
+ - Local: http://localhost:8765
16
+ - Same network (phone/tablet): http://<your-computer-ip>:8765
17
+
18
+ Environment:
19
+ REMOTE_QUESTIONS_PORT - Port to run on (default: 8765)
20
+ CLAUDE_PROJECT_DIR - Project directory to monitor (default: .)
21
+
22
+ Version: 4.6.1
23
+ """
24
+
25
+ import json
26
+ import os
27
+ import sys
28
+ from http.server import HTTPServer, BaseHTTPRequestHandler
29
+ from pathlib import Path
30
+ from datetime import datetime
31
+ from urllib.parse import parse_qs
32
+ import threading
33
+ import time
34
+
35
+ DEFAULT_PORT = 8765
36
+ QUESTION_FILE = ".claude/current-question.json"
37
+ ANSWER_FILE = ".claude/pending-answer.json"
38
+ STATE_FILE = ".claude/hustle-build-state.json"
39
+ ACTIVITY_LOG_FILE = ".claude/workflow-logs/activity.json"
40
+
41
+ # Full dashboard HTML template
42
+ HTML_TEMPLATE = '''<!DOCTYPE html>
43
+ <html lang="en">
44
+ <head>
45
+ <meta charset="UTF-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <title>Hustle Build Dashboard</title>
48
+ <style>
49
+ * { box-sizing: border-box; margin: 0; padding: 0; }
50
+ body {
51
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
52
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
53
+ min-height: 100vh;
54
+ color: #e0e0e0;
55
+ padding: 15px;
56
+ }
57
+ .container {
58
+ max-width: 900px;
59
+ margin: 0 auto;
60
+ }
61
+
62
+ /* Header */
63
+ .header {
64
+ background: rgba(255,255,255,0.05);
65
+ border-radius: 12px;
66
+ padding: 20px;
67
+ margin-bottom: 15px;
68
+ border: 1px solid rgba(255,255,255,0.1);
69
+ }
70
+ .header-top {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: flex-start;
74
+ flex-wrap: wrap;
75
+ gap: 10px;
76
+ }
77
+ .header h1 {
78
+ color: #00d4ff;
79
+ font-size: 1.4rem;
80
+ margin-bottom: 5px;
81
+ }
82
+ .header .subtitle {
83
+ color: #888;
84
+ font-size: 0.85rem;
85
+ }
86
+ .build-status {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ padding: 8px 16px;
91
+ border-radius: 20px;
92
+ font-size: 0.85rem;
93
+ font-weight: 600;
94
+ }
95
+ .build-status.in_progress {
96
+ background: rgba(0,212,255,0.2);
97
+ color: #00d4ff;
98
+ }
99
+ .build-status.complete {
100
+ background: rgba(0,255,128,0.2);
101
+ color: #00ff80;
102
+ }
103
+ .build-status.pending {
104
+ background: rgba(255,204,0,0.2);
105
+ color: #ffcc00;
106
+ }
107
+ .build-status.no-build {
108
+ background: rgba(255,255,255,0.1);
109
+ color: #888;
110
+ }
111
+ .build-meta {
112
+ display: flex;
113
+ gap: 20px;
114
+ margin-top: 15px;
115
+ flex-wrap: wrap;
116
+ }
117
+ .build-meta-item {
118
+ font-size: 0.8rem;
119
+ }
120
+ .build-meta-item .label {
121
+ color: #666;
122
+ }
123
+ .build-meta-item .value {
124
+ color: #aaa;
125
+ }
126
+
127
+ /* Phase Progress */
128
+ .card {
129
+ background: rgba(255,255,255,0.05);
130
+ border-radius: 12px;
131
+ padding: 20px;
132
+ margin-bottom: 15px;
133
+ border: 1px solid rgba(255,255,255,0.1);
134
+ }
135
+ .card-title {
136
+ color: #888;
137
+ font-size: 0.8rem;
138
+ text-transform: uppercase;
139
+ letter-spacing: 1px;
140
+ margin-bottom: 15px;
141
+ }
142
+ .phases {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 6px;
146
+ }
147
+ .phase-item {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 10px;
151
+ padding: 8px 12px;
152
+ background: rgba(255,255,255,0.03);
153
+ border-radius: 6px;
154
+ font-size: 0.9rem;
155
+ }
156
+ .phase-icon {
157
+ width: 20px;
158
+ height: 20px;
159
+ border-radius: 50%;
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ font-size: 0.7rem;
164
+ flex-shrink: 0;
165
+ }
166
+ .phase-icon.completed {
167
+ background: #00ff80;
168
+ color: #1a1a2e;
169
+ }
170
+ .phase-icon.in_progress {
171
+ background: #00d4ff;
172
+ color: #1a1a2e;
173
+ animation: pulse 1.5s infinite;
174
+ }
175
+ .phase-icon.pending {
176
+ background: rgba(255,255,255,0.1);
177
+ color: #666;
178
+ }
179
+ @keyframes pulse {
180
+ 0%, 100% { opacity: 1; transform: scale(1); }
181
+ 50% { opacity: 0.7; transform: scale(0.95); }
182
+ }
183
+ .phase-name {
184
+ flex: 1;
185
+ }
186
+ .phase-name.active {
187
+ color: #00d4ff;
188
+ font-weight: 600;
189
+ }
190
+ .phase-name.completed {
191
+ color: #00ff80;
192
+ }
193
+ .phase-name.pending {
194
+ color: #666;
195
+ }
196
+
197
+ /* Question Card */
198
+ .question-card {
199
+ background: linear-gradient(135deg, rgba(0,212,255,0.1) 0%, rgba(0,168,204,0.05) 100%);
200
+ border-color: rgba(0,212,255,0.3);
201
+ }
202
+ .phase-badge {
203
+ background: #00d4ff;
204
+ color: #1a1a2e;
205
+ padding: 4px 12px;
206
+ border-radius: 20px;
207
+ font-size: 0.75rem;
208
+ font-weight: 600;
209
+ display: inline-block;
210
+ margin-bottom: 12px;
211
+ }
212
+ .question-text {
213
+ font-size: 1.05rem;
214
+ line-height: 1.5;
215
+ margin-bottom: 15px;
216
+ }
217
+ .options {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 8px;
221
+ }
222
+ .option {
223
+ background: rgba(255,255,255,0.08);
224
+ border: 2px solid transparent;
225
+ border-radius: 8px;
226
+ padding: 12px 15px;
227
+ cursor: pointer;
228
+ transition: all 0.2s;
229
+ }
230
+ .option:hover {
231
+ background: rgba(0,212,255,0.1);
232
+ border-color: #00d4ff;
233
+ }
234
+ .option.selected {
235
+ background: rgba(0,212,255,0.2);
236
+ border-color: #00d4ff;
237
+ }
238
+ .option-label {
239
+ font-weight: 600;
240
+ font-size: 0.95rem;
241
+ }
242
+ .option-description {
243
+ font-size: 0.85rem;
244
+ color: #aaa;
245
+ margin-top: 3px;
246
+ }
247
+ .custom-input {
248
+ display: none;
249
+ margin-top: 12px;
250
+ }
251
+ .custom-input.visible {
252
+ display: block;
253
+ }
254
+ .custom-input textarea {
255
+ width: 100%;
256
+ padding: 12px;
257
+ background: rgba(255,255,255,0.08);
258
+ border: 1px solid rgba(255,255,255,0.2);
259
+ border-radius: 8px;
260
+ color: #e0e0e0;
261
+ font-family: inherit;
262
+ font-size: 0.95rem;
263
+ resize: vertical;
264
+ min-height: 70px;
265
+ }
266
+ .custom-input textarea:focus {
267
+ outline: none;
268
+ border-color: #00d4ff;
269
+ }
270
+ .submit-btn {
271
+ width: 100%;
272
+ background: linear-gradient(135deg, #00d4ff 0%, #00a8cc 100%);
273
+ color: #1a1a2e;
274
+ border: none;
275
+ border-radius: 8px;
276
+ padding: 14px;
277
+ font-size: 0.95rem;
278
+ font-weight: 600;
279
+ cursor: pointer;
280
+ margin-top: 15px;
281
+ transition: transform 0.2s, box-shadow 0.2s;
282
+ }
283
+ .submit-btn:hover {
284
+ transform: translateY(-2px);
285
+ box-shadow: 0 5px 20px rgba(0,212,255,0.3);
286
+ }
287
+ .submit-btn:disabled {
288
+ background: #444;
289
+ cursor: not-allowed;
290
+ transform: none;
291
+ box-shadow: none;
292
+ }
293
+ .no-question {
294
+ text-align: center;
295
+ padding: 30px 20px;
296
+ color: #666;
297
+ }
298
+ .no-question .icon {
299
+ font-size: 2.5rem;
300
+ margin-bottom: 10px;
301
+ }
302
+ .success-msg {
303
+ background: rgba(0,255,128,0.1);
304
+ color: #00ff80;
305
+ border-radius: 8px;
306
+ padding: 15px;
307
+ text-align: center;
308
+ }
309
+
310
+ /* Build Queue */
311
+ .queue-table {
312
+ width: 100%;
313
+ border-collapse: collapse;
314
+ font-size: 0.85rem;
315
+ }
316
+ .queue-table th {
317
+ text-align: left;
318
+ padding: 10px;
319
+ color: #666;
320
+ border-bottom: 1px solid rgba(255,255,255,0.1);
321
+ font-weight: 500;
322
+ }
323
+ .queue-table td {
324
+ padding: 10px;
325
+ border-bottom: 1px solid rgba(255,255,255,0.05);
326
+ }
327
+ .queue-type {
328
+ padding: 3px 8px;
329
+ border-radius: 4px;
330
+ font-size: 0.75rem;
331
+ font-weight: 600;
332
+ }
333
+ .queue-type.api { background: rgba(255,107,107,0.2); color: #ff6b6b; }
334
+ .queue-type.component { background: rgba(78,205,196,0.2); color: #4ecdc4; }
335
+ .queue-type.page { background: rgba(255,230,109,0.2); color: #ffe66d; }
336
+ .queue-type.combined { background: rgba(168,130,255,0.2); color: #a882ff; }
337
+
338
+ .queue-status {
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 6px;
342
+ }
343
+ .status-dot {
344
+ width: 8px;
345
+ height: 8px;
346
+ border-radius: 50%;
347
+ }
348
+ .status-dot.done { background: #00ff80; }
349
+ .status-dot.building { background: #00d4ff; animation: pulse 1.5s infinite; }
350
+ .status-dot.pending { background: #666; }
351
+ .status-dot.failed { background: #ff6b6b; }
352
+
353
+ /* Activity Log */
354
+ .activity-list {
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: 8px;
358
+ }
359
+ .activity-item {
360
+ display: flex;
361
+ gap: 10px;
362
+ padding: 10px;
363
+ background: rgba(255,255,255,0.03);
364
+ border-radius: 6px;
365
+ font-size: 0.85rem;
366
+ }
367
+ .activity-time {
368
+ color: #666;
369
+ white-space: nowrap;
370
+ font-family: monospace;
371
+ }
372
+ .activity-msg {
373
+ flex: 1;
374
+ color: #aaa;
375
+ }
376
+ .activity-msg strong {
377
+ color: #e0e0e0;
378
+ }
379
+
380
+ /* Footer */
381
+ .footer {
382
+ text-align: center;
383
+ padding: 15px;
384
+ color: #444;
385
+ font-size: 0.75rem;
386
+ }
387
+
388
+ /* Grid layout for larger screens */
389
+ @media (min-width: 768px) {
390
+ .grid-2 {
391
+ display: grid;
392
+ grid-template-columns: 1fr 1fr;
393
+ gap: 15px;
394
+ }
395
+ }
396
+
397
+ /* Small mobile adjustments */
398
+ @media (max-width: 480px) {
399
+ body { padding: 10px; }
400
+ .header { padding: 15px; }
401
+ .card { padding: 15px; }
402
+ .header h1 { font-size: 1.2rem; }
403
+ }
404
+ </style>
405
+ </head>
406
+ <body>
407
+ <div class="container">
408
+ <!-- Header -->
409
+ <div class="header">
410
+ <div class="header-top">
411
+ <div>
412
+ <h1>HUSTLE BUILD DASHBOARD</h1>
413
+ <div class="subtitle" id="buildName">No active build</div>
414
+ </div>
415
+ <div style="display: flex; gap: 10px; align-items: center;">
416
+ <button id="notifyBtn" onclick="requestNotificationPermission()"
417
+ style="padding: 8px 12px; border-radius: 20px; border: none; cursor: pointer; font-size: 0.8rem; transition: all 0.2s;">
418
+ &#x1F515; Enable
419
+ </button>
420
+ <div class="build-status no-build" id="buildStatus">
421
+ <span id="buildStatusText">IDLE</span>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ <div class="build-meta" id="buildMeta" style="display: none;">
426
+ <div class="build-meta-item">
427
+ <span class="label">Started:</span>
428
+ <span class="value" id="buildStarted">-</span>
429
+ </div>
430
+ <div class="build-meta-item">
431
+ <span class="label">Phase:</span>
432
+ <span class="value" id="buildPhase">-</span>
433
+ </div>
434
+ <div class="build-meta-item">
435
+ <span class="label">Mode:</span>
436
+ <span class="value" id="buildMode">-</span>
437
+ </div>
438
+ </div>
439
+ </div>
440
+
441
+ <!-- Phase Progress -->
442
+ <div class="card" id="phaseCard">
443
+ <div class="card-title">Phase Progress</div>
444
+ <div class="phases" id="phaseList">
445
+ <div class="phase-item">
446
+ <div class="phase-icon pending">1</div>
447
+ <div class="phase-name pending">Document Intake & Parsing</div>
448
+ </div>
449
+ <div class="phase-item">
450
+ <div class="phase-icon pending">2</div>
451
+ <div class="phase-name pending">Parse Request</div>
452
+ </div>
453
+ <div class="phase-item">
454
+ <div class="phase-icon pending">3</div>
455
+ <div class="phase-name pending">Decompose Into Workflows</div>
456
+ </div>
457
+ <div class="phase-item">
458
+ <div class="phase-icon pending">4</div>
459
+ <div class="phase-name pending">Orchestrator Interview</div>
460
+ </div>
461
+ <div class="phase-item">
462
+ <div class="phase-icon pending">5</div>
463
+ <div class="phase-name pending">Create Orchestration State</div>
464
+ </div>
465
+ <div class="phase-item">
466
+ <div class="phase-icon pending">6</div>
467
+ <div class="phase-name pending">Execute Workflows</div>
468
+ </div>
469
+ <div class="phase-item">
470
+ <div class="phase-icon pending">7</div>
471
+ <div class="phase-name pending">Cross-Workflow Wiring</div>
472
+ </div>
473
+ <div class="phase-item">
474
+ <div class="phase-icon pending">8</div>
475
+ <div class="phase-name pending">Final Verification</div>
476
+ </div>
477
+ <div class="phase-item">
478
+ <div class="phase-icon pending">9</div>
479
+ <div class="phase-name pending">Documentation Rollup</div>
480
+ </div>
481
+ <div class="phase-item">
482
+ <div class="phase-icon pending">10</div>
483
+ <div class="phase-name pending">Completion</div>
484
+ </div>
485
+ </div>
486
+ </div>
487
+
488
+ <!-- Current Question -->
489
+ <div class="card question-card" id="questionCard">
490
+ <div class="card-title">Current Question</div>
491
+ <div id="questionContent">
492
+ <div class="no-question">
493
+ <div class="icon">&#x1F4AD;</div>
494
+ <div>No pending questions</div>
495
+ <div style="font-size: 0.8rem; margin-top: 5px;">Polling every 2 seconds...</div>
496
+ </div>
497
+ </div>
498
+ </div>
499
+
500
+ <div class="grid-2">
501
+ <!-- Build Queue -->
502
+ <div class="card" id="queueCard">
503
+ <div class="card-title">Build Queue</div>
504
+ <div id="queueContent">
505
+ <div class="no-question" style="padding: 20px;">
506
+ <div>No items in queue</div>
507
+ </div>
508
+ </div>
509
+ </div>
510
+
511
+ <!-- Activity Log -->
512
+ <div class="card">
513
+ <div class="card-title">Recent Activity</div>
514
+ <div class="activity-list" id="activityList">
515
+ <div class="activity-item">
516
+ <span class="activity-time">--:--</span>
517
+ <span class="activity-msg">Waiting for build activity...</span>
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </div>
522
+
523
+ <div class="footer">
524
+ Hustle Build v4.6.1 | Polling every 2 seconds | <span id="lastUpdate">-</span>
525
+ </div>
526
+ </div>
527
+
528
+ <script>
529
+ const PHASES = [
530
+ "Document Intake & Parsing",
531
+ "Parse Request",
532
+ "Decompose Into Workflows",
533
+ "Orchestrator Interview",
534
+ "Create Orchestration State",
535
+ "Execute Workflows",
536
+ "Cross-Workflow Wiring",
537
+ "Final Verification",
538
+ "Documentation Rollup",
539
+ "Completion"
540
+ ];
541
+
542
+ let currentQuestion = null;
543
+ let selectedOption = null;
544
+ let answerHistory = JSON.parse(localStorage.getItem('answerHistory') || '[]');
545
+ let notificationsEnabled = false;
546
+ let lastPhase = null;
547
+ let lastBuildStatus = null;
548
+
549
+ // Browser Notification Support
550
+ async function requestNotificationPermission() {
551
+ if (!('Notification' in window)) {
552
+ console.log('Browser does not support notifications');
553
+ return;
554
+ }
555
+
556
+ try {
557
+ const permission = await Notification.requestPermission();
558
+ notificationsEnabled = permission === 'granted';
559
+ updateNotificationButton();
560
+
561
+ if (notificationsEnabled) {
562
+ showNotification('Notifications Enabled', 'You will receive alerts for questions and phase changes', 'setup');
563
+ }
564
+ } catch (e) {
565
+ console.error('Notification permission error:', e);
566
+ }
567
+ }
568
+
569
+ function updateNotificationButton() {
570
+ const btn = document.getElementById('notifyBtn');
571
+ if (btn) {
572
+ if (notificationsEnabled) {
573
+ btn.innerHTML = '&#x1F514; ON';
574
+ btn.style.background = 'rgba(0,255,128,0.2)';
575
+ btn.style.color = '#00ff80';
576
+ } else {
577
+ btn.innerHTML = '&#x1F515; Enable';
578
+ btn.style.background = 'rgba(255,255,255,0.1)';
579
+ btn.style.color = '#888';
580
+ }
581
+ }
582
+ }
583
+
584
+ function showNotification(title, body, tag) {
585
+ if (!notificationsEnabled) return;
586
+
587
+ // Only show if page is not visible (user is away)
588
+ if (document.visibilityState === 'hidden' || !document.hasFocus()) {
589
+ try {
590
+ const notification = new Notification(title, {
591
+ body: body,
592
+ icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🔨</text></svg>',
593
+ tag: tag || 'hustle-build',
594
+ requireInteraction: tag === 'question'
595
+ });
596
+
597
+ notification.onclick = () => {
598
+ window.focus();
599
+ notification.close();
600
+ };
601
+
602
+ // Auto-close non-question notifications after 5 seconds
603
+ if (tag !== 'question') {
604
+ setTimeout(() => notification.close(), 5000);
605
+ }
606
+ } catch (e) {
607
+ console.error('Notification error:', e);
608
+ }
609
+ }
610
+ }
611
+
612
+ // Check for phase/status changes and notify
613
+ function checkForChanges(data) {
614
+ if (!data.build) return;
615
+
616
+ const currentPhase = data.build.current_phase;
617
+ const currentStatus = data.build.status;
618
+
619
+ // Notify on phase change
620
+ if (lastPhase !== null && currentPhase !== lastPhase) {
621
+ const phaseName = PHASES[currentPhase - 1] || `Phase ${currentPhase}`;
622
+ showNotification(
623
+ `Phase ${currentPhase}/10: ${phaseName}`,
624
+ 'Build progressed to next phase',
625
+ 'phase-change'
626
+ );
627
+ }
628
+
629
+ // Notify on build completion
630
+ if (lastBuildStatus !== 'complete' && currentStatus === 'complete') {
631
+ showNotification(
632
+ 'Build Complete! ✅',
633
+ data.build.name || 'Your build has finished',
634
+ 'build-complete'
635
+ );
636
+ }
637
+
638
+ lastPhase = currentPhase;
639
+ lastBuildStatus = currentStatus;
640
+ }
641
+
642
+ // Initialize notifications on page load
643
+ document.addEventListener('DOMContentLoaded', () => {
644
+ // Check if already granted
645
+ if ('Notification' in window && Notification.permission === 'granted') {
646
+ notificationsEnabled = true;
647
+ }
648
+ updateNotificationButton();
649
+ });
650
+
651
+ // Fetch build status
652
+ async function fetchStatus() {
653
+ try {
654
+ const response = await fetch('/api/status');
655
+ const data = await response.json();
656
+ updateBuildStatus(data);
657
+ checkForChanges(data);
658
+ updateLastUpdate();
659
+ } catch (e) {
660
+ console.error('Error fetching status:', e);
661
+ }
662
+ }
663
+
664
+ // Fetch current question
665
+ async function fetchQuestion() {
666
+ try {
667
+ const response = await fetch('/api/question');
668
+ const data = await response.json();
669
+
670
+ if (data.question && data.question !== currentQuestion?.question) {
671
+ currentQuestion = data;
672
+ renderQuestion(data);
673
+ // Notify about new question
674
+ showNotification(
675
+ '❓ Question Needs Answer',
676
+ data.question.substring(0, 100),
677
+ 'question'
678
+ );
679
+ } else if (!data.question && currentQuestion) {
680
+ currentQuestion = null;
681
+ renderNoQuestion();
682
+ }
683
+ } catch (e) {
684
+ console.error('Error fetching question:', e);
685
+ }
686
+ }
687
+
688
+ function updateBuildStatus(data) {
689
+ const nameEl = document.getElementById('buildName');
690
+ const statusEl = document.getElementById('buildStatus');
691
+ const statusTextEl = document.getElementById('buildStatusText');
692
+ const metaEl = document.getElementById('buildMeta');
693
+ const startedEl = document.getElementById('buildStarted');
694
+ const phaseEl = document.getElementById('buildPhase');
695
+ const modeEl = document.getElementById('buildMode');
696
+
697
+ if (data.build) {
698
+ const build = data.build;
699
+ nameEl.textContent = build.name || build.build_id || 'Active Build';
700
+
701
+ statusEl.className = 'build-status ' + (build.status || 'pending');
702
+ statusTextEl.textContent = (build.status || 'PENDING').toUpperCase();
703
+
704
+ metaEl.style.display = 'flex';
705
+ startedEl.textContent = formatTime(build.created_at);
706
+ phaseEl.textContent = build.current_phase ? `${build.current_phase}/10` : '-';
707
+ modeEl.textContent = build.mode || 'interactive';
708
+
709
+ // Update phase progress
710
+ updatePhaseProgress(build.current_phase || 0, build.phase_statuses || {});
711
+
712
+ // Update build queue
713
+ updateBuildQueue(build.decomposition);
714
+
715
+ // Update activity
716
+ updateActivity(build.activity || data.activity || []);
717
+ } else {
718
+ nameEl.textContent = 'No active build';
719
+ statusEl.className = 'build-status no-build';
720
+ statusTextEl.textContent = 'IDLE';
721
+ metaEl.style.display = 'none';
722
+ resetPhaseProgress();
723
+ }
724
+ }
725
+
726
+ function updatePhaseProgress(currentPhase, statuses) {
727
+ const phaseItems = document.querySelectorAll('.phase-item');
728
+ phaseItems.forEach((item, i) => {
729
+ const icon = item.querySelector('.phase-icon');
730
+ const name = item.querySelector('.phase-name');
731
+ const phaseNum = i + 1;
732
+ const status = statuses[phaseNum] || (phaseNum < currentPhase ? 'completed' : (phaseNum === currentPhase ? 'in_progress' : 'pending'));
733
+
734
+ icon.className = 'phase-icon ' + status;
735
+ icon.textContent = status === 'completed' ? '\\u2713' : phaseNum;
736
+
737
+ name.className = 'phase-name ' + (status === 'in_progress' ? 'active' : status);
738
+ });
739
+ }
740
+
741
+ function resetPhaseProgress() {
742
+ const phaseItems = document.querySelectorAll('.phase-item');
743
+ phaseItems.forEach((item, i) => {
744
+ const icon = item.querySelector('.phase-icon');
745
+ const name = item.querySelector('.phase-name');
746
+ icon.className = 'phase-icon pending';
747
+ icon.textContent = i + 1;
748
+ name.className = 'phase-name pending';
749
+ });
750
+ }
751
+
752
+ function updateBuildQueue(decomposition) {
753
+ const container = document.getElementById('queueContent');
754
+ if (!decomposition) {
755
+ container.innerHTML = '<div class="no-question" style="padding: 20px;"><div>No items in queue</div></div>';
756
+ return;
757
+ }
758
+
759
+ const items = [];
760
+ ['apis', 'components', 'combined_apis', 'pages'].forEach(type => {
761
+ const typeItems = decomposition[type] || [];
762
+ typeItems.forEach(item => {
763
+ items.push({...item, type: type.replace('_', ' ').replace(/s$/, '')});
764
+ });
765
+ });
766
+
767
+ if (items.length === 0) {
768
+ container.innerHTML = '<div class="no-question" style="padding: 20px;"><div>No items in queue</div></div>';
769
+ return;
770
+ }
771
+
772
+ const rows = items.map(item => {
773
+ const typeClass = item.type.toLowerCase().split(' ')[0];
774
+ const statusClass = item.status === 'complete' ? 'done' : (item.status === 'in_progress' ? 'building' : (item.status === 'failed' ? 'failed' : 'pending'));
775
+ const deps = (item.depends_on || []).join(', ') || '-';
776
+ return `
777
+ <tr>
778
+ <td><span class="queue-type ${typeClass}">${item.type.toUpperCase()}</span></td>
779
+ <td>${item.name}</td>
780
+ <td>
781
+ <div class="queue-status">
782
+ <div class="status-dot ${statusClass}"></div>
783
+ <span>${item.status || 'pending'}</span>
784
+ </div>
785
+ </td>
786
+ <td style="color: #666; font-size: 0.8rem;">${deps}</td>
787
+ </tr>
788
+ `;
789
+ }).join('');
790
+
791
+ container.innerHTML = `
792
+ <table class="queue-table">
793
+ <thead>
794
+ <tr><th>Type</th><th>Name</th><th>Status</th><th>Deps</th></tr>
795
+ </thead>
796
+ <tbody>${rows}</tbody>
797
+ </table>
798
+ `;
799
+ }
800
+
801
+ function updateActivity(activity) {
802
+ const container = document.getElementById('activityList');
803
+ if (!activity || activity.length === 0) {
804
+ container.innerHTML = `
805
+ <div class="activity-item">
806
+ <span class="activity-time">--:--</span>
807
+ <span class="activity-msg">Waiting for build activity...</span>
808
+ </div>
809
+ `;
810
+ return;
811
+ }
812
+
813
+ container.innerHTML = activity.slice(0, 8).map(item => `
814
+ <div class="activity-item">
815
+ <span class="activity-time">${formatTime(item.time)}</span>
816
+ <span class="activity-msg">${item.message}</span>
817
+ </div>
818
+ `).join('');
819
+ }
820
+
821
+ function formatTime(timestamp) {
822
+ if (!timestamp) return '-';
823
+ try {
824
+ const date = new Date(timestamp);
825
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
826
+ } catch (e) {
827
+ return timestamp;
828
+ }
829
+ }
830
+
831
+ function updateLastUpdate() {
832
+ document.getElementById('lastUpdate').textContent = 'Updated: ' + new Date().toLocaleTimeString();
833
+ }
834
+
835
+ function renderQuestion(data) {
836
+ const content = document.getElementById('questionContent');
837
+ const optionsHtml = data.options.map((opt, i) => {
838
+ const label = typeof opt === 'string' ? opt : opt.label;
839
+ const desc = typeof opt === 'object' ? opt.description : '';
840
+ const isOther = label.toLowerCase().includes('other');
841
+ return `
842
+ <div class="option" data-index="${i}" data-other="${isOther}" onclick="selectOption(${i}, ${isOther})">
843
+ <div class="option-label">${label}</div>
844
+ ${desc ? `<div class="option-description">${desc}</div>` : ''}
845
+ </div>
846
+ `;
847
+ }).join('');
848
+
849
+ content.innerHTML = `
850
+ <span class="phase-badge">${data.phase || data.header || 'Question'}</span>
851
+ <div class="question-text">${data.question}</div>
852
+ <div class="options">${optionsHtml}</div>
853
+ <div class="custom-input" id="customInput">
854
+ <textarea id="customAnswer" placeholder="Enter your custom answer..."></textarea>
855
+ </div>
856
+ <button class="submit-btn" id="submitBtn" onclick="submitAnswer()" disabled>
857
+ Select an option to continue
858
+ </button>
859
+ `;
860
+ }
861
+
862
+ function renderNoQuestion() {
863
+ document.getElementById('questionContent').innerHTML = `
864
+ <div class="no-question">
865
+ <div class="icon">&#x1F4AD;</div>
866
+ <div>No pending questions</div>
867
+ <div style="font-size: 0.8rem; margin-top: 5px;">Polling every 2 seconds...</div>
868
+ </div>
869
+ `;
870
+ }
871
+
872
+ function selectOption(index, isOther) {
873
+ selectedOption = index;
874
+ document.querySelectorAll('.option').forEach((el, i) => {
875
+ el.classList.toggle('selected', i === index);
876
+ });
877
+
878
+ const customInput = document.getElementById('customInput');
879
+ if (customInput) {
880
+ if (isOther) {
881
+ customInput.classList.add('visible');
882
+ } else {
883
+ customInput.classList.remove('visible');
884
+ }
885
+ }
886
+
887
+ const btn = document.getElementById('submitBtn');
888
+ if (btn) {
889
+ btn.disabled = false;
890
+ btn.textContent = 'Submit Answer';
891
+ }
892
+ }
893
+
894
+ async function submitAnswer() {
895
+ if (selectedOption === null || !currentQuestion) return;
896
+
897
+ const btn = document.getElementById('submitBtn');
898
+ btn.disabled = true;
899
+ btn.textContent = 'Submitting...';
900
+
901
+ const opt = currentQuestion.options[selectedOption];
902
+ let answer = typeof opt === 'string' ? opt : opt.label;
903
+
904
+ if (answer.toLowerCase().includes('other')) {
905
+ const customText = document.getElementById('customAnswer')?.value;
906
+ if (customText) answer = customText;
907
+ }
908
+
909
+ try {
910
+ const response = await fetch('/api/answer', {
911
+ method: 'POST',
912
+ headers: {'Content-Type': 'application/json'},
913
+ body: JSON.stringify({
914
+ question: currentQuestion.question,
915
+ answer: answer,
916
+ option_index: selectedOption,
917
+ phase: currentQuestion.phase || currentQuestion.header
918
+ })
919
+ });
920
+
921
+ if (response.ok) {
922
+ answerHistory.unshift({
923
+ time: new Date().toISOString(),
924
+ question: currentQuestion.question.substring(0, 50) + '...',
925
+ answer: answer
926
+ });
927
+ answerHistory = answerHistory.slice(0, 10);
928
+ localStorage.setItem('answerHistory', JSON.stringify(answerHistory));
929
+
930
+ document.getElementById('questionContent').innerHTML = `
931
+ <div class="success-msg">
932
+ <div style="font-size: 1.5rem; margin-bottom: 8px;">&#x2713;</div>
933
+ <div>Answer submitted!</div>
934
+ <div style="margin-top: 8px; color: #aaa; font-size: 0.85rem;">"${answer}"</div>
935
+ </div>
936
+ `;
937
+ currentQuestion = null;
938
+ selectedOption = null;
939
+
940
+ setTimeout(renderNoQuestion, 2000);
941
+ }
942
+ } catch (e) {
943
+ console.error('Error submitting:', e);
944
+ btn.disabled = false;
945
+ btn.textContent = 'Error - Try Again';
946
+ }
947
+ }
948
+
949
+ // Start polling
950
+ setInterval(fetchStatus, 2000);
951
+ setInterval(fetchQuestion, 2000);
952
+ fetchStatus();
953
+ fetchQuestion();
954
+ </script>
955
+ </body>
956
+ </html>
957
+ '''
958
+
959
+
960
+ class QuestionHandler(BaseHTTPRequestHandler):
961
+ """HTTP handler for the build dashboard and remote question interface."""
962
+
963
+ def get_project_dir(self):
964
+ return Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
965
+
966
+ def load_state(self):
967
+ """Load the hustle-build state file."""
968
+ state_file = self.get_project_dir() / STATE_FILE
969
+ if state_file.exists():
970
+ try:
971
+ return json.loads(state_file.read_text())
972
+ except Exception:
973
+ pass
974
+ return None
975
+
976
+ def load_activity(self):
977
+ """Load activity log."""
978
+ activity_file = self.get_project_dir() / ACTIVITY_LOG_FILE
979
+ if activity_file.exists():
980
+ try:
981
+ data = json.loads(activity_file.read_text())
982
+ return data.get('entries', data) if isinstance(data, dict) else data
983
+ except Exception:
984
+ pass
985
+ return []
986
+
987
+ def do_GET(self):
988
+ if self.path == '/' or self.path == '/index.html':
989
+ self.send_response(200)
990
+ self.send_header('Content-type', 'text/html; charset=utf-8')
991
+ self.send_header('Cache-Control', 'no-cache')
992
+ self.end_headers()
993
+ self.wfile.write(HTML_TEMPLATE.encode('utf-8'))
994
+
995
+ elif self.path == '/api/question':
996
+ self.send_response(200)
997
+ self.send_header('Content-type', 'application/json')
998
+ self.send_header('Cache-Control', 'no-cache')
999
+ self.end_headers()
1000
+
1001
+ question_file = self.get_project_dir() / QUESTION_FILE
1002
+ if question_file.exists():
1003
+ try:
1004
+ data = json.loads(question_file.read_text())
1005
+ self.wfile.write(json.dumps(data).encode('utf-8'))
1006
+ except Exception:
1007
+ self.wfile.write(json.dumps({}).encode('utf-8'))
1008
+ else:
1009
+ self.wfile.write(json.dumps({}).encode('utf-8'))
1010
+
1011
+ elif self.path == '/api/status':
1012
+ self.send_response(200)
1013
+ self.send_header('Content-type', 'application/json')
1014
+ self.send_header('Cache-Control', 'no-cache')
1015
+ self.end_headers()
1016
+
1017
+ # Load build state
1018
+ state = self.load_state()
1019
+ activity = self.load_activity()
1020
+
1021
+ # Check for pending question
1022
+ question_file = self.get_project_dir() / QUESTION_FILE
1023
+ has_question = question_file.exists()
1024
+
1025
+ response = {
1026
+ "status": "ready",
1027
+ "has_question": has_question,
1028
+ "timestamp": datetime.now().isoformat()
1029
+ }
1030
+
1031
+ if state:
1032
+ # Extract build info from state
1033
+ response["build"] = {
1034
+ "build_id": state.get("build_id"),
1035
+ "name": state.get("request", {}).get("original", "")[:50] if state.get("request") else state.get("build_id"),
1036
+ "status": state.get("status", "pending"),
1037
+ "mode": state.get("mode", "interactive"),
1038
+ "created_at": state.get("created_at"),
1039
+ "current_phase": self._get_current_phase(state),
1040
+ "phase_statuses": self._get_phase_statuses(state),
1041
+ "decomposition": state.get("decomposition"),
1042
+ "activity": activity[-10:] if activity else []
1043
+ }
1044
+ else:
1045
+ response["build"] = None
1046
+ response["activity"] = activity[-10:] if activity else []
1047
+
1048
+ self.wfile.write(json.dumps(response).encode('utf-8'))
1049
+
1050
+ else:
1051
+ self.send_response(404)
1052
+ self.end_headers()
1053
+
1054
+ def _get_current_phase(self, state):
1055
+ """Determine current phase from state."""
1056
+ # Check for active sub-workflow (Phase 6)
1057
+ if state.get("active_sub_workflow"):
1058
+ return 6
1059
+
1060
+ # Check decomposition status
1061
+ decomp = state.get("decomposition", {})
1062
+ has_decomp = any(decomp.get(k) for k in ["apis", "components", "pages", "combined_apis"])
1063
+
1064
+ # Check interview status
1065
+ interview = state.get("orchestrator_interview", {})
1066
+ interview_complete = interview.get("status") == "complete"
1067
+
1068
+ if state.get("status") == "complete":
1069
+ return 10
1070
+ elif has_decomp and interview_complete:
1071
+ # Check if any workflows are done
1072
+ all_done = True
1073
+ for category in ["apis", "components", "pages", "combined_apis"]:
1074
+ for item in decomp.get(category, []):
1075
+ if item.get("status") != "complete":
1076
+ all_done = False
1077
+ break
1078
+ if all_done and has_decomp:
1079
+ return 7 # Wiring phase
1080
+ return 6 # Still executing
1081
+ elif interview_complete:
1082
+ return 5 # Creating state
1083
+ elif has_decomp:
1084
+ return 4 # Interview
1085
+ elif state.get("project_spec", {}).get("extracted"):
1086
+ return 3 # Decomposition
1087
+ elif state.get("project_spec", {}).get("raw_content"):
1088
+ return 1 # Document parsing
1089
+
1090
+ return 2 # Parse request
1091
+
1092
+ def _get_phase_statuses(self, state):
1093
+ """Get status of each phase."""
1094
+ statuses = {}
1095
+ current = self._get_current_phase(state)
1096
+ for i in range(1, 11):
1097
+ if i < current:
1098
+ statuses[i] = "completed"
1099
+ elif i == current:
1100
+ statuses[i] = "in_progress"
1101
+ else:
1102
+ statuses[i] = "pending"
1103
+ return statuses
1104
+
1105
+ def do_POST(self):
1106
+ if self.path == '/api/answer':
1107
+ content_length = int(self.headers.get('Content-Length', 0))
1108
+ post_data = self.rfile.read(content_length)
1109
+
1110
+ try:
1111
+ answer_data = json.loads(post_data.decode('utf-8'))
1112
+ answer_data['submitted_at'] = datetime.now().isoformat()
1113
+
1114
+ # Write answer to pending file
1115
+ answer_file = self.get_project_dir() / ANSWER_FILE
1116
+ answer_file.parent.mkdir(parents=True, exist_ok=True)
1117
+ answer_file.write_text(json.dumps(answer_data, indent=2))
1118
+
1119
+ # Clear the question file
1120
+ question_file = self.get_project_dir() / QUESTION_FILE
1121
+ if question_file.exists():
1122
+ question_file.unlink()
1123
+
1124
+ # Log activity
1125
+ self._log_activity(f"Answer submitted: {answer_data.get('answer', '')[:50]}")
1126
+
1127
+ self.send_response(200)
1128
+ self.send_header('Content-type', 'application/json')
1129
+ self.end_headers()
1130
+ self.wfile.write(json.dumps({"status": "success"}).encode('utf-8'))
1131
+
1132
+ except Exception as e:
1133
+ self.send_response(500)
1134
+ self.send_header('Content-type', 'application/json')
1135
+ self.end_headers()
1136
+ self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
1137
+ else:
1138
+ self.send_response(404)
1139
+ self.end_headers()
1140
+
1141
+ def _log_activity(self, message):
1142
+ """Add entry to activity log."""
1143
+ activity_file = self.get_project_dir() / ACTIVITY_LOG_FILE
1144
+ activity_file.parent.mkdir(parents=True, exist_ok=True)
1145
+
1146
+ entries = []
1147
+ if activity_file.exists():
1148
+ try:
1149
+ data = json.loads(activity_file.read_text())
1150
+ entries = data.get('entries', data) if isinstance(data, dict) else data
1151
+ except Exception:
1152
+ pass
1153
+
1154
+ entries.append({
1155
+ "time": datetime.now().isoformat(),
1156
+ "message": message
1157
+ })
1158
+ entries = entries[-50:] # Keep last 50
1159
+
1160
+ activity_file.write_text(json.dumps({"entries": entries}, indent=2))
1161
+
1162
+ def log_message(self, format, *args):
1163
+ # Custom log format
1164
+ print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
1165
+
1166
+
1167
+ def main():
1168
+ # Get port from argument or environment
1169
+ port = DEFAULT_PORT
1170
+ if len(sys.argv) > 1:
1171
+ try:
1172
+ port = int(sys.argv[1])
1173
+ except ValueError:
1174
+ pass
1175
+ port = int(os.environ.get('REMOTE_QUESTIONS_PORT', port))
1176
+
1177
+ project_dir = os.environ.get('CLAUDE_PROJECT_DIR', '.')
1178
+
1179
+ # Ensure directories exist
1180
+ claude_dir = Path(project_dir) / ".claude"
1181
+ claude_dir.mkdir(parents=True, exist_ok=True)
1182
+ (Path(project_dir) / ".claude/workflow-logs").mkdir(parents=True, exist_ok=True)
1183
+
1184
+ server = HTTPServer(('0.0.0.0', port), QuestionHandler)
1185
+
1186
+ print(f"""
1187
+ ================================================================================
1188
+ HUSTLE BUILD DASHBOARD SERVER v4.6.1
1189
+ ================================================================================
1190
+
1191
+ Dashboard running at: http://localhost:{port}
1192
+
1193
+ Features:
1194
+ - Build progress & phase status
1195
+ - Question interface for remote answering
1196
+ - Build queue with dependencies
1197
+ - Activity log
1198
+ - Browser notifications (click Enable in header)
1199
+
1200
+ Access from phone/tablet on same network:
1201
+ http://<your-computer-ip>:{port}
1202
+ (Find IP: ipconfig/ifconfig, look for 192.168.x.x)
1203
+
1204
+ API Endpoints:
1205
+ GET / - Full dashboard HTML
1206
+ GET /api/status - Build state JSON
1207
+ GET /api/question - Current question JSON
1208
+ POST /api/answer - Submit answer
1209
+
1210
+ Project directory: {os.path.abspath(project_dir)}
1211
+
1212
+ Press Ctrl+C to stop.
1213
+ ================================================================================
1214
+ """)
1215
+
1216
+ try:
1217
+ server.serve_forever()
1218
+ except KeyboardInterrupt:
1219
+ print("\nShutting down...")
1220
+ server.shutdown()
1221
+
1222
+
1223
+ if __name__ == "__main__":
1224
+ main()